This commit is contained in:
2026-04-27 11:14:43 +02:00
parent 393b653cca
commit eb0db21d29
7 changed files with 61 additions and 54 deletions
+18 -22
View File
@@ -14,11 +14,7 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { InteractionManager } from "@/stateManager/InteractionManager"; import { InteractionManager } from "@/stateManager/InteractionManager";
import { INTERACTION_RADIUS } from "@/data/interactionConfig"; import { INTERACTION_RADIUS } from "@/data/interactionConfig";
import type { Vector3Tuple } from "@/types/3d"; import type { Vector3Tuple } from "@/types/3d";
import type { import type { InteractableHandle, InteractableKind } from "@/types/interaction";
GrabInteractableHandle,
InteractableHandle,
TriggerInteractableHandle,
} from "@/types/interaction";
interface InteractableObjectBaseProps { interface InteractableObjectBaseProps {
label: string; label: string;
@@ -41,6 +37,13 @@ type InteractableObjectProps =
| TriggerInteractableObjectProps | TriggerInteractableObjectProps
| GrabInteractableObjectProps; | GrabInteractableObjectProps;
type MutableInteractableHandle = {
kind: InteractableKind;
label: string;
onPress: () => void;
onRelease?: () => void;
};
const _cameraPos = new THREE.Vector3(); const _cameraPos = new THREE.Vector3();
const _cameraDir = new THREE.Vector3(); const _cameraDir = new THREE.Vector3();
const _objectPos = new THREE.Vector3(); const _objectPos = new THREE.Vector3();
@@ -50,6 +53,7 @@ export function InteractableObject(
props: InteractableObjectProps, props: InteractableObjectProps,
): React.JSX.Element { ): React.JSX.Element {
const { kind, label, position, bodyRef, onPress, children } = props; const { kind, label, position, bodyRef, onPress, children } = props;
const onRelease = props.kind === "grab" ? props.onRelease : undefined;
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const debugSphereRef = useRef<THREE.Mesh>(null); const debugSphereRef = useRef<THREE.Mesh>(null);
@@ -61,27 +65,19 @@ export function InteractableObject(
); );
useEffect(() => { useEffect(() => {
if (props.kind === "grab") { const current = handle.current as MutableInteractableHandle;
const current = handle.current as GrabInteractableHandle; current.kind = kind;
current.label = label; current.label = label;
current.onPress = onPress; current.onPress = onPress;
current.onRelease = props.onRelease;
if (kind === "grab" && onRelease) {
current.onRelease = onRelease;
return; return;
} }
delete current.onRelease;
return undefined; return undefined;
}, [label, onPress, props]); }, [kind, label, onPress, onRelease]);
useEffect(() => {
if (kind === "grab") {
return undefined;
}
const current = handle.current as TriggerInteractableHandle;
current.label = label;
current.onPress = onPress;
return undefined;
}, [kind, label, onPress]);
const setupInteractionDebugFolder = useCallback((folder: GUI) => { const setupInteractionDebugFolder = useCallback((folder: GUI) => {
folder folder
+4 -4
View File
@@ -1,3 +1,5 @@
import type { Vector3Tuple } from "@/types/3d";
export const PLAYER_EYE_HEIGHT = 1.75; export const PLAYER_EYE_HEIGHT = 1.75;
export const PLAYER_CAPSULE_RADIUS = 0.35; export const PLAYER_CAPSULE_RADIUS = 0.35;
@@ -9,7 +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_X = 0; export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 100, 0];
export const PLAYER_SPAWN_Z = 0; export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
export const PLAYER_SPAWN_Y_GAME = 100;
export const PLAYER_SPAWN_Y_PHYSICS = 3;
@@ -4,11 +4,13 @@ import {
DEBUG_CAMERA_MAX_DISTANCE, DEBUG_CAMERA_MAX_DISTANCE,
DEBUG_CAMERA_MIN_DISTANCE, DEBUG_CAMERA_MIN_DISTANCE,
} from "@/data/debugConfig"; } from "@/data/debugConfig";
import { import { PLAYER_EYE_HEIGHT, PLAYER_SPAWN_POSITION_GAME } from "@/data/playerConfig";
const DEBUG_CAMERA_TARGET = [
PLAYER_SPAWN_POSITION_GAME[0],
PLAYER_EYE_HEIGHT, PLAYER_EYE_HEIGHT,
PLAYER_SPAWN_X, PLAYER_SPAWN_POSITION_GAME[2],
PLAYER_SPAWN_Z, ] as const;
} from "@/data/playerConfig";
export function DebugCameraControls(): React.JSX.Element { export function DebugCameraControls(): React.JSX.Element {
return ( return (
@@ -17,7 +19,7 @@ export function DebugCameraControls(): React.JSX.Element {
dampingFactor={DEBUG_CAMERA_DAMPING_FACTOR} dampingFactor={DEBUG_CAMERA_DAMPING_FACTOR}
minDistance={DEBUG_CAMERA_MIN_DISTANCE} minDistance={DEBUG_CAMERA_MIN_DISTANCE}
maxDistance={DEBUG_CAMERA_MAX_DISTANCE} maxDistance={DEBUG_CAMERA_MAX_DISTANCE}
target={[PLAYER_SPAWN_X, PLAYER_EYE_HEIGHT, PLAYER_SPAWN_Z]} target={DEBUG_CAMERA_TARGET}
/> />
); );
} }
+7 -5
View File
@@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import type { Octree } from "three/addons/math/Octree.js"; import type { Octree } from "three/addons/math/Octree.js";
import { import {
PLAYER_SPAWN_Y_GAME, PLAYER_SPAWN_POSITION_GAME,
PLAYER_SPAWN_Y_PHYSICS, PLAYER_SPAWN_POSITION_PHYSICS,
} from "@/data/playerConfig"; } from "@/data/playerConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
@@ -18,6 +18,10 @@ export function World(): React.JSX.Element {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const [octree, setOctree] = useState<Octree | null>(null); const [octree, setOctree] = useState<Octree | null>(null);
const playerSpawnPosition =
sceneMode === "game"
? PLAYER_SPAWN_POSITION_GAME
: PLAYER_SPAWN_POSITION_PHYSICS;
return ( return (
<> <>
@@ -35,9 +39,7 @@ export function World(): React.JSX.Element {
{cameraMode !== "debug" ? ( {cameraMode !== "debug" ? (
<PlayerComponent <PlayerComponent
octree={octree} octree={octree}
spawnY={ spawnPosition={playerSpawnPosition}
sceneMode === "game" ? PLAYER_SPAWN_Y_GAME : PLAYER_SPAWN_Y_PHYSICS
}
/> />
) : null} ) : null}
</> </>
+6 -6
View File
@@ -1,29 +1,29 @@
import { useEffect } from "react"; import { useEffect } 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 { PLAYER_SPAWN_X, PLAYER_SPAWN_Z } from "@/data/playerConfig"; import type { Vector3Tuple } from "@/types/3d";
import { PlayerCamera } from "@/world/player/PlayerCamera"; import { PlayerCamera } from "@/world/player/PlayerCamera";
import { PlayerController } from "@/world/player/PlayerController"; import { PlayerController } from "@/world/player/PlayerController";
interface PlayerComponentProps { interface PlayerComponentProps {
octree: Octree | null; octree: Octree | null;
spawnY: number; spawnPosition: Vector3Tuple;
} }
export function PlayerComponent({ export function PlayerComponent({
spawnY, spawnPosition,
octree, octree,
}: PlayerComponentProps): React.JSX.Element { }: PlayerComponentProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
useEffect(() => { useEffect(() => {
camera.position.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z); camera.position.set(...spawnPosition);
}, [camera, spawnY]); }, [camera, spawnPosition]);
return ( return (
<> <>
<PlayerCamera /> <PlayerCamera />
<PlayerController octree={octree} /> <PlayerController octree={octree} spawnPosition={spawnPosition} />
</> </>
); );
} }
+17 -10
View File
@@ -20,12 +20,11 @@ import {
PLAYER_GRAVITY, PLAYER_GRAVITY,
PLAYER_JUMP_SPEED, PLAYER_JUMP_SPEED,
PLAYER_MAX_DELTA, PLAYER_MAX_DELTA,
PLAYER_SPAWN_X,
PLAYER_SPAWN_Z,
PLAYER_WALK_SPEED, PLAYER_WALK_SPEED,
PLAYER_XZ_DAMPING_FACTOR, PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/playerConfig"; } from "@/data/playerConfig";
import { InteractionManager } from "@/stateManager/InteractionManager"; import { InteractionManager } from "@/stateManager/InteractionManager";
import type { Vector3Tuple } from "@/types/3d";
type Keys = { type Keys = {
forward: boolean; forward: boolean;
@@ -45,6 +44,7 @@ const DEFAULT_KEYS: Keys = {
interface PlayerControllerProps { interface PlayerControllerProps {
octree: Octree | null; octree: Octree | null;
spawnPosition: Vector3Tuple;
} }
const _forward = new THREE.Vector3(); const _forward = new THREE.Vector3();
@@ -52,8 +52,12 @@ const _right = new THREE.Vector3();
const _wishDir = new THREE.Vector3(); const _wishDir = new THREE.Vector3();
const _up = new THREE.Vector3(0, 1, 0); const _up = new THREE.Vector3(0, 1, 0);
const _translateVec = new THREE.Vector3(); const _translateVec = new THREE.Vector3();
const _collisionCorrection = new THREE.Vector3();
export function PlayerController({ octree }: PlayerControllerProps): null { export function PlayerController({
octree,
spawnPosition,
}: PlayerControllerProps): null {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const keys = useRef<Keys>({ ...DEFAULT_KEYS }); const keys = useRef<Keys>({ ...DEFAULT_KEYS });
const velocity = useRef(new THREE.Vector3()); const velocity = useRef(new THREE.Vector3());
@@ -69,14 +73,17 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
); );
useEffect(() => { useEffect(() => {
const spawnY = camera.position.y;
capsule.current.start.set( capsule.current.start.set(
PLAYER_SPAWN_X, spawnPosition[0],
spawnY - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
PLAYER_SPAWN_Z, spawnPosition[2],
); );
capsule.current.end.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z); capsule.current.end.set(...spawnPosition);
}, [camera]); velocity.current.set(0, 0, 0);
onFloor.current = false;
wantsJump.current = false;
camera.position.copy(capsule.current.end);
}, [camera, spawnPosition]);
useEffect(() => { useEffect(() => {
const interaction = InteractionManager.getInstance(); const interaction = InteractionManager.getInstance();
@@ -215,7 +222,7 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
} }
capsule.current.translate( capsule.current.translate(
result.normal.clone().multiplyScalar(result.depth), _collisionCorrection.copy(result.normal).multiplyScalar(result.depth),
); );
} }
} }
+2 -2
View File
@@ -1,12 +1,12 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "node:path"; import { fileURLToPath } from "node:url";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": fileURLToPath(new URL("./src", import.meta.url)),
}, },
}, },
}); });