feat: add player camera
This commit is contained in:
@@ -8,6 +8,8 @@ Append `?debug` to the URL:
|
|||||||
http://localhost:5173?debug
|
http://localhost:5173?debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The free debug camera is toggled from the debug panel, not mounted permanently.
|
||||||
|
|
||||||
## Debug singleton
|
## Debug singleton
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ npm run dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:5173` — standard experience.
|
Open `http://localhost:5173` — standard experience.
|
||||||
Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay + free debug camera.
|
Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay. The free debug camera is enabled from the debug panel.
|
||||||
|
|
||||||
## 🧭 Conventions
|
## 🧭 Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { Crosshair } from "@/components/ui/Crosshair";
|
||||||
import { DebugPerf } from "@/debug/DebugPerf";
|
import { DebugPerf } from "@/debug/DebugPerf";
|
||||||
import { World } from "@/world/World";
|
import { World } from "@/world/World";
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
||||||
<World />
|
<World />
|
||||||
<DebugPerf />
|
<DebugPerf />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
<Crosshair />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { useCameraMode } from "@/debug/useCameraMode";
|
||||||
|
|
||||||
|
export function Crosshair(): React.JSX.Element | null {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
|
||||||
|
if (cameraMode !== "player") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="crosshair" aria-hidden="true" />;
|
||||||
|
}
|
||||||
+40
-4
@@ -1,25 +1,40 @@
|
|||||||
import GUI from "lil-gui";
|
import GUI from "lil-gui";
|
||||||
|
|
||||||
|
export type CameraMode = "player" | "debug";
|
||||||
|
|
||||||
export class Debug {
|
export class Debug {
|
||||||
private static instance: Debug | null = null;
|
private static instance: Debug | null = null;
|
||||||
|
|
||||||
public readonly active: boolean;
|
public readonly active: boolean;
|
||||||
private readonly gui: GUI | null;
|
private readonly gui: GUI | null;
|
||||||
private readonly folders = new Map<string, GUI>();
|
private readonly folders = new Map<string, GUI>();
|
||||||
|
private readonly listeners = new Set<() => void>();
|
||||||
|
private readonly controls = { cameraMode: "player" as CameraMode };
|
||||||
|
private cameraMode: CameraMode = "player";
|
||||||
|
|
||||||
static getInstance(): Debug {
|
static getInstance(): Debug {
|
||||||
if (!Debug.instance) {
|
if (!Debug.instance) {
|
||||||
Debug.instance = new Debug();
|
Debug.instance = new Debug();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Debug.instance;
|
return Debug.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.active = new URLSearchParams(window.location.search).has("debug");
|
this.active = new URLSearchParams(window.location.search).has("debug");
|
||||||
if (this.active) {
|
this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;
|
||||||
this.gui = new GUI({ title: "La-Fabrik Debug" });
|
|
||||||
} else {
|
if (this.gui) {
|
||||||
this.gui = null;
|
const folder = this.createFolder("Debug");
|
||||||
|
|
||||||
|
folder
|
||||||
|
?.add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
|
||||||
|
.name("Camera Mode")
|
||||||
|
.onChange((value: CameraMode) => {
|
||||||
|
this.controls.cameraMode = value;
|
||||||
|
this.cameraMode = value;
|
||||||
|
this.emit();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,9 +55,30 @@ export class Debug {
|
|||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isDebugCameraEnabled(): boolean {
|
||||||
|
return this.cameraMode === "debug";
|
||||||
|
}
|
||||||
|
|
||||||
|
getCameraMode(): CameraMode {
|
||||||
|
return this.cameraMode;
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
this.listeners.clear();
|
||||||
this.folders.clear();
|
this.folders.clear();
|
||||||
this.gui?.destroy();
|
this.gui?.destroy();
|
||||||
Debug.instance = null;
|
Debug.instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emit(): void {
|
||||||
|
this.listeners.forEach((listener) => listener());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { OrbitControls } from "@react-three/drei";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
|
||||||
import { Debug } from "@/debug/Debug";
|
|
||||||
|
|
||||||
export function DebugCameraControls(): React.JSX.Element {
|
export function DebugCameraControls(): React.JSX.Element {
|
||||||
const controls = useRef<OrbitControlsImpl | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const debug = Debug.getInstance();
|
|
||||||
|
|
||||||
if (!debug.active || !controls.current) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = debug.createFolder("Camera");
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = controls.current.target;
|
|
||||||
const cameraState = {
|
|
||||||
targetX: target.x,
|
|
||||||
targetY: target.y,
|
|
||||||
targetZ: target.z,
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncTarget = (): void => {
|
|
||||||
if (!controls.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controls.current.target.set(
|
|
||||||
cameraState.targetX,
|
|
||||||
cameraState.targetY,
|
|
||||||
cameraState.targetZ,
|
|
||||||
);
|
|
||||||
controls.current.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
folder
|
|
||||||
.add(cameraState, "targetX", -100, 100, 0.1)
|
|
||||||
.name("Target X")
|
|
||||||
.onChange(syncTarget);
|
|
||||||
folder
|
|
||||||
.add(cameraState, "targetY", -20, 50, 0.1)
|
|
||||||
.name("Target Y")
|
|
||||||
.onChange(syncTarget);
|
|
||||||
folder
|
|
||||||
.add(cameraState, "targetZ", -100, 100, 0.1)
|
|
||||||
.name("Target Z")
|
|
||||||
.onChange(syncTarget);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
ref={controls}
|
|
||||||
enableDamping
|
enableDamping
|
||||||
dampingFactor={0.08}
|
dampingFactor={0.05}
|
||||||
minDistance={20}
|
minDistance={100}
|
||||||
maxDistance={220}
|
maxDistance={1000}
|
||||||
target={[0, 12, 0]}
|
target={[0, 1.75, 0]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import type { CameraMode } from "@/debug/Debug";
|
||||||
|
import { Debug } from "@/debug/Debug";
|
||||||
|
|
||||||
|
export function useCameraMode(): CameraMode {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(listener) => debug.subscribe(listener),
|
||||||
|
() => debug.getCameraMode(),
|
||||||
|
() => debug.getCameraMode(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ body,
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: #04070d;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@@ -25,3 +26,17 @@ select {
|
|||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crosshair {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.92);
|
||||||
|
border-radius: 999px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|||||||
+73
-2
@@ -1,9 +1,80 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import type { AmbientLight, DirectionalLight } from "three";
|
||||||
|
import { Debug } from "@/debug/Debug";
|
||||||
|
|
||||||
|
type LightingState = {
|
||||||
|
ambientIntensity: number;
|
||||||
|
sunIntensity: number;
|
||||||
|
sunX: number;
|
||||||
|
sunY: number;
|
||||||
|
sunZ: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIGHTING_STATE: LightingState = {
|
||||||
|
ambientIntensity: 1.8,
|
||||||
|
sunIntensity: 2.8,
|
||||||
|
sunX: 60,
|
||||||
|
sunY: 80,
|
||||||
|
sunZ: 30,
|
||||||
|
};
|
||||||
|
|
||||||
export function Lighting(): React.JSX.Element {
|
export function Lighting(): React.JSX.Element {
|
||||||
|
const ambient = useRef<AmbientLight>(null);
|
||||||
|
const sun = useRef<DirectionalLight>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
if (!debug.active) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = debug.createFolder("Lighting");
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.add(LIGHTING_STATE, "ambientIntensity", 0, 5, 0.1).name("Ambient");
|
||||||
|
folder.add(LIGHTING_STATE, "sunIntensity", 0, 8, 0.1).name("Sun Intensity");
|
||||||
|
folder.add(LIGHTING_STATE, "sunX", -100, 100, 1).name("Sun X");
|
||||||
|
folder.add(LIGHTING_STATE, "sunY", 0, 150, 1).name("Sun Y");
|
||||||
|
folder.add(LIGHTING_STATE, "sunZ", -100, 100, 1).name("Sun Z");
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (ambient.current) {
|
||||||
|
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sun.current) {
|
||||||
|
sun.current.position.set(
|
||||||
|
LIGHTING_STATE.sunX,
|
||||||
|
LIGHTING_STATE.sunY,
|
||||||
|
LIGHTING_STATE.sunZ,
|
||||||
|
);
|
||||||
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ambientLight
|
||||||
|
ref={ambient}
|
||||||
|
intensity={LIGHTING_STATE.ambientIntensity}
|
||||||
|
color="#dbeafe"
|
||||||
|
/>
|
||||||
<directionalLight
|
<directionalLight
|
||||||
position={[60, 80, 30]}
|
ref={sun}
|
||||||
intensity={2.8}
|
position={[
|
||||||
|
LIGHTING_STATE.sunX,
|
||||||
|
LIGHTING_STATE.sunY,
|
||||||
|
LIGHTING_STATE.sunZ,
|
||||||
|
]}
|
||||||
|
intensity={LIGHTING_STATE.sunIntensity}
|
||||||
color="#fff7ed"
|
color="#fff7ed"
|
||||||
castShadow
|
castShadow
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-71
@@ -1,28 +1,14 @@
|
|||||||
// # route path src/world/Map.tsx
|
import { useMemo } from "react";
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
||||||
import { useFrame } from "@react-three/fiber";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Debug } from "@/debug/Debug";
|
|
||||||
|
|
||||||
const MODEL_PATH = "/models/map/blocking/model.glb";
|
const MODEL_PATH = "/models/map/blocking/model.glb";
|
||||||
|
|
||||||
type MapDebugState = {
|
type CenteredModel = {
|
||||||
positionX: number;
|
object: THREE.Object3D;
|
||||||
positionY: number;
|
|
||||||
positionZ: number;
|
|
||||||
rotationY: number;
|
|
||||||
scale: number;
|
scale: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DEBUG_STATE: MapDebugState = {
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
positionZ: 0,
|
|
||||||
rotationY: 0,
|
|
||||||
scale: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
function centerModel(model: THREE.Object3D): number {
|
function centerModel(model: THREE.Object3D): number {
|
||||||
model.updateMatrixWorld(true);
|
model.updateMatrixWorld(true);
|
||||||
|
|
||||||
@@ -36,64 +22,17 @@ function centerModel(model: THREE.Object3D): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Map(): React.JSX.Element {
|
export function Map(): React.JSX.Element {
|
||||||
const root = useRef<THREE.Group>(null);
|
|
||||||
const debugState = useRef<MapDebugState>({ ...DEFAULT_DEBUG_STATE });
|
|
||||||
const { scene } = useGLTF(MODEL_PATH);
|
const { scene } = useGLTF(MODEL_PATH);
|
||||||
const model = useMemo(() => scene.clone(true), [scene]);
|
const centeredModel = useMemo<CenteredModel>(() => {
|
||||||
|
const object = scene.clone(true);
|
||||||
|
const scale = centerModel(object);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
return { object, scale };
|
||||||
debugState.current.scale = centerModel(model);
|
}, [scene]);
|
||||||
}, [model]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const debug = Debug.getInstance();
|
|
||||||
|
|
||||||
if (!debug.active) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = debug.createFolder("Map");
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
folder
|
|
||||||
.add(debugState.current, "positionX", -100, 100, 0.1)
|
|
||||||
.name("Position X");
|
|
||||||
folder
|
|
||||||
.add(debugState.current, "positionY", -20, 50, 0.1)
|
|
||||||
.name("Position Y");
|
|
||||||
folder
|
|
||||||
.add(debugState.current, "positionZ", -100, 100, 0.1)
|
|
||||||
.name("Position Z");
|
|
||||||
folder
|
|
||||||
.add(debugState.current, "rotationY", -Math.PI, Math.PI, 0.01)
|
|
||||||
.name("Rotation Y");
|
|
||||||
folder.add(debugState.current, "scale", 0.1, 10, 0.01).name("Scale");
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
const currentRoot = root.current;
|
|
||||||
|
|
||||||
if (!currentRoot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRoot.position.set(
|
|
||||||
debugState.current.positionX,
|
|
||||||
debugState.current.positionY,
|
|
||||||
debugState.current.positionZ,
|
|
||||||
);
|
|
||||||
currentRoot.rotation.y = debugState.current.rotationY;
|
|
||||||
currentRoot.scale.setScalar(debugState.current.scale);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={root}>
|
<group scale={centeredModel.scale}>
|
||||||
<primitive object={model} />
|
<primitive object={centeredModel.object} />
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -1,17 +1,21 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { DebugCameraControls } from "@/debug/scene/DebugCameraControls";
|
import { DebugCameraControls } from "@/debug/scene/DebugCameraControls";
|
||||||
import { DebugHelpers } from "@/debug/scene/DebugHelpers";
|
import { DebugHelpers } from "@/debug/scene/DebugHelpers";
|
||||||
|
import { useCameraMode } from "@/debug/useCameraMode";
|
||||||
import { Environment } from "@/world/Environment";
|
import { Environment } from "@/world/Environment";
|
||||||
import { Lighting } from "@/world/Lighting";
|
import { Lighting } from "@/world/Lighting";
|
||||||
import { Map } from "@/world/Map";
|
import { Map } from "@/world/Map";
|
||||||
|
import { FPSController } from "@/world/player/FPSController";
|
||||||
|
|
||||||
export function World(): React.JSX.Element {
|
export function World(): React.JSX.Element {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Environment />
|
<Environment />
|
||||||
<Lighting />
|
<Lighting />
|
||||||
<DebugHelpers />
|
<DebugHelpers />
|
||||||
<DebugCameraControls />
|
{cameraMode === "debug" ? <DebugCameraControls /> : <FPSController />}
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Map />
|
<Map />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1 +1,124 @@
|
|||||||
// src/world/player/FPSController.tsx
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { PointerLockControls } from "@react-three/drei";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
const PLAYER_EYE_HEIGHT = 1.75;
|
||||||
|
const PLAYER_SPAWN_POSITION = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 6);
|
||||||
|
const PLAYER_LOOK_AT = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 0);
|
||||||
|
const MOVE_SPEED = 5;
|
||||||
|
|
||||||
|
type PlayerKeys = {
|
||||||
|
forward: boolean;
|
||||||
|
backward: boolean;
|
||||||
|
left: boolean;
|
||||||
|
right: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_KEYS: PlayerKeys = {
|
||||||
|
forward: false,
|
||||||
|
backward: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FPSController(): React.JSX.Element {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const keys = useRef<PlayerKeys>({ ...DEFAULT_KEYS });
|
||||||
|
const interact = useRef<() => void>(() => {});
|
||||||
|
const forward = useMemo(() => new THREE.Vector3(), []);
|
||||||
|
const right = useMemo(() => new THREE.Vector3(), []);
|
||||||
|
const movement = useMemo(() => new THREE.Vector3(), []);
|
||||||
|
const up = useMemo(() => new THREE.Vector3(0, 1, 0), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
camera.position.copy(PLAYER_SPAWN_POSITION);
|
||||||
|
camera.lookAt(PLAYER_LOOK_AT);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.exitPointerLock?.();
|
||||||
|
};
|
||||||
|
}, [camera]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyChange =
|
||||||
|
(pressed: boolean) =>
|
||||||
|
(event: KeyboardEvent): void => {
|
||||||
|
switch (event.code) {
|
||||||
|
case "KeyZ":
|
||||||
|
keys.current.forward = pressed;
|
||||||
|
break;
|
||||||
|
case "KeyS":
|
||||||
|
keys.current.backward = pressed;
|
||||||
|
break;
|
||||||
|
case "KeyQ":
|
||||||
|
keys.current.left = pressed;
|
||||||
|
break;
|
||||||
|
case "KeyD":
|
||||||
|
keys.current.right = pressed;
|
||||||
|
break;
|
||||||
|
case "KeyE":
|
||||||
|
if (pressed) {
|
||||||
|
interact.current();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = handleKeyChange(true);
|
||||||
|
const handleKeyUp = handleKeyChange(false);
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
movement.set(0, 0, 0);
|
||||||
|
|
||||||
|
camera.getWorldDirection(forward);
|
||||||
|
forward.y = 0;
|
||||||
|
|
||||||
|
if (forward.lengthSq() > 0) {
|
||||||
|
forward.normalize();
|
||||||
|
right.crossVectors(forward, up).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.current.forward) {
|
||||||
|
movement.add(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.current.backward) {
|
||||||
|
movement.sub(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.current.left) {
|
||||||
|
movement.sub(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.current.right) {
|
||||||
|
movement.add(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movement.lengthSq() > 0) {
|
||||||
|
movement.normalize().multiplyScalar(MOVE_SPEED * delta);
|
||||||
|
camera.position.add(movement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera.position.y < PLAYER_EYE_HEIGHT) {
|
||||||
|
camera.position.y = PLAYER_EYE_HEIGHT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <PointerLockControls />;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user