You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
9.6 KiB

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<string, string> = {
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<ActionName, ActionName> = {
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<PerspectiveCamera>()
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 <perspectiveCamera ref={ref} {...props} />
}
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<Group>()
const collisionsRef = useRef<Mesh<CircleGeometry, MeshBasicMaterial>>(null)
const velocity = useRef<Vector3>(new Vector3())
const direction = useRef<Vector3>(new Vector3())
const currentPositionClone = useRef<Vector3>(new Vector3())
const pCameraQuaternion = useRef<Quaternion>(new Quaternion())
const worldOrigin = useRef<Vector3>(new Vector3())
const speed = useRef<number>(SPEED)
const bottomRaycaster = useRef(
new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5),
)
const collisionCircle = useRef<Mesh>()
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<Raycaster[]>(
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 (
<>
<group ref={groupRef} position={InitialPosition}>
<FirstPersonCamera position={[0, -0.1, 0]} />
<mesh
ref={collisionCircle}
position={[0, -HEIGHT * 0.1, 0]}
geometry={collisionCircleGeometry}
material={collisionCircleMaterial}
visible={false}
/>
</group>
<Collisions ref={collisionsRef} visible={false} />
</>
)
}
export default Player