update: add a physic scenne

This commit is contained in:
Tom Boullay
2026-04-17 10:48:18 +02:00
parent 1d4f223c35
commit 5111f2e558
22 changed files with 2050 additions and 216 deletions
+4 -17
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react";
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { AmbientLight, DirectionalLight } from "three";
import { Debug } from "@/utils/debug/Debug";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
type LightingState = {
ambientIntensity: number;
@@ -23,26 +23,13 @@ export function Lighting(): React.JSX.Element {
const ambient = useRef<AmbientLight>(null);
const sun = useRef<DirectionalLight>(null);
useEffect(() => {
const debug = Debug.getInstance();
if (!debug.active) {
return;
}
const folder = debug.createFolder("Lighting");
// null = already registered (StrictMode double-mount), skip adding controls
if (!folder) {
return;
}
useDebugFolder("Lighting", (folder) => {
folder.add(LIGHTING_STATE, "ambientIntensity", 0, 5, 0.1).name("Ambient");
folder.add(LIGHTING_STATE, "sunIntensity", 0, 8, 0.1).name("Sun Intensity");
folder.add(LIGHTING_STATE, "sunX", -100, 100, 1).name("Sun X");
folder.add(LIGHTING_STATE, "sunY", 0, 150, 1).name("Sun Y");
folder.add(LIGHTING_STATE, "sunZ", -100, 100, 1).name("Sun Z");
}, []);
});
useFrame(() => {
if (ambient.current) {
+11 -1
View File
@@ -1,8 +1,11 @@
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers";
import { Environment } from "@/world/Environment";
import { Lighting } from "@/world/Lighting";
import { GrabCube } from "@/world/objects/GrabCube";
import { TriggerSphere } from "@/world/objects/TriggerSphere";
import { PlayerComponent } from "@/world/player/PlayerComponent";
export function World(): React.JSX.Element {
@@ -14,7 +17,14 @@ export function World(): React.JSX.Element {
<Lighting />
<DebugHelpers />
{cameraMode === "debug" ? <DebugCameraControls /> : null}
{cameraMode === "debug" ? null : <PlayerComponent />}
<Physics>
<RigidBody type="fixed">
<CuboidCollider args={[50, 0.1, 50]} position={[0, -0.1, 0]} />
</RigidBody>
<GrabCube />
<TriggerSphere />
{cameraMode === "debug" ? null : <PlayerComponent />}
</Physics>
</>
);
}
+80
View File
@@ -0,0 +1,80 @@
import { useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import type { RapierRigidBody } from "@react-three/rapier";
import * as THREE from "three";
import { InteractableObject } from "@/components/3d/InteractableObject";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
const CUBE_SIZE = 0.5;
const HOLD_DISTANCE = 2;
const SPAWN_POSITION: [number, number, number] = [0, 1, -3];
const params = { stiffness: 15, throwBoost: 1.0 };
const _holdTarget = new THREE.Vector3();
const _currentPos = new THREE.Vector3();
const _velocity = new THREE.Vector3();
export function GrabCube(): React.JSX.Element {
const camera = useThree((state) => state.camera);
const rbRef = useRef<RapierRigidBody>(null);
const isHolding = useRef(false);
useDebugFolder("GrabCube", (folder) => {
folder.add(params, "stiffness", 1, 50, 1).name("Hold stiffness");
folder.add(params, "throwBoost", 0.5, 3.0, 0.1).name("Throw boost");
});
useFrame(() => {
if (!isHolding.current || !rbRef.current) return;
camera.getWorldDirection(_holdTarget);
_holdTarget.multiplyScalar(HOLD_DISTANCE).add(camera.position);
const t = rbRef.current.translation();
_currentPos.set(t.x, t.y, t.z);
_velocity
.subVectors(_holdTarget, _currentPos)
.multiplyScalar(params.stiffness);
rbRef.current.setLinvel(
{ x: _velocity.x, y: _velocity.y, z: _velocity.z },
true,
);
rbRef.current.setAngvel({ x: 0, y: 0, z: 0 }, true);
});
return (
<InteractableObject
kind="grab"
label="Prendre"
position={SPAWN_POSITION}
rigidBodyType="dynamic"
colliders="cuboid"
rbRef={rbRef}
onPress={() => {
isHolding.current = true;
}}
onRelease={() => {
isHolding.current = false;
if (rbRef.current && params.throwBoost !== 1.0) {
const v = rbRef.current.linvel();
rbRef.current.setLinvel(
{
x: v.x * params.throwBoost,
y: v.y * params.throwBoost,
z: v.z * params.throwBoost,
},
true,
);
}
}}
>
<mesh castShadow receiveShadow>
<boxGeometry args={[CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]} />
<meshStandardMaterial color="#e07b39" roughness={0.6} metalness={0.1} />
</mesh>
</InteractableObject>
);
}
+32
View File
@@ -0,0 +1,32 @@
import { AudioManager } from "@/stateManager/AudioManager";
import { InteractableObject } from "@/components/3d/InteractableObject";
const SPHERE_RADIUS = 0.4;
const SPAWN_POSITION: [number, number, number] = [3, 2, -3];
const SOUND_PATH = "/sounds/fa.mp3";
interface TriggerSphereProps {
soundPath?: string;
}
export function TriggerSphere({
soundPath = SOUND_PATH,
}: TriggerSphereProps): React.JSX.Element {
return (
<InteractableObject
kind="trigger"
label="Interagir"
position={SPAWN_POSITION}
rigidBodyType="fixed"
colliders="ball"
onPress={() => {
AudioManager.getInstance().playSound(soundPath);
}}
>
<mesh castShadow receiveShadow>
<sphereGeometry args={[SPHERE_RADIUS, 32, 32]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.5} />
</mesh>
</InteractableObject>
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ export const PLAYER_EYE_HEIGHT = 1.75;
export function PlayerCamera(): React.JSX.Element {
useEffect(() => {
return () => {
document.exitPointerLock?.();
document.exitPointerLock();
};
}, []);
+1 -3
View File
@@ -3,13 +3,11 @@ import { useThree } from "@react-three/fiber";
import { PlayerCamera, PLAYER_EYE_HEIGHT } from "@/world/player/PlayerCamera";
import { PlayerController } from "@/world/player/PlayerController";
const SPAWN_POSITION = { x: 0, y: PLAYER_EYE_HEIGHT, z: 0 };
export function PlayerComponent(): React.JSX.Element {
const camera = useThree((state) => state.camera);
useEffect(() => {
camera.position.set(SPAWN_POSITION.x, SPAWN_POSITION.y, SPAWN_POSITION.z);
camera.position.set(0, PLAYER_EYE_HEIGHT, 0);
}, [camera]);
return (
+75 -26
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { InteractionManager } from "@/stateManager/InteractionManager";
import { PLAYER_EYE_HEIGHT } from "@/world/player/PlayerCamera";
const MOVE_SPEED = 5;
@@ -35,41 +36,89 @@ export function PlayerController(): null {
const up = useRef(new THREE.Vector3(0, 1, 0));
useEffect(() => {
const handleKeyChange =
(pressed: boolean) =>
(event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) {
case "z":
keys.current.forward = pressed;
break;
case "s":
keys.current.backward = pressed;
break;
case "q":
keys.current.left = pressed;
break;
case "d":
keys.current.right = pressed;
break;
case " ":
if (pressed) keys.current.jump = true;
break;
default:
return;
}
const interaction = InteractionManager.getInstance();
event.preventDefault();
};
const handleKeyDown = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) {
case "z":
keys.current.forward = true;
break;
case "s":
keys.current.backward = true;
break;
case "q":
keys.current.left = true;
break;
case "d":
keys.current.right = true;
break;
case " ":
keys.current.jump = true;
break;
case "e":
if (interaction.getState().focused?.kind === "trigger") {
interaction.pressInteract();
}
break;
default:
return;
}
const handleKeyDown = handleKeyChange(true);
const handleKeyUp = handleKeyChange(false);
event.preventDefault();
};
const handleKeyUp = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) {
case "z":
keys.current.forward = false;
break;
case "s":
keys.current.backward = false;
break;
case "q":
keys.current.left = false;
break;
case "d":
keys.current.right = false;
break;
case "e":
if (interaction.getState().focused?.kind === "trigger") {
interaction.releaseInteract();
}
break;
default:
return;
}
event.preventDefault();
};
const handleMouseDown = (event: MouseEvent): void => {
if (event.button !== 0) return;
if (interaction.getState().focused?.kind === "grab") {
interaction.pressInteract();
}
};
const handleMouseUp = (event: MouseEvent): void => {
if (event.button !== 0) return;
if (interaction.getState().holding) {
interaction.releaseInteract();
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
document.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("mouseup", handleMouseUp);
keys.current = { ...DEFAULT_KEYS };
};
}, []);