Spaces:
Sleeping
Sleeping
File size: 8,756 Bytes
a8b92bc 1829efb d55e335 b5a2d17 1829efb 31abac2 1829efb 31abac2 1829efb b5a2d17 d55e335 b5a2d17 d55e335 b5a2d17 d55e335 b5a2d17 d55e335 b5a2d17 1829efb b5a2d17 1829efb b5a2d17 1829efb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | import { useRef, useEffect, useState, forwardRef, useImperativeHandle, useCallback } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import * as THREE from 'three'
import { useCameraGestures } from '../camera/useCameraGestures'
import { cameraFocusTarget } from '../camera/cameraFocus'
import { useDebugStore } from '../../store/debug'
import { isEditableTarget } from '../../utils/dom'
import { CHARACTER_SPAWN } from './spawn'
export interface FlyControllerHandle {
reset: () => void
}
interface FlyControllerProps {
preserveCameraOnMount?: boolean
}
const SPEED = 6
const SHIFT_MULT = 3
const SMOOTH = 0.12
const DOLLY_UNITS_PER_PIXEL = 0.02
const DEFAULT_YAW = 0
const _forward = new THREE.Vector3()
const _dollyForward = new THREE.Vector3()
const _right = new THREE.Vector3()
const _up = new THREE.Vector3(0, 1, 0)
const _move = new THREE.Vector3()
const _euler = new THREE.Euler(0, 0, 0, 'YXZ')
export const FlyController = forwardRef<FlyControllerHandle, FlyControllerProps>(function FlyController({ preserveCameraOnMount = false }, ref) {
const { camera, gl } = useThree()
const mouseSensitivity = useDebugStore((s) => s.flyMouseSensitivity)
const keys = useRef(new Set<string>())
const rawYaw = useRef(0)
const rawPitch = useRef(0)
const smoothYaw = useRef(0)
const smoothPitch = useRef(0)
const touchLook = useRef<{ id: number; x: number; y: number } | null>(null)
const touchMove = useRef<{ id: number; x: number; y: number } | null>(null)
const [joystickPos, setJoystickPos] = useState({ x: 0, y: 0 })
const reset = useCallback(() => {
let spawnX = CHARACTER_SPAWN.x
let spawnY = CHARACTER_SPAWN.y
let spawnZ = CHARACTER_SPAWN.z
let spawnYaw = DEFAULT_YAW
let spawnPitch = 0
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search)
const spawnParam = params.get('spawn')
if (spawnParam) {
const parts = spawnParam.split(',').map(parseFloat)
if (parts.length >= 3 && !parts.slice(0, 3).some(isNaN)) {
spawnX = parts[0]
spawnY = parts[1]
spawnZ = parts[2]
}
if (parts.length >= 5 && !parts.slice(3, 5).some(isNaN)) {
spawnYaw = parts[3]
spawnPitch = parts[4]
}
}
}
camera.position.set(spawnX, spawnY, spawnZ)
cameraFocusTarget.current = null
keys.current.clear()
rawYaw.current = spawnYaw
rawPitch.current = spawnPitch
smoothYaw.current = spawnYaw
smoothPitch.current = spawnPitch
camera.quaternion.setFromEuler(_euler.set(spawnPitch, spawnYaw, 0))
}, [camera])
const preserveCamera = useCallback(() => {
cameraFocusTarget.current = null
keys.current.clear()
_euler.setFromQuaternion(camera.quaternion, 'YXZ')
rawYaw.current = _euler.y
rawPitch.current = _euler.x
smoothYaw.current = _euler.y
smoothPitch.current = _euler.x
}, [camera])
const applyDolly = useCallback((deltaY: number) => {
_dollyForward.set(0, 0, -1).applyQuaternion(camera.quaternion).normalize()
camera.position.addScaledVector(_dollyForward, -deltaY * DOLLY_UNITS_PER_PIXEL)
}, [camera])
const applyTumble = useCallback((dx: number, dy: number) => {
cameraFocusTarget.current = null
rawYaw.current -= dx * mouseSensitivity
rawPitch.current -= dy * mouseSensitivity
rawPitch.current = Math.max(-Math.PI / 2.2, Math.min(Math.PI / 2.2, rawPitch.current))
}, [mouseSensitivity])
useCameraGestures({ domElement: gl.domElement, onDollyPixels: applyDolly, onTumblePixels: applyTumble })
useImperativeHandle(ref, () => ({
reset,
}), [reset])
useEffect(() => {
if (preserveCameraOnMount) preserveCamera()
else reset()
}, [preserveCamera, preserveCameraOnMount, reset])
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (isEditableTarget(e.target)) {
keys.current.delete(e.code)
return
}
if (e.type === 'keydown') keys.current.add(e.code)
else keys.current.delete(e.code)
}
window.addEventListener('keydown', onKey)
window.addEventListener('keyup', onKey)
const onTouchStart = (e: TouchEvent) => {
for (const touch of Array.from(e.changedTouches)) {
const isLeft = touch.clientX < window.innerWidth / 2
if (isLeft && !touchMove.current) {
touchMove.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
setJoystickPos({ x: touch.clientX, y: touch.clientY })
} else if (!isLeft && !touchLook.current) {
touchLook.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
}
}
}
const onTouchMove = (e: TouchEvent) => {
for (const touch of Array.from(e.changedTouches)) {
if (touchLook.current?.id === touch.identifier) {
const dx = touch.clientX - touchLook.current.x
const dy = touch.clientY - touchLook.current.y
rawYaw.current -= dx * 0.004
rawPitch.current -= dy * 0.004
rawPitch.current = Math.max(-Math.PI / 2.2, Math.min(Math.PI / 2.2, rawPitch.current))
touchLook.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
}
if (touchMove.current?.id === touch.identifier) {
touchMove.current = { ...touchMove.current, x: touch.clientX, y: touch.clientY }
}
}
}
const onTouchEnd = (e: TouchEvent) => {
for (const touch of Array.from(e.changedTouches)) {
if (touchLook.current?.id === touch.identifier) touchLook.current = null
if (touchMove.current?.id === touch.identifier) {
touchMove.current = null
setJoystickPos({ x: 0, y: 0 })
}
}
}
gl.domElement.addEventListener('touchstart', onTouchStart, { passive: true })
gl.domElement.addEventListener('touchmove', onTouchMove, { passive: true })
gl.domElement.addEventListener('touchend', onTouchEnd, { passive: true })
return () => {
window.removeEventListener('keydown', onKey)
window.removeEventListener('keyup', onKey)
gl.domElement.removeEventListener('touchstart', onTouchStart)
gl.domElement.removeEventListener('touchmove', onTouchMove)
gl.domElement.removeEventListener('touchend', onTouchEnd)
}
}, [gl])
useFrame((_state, delta) => {
// Camera focus: lerp rawYaw/rawPitch toward clicked object (only when pointer is not locked)
const focusTarget = cameraFocusTarget.current
if (focusTarget && document.pointerLockElement !== gl.domElement) {
const dir = new THREE.Vector3().subVectors(focusTarget, camera.position).normalize()
const targetPitch = Math.asin(Math.max(-1, Math.min(1, dir.y)))
const targetYaw = Math.atan2(-dir.x, -dir.z)
const t = 1 - Math.pow(0.04, delta) // ~smooth decay
// wrap yaw diff to [-π, π] to avoid spinning the long way
const yawDiff = ((targetYaw - rawYaw.current + Math.PI * 3) % (Math.PI * 2)) - Math.PI
rawYaw.current += yawDiff * t
rawPitch.current += (targetPitch - rawPitch.current) * t
rawPitch.current = Math.max(-Math.PI / 2.2, Math.min(Math.PI / 2.2, rawPitch.current))
if (Math.abs(yawDiff * t) < 0.0005 && Math.abs((targetPitch - rawPitch.current) * t) < 0.0005) {
cameraFocusTarget.current = null
}
}
smoothYaw.current += (rawYaw.current - smoothYaw.current) * (1 - Math.pow(1 - SMOOTH, 1))
smoothPitch.current += (rawPitch.current - smoothPitch.current) * (1 - Math.pow(1 - SMOOTH, 1))
_euler.set(smoothPitch.current, smoothYaw.current, 0)
camera.quaternion.setFromEuler(_euler)
let fwd = 0, strafe = 0, vert = 0
const k = keys.current
if (k.has('KeyW') || k.has('ArrowUp')) fwd += 1
if (k.has('KeyS') || k.has('ArrowDown')) fwd -= 1
if (k.has('KeyA') || k.has('ArrowLeft')) strafe -= 1
if (k.has('KeyD') || k.has('ArrowRight')) strafe += 1
if (k.has('KeyE')) vert += 1
if (k.has('KeyQ')) vert -= 1
if (touchMove.current) {
const origin = joystickPos
const dx = (touchMove.current.x - origin.x) / 50
const dy = (touchMove.current.y - origin.y) / 50
strafe += Math.max(-1, Math.min(1, dx))
fwd -= Math.max(-1, Math.min(1, dy))
}
_forward.set(0, 0, -1).applyQuaternion(camera.quaternion)
_right.set(1, 0, 0).applyQuaternion(camera.quaternion)
_move.set(0, 0, 0)
.addScaledVector(_forward, fwd)
.addScaledVector(_right, strafe)
.addScaledVector(_up, vert)
if (_move.lengthSq() > 1) _move.normalize()
const speed = SPEED * (k.has('ShiftLeft') || k.has('ShiftRight') ? SHIFT_MULT : 1)
camera.position.addScaledVector(_move, speed * delta)
})
return null
})
|