diff --git a/src/components/CharacterView.tsx b/src/components/CharacterView.tsx index c69bed7..911d720 100644 --- a/src/components/CharacterView.tsx +++ b/src/components/CharacterView.tsx @@ -2,14 +2,22 @@ import { observer } from "mobx-react-lite"; import type { Character } from "../types"; import { SyncRigidBody } from "./SyncRigidBody"; import { state } from "../state"; -import { useRef } from "react"; -import { Vector3 } from "three"; -import { useFrame } from "@react-three/fiber"; -import { PointerLockControls, useKeyboardControls } from "@react-three/drei"; +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; -const _fwd = new Vector3(); +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; @@ -18,50 +26,86 @@ type CharacterViewProps = { export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) { const pos = character.transform.position; - const rbRef = useRef(null); - const [, get] = useKeyboardControls(); + const { gl } = useThree(); + + const yawRef = useRef(0); + const pitchRef = useRef(0); + const mouseRef = useRef({ x: 0, y: 0 }); + + 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 (state.game?.isPaused || !rbRef.current) + if (editMode) return; - if (!editMode) { + 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 } = get(); - - camera.getWorldDirection(_fwd); - _fwd.y = 0; - _fwd.normalize(); - const fx = _fwd.x, fz = _fwd.z; - + 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); - - const vx = (fx * fwdScale - fz * rightScale) * SPEED; - const vz = (fz * fwdScale + fx * rightScale) * SPEED; - + // 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(); rbRef.current.setLinvel({ x: vx, y: cur.y, z: vz }, true); } }); - return (<> - {!editMode && } + return ( { - state.game?.setCharacterTransform( - { - position: data.position, - look: character.transform.look, - }, - data.linearVelocity, - undefined, - ); + // state.game?.setCharacterTransform( + // { position: data.position, look: character.transform.look }, + // data.linearVelocity, + // undefined, + // ); }} > @@ -71,5 +115,5 @@ export const CharacterView = observer(function ({ character, editMode }: Charact - ); + ); }); diff --git a/src/components/GameView.tsx b/src/components/GameView.tsx index 3a306c3..32b30cf 100644 --- a/src/components/GameView.tsx +++ b/src/components/GameView.tsx @@ -1,22 +1,15 @@ import { observer } from "mobx-react-lite"; import { SceneView } from "./SceneView"; import { state } from "../state"; -import { useFrame, useThree } from "@react-three/fiber"; +import { useFrame } from "@react-three/fiber"; import { Suspense } from "react"; import { Physics } from "@react-three/rapier"; export const GameView = observer(function () { const game = state.game; - const { camera } = useThree(); useFrame((_, delta) => { state.game?.tick(delta); - - 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]); }); if (!game) diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index f0001df..52e8d19 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -31,41 +31,42 @@ export const ThreeView = observer(function () { const infoRef = useRef(null); return ( -
- state.worldEditor.resetSelectedObject()} - > - - - {isGame ? : } - -
-
- { - state.game - ? <> - - { - state.game!.isPaused - ? - : - } - - : - } +
+ state.worldEditor.resetSelectedObject()} + > + + + {isGame ? : } + +
+
+ { + state.game + ? <> + + { + state.game!.isPaused + ? + : + } + + : + } +
-
) });