Compare commits

..

No commits in common. "d7b484e1be4c71d10af82f07b6f5578fe28a3d40" and "63bd5e371536c08e7a7d1ab5df534c149b90dbf1" have entirely different histories.

5 changed files with 103 additions and 164 deletions

View File

@ -2,128 +2,30 @@ import { observer } from "mobx-react-lite";
import type { Character } from "../types"; import type { Character } from "../types";
import { SyncRigidBody } from "./SyncRigidBody"; import { SyncRigidBody } from "./SyncRigidBody";
import { state } from "../state"; import { state } from "../state";
import { useEffect, useRef } from "react";
import { Euler, Quaternion, Vector3 } from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { useKeyboardControls } from "@react-three/drei";
import type { RapierRigidBody } from "@react-three/rapier";
const SPEED = 5; export const CharacterView = observer(function ({ character }: { character: Character }) {
const JUMP_SPEED = 8;
const SENSITIVITY = 0.002;
// Shoulder offset in character-local space: right, up, behind
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
const _q = new Quaternion();
const _e = new Euler(0, 0, 0, 'YXZ');
const _offset = new Vector3();
const _charPos = new Vector3();
const _lookAt = new Vector3();
type CharacterViewProps = {
character: Character;
editMode?: boolean;
}
export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
const pos = character.transform.position; const pos = character.transform.position;
const rbRef = useRef<RapierRigidBody>(null);
const [, get] = useKeyboardControls();
const { gl } = useThree();
const groundContacts = useRef(0);
const yawRef = useRef(0);
const pitchRef = useRef(0);
const mouseRef = useRef({ x: 0, y: 0 });
const jumpPressedRef = useRef(false);
useEffect(() => {
if (editMode)
return;
const canvas = gl.domElement;
const onClick = () => canvas.requestPointerLock();
const onMouseMove = (e: MouseEvent) => {
if (document.pointerLockElement !== canvas) return;
mouseRef.current.x += e.movementX;
mouseRef.current.y += e.movementY;
};
canvas.addEventListener('click', onClick);
document.addEventListener('mousemove', onMouseMove);
return () => {
canvas.removeEventListener('click', onClick);
document.removeEventListener('mousemove', onMouseMove);
};
}, [gl, editMode]);
useFrame(({ camera }) => {
if (editMode)
return;
if (!rbRef.current)
return;
yawRef.current -= mouseRef.current.x * SENSITIVITY;
pitchRef.current = Math.max(-0.4, Math.min(0.6, pitchRef.current - mouseRef.current.y * SENSITIVITY));
mouseRef.current.x = 0;
mouseRef.current.y = 0;
const yaw = yawRef.current;
// Rotate character body on Y axis only
_q.setFromEuler(_e.set(0, yaw, 0));
rbRef.current.setRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w }, true);
// Pin camera to shoulder offset, rotated by yaw + pitch
const t = rbRef.current.translation();
_charPos.set(t.x, t.y, t.z);
_offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yaw, 0, 'YXZ'));
camera.position.copy(_charPos).add(_offset);
_lookAt.set(t.x, t.y + 1, t.z);
camera.lookAt(_lookAt);
// Movement relative to character facing direction
if (!state.game?.isPaused && !editMode) {
const { forward, backward, left, right, jump } = get();
const fwdX = -Math.sin(yaw);
const fwdZ = -Math.cos(yaw);
const fwdScale = (forward ? 1 : 0) - (backward ? 1 : 0);
const rightScale = (right ? 1 : 0) - (left ? 1 : 0);
// right vector = cross(fwd, up) = (-fwdZ, 0, fwdX)
const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED;
const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;
const cur = rbRef.current.linvel();
const isGrounded = groundContacts.current > 0;
const vy = cur.y +
(jump && !jumpPressedRef.current && isGrounded
? JUMP_SPEED
: 0);
jumpPressedRef.current = jump;
rbRef.current.setLinvel({ x: vx, y: vy, z: vz }, true);
}
});
return ( return (
<SyncRigidBody <SyncRigidBody
ref={rbRef} colliders="hull"
colliders="cuboid" restitution={2}
lockRotations
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
onCollisionEnter={() => { groundContacts.current++; }}
onCollisionExit={() => { groundContacts.current--; }}
onSync={(data) => { onSync={(data) => {
// state.game?.setCharacterTransform( state.game?.setCharacterTransform(
// { position: data.position, look: character.transform.look }, {
// data.linearVelocity, position: data.position,
// undefined, look: character.transform.look,
// ); },
data.linearVelocity,
undefined, // do not change radial velocity
);
}} }}
> >
<group> <group
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}> position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
rotation={character.transform.look}
>
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
<coneGeometry args={[0.55, 0.8, 4]} /> <coneGeometry args={[0.55, 0.8, 4]} />
<meshStandardMaterial color="yellow" /> <meshStandardMaterial color="yellow" />
</mesh> </mesh>

