import { observer } from "mobx-react-lite"; import type { Character } from "../types"; import { SyncRigidBody } from "./SyncRigidBody"; 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 { useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, RoundCuboidCollider } from "@react-three/rapier"; import { joystickValues } from "../joystickInput"; const SPEED = 5; const JUMP_SPEED = 8; const GRAVITY = 20; const SENSITIVITY = 0.002; const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5); const LOOK_RATE = 2000; // recreate private types type RapierWorldCreateCharacterControllerFunction = ReturnType['world']['createCharacterController']; type KinematicCharacterController = ReturnType; 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 rbRef = useRef(null); const colliderRef = useRef(null); const [, get] = useKeyboardControls(); const { gl } = useThree(); const { world } = useRapier(); const yawRef = useRef(0); const pitchRef = useRef(0); const mouseRef = useRef({ x: 0, y: 0 }); const jumpPressedRef = useRef(false); const vyRef = useRef(0); const controllerRef = useRef(null); useEffect(() => { const controller = world.createCharacterController(0.01); controller.setUp({ x: 0, y: 1, z: 0 }); controller.setMaxSlopeClimbAngle(45 * Math.PI / 180); controller.setMinSlopeSlideAngle(30 * Math.PI / 180); controller.enableAutostep(0.5, 0.2, true); controller.enableSnapToGround(0.5); controller.setApplyImpulsesToDynamicBodies(true); controllerRef.current = controller; return () => { world.removeCharacterController(controller); }; }, [world]); 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]); useBeforePhysicsStep((world) => { if (editMode) return; const rb = rbRef.current; const collider = colliderRef.current; const controller = controllerRef.current; if (!rb || !collider || !controller) return; if (state.game?.isPaused) return; const dt = world.timestep; const yaw = yawRef.current; const { forward, backward, left, right, jump } = get(); const fwdX = -Math.sin(yaw); const fwdZ = -Math.cos(yaw); const fwdScale = Math.max(-1, Math.min(1, (forward ? 1 : 0) - (backward ? 1 : 0) + joystickValues.move.y)); const rightScale = Math.max(-1, Math.min(1, (right ? 1 : 0) - (left ? 1 : 0) + joystickValues.move.x)); const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED; const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED; const jumpInput = jump || joystickValues.jump; const isGrounded = controller.computedGrounded(); if (jumpInput && !jumpPressedRef.current && isGrounded) { vyRef.current = JUMP_SPEED; } else if (!isGrounded) { vyRef.current -= GRAVITY * dt; } else { vyRef.current = 0; } jumpPressedRef.current = jumpInput; controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt }); const corrected = controller.computedMovement(); const t = rb.translation(); rb.setNextKinematicTranslation({ x: t.x + corrected.x, y: t.y + corrected.y, z: t.z + corrected.z, }); _q.setFromEuler(_e.set(0, yaw, 0)); rb.setNextKinematicRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w }); }); useFrame(({ camera }, delta) => { if (editMode) return; const rb = rbRef.current; if (!rb) return; mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta; mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta; yawRef.current -= mouseRef.current.x * SENSITIVITY; pitchRef.current = Math.max(-0.85, Math.min(0.5, pitchRef.current - mouseRef.current.y * SENSITIVITY)); mouseRef.current.x = 0; mouseRef.current.y = 0; const t = rb.translation(); _charPos.set(t.x, t.y, t.z); _offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ')); camera.position.copy(_charPos).add(_offset); _lookAt.set(t.x, t.y + 1, t.z); camera.lookAt(_lookAt); }); return ( { }} > {/* */} {/* */} {/* */} ); });