From ffca1e9e5f256afc1e360d7d2e53f2bf3104723b Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 11 May 2026 23:52:57 +0200 Subject: [PATCH] fix: stabilize game scene loading and player spawn --- public/models/fermier/model.gltf | 2 +- public/models/galet/model.gltf | 2 +- public/models/gant_l/model.gltf | 2 +- public/models/gant_l_pad/model.gltf | 2 +- public/models/gant_r/model.gltf | 2 +- public/models/gant_r_pad/model.gltf | 2 +- public/models/gerant/model.gltf | 2 +- public/models/lafabrik/model.gltf | 2 +- public/models/persoprincipal/model.gltf | 2 +- public/models/sapin/model.gltf | 2 +- public/sounds/dialogue/dialogues.json | 3 +- src/data/player/playerConfig.ts | 2 +- src/hooks/world/useWorldSceneLoading.ts | 17 +++- src/pages/page.tsx | 8 +- src/world/GameCinematics.tsx | 5 +- src/world/GameDialogues.tsx | 5 +- src/world/GameMap.tsx | 130 +++++++++++++++--------- src/world/GameMapCollision.tsx | 22 +++- src/world/GameMusic.tsx | 2 +- src/world/World.tsx | 65 +++++++----- src/world/player/Player.tsx | 4 +- src/world/player/PlayerController.tsx | 29 ++++-- 22 files changed, 202 insertions(+), 110 deletions(-) diff --git a/public/models/fermier/model.gltf b/public/models/fermier/model.gltf index 01e08d4..00cf457 100644 --- a/public/models/fermier/model.gltf +++ b/public/models/fermier/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1faa8f43d3026dc0a49e52be425539ba761793cda4fe24a421004af5d44da4b7 +oid sha256:dd70a57172bc7c5c68851ec5da9ff694396c157c89e9d3e20ce5d4c00e50b5fb size 3146 diff --git a/public/models/galet/model.gltf b/public/models/galet/model.gltf index e2324f3..2602f0c 100644 --- a/public/models/galet/model.gltf +++ b/public/models/galet/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15a4dcd7c5faf03913dd79f1ebbc0970c9db4f2733c36dcf53bb5ef652974c9c +oid sha256:2d1604a1906f46d8c5f995b5d9dcbc545a0c9d165bed954e949a60c165c958e5 size 3495 diff --git a/public/models/gant_l/model.gltf b/public/models/gant_l/model.gltf index da03db6..06919b0 100644 --- a/public/models/gant_l/model.gltf +++ b/public/models/gant_l/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01cdac724dfe936d112bd19202694a16196f54b6726b9e7e4e5102235fd41980 +oid sha256:cb31d7f73070f30f152c974afe0557bbd4c8b1468a5247c71eaf82afd6cc67bb size 10303 diff --git a/public/models/gant_l_pad/model.gltf b/public/models/gant_l_pad/model.gltf index 846cf49..4290eeb 100644 --- a/public/models/gant_l_pad/model.gltf +++ b/public/models/gant_l_pad/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fee94fb22e76ebc51ebc30fdd9415eb7db02f4298355965d99889b4329301393 +oid sha256:4b8c8dcc940d9ac53733b9cc747ec366ff8fe9da4b546e7e8d74348f4cb08044 size 6192 diff --git a/public/models/gant_r/model.gltf b/public/models/gant_r/model.gltf index 3618596..6f22319 100644 --- a/public/models/gant_r/model.gltf +++ b/public/models/gant_r/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c82eb9596e0829193b8c860670ff9cad959dfcced8d17183c2347346870d267b +oid sha256:b51af270cf5c4900b17dfefa48f0e622d1b5965214a89df73fbf4e78d65da5ba size 31499 diff --git a/public/models/gant_r_pad/model.gltf b/public/models/gant_r_pad/model.gltf index ade1881..0ce8568 100644 --- a/public/models/gant_r_pad/model.gltf +++ b/public/models/gant_r_pad/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4e97e95e1f38f6d76cc49e28e9a10d4d5d0e703511018e5933548faca3cd67b +oid sha256:623937d431f5eddf460a75cabdf5696253f2beda12fa68492812727e9beec1d1 size 6900 diff --git a/public/models/gerant/model.gltf b/public/models/gerant/model.gltf index bba9e92..bd9ffde 100644 --- a/public/models/gerant/model.gltf +++ b/public/models/gerant/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:143af47426c600085097223fb27f7ac8f3ab2e23071b90cda6840f5eabef7a62 +oid sha256:ece13259924b9c5aa96c066c6e59e4e66d9570aa03d26a7aa2fbe9c8b4a2ce56 size 3141 diff --git a/public/models/lafabrik/model.gltf b/public/models/lafabrik/model.gltf index cb63296..488b10b 100644 --- a/public/models/lafabrik/model.gltf +++ b/public/models/lafabrik/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:615bf4ed9c84530bdec465a6bf97262f6e1fc55dd5846bd8c3e698b82309fc68 +oid sha256:f06b05c03a20434575bca19216f6ac543671b1f9747c325d31e339ea1e8b271e size 124737 diff --git a/public/models/persoprincipal/model.gltf b/public/models/persoprincipal/model.gltf index faa8ff2..c9ec264 100644 --- a/public/models/persoprincipal/model.gltf +++ b/public/models/persoprincipal/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:955c5c954519afef152989a1fb4d187e115fd5893a6a8f7119e4818bf87dc3ac +oid sha256:7e3ed9f4faf333db1ea120580ff4ac6f9c072ac072093ff64d0ade2a1cdd7c2f size 3136 diff --git a/public/models/sapin/model.gltf b/public/models/sapin/model.gltf index df975c6..65c04f4 100644 --- a/public/models/sapin/model.gltf +++ b/public/models/sapin/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:898e7cea4edeb0a5f3d46312fe6b63ec5f5db8d5642e2202ad86decf3817e40f +oid sha256:e3f66d375ba33367cc0349b3c54dce6009b9cbe3bfb941d4c05a3e224451740a size 5811 diff --git a/public/sounds/dialogue/dialogues.json b/public/sounds/dialogue/dialogues.json index ff0d23d..af596ef 100644 --- a/public/sounds/dialogue/dialogues.json +++ b/public/sounds/dialogue/dialogues.json @@ -31,7 +31,8 @@ "id": "narrateur_bienvenueaaltera", "voice": "narrateur", "audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3", - "subtitleCueIndex": 1 + "subtitleCueIndex": 1, + "timecode": 0 }, { "id": "narrateur_intro_prenom", diff --git a/src/data/player/playerConfig.ts b/src/data/player/playerConfig.ts index 699ee49..360cebe 100644 --- a/src/data/player/playerConfig.ts +++ b/src/data/player/playerConfig.ts @@ -11,5 +11,5 @@ export const PLAYER_MAX_DELTA = 0.05; export const PLAYER_ACCELERATION_MULTIPLIER = 9; export const PLAYER_XZ_DAMPING_FACTOR = 8; -export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 100, 0]; +export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0]; export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0]; diff --git a/src/hooks/world/useWorldSceneLoading.ts b/src/hooks/world/useWorldSceneLoading.ts index dffb8c5..24020af 100644 --- a/src/hooks/world/useWorldSceneLoading.ts +++ b/src/hooks/world/useWorldSceneLoading.ts @@ -10,7 +10,9 @@ interface UseWorldSceneLoadingOptions { interface UseWorldSceneLoadingResult { octree: Octree | null; + gameplayReady: boolean; showGameStage: boolean; + handleGameStageLoaded: () => void; handleGameMapLoaded: () => void; handleOctreeReady: (octree: Octree) => void; } @@ -21,15 +23,26 @@ export function useWorldSceneLoading({ }: UseWorldSceneLoadingOptions): UseWorldSceneLoadingResult { const [octree, setOctree] = useState(null); const [gameMapLoaded, setGameMapLoaded] = useState(false); + const [gameStageLoaded, setGameStageLoaded] = useState(false); const showGameStage = sceneMode === "game" && gameMapLoaded; + const gameplayReady = showGameStage && gameStageLoaded && octree !== null; const sceneReady = - (sceneMode === "game" && gameMapLoaded) || + (sceneMode === "game" && gameplayReady) || (sceneMode === "physics" && octree !== null); const handleGameMapLoaded = useCallback(() => { setGameMapLoaded(true); }, []); + const handleGameStageLoaded = useCallback(() => { + setGameStageLoaded(true); + onLoadingStateChange?.({ + currentStep: "Initialisation gameplay", + progress: 0.96, + status: "loading", + }); + }, [onLoadingStateChange]); + const handleOctreeReady = useCallback( (nextOctree: Octree) => { setOctree(nextOctree); @@ -74,7 +87,9 @@ export function useWorldSceneLoading({ return { octree, + gameplayReady, showGameStage, + handleGameStageLoaded, handleGameMapLoaded, handleOctreeReady, }; diff --git a/src/pages/page.tsx b/src/pages/page.tsx index 9728cd8..d389ef6 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -18,13 +18,13 @@ export function HomePage(): React.JSX.Element { const handleSceneLoadingStateChange = useCallback( (nextState: SceneLoadingState) => { setSceneLoadingState((currentState) => { - const shouldRestartProgress = currentState.status === "ready"; + if (currentState.status === "ready" && nextState.status === "loading") { + return currentState; + } return { ...nextState, - progress: shouldRestartProgress - ? nextState.progress - : Math.max(currentState.progress, nextState.progress), + progress: Math.max(currentState.progress, nextState.progress), }; }); }, diff --git a/src/world/GameCinematics.tsx b/src/world/GameCinematics.tsx index 2aeefb9..7f60ca7 100644 --- a/src/world/GameCinematics.tsx +++ b/src/world/GameCinematics.tsx @@ -22,6 +22,7 @@ export function GameCinematics(): null { const playedCinematicsRef = useRef(new Set()); const timelineRef = useRef(null); const activeAudiosRef = useRef(new Set()); + const startedAtRef = useRef(null); useEffect(() => { let mounted = true; @@ -59,7 +60,9 @@ export function GameCinematics(): null { useFrame(({ clock }) => { if (!manifest) return; - const elapsedTime = clock.getElapsedTime(); + startedAtRef.current ??= clock.getElapsedTime(); + + const elapsedTime = clock.getElapsedTime() - startedAtRef.current; manifest.cinematics.forEach((cinematic) => { if (cinematic.timecode === undefined) return; diff --git a/src/world/GameDialogues.tsx b/src/world/GameDialogues.tsx index 3bbfe4c..5a0a89d 100644 --- a/src/world/GameDialogues.tsx +++ b/src/world/GameDialogues.tsx @@ -12,6 +12,7 @@ export function GameDialogues(): null { const [manifest, setManifest] = useState(null); const playedDialoguesRef = useRef(new Set()); const activeAudiosRef = useRef(new Set()); + const startedAtRef = useRef(null); useEffect(() => { let mounted = true; @@ -38,7 +39,9 @@ export function GameDialogues(): null { useFrame(({ clock }) => { if (!manifest) return; - const elapsedTime = clock.getElapsedTime(); + startedAtRef.current ??= clock.getElapsedTime(); + + const elapsedTime = clock.getElapsedTime() - startedAtRef.current; manifest.dialogues.forEach((dialogue) => { if (dialogue.timecode === undefined) return; diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index 9251f8f..bef9062 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -1,5 +1,12 @@ import type { ReactNode } from "react"; -import { Component, Suspense, useEffect, useState } from "react"; +import { + Component, + Suspense, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { GameMapCollision } from "@/world/GameMapCollision"; @@ -20,6 +27,7 @@ interface ErrorBoundaryProps { fallback: ReactNode; modelUrl: string | null; node: MapNode; + onSettled: () => void; } interface ErrorBoundaryState { @@ -50,6 +58,7 @@ class ModelErrorBoundary extends Component< }, error, ); + this.props.onSettled(); } render(): ReactNode { @@ -68,19 +77,39 @@ interface GameMapProps { buildOctree?: boolean; } -const MAP_RENDER_BATCH_SIZE = 12; - export function GameMap({ buildOctree = true, onLoaded, onLoadingStateChange, onOctreeReady, }: GameMapProps): React.JSX.Element { + const settledMapNodesRef = useRef(new Set()); const [mapNodes, setMapNodes] = useState([]); const [mapLoaded, setMapLoaded] = useState(false); - const [visibleNodeCount, setVisibleNodeCount] = useState(0); - const visibleMapNodes = mapNodes.slice(0, visibleNodeCount); - const mapReady = mapLoaded && visibleNodeCount >= mapNodes.length; + const [settledMapNodeCount, setSettledMapNodeCount] = useState(0); + const mapReady = mapLoaded && settledMapNodeCount >= mapNodes.length; + + const handleMapNodeSettled = useCallback((index: number) => { + if (settledMapNodesRef.current.has(index)) return; + + settledMapNodesRef.current.add(index); + setSettledMapNodeCount(settledMapNodesRef.current.size); + }, []); + + const showEmptyMap = useCallback( + (currentStep: string) => { + setMapNodes([]); + setMapLoaded(true); + settledMapNodesRef.current.clear(); + setSettledMapNodeCount(0); + onLoadingStateChange?.({ + currentStep, + progress: 0.7, + status: "loading", + }); + }, + [onLoadingStateChange], + ); useEffect(() => { onLoadingStateChange?.({ @@ -94,11 +123,7 @@ export function GameMap({ const sceneData = await loadMapSceneData(); if (!sceneData) { logger.warn("GameMap", "map.json not found"); - onLoadingStateChange?.({ - currentStep: "Map introuvable", - progress: 1, - status: "loading", - }); + showEmptyMap("Map introuvable"); return; } @@ -128,9 +153,10 @@ export function GameMap({ setMapNodes(loadedMapNodes); setMapLoaded(true); - setVisibleNodeCount(0); + settledMapNodesRef.current.clear(); + setSettledMapNodeCount(0); onLoadingStateChange?.({ - currentStep: "Montage progressif des models", + currentStep: "Chargement des modèles de la map", progress: 0.25, status: "loading", }); @@ -138,63 +164,41 @@ export function GameMap({ logger.error("GameMap", "Error loading map", { error: error instanceof Error ? error : new Error(String(error)), }); - onLoadingStateChange?.({ - currentStep: "Erreur de chargement de la map", - progress: 1, - status: "loading", - }); + showEmptyMap("Erreur de chargement de la map"); } }; loadMap(); - }, [onLoaded, onLoadingStateChange]); - - useEffect(() => { - if (mapNodes.length === 0 || visibleNodeCount >= mapNodes.length) return; - - const frameId = window.requestAnimationFrame(() => { - setVisibleNodeCount((current) => - Math.min(current + MAP_RENDER_BATCH_SIZE, mapNodes.length), - ); - }); - - return () => { - window.cancelAnimationFrame(frameId); - }; - }, [mapNodes.length, visibleNodeCount]); + }, [onLoadingStateChange, showEmptyMap]); useEffect(() => { if (mapNodes.length === 0) return; const renderProgress = - mapNodes.length === 0 ? 1 : visibleNodeCount / mapNodes.length; + mapNodes.length === 0 ? 1 : settledMapNodeCount / mapNodes.length; onLoadingStateChange?.({ - currentStep: "Montage progressif des models", + currentStep: "Chargement des modèles de la map", progress: 0.25 + renderProgress * 0.45, status: "loading", }); - }, [mapNodes.length, onLoadingStateChange, visibleNodeCount]); + }, [mapNodes.length, onLoadingStateChange, settledMapNodeCount]); return ( <> - {visibleMapNodes.map((mapNode, index) => ( + {mapNodes.map((mapNode, index) => ( } modelUrl={mapNode.modelUrl} node={mapNode.node} + onSettled={() => handleMapNodeSettled(index)} > - {mapNode.modelUrl ? ( - }> - - - ) : ( - - )} + handleMapNodeSettled(index)} + /> ))} @@ -210,12 +214,40 @@ export function GameMap({ ); } +function MapNodeInstance({ + node, + modelUrl, + onSettled, +}: { + node: MapNode; + modelUrl: string | null; + onSettled: () => void; +}): React.JSX.Element { + useEffect(() => { + if (modelUrl !== null) return; + + onSettled(); + }, [modelUrl, onSettled]); + + if (!modelUrl) { + return ; + } + + return ( + }> + + + ); +} + function ModelInstance({ node, modelUrl, + onLoaded, }: { node: MapNode; modelUrl: string; + onLoaded: () => void; }): React.JSX.Element { const { position, rotation, scale } = node; const { scene } = useLoggedGLTF(modelUrl, { @@ -226,6 +258,10 @@ function ModelInstance({ }); const sceneInstance = useClonedObject(scene); + useEffect(() => { + onLoaded(); + }, [onLoaded]); + return ( (null); const settledCollisionNodesRef = useRef(new Set()); + const loadedNotifiedRef = useRef(false); const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0); const collisionNodes = nodes.filter(isCollisionNode); const collisionReady = mapReady && settledCollisionNodeCount >= collisionNodes.length; + const notifyLoaded = useCallback(() => { + if (loadedNotifiedRef.current) return; + + loadedNotifiedRef.current = true; + onLoaded?.(); + }, [onLoaded]); + const handleCollisionNodeSettled = useCallback((index: number) => { if (settledCollisionNodesRef.current.has(index)) return; @@ -122,9 +130,9 @@ export function GameMapCollision({ status: "loading", }); onOctreeReady(octree); - onLoaded?.(); + notifyLoaded(); }, - [onLoaded, onLoadingStateChange, onOctreeReady], + [notifyLoaded, onLoadingStateChange, onOctreeReady], ); useOctreeGraphNode( @@ -138,7 +146,12 @@ export function GameMapCollision({ if (!mapReady) return; if (collisionNodes.length === 0) { - onLoaded?.(); + notifyLoaded(); + return; + } + + if (collisionReady && !buildOctree) { + notifyLoaded(); return; } @@ -150,10 +163,11 @@ export function GameMapCollision({ status: "loading", }); }, [ + buildOctree, collisionNodes.length, collisionReady, mapReady, - onLoaded, + notifyLoaded, onLoadingStateChange, ]); diff --git a/src/world/GameMusic.tsx b/src/world/GameMusic.tsx index a452c5d..31d2119 100644 --- a/src/world/GameMusic.tsx +++ b/src/world/GameMusic.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { AudioManager } from "@/managers/AudioManager"; const GAME_MUSIC_PATH = "/sounds/musique/test.mp3"; -const GAME_MUSIC_VOLUME = 0.45; +const GAME_MUSIC_VOLUME = 0.33; export function GameMusic(): null { useEffect(() => { diff --git a/src/world/World.tsx b/src/world/World.tsx index a76c744..4198367 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { Suspense, useEffect } from "react"; import { Physics } from "@react-three/rapier"; import { PLAYER_SPAWN_POSITION_GAME, @@ -8,6 +8,7 @@ import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; +import { useGameStore } from "@/managers/stores/useGameStore"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; @@ -26,23 +27,19 @@ interface WorldProps { onLoadingStateChange?: SceneLoadingChangeHandler | undefined; } -function hasBootFlag(name: string): boolean { - if (typeof window === "undefined") return false; - return new URLSearchParams(window.location.search).has(name); -} - export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); + const mainState = useGameStore((state) => state.mainState); const { status, usageStatus } = useHandTrackingSnapshot(); - const { octree, showGameStage, handleGameMapLoaded, handleOctreeReady } = - useWorldSceneLoading({ sceneMode, onLoadingStateChange }); - const noCinematics = hasBootFlag("noCinematics"); - const noDialogues = hasBootFlag("noDialogues"); - const noMap = hasBootFlag("noMap"); - const noMusic = hasBootFlag("noMusic"); - const noOctree = hasBootFlag("noOctree"); - const noPlayer = hasBootFlag("noPlayer"); + const { + octree, + gameplayReady, + showGameStage, + handleGameStageLoaded, + handleGameMapLoaded, + handleOctreeReady, + } = useWorldSceneLoading({ sceneMode, onLoadingStateChange }); const playerSpawnPosition = sceneMode === "game" ? PLAYER_SPAWN_POSITION_GAME @@ -50,6 +47,9 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { const showHandTrackingGloves = sceneMode === "physics" || (status !== "idle" && usageStatus !== "inactive"); + const spawnPlayer = + cameraMode !== "debug" && + (sceneMode === "game" ? gameplayReady : octree !== null); return ( <> @@ -65,30 +65,41 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( <> - {noMusic ? null : } - {noCinematics ? null : } - {noDialogues ? null : } - {noMap ? null : ( - - )} - {noMap || showGameStage ? ( + + {showGameStage ? ( + ) : null} + {spawnPlayer ? ( + <> + + {mainState === "outro" ? : null} + + + + ) : null} ) : ( )} - {cameraMode !== "debug" && !noPlayer ? ( + {sceneMode !== "game" && spawnPlayer ? ( ) : null} ); } + +function GameStageLoaded({ onLoaded }: { onLoaded: () => void }): null { + useEffect(() => { + onLoaded(); + }, [onLoaded]); + + return null; +} diff --git a/src/world/player/Player.tsx b/src/world/player/Player.tsx index d30ac2a..435dbfa 100644 --- a/src/world/player/Player.tsx +++ b/src/world/player/Player.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useLayoutEffect } from "react"; import { useThree } from "@react-three/fiber"; import type { Octree } from "three/addons/math/Octree.js"; import type { Vector3Tuple } from "@/types/three/three"; @@ -16,7 +16,7 @@ export function Player({ }: PlayerProps): React.JSX.Element { const camera = useThree((state) => state.camera); - useEffect(() => { + useLayoutEffect(() => { camera.position.set(...spawnPosition); }, [camera, spawnPosition]); diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index 9a9fe06..f06f6be 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useLayoutEffect, useRef } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { Capsule } from "three/addons/math/Capsule.js"; @@ -57,6 +57,18 @@ const _up = new THREE.Vector3(0, 1, 0); const _translateVec = new THREE.Vector3(); const _collisionCorrection = new THREE.Vector3(); +function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule { + return new Capsule( + new THREE.Vector3( + spawnPosition[0], + spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, + spawnPosition[2], + ), + new THREE.Vector3(...spawnPosition), + PLAYER_CAPSULE_RADIUS, + ); +} + function isPlayerInputLocked(): boolean { return ( useSettingsStore.getState().isSettingsMenuOpen || @@ -94,16 +106,10 @@ export function PlayerController({ const velocity = useRef(new THREE.Vector3()); const onFloor = useRef(false); const wantsJump = useRef(false); + const initializedRef = useRef(false); + const capsule = useRef(createSpawnCapsule(spawnPosition)); - const capsule = useRef( - new Capsule( - new THREE.Vector3(0, PLAYER_CAPSULE_RADIUS, 0), - new THREE.Vector3(0, PLAYER_EYE_HEIGHT - PLAYER_CAPSULE_RADIUS, 0), - PLAYER_CAPSULE_RADIUS, - ), - ); - - useEffect(() => { + useLayoutEffect(() => { capsule.current.start.set( spawnPosition[0], spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, @@ -114,6 +120,7 @@ export function PlayerController({ onFloor.current = false; wantsJump.current = false; camera.position.copy(capsule.current.end); + initializedRef.current = true; }, [camera, spawnPosition]); useEffect(() => { @@ -200,6 +207,8 @@ export function PlayerController({ }, []); useFrame((_, delta) => { + if (!initializedRef.current) return; + if (isPlayerInputLocked()) { keys.current = { ...DEFAULT_KEYS }; velocity.current.set(0, 0, 0);