Add air direction only setting for player movement

This commit is contained in:
2026-04-12 02:18:53 +02:00
parent 6b825a7f28
commit 42cee3d19a
13 changed files with 301 additions and 28 deletions

View File

@@ -65,6 +65,7 @@ function cloneRuntimePlayerMovement(
maxHoldMs: movement.jump.maxHoldMs,
moveWhileJumping: movement.jump.moveWhileJumping,
moveWhileFalling: movement.jump.moveWhileFalling,
directionOnly: movement.jump.directionOnly,
bunnyHop: movement.jump.bunnyHop,
bunnyHopBoost: movement.jump.bunnyHopBoost
},
@@ -313,6 +314,7 @@ export class FirstPersonNavigationController implements NavigationController {
dt,
feetPosition: this.feetPosition,
movementYawRadians: this.yawRadians,
airDirectionYawRadians: this.yawRadians,
standingShape: this.standingPlayerShape,
verticalVelocity: this.verticalVelocity,
previousLocomotionState: this.locomotionState,

View File

@@ -74,6 +74,7 @@ export interface StepPlayerLocomotionOptions {
dt: number;
feetPosition: Vec3;
movementYawRadians: number;
airDirectionYawRadians?: number;
standingShape: FirstPersonPlayerShape;
verticalVelocity: number;
previousLocomotionState?: RuntimeLocomotionState;
@@ -190,18 +191,50 @@ function computePlanarMotion(
requestedPlanarSpeed: number,
dt: number
): { motion: Vec3; inputMagnitude: number } {
const inputX = input.moveRight - input.moveLeft;
const inputZ = input.moveForward - input.moveBackward;
const rawMagnitude = Math.hypot(inputX, inputZ);
const inputMagnitude = clampUnitInterval(rawMagnitude);
const directionResult = computePlanarInputDirection(
movementYawRadians,
input
);
if (rawMagnitude <= 0 || requestedPlanarSpeed <= 0 || dt <= 0) {
if (
directionResult.direction === null ||
requestedPlanarSpeed <= 0 ||
dt <= 0
) {
return {
motion: {
x: 0,
y: 0,
z: 0
},
inputMagnitude: directionResult.inputMagnitude
};
}
const planarDistance = requestedPlanarSpeed * dt;
return {
motion: {
x: directionResult.direction.x * planarDistance,
y: 0,
z: directionResult.direction.z * planarDistance
},
inputMagnitude: directionResult.inputMagnitude
};
}
function computePlanarInputDirection(
movementYawRadians: number,
input: PlayerStartActionInputState
): { direction: Vec3 | null; inputMagnitude: number } {
const inputX = input.moveRight - input.moveLeft;
const inputZ = input.moveForward - input.moveBackward;
const rawMagnitude = Math.hypot(inputX, inputZ);
const inputMagnitude = clampUnitInterval(rawMagnitude);
if (rawMagnitude <= 0) {
return {
direction: null,
inputMagnitude
};
}
@@ -212,17 +245,24 @@ function computePlanarMotion(
const forwardZ = Math.cos(movementYawRadians);
const rightX = -Math.cos(movementYawRadians);
const rightZ = Math.sin(movementYawRadians);
const planarDistance = requestedPlanarSpeed * dt;
const directionX =
forwardX * normalizedInputZ + rightX * normalizedInputX;
const directionZ =
forwardZ * normalizedInputZ + rightZ * normalizedInputX;
const directionMagnitude = Math.hypot(directionX, directionZ);
if (directionMagnitude <= 0) {
return {
direction: null,
inputMagnitude
};
}
return {
motion: {
x:
(forwardX * normalizedInputZ + rightX * normalizedInputX) *
planarDistance,
direction: {
x: directionX / directionMagnitude,
y: 0,
z:
(forwardZ * normalizedInputZ + rightZ * normalizedInputX) *
planarDistance
z: directionZ / directionMagnitude
},
inputMagnitude
};
@@ -251,6 +291,48 @@ function clearPlanarMovementInput(
};
}
function computeDirectionalAirMotion(options: {
directionYawRadians: number;
input: PlayerStartActionInputState;
previousPlanarDisplacement: Vec3;
dt: number;
}): { motion: Vec3; inputMagnitude: number } {
const directionResult = computePlanarInputDirection(
options.directionYawRadians,
options.input
);
const planarSpeed = computePlanarSpeedFromDisplacement(
options.previousPlanarDisplacement,
options.dt
);
if (
directionResult.direction === null ||
planarSpeed <= 0 ||
options.dt <= 0
) {
return {
motion: {
x: 0,
y: 0,
z: 0
},
inputMagnitude: directionResult.inputMagnitude
};
}
const planarDistance = planarSpeed * options.dt;
return {
motion: {
x: directionResult.direction.x * planarDistance,
y: 0,
z: directionResult.direction.z * planarDistance
},
inputMagnitude: directionResult.inputMagnitude
};
}
function isWaterLocomotionMode(
locomotionMode: RuntimeLocomotionMode | null | undefined
): boolean {
@@ -502,6 +584,16 @@ export function stepPlayerLocomotion(
const planarInput = airMovementAllowed
? options.input
: clearPlanarMovementInput(options.input);
const directionalAirControlActive =
!currentlyGrounded &&
!jumpTriggered &&
!currentSwimmableWater &&
airMovementAllowed &&
options.movement.jump.directionOnly;
const previousPlanarSpeed = computePlanarSpeedFromDisplacement(
options.previousPlanarDisplacement,
options.dt
);
const requestedPlanarSpeed =
activeShape.mode !== "none" && !currentSwimmableWater
@@ -511,12 +603,20 @@ export function stepPlayerLocomotion(
? groundedRequestedPlanarSpeed
: airborneRequestedPlanarSpeed
: groundedRequestedPlanarSpeed;
const planarMotionFromInput = computePlanarMotion(
options.movementYawRadians,
planarInput,
requestedPlanarSpeed,
options.dt
);
const planarMotionFromInput = directionalAirControlActive
? computeDirectionalAirMotion({
directionYawRadians:
options.airDirectionYawRadians ?? options.movementYawRadians,
input: planarInput,
previousPlanarDisplacement: options.previousPlanarDisplacement,
dt: options.dt
})
: computePlanarMotion(
options.movementYawRadians,
planarInput,
requestedPlanarSpeed,
options.dt
);
const preserveAirborneMomentum =
activeShape.mode !== "none" &&
!currentSwimmableWater &&
@@ -710,11 +810,10 @@ export function stepPlayerLocomotion(
sprinting,
inputMagnitude: planarMotion.inputMagnitude,
requestedPlanarSpeed: preserveAirborneMomentum
? computePlanarSpeedFromDisplacement(
options.previousPlanarDisplacement,
options.dt
)
: requestedPlanarSpeed * planarMotion.inputMagnitude,
? previousPlanarSpeed
: (directionalAirControlActive
? previousPlanarSpeed
: requestedPlanarSpeed) * planarMotion.inputMagnitude,
planarSpeed: actualPlanarSpeed,
verticalVelocity,
contact: resolveContactState(resolvedMotion, groundProbe, grounded)

View File

@@ -287,6 +287,7 @@ function clonePlayerStartJumpSettings(
maxHoldMs: jump.maxHoldMs,
moveWhileJumping: jump.moveWhileJumping,
moveWhileFalling: jump.moveWhileFalling,
directionOnly: jump.directionOnly,
bunnyHop: jump.bunnyHop,
bunnyHopBoost: jump.bunnyHopBoost
};

View File

@@ -79,6 +79,7 @@ function cloneRuntimePlayerMovement(
maxHoldMs: movement.jump.maxHoldMs,
moveWhileJumping: movement.jump.moveWhileJumping,
moveWhileFalling: movement.jump.moveWhileFalling,
directionOnly: movement.jump.directionOnly,
bunnyHop: movement.jump.bunnyHop,
bunnyHopBoost: movement.jump.bunnyHopBoost
},
@@ -285,6 +286,7 @@ export class ThirdPersonNavigationController implements NavigationController {
dt,
feetPosition: this.feetPosition,
movementYawRadians: this.cameraYawRadians,
airDirectionYawRadians: this.yawRadians,
standingShape: this.standingPlayerShape,
verticalVelocity: this.verticalVelocity,
previousLocomotionState: this.locomotionState,