import React, { useEffect, useRef, useState } from 'react' import { useThree, useFrame } from 'react-three-fiber' import { Geometry, Event, Group, PerspectiveCamera, Vector3, Raycaster, Mesh, CircleGeometry, } from 'three' import { relativeAngle } from './lib/math' import Collisions from './Collisions' import useStore from '../store' import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial' import { Quaternion } from 'three/src/math/Quaternion' import { ActionName } from './models/Human' const SPEED = 0.8 export const HEIGHT = 1.5 const CIRCLE_RADIUS = 0.5 const CIRCLE_SEGMENTS = 8 const InitialPosition = new Vector3(49.92, 3.15, 34.52) const keys: Record = { KeyW: 'left', KeyS: 'right', KeyA: 'backward', KeyD: 'forward', ArrowUp: 'left', ArrowDown: 'right', ArrowLeft: 'backward', ArrowRight: 'forward', ShiftLeft: 'run', Space: 'jump', } const moveFieldByKey = (key: string) => keys[key] export const animations: Record = { idle: 'idle', turn_right: 'turn_right', turn_left: 'turn_left', walk_backwards: 'walk_backwards', walk: 'walk', walk_right: 'walk_right', walk_left: 'walk_left', run: 'run', run_right: 'run_right', run_left: 'run_left', jump: 'jump', } function FirstPersonCamera(props: JSX.IntrinsicElements['perspectiveCamera']) { const ref = useRef() const { setDefaultCamera } = useThree() // Make the camera known to the system useEffect(() => { if (ref.current) setDefaultCamera(ref.current) }, [ref.current]) // Update it every frame useFrame(() => ref.current?.updateMatrixWorld()) return } const usePlayerControls = () => { const [movement, setMovement] = useState({ forward: false, backward: false, left: false, right: false, run: false, jump: false, }) useEffect(() => { const handleKeyDown = (e: Event) => { setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: true })) } const handleKeyUp = (e: Event) => { setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: false })) } document.addEventListener('keydown', handleKeyDown) document.addEventListener('keyup', handleKeyUp) return () => { document.removeEventListener('keydown', handleKeyDown) document.removeEventListener('keyup', handleKeyUp) } }, []) return movement } // define an axis, usually just up const upVector = new Vector3(0, 1, 0) // TODO Improve physics in player const Player = () => { const socket = useStore((state) => state.socket) const initialTransform = useStore((state) => state.initialTransform) const pointerLocked = useStore((state) => state.pointerLocked) const { jump, forward, backward, left, right, run } = usePlayerControls() const { camera } = useThree() const groupRef = useRef() const collisionsRef = useRef>(null) const velocity = useRef(new Vector3()) const direction = useRef(new Vector3()) const currentPositionClone = useRef(new Vector3()) const pCameraQuaternion = useRef(new Quaternion()) const worldOrigin = useRef(new Vector3()) const speed = useRef(SPEED) const bottomRaycaster = useRef( new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5), ) const collisionCircle = useRef() const collisionCircleGeometry = new CircleGeometry(CIRCLE_RADIUS, CIRCLE_SEGMENTS) collisionCircleGeometry.rotateX(Math.PI / 2) collisionCircle const collisionCircleMaterial = new MeshBasicMaterial({ color: 0xff0000, wireframe: true, }) const wallRaycasters = useRef( Array(CIRCLE_SEGMENTS) .fill(undefined) .map((_, i) => { const vert = collisionCircleGeometry.vertices[i + 1].clone() const direction = vert.sub(collisionCircleGeometry.vertices[0]).normalize() return new Raycaster(new Vector3(), direction, 0, CIRCLE_RADIUS) }), ) useEffect(() => { if (initialTransform && groupRef.current) { groupRef.current.position.fromArray(initialTransform.position) camera.quaternion.fromArray(initialTransform.rotation) } }, [initialTransform, groupRef.current]) useEffect(() => { const socketEmitTransformInterval = setInterval(() => { if (socket && groupRef.current && camera) { const cameraRotation = camera.quaternion.toArray() const [x, y, z] = direction.current.toArray() let anim = animations.idle if (velocity.current.y !== 0) { anim = animations.jump } else if (x === 0 && z === 0) { const rotationAngle = relativeAngle( upVector, camera.quaternion, pCameraQuaternion.current, ) if (Math.abs(rotationAngle) > 0.5) { anim = rotationAngle > 1 ? animations.turn_right : animations.turn_left } else { anim = animations.idle } } else if (x < 0) { anim = animations.walk_backwards } else if (x > 0 && z === 0) { anim = speed.current > SPEED ? animations.run : animations.walk } else if (x >= 0 && z > 0) { anim = speed.current > SPEED ? animations.run_right : animations.walk_right } else if (x >= 0 && z < 0) { anim = speed.current > SPEED ? animations.run_left : animations.walk_left } // get the signed difference in these angles socket.emit('transform', { position: [ groupRef.current?.position.x, groupRef.current?.position.y, groupRef.current?.position.z, ], rotation: [...cameraRotation], animation: anim, }) pCameraQuaternion.current.copy(camera.quaternion) } }, 33) return () => { clearInterval(socketEmitTransformInterval) } }, [socket, groupRef.current, camera]) const moveForward = (distance: number) => { if (groupRef.current) { worldOrigin.current.setFromMatrixColumn(camera.matrix, 0) worldOrigin.current.crossVectors(camera.up, worldOrigin.current) groupRef.current.position.addScaledVector(worldOrigin.current, distance) } } const moveRight = (distance: number) => { if (groupRef.current) { worldOrigin.current.setFromMatrixColumn(camera.matrix, 0) groupRef.current.position.addScaledVector(worldOrigin.current, distance) } } useFrame((_, delta) => { if (!groupRef.current || !collisionsRef.current || !collisionCircle.current) return if (pointerLocked) { // Slowdown velocity.current.x -= velocity.current.x * 9.0 * delta velocity.current.z -= velocity.current.z * 9.0 * delta // Fall bottomRaycaster.current.ray.origin.copy(groupRef.current.position) let intersections = [] intersections = bottomRaycaster.current.intersectObject(collisionsRef.current, true) if (jump && velocity.current.y === 0) { velocity.current.y = 5 } if (intersections.length < 1) { velocity.current.y -= 9.8 * 8 * delta } else { if (velocity.current.y <= 0 && intersections[0].distance < HEIGHT) { velocity.current.y = 0 groupRef.current.position.y = intersections[0].point.y + HEIGHT } } // Reset if (groupRef.current.position.y < -50) { groupRef.current.position.copy(InitialPosition) velocity.current.y = 0 } // Direction direction.current.z = Number(forward) - Number(backward) direction.current.x = Number(left) - Number(right) direction.current.normalize() // Running speed.current = run && direction.current.x >= 0 ? SPEED * 1.5 : SPEED // Move if (forward || backward) velocity.current.z -= direction.current.z * SPEED * delta * speed.current if (left || right) velocity.current.x -= direction.current.x * SPEED * delta * speed.current // Wall collisions const collisionCircleGeometry = collisionCircle.current.geometry as Geometry if (collisionCircleGeometry.vertices) { for (let i = 0; i < CIRCLE_SEGMENTS; i++) { wallRaycasters.current[i].ray.origin.copy(groupRef.current.position) wallRaycasters.current[i].ray.origin.y -= HEIGHT * 0.6 let wallIntersections = [] wallIntersections = wallRaycasters.current[i].intersectObject( collisionsRef.current, true, ) if ( wallIntersections.length > 0 && wallIntersections[0].distance < CIRCLE_RADIUS ) { const distance = CIRCLE_RADIUS - wallIntersections[0].distance currentPositionClone.current.copy(wallRaycasters.current[i].ray.origin) const direction = wallIntersections[0].point .sub(currentPositionClone.current) .normalize() .multiplyScalar(distance) groupRef.current.position.sub(direction) } } } // Apply speed moveForward(-velocity.current.x * speed.current) moveRight(-velocity.current.z * speed.current) groupRef.current.position.y += velocity.current.y * delta } }) return ( <> ) } export default Player