Add volume state tracking and locomotion state management in FirstPersonNavigationController

This commit is contained in:
2026-04-06 08:22:12 +02:00
parent 85f6b4d74a
commit e17a4c3373

View File

@@ -3,7 +3,7 @@ import { Euler, Vector3 } from "three";
import type { Vec3 } from "../core/vector"; import type { Vec3 } from "../core/vector";
import { getFirstPersonPlayerEyeHeight } from "./player-collision"; import { getFirstPersonPlayerEyeHeight } from "./player-collision";
import type { NavigationController, RuntimeControllerContext } from "./navigation-controller"; import type { NavigationController, RuntimeControllerContext, RuntimeLocomotionState } from "./navigation-controller";
const LOOK_SENSITIVITY = 0.0022; const LOOK_SENSITIVITY = 0.0022;
const MOVE_SPEED = 4.5; const MOVE_SPEED = 4.5;
@@ -39,6 +39,9 @@ export class FirstPersonNavigationController implements NavigationController {
private pitchRadians = 0; private pitchRadians = 0;
private verticalVelocity = 0; private verticalVelocity = 0;
private grounded = false; private grounded = false;
private locomotionState: RuntimeLocomotionState = "flying";
private inWaterVolume = false;
private inFogVolume = false;
private pointerLocked = false; private pointerLocked = false;
private initializedFromSpawn = false; private initializedFromSpawn = false;
@@ -54,6 +57,9 @@ export class FirstPersonNavigationController implements NavigationController {
this.pitchRadians = 0; this.pitchRadians = 0;
this.verticalVelocity = 0; this.verticalVelocity = 0;
this.grounded = false; this.grounded = false;
this.locomotionState = "flying";
this.inWaterVolume = false;
this.inFogVolume = false;
this.initializedFromSpawn = true; this.initializedFromSpawn = true;
} }
@@ -96,6 +102,7 @@ export class FirstPersonNavigationController implements NavigationController {
} }
const playerShape = this.context.getRuntimeScene().playerCollider; const playerShape = this.context.getRuntimeScene().playerCollider;
const currentVolumeState = this.context.resolvePlayerVolumeState(this.feetPosition);
const inputX = (this.pressedKeys.has("KeyD") ? 1 : 0) - (this.pressedKeys.has("KeyA") ? 1 : 0); const inputX = (this.pressedKeys.has("KeyD") ? 1 : 0) - (this.pressedKeys.has("KeyA") ? 1 : 0);
const inputZ = (this.pressedKeys.has("KeyW") ? 1 : 0) - (this.pressedKeys.has("KeyS") ? 1 : 0); const inputZ = (this.pressedKeys.has("KeyW") ? 1 : 0) - (this.pressedKeys.has("KeyS") ? 1 : 0);
const inputLength = Math.hypot(inputX, inputZ); const inputLength = Math.hypot(inputX, inputZ);
@@ -117,6 +124,8 @@ export class FirstPersonNavigationController implements NavigationController {
if (playerShape.mode === "none") { if (playerShape.mode === "none") {
this.verticalVelocity = 0; this.verticalVelocity = 0;
} else if (currentVolumeState.inWater) {
this.verticalVelocity = 0;
} else { } else {
this.verticalVelocity -= GRAVITY * dt; this.verticalVelocity -= GRAVITY * dt;
} }
@@ -125,7 +134,7 @@ export class FirstPersonNavigationController implements NavigationController {
this.feetPosition, this.feetPosition,
{ {
x: horizontalX, x: horizontalX,
y: playerShape.mode === "none" ? 0 : this.verticalVelocity * dt, y: playerShape.mode === "none" || currentVolumeState.inWater ? 0 : this.verticalVelocity * dt,
z: horizontalZ z: horizontalZ
}, },
playerShape playerShape
@@ -138,10 +147,25 @@ export class FirstPersonNavigationController implements NavigationController {
} }
this.feetPosition = resolvedMotion.feetPosition; this.feetPosition = resolvedMotion.feetPosition;
this.grounded = resolvedMotion.grounded; const nextVolumeState = this.context.resolvePlayerVolumeState(this.feetPosition);
this.inWaterVolume = nextVolumeState.inWater;
this.inFogVolume = nextVolumeState.inFog;
this.grounded = nextVolumeState.inWater ? false : resolvedMotion.grounded;
if (playerShape.mode === "none") {
this.locomotionState = "flying";
} else if (this.inWaterVolume) {
this.locomotionState = "swimming";
} else if (this.grounded) {
this.locomotionState = "grounded";
} else {
this.locomotionState = "flying";
}
if (this.grounded && this.verticalVelocity < 0) { if (this.grounded && this.verticalVelocity < 0) {
this.verticalVelocity = 0; this.verticalVelocity = 0;
} else if (this.inWaterVolume) {
this.verticalVelocity = 0;
} }
this.updateCameraTransform(); this.updateCameraTransform();
@@ -156,6 +180,9 @@ export class FirstPersonNavigationController implements NavigationController {
this.pitchRadians = 0; this.pitchRadians = 0;
this.verticalVelocity = 0; this.verticalVelocity = 0;
this.grounded = false; this.grounded = false;
this.locomotionState = "flying";
this.inWaterVolume = false;
this.inFogVolume = false;
this.updateCameraTransform(); this.updateCameraTransform();
this.publishTelemetry(); this.publishTelemetry();
} }
@@ -188,6 +215,9 @@ export class FirstPersonNavigationController implements NavigationController {
}, },
eyePosition: toEyePosition(this.feetPosition, getFirstPersonPlayerEyeHeight(this.context.getRuntimeScene().playerCollider)), eyePosition: toEyePosition(this.feetPosition, getFirstPersonPlayerEyeHeight(this.context.getRuntimeScene().playerCollider)),
grounded: this.grounded, grounded: this.grounded,
locomotionState: this.locomotionState,
inWaterVolume: this.inWaterVolume,
inFogVolume: this.inFogVolume,
pointerLocked: this.pointerLocked, pointerLocked: this.pointerLocked,
spawn: this.context.getRuntimeScene().spawn spawn: this.context.getRuntimeScene().spawn
}); });