From 8ef089c83393f47ae12deab0465481ca17b403a7 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 4 Jun 2026 13:52:36 +0300 Subject: [PATCH] using rapier KinematicCharacterController --- src/components/CharacterView.tsx | 125 ++++++++++++++++++------------- 1 file changed, 72 insertions(+), 53 deletions(-) diff --git a/src/components/CharacterView.tsx b/src/components/CharacterView.tsx index 9d33ed2..33c6753 100644 --- a/src/components/CharacterView.tsx +++ b/src/components/CharacterView.tsx @@ -6,14 +6,18 @@ 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"; +import { CuboidCollider, useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, type RapierContext } from "@react-three/rapier"; const SPEED = 5; const JUMP_SPEED = 8; +const GRAVITY = 20; const SENSITIVITY = 0.002; -// Shoulder offset in character-local space: right, up, behind const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5); +// 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(); @@ -28,19 +32,32 @@ type CharacterViewProps = { 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 groundContacts = useRef(0); + 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(() => { - if (editMode) - return; + 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) => { @@ -56,72 +73,74 @@ export const CharacterView = observer(function ({ character, editMode }: Charact }; }, [gl, editMode]); - useFrame(({ camera }) => { - if (editMode) - return; + 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; - if (!rbRef.current) - 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 = (forward ? 1 : 0) - (backward ? 1 : 0); + const rightScale = (right ? 1 : 0) - (left ? 1 : 0); + const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED; + const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED; + + const isGrounded = controller.computedGrounded(); + if (jump && !jumpPressedRef.current && isGrounded) { + vyRef.current = JUMP_SPEED; + } else if (!isGrounded) { + vyRef.current -= GRAVITY * dt; + } else { + vyRef.current = 0; + } + jumpPressedRef.current = jump; + + 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 }) => { + if (editMode) return; + const rb = rbRef.current; + if (!rb) 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(); + const t = rb.translation(); _charPos.set(t.x, t.y, t.z); - _offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yaw, 0, 'YXZ')); + _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); - - // 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 ( { groundContacts.current++; }} - onCollisionExit={() => { groundContacts.current--; }} - onSync={(data) => { - // state.game?.setCharacterTransform( - // { position: data.position, look: character.transform.look }, - // data.linearVelocity, - // undefined, - // ); - }} + onSync={() => { }} > +