Add air direction only setting for player movement
This commit is contained in:
@@ -10924,6 +10924,29 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Air Direction Only</span>
|
||||
<input
|
||||
data-testid="player-start-movement-air-direction-only-enabled"
|
||||
type="checkbox"
|
||||
checked={
|
||||
playerStartMovementTemplateDraft.jump.directionOnly
|
||||
}
|
||||
onChange={(event) =>
|
||||
commitPlayerStartMovementTemplateDraft(
|
||||
{
|
||||
jump: {
|
||||
directionOnly:
|
||||
event.currentTarget.checked
|
||||
}
|
||||
},
|
||||
{
|
||||
schedule: true
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Bunny Hopping</span>
|
||||
<input
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
MULTI_SCENE_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||
MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION,
|
||||
@@ -1369,6 +1370,11 @@ function readPlayerStartMovementTemplate(value: unknown, label: string) {
|
||||
`${label}.jump.moveWhileFalling`,
|
||||
preset.jump.moveWhileFalling
|
||||
),
|
||||
directionOnly: readOptionalBoolean(
|
||||
jump?.directionOnly,
|
||||
`${label}.jump.directionOnly`,
|
||||
preset.jump.directionOnly
|
||||
),
|
||||
maxHoldMs:
|
||||
jump?.maxHoldMs === undefined
|
||||
? preset.jump.maxHoldMs
|
||||
@@ -2866,6 +2872,7 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
||||
if (
|
||||
source.version !== SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== 33 &&
|
||||
source.version !== PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PROJECT_NAME_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION &&
|
||||
|
||||
@@ -1370,6 +1370,17 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isBoolean(entity.movementTemplate?.jump?.directionOnly)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
"invalid-player-start-air-direction-only",
|
||||
"Player Start air direction only setting must be a boolean.",
|
||||
`${path}.movementTemplate.jump.directionOnly`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isBoolean(entity.movementTemplate?.jump?.bunnyHop)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
type WorldSettings
|
||||
} from "./world-settings";
|
||||
|
||||
export const SCENE_DOCUMENT_VERSION = 34 as const;
|
||||
export const SCENE_DOCUMENT_VERSION = 35 as const;
|
||||
export const PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION =
|
||||
35 as const;
|
||||
export const PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION = 34 as const;
|
||||
export const WHITEBOX_BEVEL_SCENE_DOCUMENT_VERSION = 32 as const;
|
||||
export const PLAYER_START_LOCOMOTION_CORE_SCENE_DOCUMENT_VERSION = 32 as const;
|
||||
|
||||
@@ -149,6 +149,7 @@ export interface PlayerStartJumpSettings {
|
||||
maxHoldMs: number;
|
||||
moveWhileJumping: boolean;
|
||||
moveWhileFalling: boolean;
|
||||
directionOnly: boolean;
|
||||
bunnyHop: boolean;
|
||||
bunnyHopBoost: number;
|
||||
}
|
||||
@@ -305,6 +306,7 @@ export const DEFAULT_PLAYER_START_VARIABLE_JUMP_HEIGHT = false;
|
||||
export const DEFAULT_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS = 160;
|
||||
export const DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING = true;
|
||||
export const DEFAULT_PLAYER_START_MOVE_WHILE_FALLING = true;
|
||||
export const DEFAULT_PLAYER_START_AIR_DIRECTION_ONLY = false;
|
||||
export const DEFAULT_PLAYER_START_BUNNY_HOP = false;
|
||||
export const DEFAULT_PLAYER_START_BUNNY_HOP_BOOST = 0.05;
|
||||
export const DEFAULT_PLAYER_START_SPRINT_SPEED_MULTIPLIER = 1.65;
|
||||
@@ -323,6 +325,7 @@ export const DEFAULT_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
|
||||
maxHoldMs: DEFAULT_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
|
||||
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
|
||||
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
|
||||
directionOnly: DEFAULT_PLAYER_START_AIR_DIRECTION_ONLY,
|
||||
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
|
||||
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
|
||||
};
|
||||
@@ -343,6 +346,7 @@ export const RESPONSIVE_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
|
||||
maxHoldMs: RESPONSIVE_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
|
||||
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
|
||||
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
|
||||
directionOnly: DEFAULT_PLAYER_START_AIR_DIRECTION_ONLY,
|
||||
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
|
||||
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
|
||||
};
|
||||
@@ -523,6 +527,7 @@ function clonePlayerStartJumpSettings(
|
||||
maxHoldMs: settings.maxHoldMs,
|
||||
moveWhileJumping: settings.moveWhileJumping,
|
||||
moveWhileFalling: settings.moveWhileFalling,
|
||||
directionOnly: settings.directionOnly,
|
||||
bunnyHop: settings.bunnyHop,
|
||||
bunnyHopBoost: settings.bunnyHopBoost
|
||||
};
|
||||
@@ -765,6 +770,8 @@ export function createPlayerStartMovementTemplate(
|
||||
overrides.jump?.moveWhileJumping ?? preset.jump.moveWhileJumping,
|
||||
moveWhileFalling:
|
||||
overrides.jump?.moveWhileFalling ?? preset.jump.moveWhileFalling,
|
||||
directionOnly:
|
||||
overrides.jump?.directionOnly ?? preset.jump.directionOnly,
|
||||
bunnyHop: overrides.jump?.bunnyHop ?? preset.jump.bunnyHop,
|
||||
bunnyHopBoost:
|
||||
overrides.jump?.bunnyHopBoost ?? preset.jump.bunnyHopBoost
|
||||
@@ -821,6 +828,10 @@ export function createPlayerStartMovementTemplate(
|
||||
jump.moveWhileFalling,
|
||||
"Player Start move while falling setting"
|
||||
);
|
||||
assertBoolean(
|
||||
jump.directionOnly,
|
||||
"Player Start air direction only setting"
|
||||
);
|
||||
assertBoolean(jump.bunnyHop, "Player Start bunny hop setting");
|
||||
assertNonNegativeFiniteNumber(
|
||||
jump.bunnyHopBoost,
|
||||
@@ -893,6 +904,8 @@ export function inferPlayerStartMovementTemplateKind(
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileJumping &&
|
||||
candidate.jump.moveWhileFalling ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileFalling &&
|
||||
candidate.jump.directionOnly ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.directionOnly &&
|
||||
candidate.jump.bunnyHop ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHop &&
|
||||
candidate.jump.bunnyHopBoost ===
|
||||
@@ -951,6 +964,7 @@ export function arePlayerStartMovementTemplatesEqual(
|
||||
left.jump.maxHoldMs === right.jump.maxHoldMs &&
|
||||
left.jump.moveWhileJumping === right.jump.moveWhileJumping &&
|
||||
left.jump.moveWhileFalling === right.jump.moveWhileFalling &&
|
||||
left.jump.directionOnly === right.jump.directionOnly &&
|
||||
left.jump.bunnyHop === right.jump.bunnyHop &&
|
||||
left.jump.bunnyHopBoost === right.jump.bunnyHopBoost &&
|
||||
left.sprint.speedMultiplier === right.sprint.speedMultiplier &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -287,6 +287,7 @@ function clonePlayerStartJumpSettings(
|
||||
maxHoldMs: jump.maxHoldMs,
|
||||
moveWhileJumping: jump.moveWhileJumping,
|
||||
moveWhileFalling: jump.moveWhileFalling,
|
||||
directionOnly: jump.directionOnly,
|
||||
bunnyHop: jump.bunnyHop,
|
||||
bunnyHopBoost: jump.bunnyHopBoost
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user