fix: stabilize game scene loading and player spawn
This commit is contained in:
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.
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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<Octree | null>(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,
|
||||
};
|
||||
|
||||
+4
-4
@@ -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),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ export function GameCinematics(): null {
|
||||
const playedCinematicsRef = useRef(new Set<string>());
|
||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||
const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
|
||||
const startedAtRef = useRef<number | null>(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;
|
||||
|
||||
@@ -12,6 +12,7 @@ export function GameDialogues(): null {
|
||||
const [manifest, setManifest] = useState<DialogueManifest | null>(null);
|
||||
const playedDialoguesRef = useRef(new Set<string>());
|
||||
const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
|
||||
const startedAtRef = useRef<number | null>(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;
|
||||
|
||||
+80
-44
@@ -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<number>());
|
||||
const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<group>
|
||||
{visibleMapNodes.map((mapNode, index) => (
|
||||
{mapNodes.map((mapNode, index) => (
|
||||
<ModelErrorBoundary
|
||||
key={index}
|
||||
fallback={<FallbackMapNode node={mapNode.node} />}
|
||||
modelUrl={mapNode.modelUrl}
|
||||
node={mapNode.node}
|
||||
onSettled={() => handleMapNodeSettled(index)}
|
||||
>
|
||||
{mapNode.modelUrl ? (
|
||||
<Suspense fallback={<FallbackMapNode node={mapNode.node} />}>
|
||||
<ModelInstance
|
||||
<MapNodeInstance
|
||||
node={mapNode.node}
|
||||
modelUrl={mapNode.modelUrl}
|
||||
onSettled={() => handleMapNodeSettled(index)}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<FallbackMapNode node={mapNode.node} />
|
||||
)}
|
||||
</ModelErrorBoundary>
|
||||
))}
|
||||
</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({
|
||||
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 (
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
|
||||
@@ -102,11 +102,19 @@ export function GameMapCollision({
|
||||
}: GameMapCollisionProps): React.JSX.Element {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const settledCollisionNodesRef = useRef(new Set<number>());
|
||||
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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+33
-22
@@ -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" ? <DebugCameraControls /> : null}
|
||||
{sceneMode === "game" ? (
|
||||
<>
|
||||
{noMusic ? null : <GameMusic />}
|
||||
{noCinematics ? null : <GameCinematics />}
|
||||
{noDialogues ? null : <GameDialogues />}
|
||||
{noMap ? null : (
|
||||
<GameMap
|
||||
buildOctree={!noOctree}
|
||||
onLoaded={handleGameMapLoaded}
|
||||
onLoadingStateChange={onLoadingStateChange}
|
||||
onOctreeReady={handleOctreeReady}
|
||||
/>
|
||||
)}
|
||||
{noMap || showGameStage ? (
|
||||
{showGameStage ? (
|
||||
<Physics>
|
||||
<GameStageLoaded onLoaded={handleGameStageLoaded} />
|
||||
<GameStageContent />
|
||||
</Physics>
|
||||
) : null}
|
||||
{spawnPlayer ? (
|
||||
<>
|
||||
<GameMusic />
|
||||
{mainState === "outro" ? <GameCinematics /> : null}
|
||||
<GameDialogues />
|
||||
<Player octree={octree} spawnPosition={playerSpawnPosition} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<TestMap onOctreeReady={handleOctreeReady} />
|
||||
)}
|
||||
|
||||
{cameraMode !== "debug" && !noPlayer ? (
|
||||
{sceneMode !== "game" && spawnPlayer ? (
|
||||
<Player octree={octree} spawnPosition={playerSpawnPosition} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GameStageLoaded({ onLoaded }: { onLoaded: () => void }): null {
|
||||
useEffect(() => {
|
||||
onLoaded();
|
||||
}, [onLoaded]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user