using rapier KinematicCharacterController
This commit is contained in:
parent
8ca8f894e4
commit
8ef089c833
|
|
@ -6,14 +6,18 @@ import { useEffect, useRef } from "react";
|
||||||
import { Euler, Quaternion, Vector3 } from "three";
|
import { Euler, Quaternion, Vector3 } from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useKeyboardControls } from "@react-three/drei";
|
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 SPEED = 5;
|
||||||
const JUMP_SPEED = 8;
|
const JUMP_SPEED = 8;
|
||||||
|
const GRAVITY = 20;
|
||||||
const SENSITIVITY = 0.002;
|
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 SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
||||||
|
|
||||||
|
// recreate private types
|
||||||
|
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
||||||
|
type KinematicCharacterController = ReturnType<RapierWorldCreateCharacterControllerFunction>;
|
||||||
|
|
||||||
const _q = new Quaternion();
|
const _q = new Quaternion();
|
||||||
const _e = new Euler(0, 0, 0, 'YXZ');
|
const _e = new Euler(0, 0, 0, 'YXZ');
|
||||||
const _offset = new Vector3();
|
const _offset = new Vector3();
|
||||||
|
|
@ -28,19 +32,32 @@ type CharacterViewProps = {
|
||||||
export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
|
export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
|
||||||
const pos = character.transform.position;
|
const pos = character.transform.position;
|
||||||
const rbRef = useRef<RapierRigidBody>(null);
|
const rbRef = useRef<RapierRigidBody>(null);
|
||||||
|
const colliderRef = useRef<RapierCollider>(null);
|
||||||
const [, get] = useKeyboardControls();
|
const [, get] = useKeyboardControls();
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
const groundContacts = useRef(0);
|
const { world } = useRapier();
|
||||||
|
|
||||||
const yawRef = useRef(0);
|
const yawRef = useRef(0);
|
||||||
const pitchRef = useRef(0);
|
const pitchRef = useRef(0);
|
||||||
const mouseRef = useRef({ x: 0, y: 0 });
|
const mouseRef = useRef({ x: 0, y: 0 });
|
||||||
const jumpPressedRef = useRef(false);
|
const jumpPressedRef = useRef(false);
|
||||||
|
const vyRef = useRef(0);
|
||||||
|
const controllerRef = useRef<KinematicCharacterController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editMode)
|
const controller = world.createCharacterController(0.01);
|
||||||
return;
|
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 canvas = gl.domElement;
|
||||||
const onClick = () => canvas.requestPointerLock();
|
const onClick = () => canvas.requestPointerLock();
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
|
@ -56,72 +73,74 @@ export const CharacterView = observer(function ({ character, editMode }: Charact
|
||||||
};
|
};
|
||||||
}, [gl, editMode]);
|
}, [gl, editMode]);
|
||||||
|
|
||||||
useFrame(({ camera }) => {
|
useBeforePhysicsStep((world) => {
|
||||||
if (editMode)
|
if (editMode) return;
|
||||||
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)
|
const dt = world.timestep;
|
||||||
return;
|
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;
|
yawRef.current -= mouseRef.current.x * SENSITIVITY;
|
||||||
pitchRef.current = Math.max(-0.4, Math.min(0.6, pitchRef.current - mouseRef.current.y * SENSITIVITY));
|
pitchRef.current = Math.max(-0.4, Math.min(0.6, pitchRef.current - mouseRef.current.y * SENSITIVITY));
|
||||||
mouseRef.current.x = 0;
|
mouseRef.current.x = 0;
|
||||||
mouseRef.current.y = 0;
|
mouseRef.current.y = 0;
|
||||||
|
|
||||||
const yaw = yawRef.current;
|
const t = rb.translation();
|
||||||
|
|
||||||
// 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);
|
_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);
|
camera.position.copy(_charPos).add(_offset);
|
||||||
_lookAt.set(t.x, t.y + 1, t.z);
|
_lookAt.set(t.x, t.y + 1, t.z);
|
||||||
camera.lookAt(_lookAt);
|
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}
|
ref={rbRef}
|
||||||
colliders="cuboid"
|
type="kinematicPosition"
|
||||||
lockRotations
|
colliders={false}
|
||||||
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||||
onCollisionEnter={() => { groundContacts.current++; }}
|
onSync={() => { }}
|
||||||
onCollisionExit={() => { groundContacts.current--; }}
|
|
||||||
onSync={(data) => {
|
|
||||||
// state.game?.setCharacterTransform(
|
|
||||||
// { position: data.position, look: character.transform.look },
|
|
||||||
// data.linearVelocity,
|
|
||||||
// undefined,
|
|
||||||
// );
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} />
|
||||||
<group>
|
<group>
|
||||||
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||||
<coneGeometry args={[0.55, 0.8, 4]} />
|
<coneGeometry args={[0.55, 0.8, 4]} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue