Refactor ThirdPersonNavigationController for improved locomotion handling

This commit is contained in:
2026-04-11 18:38:24 +02:00
parent 3cf2297038
commit 2f6651d127

View File

@@ -107,17 +107,27 @@ export class ThirdPersonNavigationController implements NavigationController {
this.context = ctx;
if (!this.initializedFromSpawn) {
const spawn = ctx.getRuntimeScene().spawn;
const runtimeScene = ctx.getRuntimeScene();
const spawn = runtimeScene.spawn;
this.feetPosition = {
...spawn.position
};
this.standingPlayerShape = cloneFirstPersonPlayerShape(
runtimeScene.playerCollider
);
this.activePlayerShape = cloneFirstPersonPlayerShape(
runtimeScene.playerCollider
);
this.yawRadians = (spawn.yawDegrees * Math.PI) / 180;
this.cameraYawRadians = this.yawRadians;
this.pitchRadians = DEFAULT_PITCH_RADIANS;
this.cameraDistance = DEFAULT_CAMERA_DISTANCE;
this.verticalVelocity = 0;
this.grounded = false;
this.locomotionState = "flying";
this.jumpPressed = false;
this.locomotionState = createIdleRuntimeLocomotionState(
runtimeScene.playerCollider.mode === "none" ? "flying" : "airborne"
);
this.inWaterVolume = false;
this.inFogVolume = false;
this.initializedFromSpawn = true;
@@ -156,6 +166,7 @@ export class ThirdPersonNavigationController implements NavigationController {
window.removeEventListener("pointerup", this.handlePointerUp);
this.pressedKeys.clear();
this.dragging = false;
this.jumpPressed = false;
ctx.setRuntimeMessage(null);
ctx.setFirstPersonTelemetry(null);
this.context = null;
@@ -174,7 +185,14 @@ export class ThirdPersonNavigationController implements NavigationController {
this.cameraDistance = DEFAULT_CAMERA_DISTANCE;
this.verticalVelocity = 0;
this.grounded = false;
this.locomotionState = "flying";
this.jumpPressed = false;
this.standingPlayerShape = cloneFirstPersonPlayerShape(
FIRST_PERSON_PLAYER_SHAPE
);
this.activePlayerShape = cloneFirstPersonPlayerShape(
FIRST_PERSON_PLAYER_SHAPE
);
this.locomotionState = createIdleRuntimeLocomotionState("flying");
this.inWaterVolume = false;
this.inFogVolume = false;
this.dragging = false;
@@ -189,12 +207,14 @@ export class ThirdPersonNavigationController implements NavigationController {
}
const runtimeScene = this.context.getRuntimeScene();
const playerShape = runtimeScene.playerCollider;
this.standingPlayerShape = cloneFirstPersonPlayerShape(
runtimeScene.playerCollider
);
const playerMovement = runtimeScene.playerMovement;
const lookInput = resolvePlayerStartLookInput(
runtimeScene.playerInputBindings
);
const inputState = resolvePlayerStartMovementActions(
const inputState = resolvePlayerStartActionInputs(
this.pressedKeys,
runtimeScene.playerInputBindings
);
@@ -206,92 +226,63 @@ export class ThirdPersonNavigationController implements NavigationController {
);
}
const currentVolumeState = this.context.resolvePlayerVolumeState(
this.feetPosition
);
const inputX = inputState.moveRight - inputState.moveLeft;
const inputZ = inputState.moveForward - inputState.moveBackward;
const inputLength = Math.hypot(inputX, inputZ);
let horizontalX = 0;
let horizontalZ = 0;
let nextYawRadians = this.yawRadians;
if (inputLength > 0) {
const normalizedInputX = inputX / inputLength;
const normalizedInputZ = inputZ / inputLength;
const moveDistance = playerMovement.moveSpeed * dt;
this.forwardVector.set(
Math.sin(this.cameraYawRadians),
0,
Math.cos(this.cameraYawRadians)
);
this.rightVector.set(
-Math.cos(this.cameraYawRadians),
0,
Math.sin(this.cameraYawRadians)
);
horizontalX =
(this.forwardVector.x * normalizedInputZ +
this.rightVector.x * normalizedInputX) *
moveDistance;
horizontalZ =
(this.forwardVector.z * normalizedInputZ +
this.rightVector.z * normalizedInputX) *
moveDistance;
nextYawRadians = Math.atan2(horizontalX, horizontalZ);
}
if (playerShape.mode === "none" || currentVolumeState.inWater) {
this.verticalVelocity = 0;
} else {
this.verticalVelocity -= GRAVITY * dt;
}
const resolvedMotion = this.context.resolveFirstPersonMotion(
this.feetPosition,
const locomotionStep = stepPlayerLocomotion(
{
x: horizontalX,
y:
playerShape.mode === "none" || currentVolumeState.inWater
? 0
: this.verticalVelocity * dt,
z: horizontalZ
},
playerShape
dt,
feetPosition: this.feetPosition,
movementYawRadians: this.cameraYawRadians,
activeShape: this.activePlayerShape,
standingShape: this.standingPlayerShape,
verticalVelocity: this.verticalVelocity,
crouched: this.locomotionState.crouched,
wasJumpPressed: this.jumpPressed,
input: inputState,
movement: playerMovement,
resolveMotion: (feetPosition, motion, shape) =>
this.context?.resolveFirstPersonMotion(feetPosition, motion, shape) ??
null,
resolveVolumeState: (feetPosition) =>
this.context?.resolvePlayerVolumeState(feetPosition) ?? {
inWater: false,
inFog: false
},
probeGround: (feetPosition, shape, maxDistance) =>
this.context?.probePlayerGround(feetPosition, shape, maxDistance) ?? {
grounded: false,
distance: null,
normal: null,
slopeDegrees: null
},
canOccupyShape: (feetPosition, shape) =>
this.context?.canOccupyPlayerShape(feetPosition, shape) ?? true
}
);
if (resolvedMotion === null) {
if (locomotionStep === null) {
this.updateCameraTransform();
this.publishTelemetry();
return;
}
this.feetPosition = resolvedMotion.feetPosition;
this.yawRadians = nextYawRadians;
const nextVolumeState = this.context.resolvePlayerVolumeState(
this.feetPosition
);
this.inWaterVolume = nextVolumeState.inWater;
this.inFogVolume = nextVolumeState.inFog;
this.grounded = nextVolumeState.inWater ? false : resolvedMotion.grounded;
this.feetPosition = locomotionStep.feetPosition;
this.activePlayerShape = locomotionStep.activeShape;
this.verticalVelocity = locomotionStep.verticalVelocity;
this.jumpPressed = locomotionStep.jumpPressed;
this.locomotionState = locomotionStep.locomotionState;
this.grounded = locomotionStep.locomotionState.grounded;
this.inWaterVolume = locomotionStep.inWaterVolume;
this.inFogVolume = locomotionStep.inFogVolume;
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) {
this.verticalVelocity = 0;
} else if (this.inWaterVolume) {
this.verticalVelocity = 0;
if (
Math.hypot(
locomotionStep.planarDisplacement.x,
locomotionStep.planarDisplacement.z
) > 1e-5
) {
this.yawRadians = Math.atan2(
locomotionStep.planarDisplacement.x,
locomotionStep.planarDisplacement.z
);
}
this.updateCameraTransform();
@@ -307,7 +298,16 @@ export class ThirdPersonNavigationController implements NavigationController {
this.pitchRadians = DEFAULT_PITCH_RADIANS;
this.verticalVelocity = 0;
this.grounded = false;
this.locomotionState = "flying";
this.jumpPressed = false;
this.activePlayerShape = cloneFirstPersonPlayerShape(
this.context?.getRuntimeScene().playerCollider ?? FIRST_PERSON_PLAYER_SHAPE
);
this.standingPlayerShape = cloneFirstPersonPlayerShape(
this.context?.getRuntimeScene().playerCollider ?? FIRST_PERSON_PLAYER_SHAPE
);
this.locomotionState = createIdleRuntimeLocomotionState(
this.activePlayerShape.mode === "none" ? "flying" : "airborne"
);
this.inWaterVolume = false;
this.inFogVolume = false;
this.updateCameraTransform();
@@ -319,9 +319,7 @@ export class ThirdPersonNavigationController implements NavigationController {
return;
}
const eyeHeight = getFirstPersonPlayerEyeHeight(
this.context.getRuntimeScene().playerCollider
);
const eyeHeight = getFirstPersonPlayerEyeHeight(this.activePlayerShape);
const pivot = {
x: this.feetPosition.x,
y: this.feetPosition.y + eyeHeight * CAMERA_PIVOT_EYE_HEIGHT_FACTOR,
@@ -357,9 +355,7 @@ export class ThirdPersonNavigationController implements NavigationController {
const eyePosition = toEyePosition(
this.feetPosition,
getFirstPersonPlayerEyeHeight(
this.context.getRuntimeScene().playerCollider
)
getFirstPersonPlayerEyeHeight(this.activePlayerShape)
);
const cameraVolumeState = this.context.resolvePlayerVolumeState({
x: this.context.camera.position.x,