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 WorldCollisions from './models/WorldCollisions' import useStore from '../store' import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial' const SPEED = 1 const HEIGHT = 1.5 const CIRCLE_RADIUS = 1.0 const CIRCLE_SEGMENTS = 8 const keys: Record = { KeyW: 'forward', KeyS: 'backward', KeyA: 'left', KeyD: 'right', ArrowUp: 'forward', ArrowDown: 'backward', ArrowLeft: 'left', ArrowRight: 'right', ShiftLeft: 'run', Space: 'jump', } const moveFieldByKey = (key: string) => keys[key] 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 } // TODO Improve physics in player const Player = () => { const socket = useStore((state) => state.socket) const pointerLocked = useStore((state) => state.pointerLocked) const { 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 worldOrigin = useRef(new Vector3()) const currentPositionClone = useRef(new Vector3()) 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) 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(() => { const socketEmitTransformInterval = setInterval(() => { if (socket && groupRef.current && camera) { const cameraRotation = camera.rotation.toArray() socket.emit('transform', { position: [ groupRef.current?.position.x, groupRef.current?.position.y, groupRef.current?.position.z, ], rotation: [cameraRotation[0], cameraRotation[1], cameraRotation[2]], }) } }, 50) return () => { clearInterval(socketEmitTransformInterval) } }, []) 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 * 10.0 * delta velocity.current.z -= velocity.current.z * 10.0 * delta // Fall bottomRaycaster.current.ray.origin.copy(groupRef.current.position) let intersections = [] intersections = bottomRaycaster.current.intersectObject( collisionsRef.current, false, ) if (intersections.length < 1) { velocity.current.y -= 9.8 * 10 * delta } else { velocity.current.y = 0 groupRef.current.position.y = intersections[0].point.y + HEIGHT } // Direction direction.current.z = Number(forward) - Number(backward) direction.current.x = Number(left) - Number(right) direction.current.normalize() // Running const speed = run ? 1.5 : 1 // Move if (forward || backward) velocity.current.z -= direction.current.z * SPEED * delta * speed if (left || right) velocity.current.x -= direction.current.x * SPEED * delta * speed // 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) let wallIntersections = [] wallIntersections = wallRaycasters.current[i].intersectObject( collisionsRef.current, false, ) if ( wallIntersections.length > 0 && wallIntersections[0].distance < CIRCLE_RADIUS ) { const distance = CIRCLE_RADIUS - wallIntersections[0].distance currentPositionClone.current.copy(groupRef.current.position) const direction = wallIntersections[0].point .sub(currentPositionClone.current) .normalize() .multiplyScalar(distance) groupRef.current.position.sub(direction) } } } // Apply speed moveForward(-velocity.current.x * speed) moveRight(-velocity.current.z * speed) groupRef.current.position.y += velocity.current.y * delta } // TODO enable jump if cheating //if (jump && Math.abs(parseFloat(velocity.current[1].toFixed(2))) < 0.05) // api.velocity.set(velocity.current[0], 10, velocity.current[2]) }) return ( <> ) } export default Player