fix: stabilize game scene loading and player spawn

This commit is contained in:
Tom Boullay
2026-05-11 23:52:57 +02:00
parent e05c67ee73
commit ffca1e9e5f
22 changed files with 202 additions and 110 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -31,7 +31,8 @@
"id": "narrateur_bienvenueaaltera", "id": "narrateur_bienvenueaaltera",
"voice": "narrateur", "voice": "narrateur",
"audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3", "audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3",
"subtitleCueIndex": 1 "subtitleCueIndex": 1,
"timecode": 0
}, },
{ {
"id": "narrateur_intro_prenom", "id": "narrateur_intro_prenom",
+1 -1
View File
@@ -11,5 +11,5 @@ export const PLAYER_MAX_DELTA = 0.05;
export const PLAYER_ACCELERATION_MULTIPLIER = 9; export const PLAYER_ACCELERATION_MULTIPLIER = 9;
export const PLAYER_XZ_DAMPING_FACTOR = 8; 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]; export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
+16 -1
View File
@@ -10,7 +10,9 @@ interface UseWorldSceneLoadingOptions {
interface UseWorldSceneLoadingResult { interface UseWorldSceneLoadingResult {
octree: Octree | null; octree: Octree | null;
gameplayReady: boolean;
showGameStage: boolean; showGameStage: boolean;
handleGameStageLoaded: () => void;
handleGameMapLoaded: () => void; handleGameMapLoaded: () => void;
handleOctreeReady: (octree: Octree) => void; handleOctreeReady: (octree: Octree) => void;
} }
@@ -21,15 +23,26 @@ export function useWorldSceneLoading({
}: UseWorldSceneLoadingOptions): UseWorldSceneLoadingResult { }: UseWorldSceneLoadingOptions): UseWorldSceneLoadingResult {
const [octree, setOctree] = useState<Octree | null>(null); const [octree, setOctree] = useState<Octree | null>(null);
const [gameMapLoaded, setGameMapLoaded] = useState(false); const [gameMapLoaded, setGameMapLoaded] = useState(false);
const [gameStageLoaded, setGameStageLoaded] = useState(false);
const showGameStage = sceneMode === "game" && gameMapLoaded; const showGameStage = sceneMode === "game" && gameMapLoaded;
const gameplayReady = showGameStage && gameStageLoaded && octree !== null;
const sceneReady = const sceneReady =
(sceneMode === "game" && gameMapLoaded) || (sceneMode === "game" && gameplayReady) ||
(sceneMode === "physics" && octree !== null); (sceneMode === "physics" && octree !== null);
const handleGameMapLoaded = useCallback(() => { const handleGameMapLoaded = useCallback(() => {
setGameMapLoaded(true); setGameMapLoaded(true);
}, []); }, []);
const handleGameStageLoaded = useCallback(() => {
setGameStageLoaded(true);
onLoadingStateChange?.({
currentStep: "Initialisation gameplay",
progress: 0.96,
status: "loading",
});
}, [onLoadingStateChange]);
const handleOctreeReady = useCallback( const handleOctreeReady = useCallback(
(nextOctree: Octree) => { (nextOctree: Octree) => {
setOctree(nextOctree); setOctree(nextOctree);
@@ -74,7 +87,9 @@ export function useWorldSceneLoading({
return { return {
octree, octree,
gameplayReady,
showGameStage, showGameStage,
handleGameStageLoaded,
handleGameMapLoaded, handleGameMapLoaded,
handleOctreeReady, handleOctreeReady,
}; };
+4 -4
View File
@@ -18,13 +18,13 @@ export function HomePage(): React.JSX.Element {
const handleSceneLoadingStateChange = useCallback( const handleSceneLoadingStateChange = useCallback(
(nextState: SceneLoadingState) => { (nextState: SceneLoadingState) => {
setSceneLoadingState((currentState) => { setSceneLoadingState((currentState) => {
const shouldRestartProgress = currentState.status === "ready"; if (currentState.status === "ready" && nextState.status === "loading") {
return currentState;
}
return { return {
...nextState, ...nextState,
progress: shouldRestartProgress progress: Math.max(currentState.progress, nextState.progress),
? nextState.progress
: Math.max(currentState.progress, nextState.progress),
}; };
}); });
}, },
+4 -1
View File
@@ -22,6 +22,7 @@ export function GameCinematics(): null {
const playedCinematicsRef = useRef(new Set<string>()); const playedCinematicsRef = useRef(new Set<string>());
const timelineRef = useRef<gsap.core.Timeline | null>(null); const timelineRef = useRef<gsap.core.Timeline | null>(null);
const activeAudiosRef = useRef(new Set<HTMLAudioElement>()); const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
const startedAtRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@@ -59,7 +60,9 @@ export function GameCinematics(): null {
useFrame(({ clock }) => { useFrame(({ clock }) => {
if (!manifest) return; if (!manifest) return;
const elapsedTime = clock.getElapsedTime(); startedAtRef.current ??= clock.getElapsedTime();
const elapsedTime = clock.getElapsedTime() - startedAtRef.current;
manifest.cinematics.forEach((cinematic) => { manifest.cinematics.forEach((cinematic) => {
if (cinematic.timecode === undefined) return; if (cinematic.timecode === undefined) return;
+4 -1
View File
@@ -12,6 +12,7 @@ export function GameDialogues(): null {
const [manifest, setManifest] = useState<DialogueManifest | null>(null); const [manifest, setManifest] = useState<DialogueManifest | null>(null);
const playedDialoguesRef = useRef(new Set<string>()); const playedDialoguesRef = useRef(new Set<string>());
const activeAudiosRef = useRef(new Set<HTMLAudioElement>()); const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
const startedAtRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@@ -38,7 +39,9 @@ export function GameDialogues(): null {
useFrame(({ clock }) => { useFrame(({ clock }) => {
if (!manifest) return; if (!manifest) return;
const elapsedTime = clock.getElapsedTime(); startedAtRef.current ??= clock.getElapsedTime();
const elapsedTime = clock.getElapsedTime() - startedAtRef.current;
manifest.dialogues.forEach((dialogue) => { manifest.dialogues.forEach((dialogue) => {
if (dialogue.timecode === undefined) return; if (dialogue.timecode === undefined) return;
+80 -44
View File
@@ -1,5 +1,12 @@
import type { ReactNode } from "react"; 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 { useClonedObject } from "@/hooks/three/useClonedObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { GameMapCollision } from "@/world/GameMapCollision"; import { GameMapCollision } from "@/world/GameMapCollision";
@@ -20,6 +27,7 @@ interface ErrorBoundaryProps {
fallback: ReactNode; fallback: ReactNode;
modelUrl: string | null; modelUrl: string | null;
node: MapNode; node: MapNode;
onSettled: () => void;
} }
interface ErrorBoundaryState { interface ErrorBoundaryState {
@@ -50,6 +58,7 @@ class ModelErrorBoundary extends Component<
}, },
error, error,
); );
this.props.onSettled();
} }
render(): ReactNode { render(): ReactNode {
@@ -68,19 +77,39 @@ interface GameMapProps {
buildOctree?: boolean; buildOctree?: boolean;
} }
const MAP_RENDER_BATCH_SIZE = 12;
export function GameMap({ export function GameMap({
buildOctree = true, buildOctree = true,
onLoaded, onLoaded,
onLoadingStateChange, onLoadingStateChange,
onOctreeReady, onOctreeReady,
}: GameMapProps): React.JSX.Element { }: GameMapProps): React.JSX.Element {
const settledMapNodesRef = useRef(new Set<number>());
const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]); const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]);
const [mapLoaded, setMapLoaded] = useState(false); const [mapLoaded, setMapLoaded] = useState(false);
const [visibleNodeCount, setVisibleNodeCount] = useState(0); const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
const visibleMapNodes = mapNodes.slice(0, visibleNodeCount); const mapReady = mapLoaded && settledMapNodeCount >= mapNodes.length;
const mapReady = mapLoaded && visibleNodeCount >= 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(() => { useEffect(() => {
onLoadingStateChange?.({ onLoadingStateChange?.({
@@ -94,11 +123,7 @@ export function GameMap({
const sceneData = await loadMapSceneData(); const sceneData = await loadMapSceneData();
if (!sceneData) { if (!sceneData) {
logger.warn("GameMap", "map.json not found"); logger.warn("GameMap", "map.json not found");
onLoadingStateChange?.({ showEmptyMap("Map introuvable");
currentStep: "Map introuvable",
progress: 1,
status: "loading",
});
return; return;
} }
@@ -128,9 +153,10 @@ export function GameMap({
setMapNodes(loadedMapNodes); setMapNodes(loadedMapNodes);
setMapLoaded(true); setMapLoaded(true);
setVisibleNodeCount(0); settledMapNodesRef.current.clear();
setSettledMapNodeCount(0);
onLoadingStateChange?.({ onLoadingStateChange?.({
currentStep: "Montage progressif des models", currentStep: "Chargement des modèles de la map",
progress: 0.25, progress: 0.25,
status: "loading", status: "loading",
}); });
@@ -138,63 +164,41 @@ export function GameMap({
logger.error("GameMap", "Error loading map", { logger.error("GameMap", "Error loading map", {
error: error instanceof Error ? error : new Error(String(error)), error: error instanceof Error ? error : new Error(String(error)),
}); });
onLoadingStateChange?.({ showEmptyMap("Erreur de chargement de la map");
currentStep: "Erreur de chargement de la map",
progress: 1,
status: "loading",
});
} }
}; };
loadMap(); loadMap();
}, [onLoaded, onLoadingStateChange]); }, [onLoadingStateChange, showEmptyMap]);
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]);
useEffect(() => { useEffect(() => {
if (mapNodes.length === 0) return; if (mapNodes.length === 0) return;
const renderProgress = const renderProgress =
mapNodes.length === 0 ? 1 : visibleNodeCount / mapNodes.length; mapNodes.length === 0 ? 1 : settledMapNodeCount / mapNodes.length;
onLoadingStateChange?.({ onLoadingStateChange?.({
currentStep: "Montage progressif des models", currentStep: "Chargement des modèles de la map",
progress: 0.25 + renderProgress * 0.45, progress: 0.25 + renderProgress * 0.45,
status: "loading", status: "loading",
}); });
}, [mapNodes.length, onLoadingStateChange, visibleNodeCount]); }, [mapNodes.length, onLoadingStateChange, settledMapNodeCount]);
return ( return (
<> <>
<group> <group>
{visibleMapNodes.map((mapNode, index) => ( {mapNodes.map((mapNode, index) => (
<ModelErrorBoundary <ModelErrorBoundary
key={index} key={index}
fallback={<FallbackMapNode node={mapNode.node} />} fallback={<FallbackMapNode node={mapNode.node} />}
modelUrl={mapNode.modelUrl} modelUrl={mapNode.modelUrl}
node={mapNode.node} node={mapNode.node}
onSettled={() => handleMapNodeSettled(index)}
> >
{mapNode.modelUrl ? ( <MapNodeInstance
<Suspense fallback={<FallbackMapNode node={mapNode.node} />}>
<ModelInstance
node={mapNode.node} node={mapNode.node}
modelUrl={mapNode.modelUrl} modelUrl={mapNode.modelUrl}
onSettled={() => handleMapNodeSettled(index)}
/> />
</Suspense>
) : (
<FallbackMapNode node={mapNode.node} />
)}
</ModelErrorBoundary> </ModelErrorBoundary>
))} ))}
</group> </group>
@@ -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 <FallbackMapNode node={node} />;
}
return (
<Suspense fallback={<FallbackMapNode node={node} />}>
<ModelInstance node={node} modelUrl={modelUrl} onLoaded={onSettled} />
</Suspense>
);
}
function ModelInstance({ function ModelInstance({
node, node,
modelUrl, modelUrl,
onLoaded,
}: { }: {
node: MapNode; node: MapNode;
modelUrl: string; modelUrl: string;
onLoaded: () => void;
}): React.JSX.Element { }): React.JSX.Element {
const { position, rotation, scale } = node; const { position, rotation, scale } = node;
const { scene } = useLoggedGLTF(modelUrl, { const { scene } = useLoggedGLTF(modelUrl, {
@@ -226,6 +258,10 @@ function ModelInstance({
}); });
const sceneInstance = useClonedObject(scene); const sceneInstance = useClonedObject(scene);
useEffect(() => {
onLoaded();
}, [onLoaded]);
return ( return (
<primitive <primitive
object={sceneInstance} object={sceneInstance}
+18 -4
View File
@@ -102,11 +102,19 @@ export function GameMapCollision({
}: GameMapCollisionProps): React.JSX.Element { }: GameMapCollisionProps): React.JSX.Element {
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const settledCollisionNodesRef = useRef(new Set<number>()); const settledCollisionNodesRef = useRef(new Set<number>());
const loadedNotifiedRef = useRef(false);
const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0); const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0);
const collisionNodes = nodes.filter(isCollisionNode); const collisionNodes = nodes.filter(isCollisionNode);
const collisionReady = const collisionReady =
mapReady && settledCollisionNodeCount >= collisionNodes.length; mapReady && settledCollisionNodeCount >= collisionNodes.length;
const notifyLoaded = useCallback(() => {
if (loadedNotifiedRef.current) return;
loadedNotifiedRef.current = true;
onLoaded?.();
}, [onLoaded]);
const handleCollisionNodeSettled = useCallback((index: number) => { const handleCollisionNodeSettled = useCallback((index: number) => {
if (settledCollisionNodesRef.current.has(index)) return; if (settledCollisionNodesRef.current.has(index)) return;
@@ -122,9 +130,9 @@ export function GameMapCollision({
status: "loading", status: "loading",
}); });
onOctreeReady(octree); onOctreeReady(octree);
onLoaded?.(); notifyLoaded();
}, },
[onLoaded, onLoadingStateChange, onOctreeReady], [notifyLoaded, onLoadingStateChange, onOctreeReady],
); );
useOctreeGraphNode( useOctreeGraphNode(
@@ -138,7 +146,12 @@ export function GameMapCollision({
if (!mapReady) return; if (!mapReady) return;
if (collisionNodes.length === 0) { if (collisionNodes.length === 0) {
onLoaded?.(); notifyLoaded();
return;
}
if (collisionReady && !buildOctree) {
notifyLoaded();
return; return;
} }
@@ -150,10 +163,11 @@ export function GameMapCollision({
status: "loading", status: "loading",
}); });
}, [ }, [
buildOctree,
collisionNodes.length, collisionNodes.length,
collisionReady, collisionReady,
mapReady, mapReady,
onLoaded, notifyLoaded,
onLoadingStateChange, onLoadingStateChange,
]); ]);
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect } from "react";
import { AudioManager } from "@/managers/AudioManager"; import { AudioManager } from "@/managers/AudioManager";
const GAME_MUSIC_PATH = "/sounds/musique/test.mp3"; const GAME_MUSIC_PATH = "/sounds/musique/test.mp3";
const GAME_MUSIC_VOLUME = 0.45; const GAME_MUSIC_VOLUME = 0.33;
export function GameMusic(): null { export function GameMusic(): null {
useEffect(() => { useEffect(() => {
+33 -22
View File
@@ -1,4 +1,4 @@
import { Suspense } from "react"; import { Suspense, useEffect } from "react";
import { Physics } from "@react-three/rapier"; import { Physics } from "@react-three/rapier";
import { import {
PLAYER_SPAWN_POSITION_GAME, PLAYER_SPAWN_POSITION_GAME,
@@ -8,6 +8,7 @@ import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
import { useGameStore } from "@/managers/stores/useGameStore";
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove";
@@ -26,23 +27,19 @@ interface WorldProps {
onLoadingStateChange?: SceneLoadingChangeHandler | undefined; 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 { export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const mainState = useGameStore((state) => state.mainState);
const { status, usageStatus } = useHandTrackingSnapshot(); const { status, usageStatus } = useHandTrackingSnapshot();
const { octree, showGameStage, handleGameMapLoaded, handleOctreeReady } = const {
useWorldSceneLoading({ sceneMode, onLoadingStateChange }); octree,
const noCinematics = hasBootFlag("noCinematics"); gameplayReady,
const noDialogues = hasBootFlag("noDialogues"); showGameStage,
const noMap = hasBootFlag("noMap"); handleGameStageLoaded,
const noMusic = hasBootFlag("noMusic"); handleGameMapLoaded,
const noOctree = hasBootFlag("noOctree"); handleOctreeReady,
const noPlayer = hasBootFlag("noPlayer"); } = useWorldSceneLoading({ sceneMode, onLoadingStateChange });
const playerSpawnPosition = const playerSpawnPosition =
sceneMode === "game" sceneMode === "game"
? PLAYER_SPAWN_POSITION_GAME ? PLAYER_SPAWN_POSITION_GAME
@@ -50,6 +47,9 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
const showHandTrackingGloves = const showHandTrackingGloves =
sceneMode === "physics" || sceneMode === "physics" ||
(status !== "idle" && usageStatus !== "inactive"); (status !== "idle" && usageStatus !== "inactive");
const spawnPlayer =
cameraMode !== "debug" &&
(sceneMode === "game" ? gameplayReady : octree !== null);
return ( return (
<> <>
@@ -65,30 +65,41 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
{cameraMode === "debug" ? <DebugCameraControls /> : null} {cameraMode === "debug" ? <DebugCameraControls /> : null}
{sceneMode === "game" ? ( {sceneMode === "game" ? (
<> <>
{noMusic ? null : <GameMusic />}
{noCinematics ? null : <GameCinematics />}
{noDialogues ? null : <GameDialogues />}
{noMap ? null : (
<GameMap <GameMap
buildOctree={!noOctree}
onLoaded={handleGameMapLoaded} onLoaded={handleGameMapLoaded}
onLoadingStateChange={onLoadingStateChange} onLoadingStateChange={onLoadingStateChange}
onOctreeReady={handleOctreeReady} onOctreeReady={handleOctreeReady}
/> />
)} {showGameStage ? (
{noMap || showGameStage ? (
<Physics> <Physics>
<GameStageLoaded onLoaded={handleGameStageLoaded} />
<GameStageContent /> <GameStageContent />
</Physics> </Physics>
) : null} ) : null}
{spawnPlayer ? (
<>
<GameMusic />
{mainState === "outro" ? <GameCinematics /> : null}
<GameDialogues />
<Player octree={octree} spawnPosition={playerSpawnPosition} />
</>
) : null}
</> </>
) : ( ) : (
<TestMap onOctreeReady={handleOctreeReady} /> <TestMap onOctreeReady={handleOctreeReady} />
)} )}
{cameraMode !== "debug" && !noPlayer ? ( {sceneMode !== "game" && spawnPlayer ? (
<Player octree={octree} spawnPosition={playerSpawnPosition} /> <Player octree={octree} spawnPosition={playerSpawnPosition} />
) : null} ) : null}
</> </>
); );
} }
function GameStageLoaded({ onLoaded }: { onLoaded: () => void }): null {
useEffect(() => {
onLoaded();
}, [onLoaded]);
return null;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useLayoutEffect } from "react";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import type { Octree } from "three/addons/math/Octree.js"; import type { Octree } from "three/addons/math/Octree.js";
import type { Vector3Tuple } from "@/types/three/three"; import type { Vector3Tuple } from "@/types/three/three";
@@ -16,7 +16,7 @@ export function Player({
}: PlayerProps): React.JSX.Element { }: PlayerProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
useEffect(() => { useLayoutEffect(() => {
camera.position.set(...spawnPosition); camera.position.set(...spawnPosition);
}, [camera, spawnPosition]); }, [camera, spawnPosition]);
+19 -10
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useLayoutEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three"; import * as THREE from "three";
import { Capsule } from "three/addons/math/Capsule.js"; 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 _translateVec = new THREE.Vector3();
const _collisionCorrection = 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 { function isPlayerInputLocked(): boolean {
return ( return (
useSettingsStore.getState().isSettingsMenuOpen || useSettingsStore.getState().isSettingsMenuOpen ||
@@ -94,16 +106,10 @@ export function PlayerController({
const velocity = useRef(new THREE.Vector3()); const velocity = useRef(new THREE.Vector3());
const onFloor = useRef(false); const onFloor = useRef(false);
const wantsJump = useRef(false); const wantsJump = useRef(false);
const initializedRef = useRef(false);
const capsule = useRef(createSpawnCapsule(spawnPosition));
const capsule = useRef( useLayoutEffect(() => {
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(() => {
capsule.current.start.set( capsule.current.start.set(
spawnPosition[0], spawnPosition[0],
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
@@ -114,6 +120,7 @@ export function PlayerController({
onFloor.current = false; onFloor.current = false;
wantsJump.current = false; wantsJump.current = false;
camera.position.copy(capsule.current.end); camera.position.copy(capsule.current.end);
initializedRef.current = true;
}, [camera, spawnPosition]); }, [camera, spawnPosition]);
useEffect(() => { useEffect(() => {
@@ -200,6 +207,8 @@ export function PlayerController({
}, []); }, []);
useFrame((_, delta) => { useFrame((_, delta) => {
if (!initializedRef.current) return;
if (isPlayerInputLocked()) { if (isPlayerInputLocked()) {
keys.current = { ...DEFAULT_KEYS }; keys.current = { ...DEFAULT_KEYS };
velocity.current.set(0, 0, 0); velocity.current.set(0, 0, 0);