View File

@ -1,12 +1,53 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { SceneView } from "./SceneView"; import { SceneView } from "./SceneView";
import { state } from "../state"; import { state } from "../state";
import { useFrame } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { Suspense } from "react"; import { PointerLockControls, useKeyboardControls } from "@react-three/drei";
import { Suspense, useEffect, useRef } from "react";
import { Physics } from "@react-three/rapier"; import { Physics } from "@react-three/rapier";
function PlayerMovement() {
const [, get] = useKeyboardControls();
const dirty = useRef(false);
useFrame(({ camera }, dt) => {
if (state.game?.isPaused)
return;
const { forward, backward, left, right } = get();
const speed = 5 * dt;
if (forward) { camera.translateZ(-speed); dirty.current = true; }
if (backward) { camera.translateZ(speed); dirty.current = true; }
if (left) { camera.translateX(-speed); dirty.current = true; }
if (right) { camera.translateX(speed); dirty.current = true; }
if (!dirty.current) return;
dirty.current = false;
const [rx, ry, rz] = camera.rotation.toArray();
state.game?.setCharacterTransform(
{
position: camera.position.toArray(),
look: [rx, ry, rz],
},
// do not change velocities
);
});
return <PointerLockControls onChange={() => { dirty.current = true; }} />;
}
export const GameView = observer(function () { export const GameView = observer(function () {
const game = state.game; const game = state.game;
const { camera } = useThree();
useEffect(() => {
if (!game)
return;
const { position, look } = game.camera;
camera.position.set(position[0], position[1], position[2]);
camera.rotation.set(look[0], look[1], look[2]);
}, []);
useFrame((_, delta) => { useFrame((_, delta) => {
state.game?.tick(delta); state.game?.tick(delta);
@ -17,7 +58,8 @@ export const GameView = observer(function () {
return (<> return (<>
<Suspense> <Suspense>
<Physics paused={game.isPaused} debug> <PlayerMovement />
<Physics paused={game.isPaused}>
<SceneView scene={game.scene} /> <SceneView scene={game.scene} />
</Physics> </Physics>
</Suspense> </Suspense>

View File

@ -13,14 +13,14 @@ export const SceneView = observer(function (props: SceneViewProps) {
// const rapier = useRapier(); // const rapier = useRapier();
// useFrame((_, dt) => { // useFrame((_, dt) => {
// if (props.editMode) // if (props.editMode)
// return; // return;
// const game = state.game; // const game = state.game;
// if (!game || game.isPaused) // if (!game || game.isPaused)
// return; // return;
// rapier.step(dt); // rapier.step(dt);
// }) // })
return (<> return (<>
@ -34,6 +34,6 @@ export const SceneView = observer(function (props: SceneViewProps) {
)) ))
} }
{/* {props.editMode && <CharacterView character={props.scene.character} />} */} {/* {props.editMode && <CharacterView character={props.scene.character} />} */}
{<CharacterView character={props.scene.character} editMode={props.editMode} />} {<CharacterView character={props.scene.character} />}
</>); </>);
}); });

View File

@ -1,6 +1,6 @@
import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-three/rapier"; import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-three/rapier";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { useImperativeHandle, useRef, type Ref } from "react"; import { useRef } from "react";
import { Euler, Quaternion } from "three"; import { Euler, Quaternion } from "three";
import type { R3, V3 } from "../types"; import type { R3, V3 } from "../types";
@ -15,7 +15,6 @@ export type SyncRigidBodyOnSyncFunction = (data: SyncRigidBodyData) => void;
type SyncRigidBodyProps = RigidBodyProps & { type SyncRigidBodyProps = RigidBodyProps & {
onSync: SyncRigidBodyOnSyncFunction; onSync: SyncRigidBodyOnSyncFunction;
ref?: Ref<RapierRigidBody | null>;
}; };
const _q = new Quaternion(); const _q = new Quaternion();
@ -39,11 +38,8 @@ function compareTwoFloatArrays(a: Float64Array, b: Float64Array, epsilon: number
return false; return false;
} }
export function SyncRigidBody({ onSync, ref, children, ...props }: SyncRigidBodyProps) { export function SyncRigidBody({ onSync, children, ...props }: SyncRigidBodyProps) {
const rbRef = useRef<RapierRigidBody>(null); const rbRef = useRef<RapierRigidBody>(null);
useImperativeHandle(ref, () => rbRef.current!);
const prevData = useRef<Float64Array>(PREV_INIT.slice()); const prevData = useRef<Float64Array>(PREV_INIT.slice());
const currentData = useRef<Float64Array>(PREV_INIT.slice()); const currentData = useRef<Float64Array>(PREV_INIT.slice());

View File

@ -31,42 +31,41 @@ export const ThreeView = observer(function () {
const infoRef = useRef<HTMLDivElement>(null); const infoRef = useRef<HTMLDivElement>(null);
return ( return (
<KeyboardControls map={[ <KeyboardControls map={[
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] }, { name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
{ name: 'backward', keys: ['ArrowDown', 's', 'S'] }, { name: 'backward', keys: ['ArrowDown', 's', 'S'] },
{ name: 'left', keys: ['ArrowLeft', 'a', 'A'] }, { name: 'left', keys: ['ArrowLeft', 'a', 'A'] },
{ name: 'right', keys: ['ArrowRight', 'd', 'D'] }, { name: 'right', keys: ['ArrowRight', 'd', 'D'] },
{ name: 'jump', keys: ['Space'] },
]}> ]}>
<div style={{ position: 'relative', width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}> <div style={{ position: 'relative', width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
<Canvas <Canvas
// camera={state.world.character.camera} // camera={state.world.character.camera}
onPointerMissed={() => state.worldEditor.resetSelectedObject()} onPointerMissed={() => state.worldEditor.resetSelectedObject()}
> >
<Stats /> <Stats />
<RenderInfoUpdater domRef={infoRef} /> <RenderInfoUpdater domRef={infoRef} />
{isGame ? <GameView /> : <SceneEditorView />} {isGame ? <GameView /> : <SceneEditorView />}
</Canvas> </Canvas>
<div ref={infoRef} style={{ <div ref={infoRef} style={{
position: 'absolute', bottom: 8, left: 8, position: 'absolute', bottom: 8, left: 8,
color: 'white', fontSize: 11, fontFamily: 'monospace', color: 'white', fontSize: 11, fontFamily: 'monospace',
background: 'rgba(0,0,0,0.5)', padding: '2px 6px', borderRadius: 3, background: 'rgba(0,0,0,0.5)', padding: '2px 6px', borderRadius: 3,
pointerEvents: 'none', pointerEvents: 'none',
}} /> }} />
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}> <div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
{ {
state.game state.game
? <> ? <>
<button onClick={() => state.stopGame()}><IconStop /></button> <button onClick={() => state.stopGame()}><IconStop /></button>
{ {
state.game!.isPaused state.game!.isPaused
? <button onClick={() => state.game!.resume()}><IconPlay /></button> ? <button onClick={() => state.game!.resume()}><IconPlay /></button>
: <button onClick={() => state.game!.pause()}><IconPause /></button> : <button onClick={() => state.game!.pause()}><IconPause /></button>
} }
</> </>
: <button onClick={() => state.startGame()}><IconPlay /></button> : <button onClick={() => state.startGame()}><IconPlay /></button>
} }
</div>
</div> </div>
</div>
</KeyboardControls> </KeyboardControls>
) )
}); });