fix(map): add world plane collision and respawn
This commit is contained in:
@@ -6,12 +6,14 @@ import {
|
|||||||
DEBUG_GRID_SIZE,
|
DEBUG_GRID_SIZE,
|
||||||
DEBUG_GRID_Y,
|
DEBUG_GRID_Y,
|
||||||
} from "@/data/debug/debugConfig";
|
} from "@/data/debug/debugConfig";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
export function DebugHelpers(): React.JSX.Element | null {
|
export function DebugHelpers(): React.JSX.Element | null {
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
|
||||||
if (!debug.active) {
|
if (!debug.active || sceneMode === "game") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const PLAYER_GRAVITY = 30;
|
|||||||
export const PLAYER_MAX_DELTA = 0.05;
|
export const PLAYER_MAX_DELTA = 0.05;
|
||||||
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
||||||
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
||||||
|
export const PLAYER_FALL_RESPAWN_Y = -20;
|
||||||
|
export const PLAYER_FALL_RESPAWN_DELAY = 3;
|
||||||
|
|
||||||
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0];
|
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0];
|
||||||
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { Vector3Tuple } from "@/types/three/three";
|
|
||||||
|
|
||||||
export const TERRAIN_BOUNDARY_CONFIG = {
|
|
||||||
enabled: true,
|
|
||||||
center: [-10, 8, -2] as Vector3Tuple,
|
|
||||||
radius: 135,
|
|
||||||
height: 28,
|
|
||||||
thickness: 3,
|
|
||||||
segments: 48,
|
|
||||||
};
|
|
||||||
@@ -35,8 +35,8 @@ export const WATER_SHADER_CONFIG = {
|
|||||||
export const WATER_STREAMING_CONFIG = {
|
export const WATER_STREAMING_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
loadDistance: 40,
|
loadDistance: 40,
|
||||||
unloadDistance: 35,
|
unloadDistance: 48,
|
||||||
updateInterval: 250,
|
updateInterval: 350,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WATER_SURFACES: WaterSurfaceConfig[] = [
|
export const WATER_SURFACES: WaterSurfaceConfig[] = [
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export const WORLD_BOUNDS_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
center: [0, 0, 0] as Vector3Tuple,
|
||||||
|
planeColor: TERRAIN_COLORS.grass1.hex,
|
||||||
|
planeY: -0.04,
|
||||||
|
planeCollisionThickness: 1,
|
||||||
|
size: [270, 260] as const,
|
||||||
|
wallHeight: 28,
|
||||||
|
wallThickness: 4,
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem"
|
|||||||
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||||
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
import { WaterSystem } from "@/world/water/WaterSystem";
|
import { WaterSystem } from "@/world/water/WaterSystem";
|
||||||
|
import { WorldPlane } from "@/world/WorldPlane";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||||
@@ -258,6 +259,7 @@ export function GameMap({
|
|||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
<MapInstancingSystem />
|
<MapInstancingSystem />
|
||||||
|
<WorldPlane />
|
||||||
<WaterSystem />
|
<WaterSystem />
|
||||||
<VegetationSystem />
|
<VegetationSystem />
|
||||||
{isMapModelVisible("terrain", { groups, models }) ? (
|
{isMapModelVisible("terrain", { groups, models }) ? (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as THREE from "three";
|
|||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||||
import { TerrainBoundaryCollision } from "@/world/collision/TerrainBoundaryCollision";
|
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
@@ -174,7 +174,7 @@ export function GameMapCollision({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef} visible={false}>
|
<group ref={groupRef} visible={false}>
|
||||||
{mapReady ? <TerrainBoundaryCollision /> : null}
|
{mapReady ? <WorldBoundsCollision /> : null}
|
||||||
{mapReady
|
{mapReady
|
||||||
? collisionNodes.map((mapNode, index) => (
|
? collisionNodes.map((mapNode, index) => (
|
||||||
<CollisionErrorBoundary
|
<CollisionErrorBoundary
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldPlane(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, planeColor, planeY, size } = WORLD_BOUNDS_CONFIG;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
name="world-plane"
|
||||||
|
position={[center[0], planeY, center[2]]}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
>
|
||||||
|
<planeGeometry args={size} />
|
||||||
|
<meshBasicMaterial color={planeColor} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { TERRAIN_BOUNDARY_CONFIG } from "@/data/world/terrainBoundaryConfig";
|
|
||||||
|
|
||||||
function createBoundarySegments(): React.JSX.Element[] {
|
|
||||||
const segments: React.JSX.Element[] = [];
|
|
||||||
const {
|
|
||||||
center,
|
|
||||||
height,
|
|
||||||
radius,
|
|
||||||
segments: segmentCount,
|
|
||||||
thickness,
|
|
||||||
} = TERRAIN_BOUNDARY_CONFIG;
|
|
||||||
const arcLength = (Math.PI * 2 * radius) / segmentCount;
|
|
||||||
|
|
||||||
for (let index = 0; index < segmentCount; index++) {
|
|
||||||
const angle = (index / segmentCount) * Math.PI * 2;
|
|
||||||
const x = center[0] + Math.cos(angle) * radius;
|
|
||||||
const z = center[2] + Math.sin(angle) * radius;
|
|
||||||
|
|
||||||
segments.push(
|
|
||||||
<mesh key={index} position={[x, center[1], z]} rotation={[0, -angle, 0]}>
|
|
||||||
<boxGeometry args={[arcLength, height, thickness]} />
|
|
||||||
<meshBasicMaterial />
|
|
||||||
</mesh>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TerrainBoundaryCollision(): React.JSX.Element | null {
|
|
||||||
if (!TERRAIN_BOUNDARY_CONFIG.enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{createBoundarySegments()}</>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { WorldPlaneCollision } from "@/world/collision/WorldPlaneCollision";
|
||||||
|
import { WorldWallsCollision } from "@/world/collision/WorldWallsCollision";
|
||||||
|
|
||||||
|
export function WorldBoundsCollision(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<group name="world-bounds-collision">
|
||||||
|
<WorldPlaneCollision />
|
||||||
|
<WorldWallsCollision />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldPlaneCollision(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, planeCollisionThickness, planeY, size } = WORLD_BOUNDS_CONFIG;
|
||||||
|
const [width, depth] = size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
name="world-plane-collision"
|
||||||
|
position={[center[0], planeY - planeCollisionThickness / 2, center[2]]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[width, planeCollisionThickness, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldWallsCollision(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, size, wallHeight, wallThickness } = WORLD_BOUNDS_CONFIG;
|
||||||
|
const [width, depth] = size;
|
||||||
|
const wallY = center[1] + wallHeight / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="world-walls-collision">
|
||||||
|
<mesh position={[center[0], wallY, center[2] - halfDepth]}>
|
||||||
|
<boxGeometry
|
||||||
|
args={[width + wallThickness * 2, wallHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0], wallY, center[2] + halfDepth]}>
|
||||||
|
<boxGeometry
|
||||||
|
args={[width + wallThickness * 2, wallHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0] - halfWidth, wallY, center[2]]}>
|
||||||
|
<boxGeometry args={[wallThickness, wallHeight, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0] + halfWidth, wallY, center[2]]}>
|
||||||
|
<boxGeometry args={[wallThickness, wallHeight, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
PLAYER_AIR_CONTROL_FACTOR,
|
PLAYER_AIR_CONTROL_FACTOR,
|
||||||
PLAYER_CAPSULE_RADIUS,
|
PLAYER_CAPSULE_RADIUS,
|
||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_FALL_RESPAWN_DELAY,
|
||||||
|
PLAYER_FALL_RESPAWN_Y,
|
||||||
PLAYER_GRAVITY,
|
PLAYER_GRAVITY,
|
||||||
PLAYER_JUMP_SPEED,
|
PLAYER_JUMP_SPEED,
|
||||||
PLAYER_MAX_DELTA,
|
PLAYER_MAX_DELTA,
|
||||||
@@ -57,6 +59,22 @@ const _up = new THREE.Vector3(0, 1, 0);
|
|||||||
const _translateVec = new THREE.Vector3();
|
const _translateVec = new THREE.Vector3();
|
||||||
const _collisionCorrection = new THREE.Vector3();
|
const _collisionCorrection = new THREE.Vector3();
|
||||||
|
|
||||||
|
function resetPlayerCapsule(
|
||||||
|
capsule: Capsule,
|
||||||
|
spawnPosition: Vector3Tuple,
|
||||||
|
camera: THREE.Camera,
|
||||||
|
velocity: THREE.Vector3,
|
||||||
|
): void {
|
||||||
|
capsule.start.set(
|
||||||
|
spawnPosition[0],
|
||||||
|
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
||||||
|
spawnPosition[2],
|
||||||
|
);
|
||||||
|
capsule.end.set(...spawnPosition);
|
||||||
|
velocity.set(0, 0, 0);
|
||||||
|
camera.position.copy(capsule.end);
|
||||||
|
}
|
||||||
|
|
||||||
function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule {
|
function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule {
|
||||||
return new Capsule(
|
return new Capsule(
|
||||||
new THREE.Vector3(
|
new THREE.Vector3(
|
||||||
@@ -104,6 +122,7 @@ export function PlayerController({
|
|||||||
const movementLockedRef = useRef(movementLocked);
|
const movementLockedRef = useRef(movementLocked);
|
||||||
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
|
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
|
||||||
const velocity = useRef(new THREE.Vector3());
|
const velocity = useRef(new THREE.Vector3());
|
||||||
|
const fallDuration = useRef(0);
|
||||||
const onFloor = useRef(false);
|
const onFloor = useRef(false);
|
||||||
const wantsJump = useRef(false);
|
const wantsJump = useRef(false);
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
@@ -112,16 +131,15 @@ export function PlayerController({
|
|||||||
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
capsule.current.start.set(
|
resetPlayerCapsule(
|
||||||
spawnPosition[0],
|
capsule.current,
|
||||||
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
spawnPosition,
|
||||||
spawnPosition[2],
|
camera,
|
||||||
|
velocity.current,
|
||||||
);
|
);
|
||||||
capsule.current.end.set(...spawnPosition);
|
fallDuration.current = 0;
|
||||||
velocity.current.set(0, 0, 0);
|
|
||||||
onFloor.current = false;
|
onFloor.current = false;
|
||||||
wantsJump.current = false;
|
wantsJump.current = false;
|
||||||
camera.position.copy(capsule.current.end);
|
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
}, [camera, spawnPosition]);
|
}, [camera, spawnPosition]);
|
||||||
|
|
||||||
@@ -211,6 +229,27 @@ export function PlayerController({
|
|||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
if (!initializedRef.current) return;
|
if (!initializedRef.current) return;
|
||||||
|
|
||||||
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
|
|
||||||
|
if (capsule.current.end.y < PLAYER_FALL_RESPAWN_Y) {
|
||||||
|
fallDuration.current += dt;
|
||||||
|
|
||||||
|
if (fallDuration.current >= PLAYER_FALL_RESPAWN_DELAY) {
|
||||||
|
resetPlayerCapsule(
|
||||||
|
capsule.current,
|
||||||
|
spawnPosition,
|
||||||
|
camera,
|
||||||
|
velocity.current,
|
||||||
|
);
|
||||||
|
fallDuration.current = 0;
|
||||||
|
onFloor.current = false;
|
||||||
|
wantsJump.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallDuration.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlayerInputLocked() || !canMove) {
|
if (isPlayerInputLocked() || !canMove) {
|
||||||
keys.current = { ...DEFAULT_KEYS };
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
velocity.current.set(0, 0, 0);
|
velocity.current.set(0, 0, 0);
|
||||||
@@ -218,8 +257,6 @@ export function PlayerController({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
|
||||||
|
|
||||||
camera.getWorldDirection(_forward);
|
camera.getWorldDirection(_forward);
|
||||||
_forward.setY(0);
|
_forward.setY(0);
|
||||||
if (_forward.lengthSq() > 0) {
|
if (_forward.lengthSq() > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user