tengri/Content/Movement/Components/AC_Movement.ts

615 lines
19 KiB
TypeScript

// Movement/Components/AC_Movement.ts
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
import { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Movement System Component
* Core deterministic movement system for 3D platformer
* Handles surface classification and movement physics calculations
*/
export class AC_Movement extends ActorComponent {
// ════════════════════════════════════════════════════════════════════════════════════════
// FUNCTIONS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Classify surface type based on normal vector
* @param SurfaceNormal - Normalized surface normal vector
* @param AngleThresholds - Angle thresholds in radians
* @returns Surface type classification
* @example
* // Classify flat ground
* ClassifySurface(new Vector(0,0,1), thresholds) // returns E_SurfaceType.Walkable
* @pure true
* @category Surface Detection
*/
public ClassifySurface(
SurfaceNormal: Vector,
AngleThresholds: S_AngleThresholds
): E_SurfaceType {
const SurfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
/**
* Check if angle is within walkable range
*/
const IsWalkableAngle = (walkableAngle: Float): boolean =>
SurfaceAngle <= walkableAngle;
/**
* Check if angle is within steep slope range
*/
const IsSteepSlopeAngle = (steepSlopeAngle: Float): boolean =>
SurfaceAngle <= steepSlopeAngle;
/**
* Check if angle is within wall range
*/
const IsWallAngle = (wallAngle: Float): boolean =>
SurfaceAngle <= wallAngle;
if (IsWalkableAngle(AngleThresholds.Walkable)) {
return E_SurfaceType.Walkable;
} else if (IsSteepSlopeAngle(AngleThresholds.SteepSlope)) {
return E_SurfaceType.SteepSlope;
} else if (IsWallAngle(AngleThresholds.Wall)) {
return E_SurfaceType.Wall;
} else {
return E_SurfaceType.Ceiling;
}
}
/**
* Check if surface allows normal walking movement
* @param SurfaceType - Surface type to check
* @returns True if surface is walkable
* @pure true
* @category Surface Detection
*/
private IsSurfaceWalkable(SurfaceType: E_SurfaceType): boolean {
return SurfaceType === E_SurfaceType.Walkable;
}
/**
* Check if surface causes sliding behavior
* @param SurfaceType - Surface type to check
* @returns True if surface is steep slope
* @pure true
* @category Surface Detection
*/
private IsSurfaceSteep(SurfaceType: E_SurfaceType): boolean {
return SurfaceType === E_SurfaceType.SteepSlope;
}
/**
* Check if surface blocks movement (collision)
* @param SurfaceType - Surface type to check
* @returns True if surface is a wall
* @pure true
* @category Surface Detection
*/
private IsSurfaceWall(SurfaceType: E_SurfaceType): boolean {
return SurfaceType === E_SurfaceType.Wall;
}
/**
* Check if surface is overhead (ceiling)
* @param SurfaceType - Surface type to check
* @returns True if surface is ceiling
* @pure true
* @category Surface Detection
*/
private IsSurfaceCeiling(SurfaceType: E_SurfaceType): boolean {
return SurfaceType === E_SurfaceType.Ceiling;
}
/**
* Check if no surface detected (airborne state)
* @param SurfaceType - Surface type to check
* @returns True if no surface contact
* @pure true
* @category Surface Detection
*/
private IsSurfaceNone(SurfaceType: E_SurfaceType): boolean {
return SurfaceType === E_SurfaceType.None;
}
/**
* Process movement input from player controller
* Normalizes input and calculates target velocity
* @param InputVector - Raw input from WASD/gamepad stick
* @param DeltaTime - Time since last frame for frame-rate independence
* @category Movement Processing
*/
public ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void {
if (this.IsInitialized) {
this.InputMagnitude = MathLibrary.VectorLength(InputVector);
this.TargetRotation = this.CalculateTargetRotation(InputVector);
this.UpdateCharacterRotation(DeltaTime);
// Only process movement on walkable surfaces
if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) {
this.ProcessGroundMovement(InputVector, DeltaTime);
} else {
this.ApplyFriction(DeltaTime);
}
// Always apply gravity
this.ApplyGravity(DeltaTime);
// Update movement state
this.UpdateMovementState();
// Calculate current speed for debug
this.UpdateCurrentSpeed();
}
}
/**
* Process ground-based movement with acceleration and max speed limits
* @param InputVector - Normalized input direction
* @param DeltaTime - Frame delta time
* @category Movement Processing
*/
private ProcessGroundMovement(InputVector: Vector, DeltaTime: Float): void {
if (this.InputMagnitude > 0.01) {
const CalculateTargetVelocity = (
inputVector: Vector,
maxSpeed: Float
): Vector =>
new Vector(
MathLibrary.Normal(inputVector).X * maxSpeed,
MathLibrary.Normal(inputVector).Y * maxSpeed,
MathLibrary.Normal(inputVector).Z * maxSpeed
);
this.CurrentVelocity = MathLibrary.VInterpTo(
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0),
CalculateTargetVelocity(InputVector, this.MaxSpeed),
DeltaTime,
this.Acceleration
);
} else {
// Apply friction when no input
this.ApplyFriction(DeltaTime);
}
}
/**
* Apply friction to horizontal velocity
* @param DeltaTime - Frame delta time
* @category Movement Processing
*/
private ApplyFriction(DeltaTime: Float): void {
this.CurrentVelocity = MathLibrary.VInterpTo(
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0),
new Vector(0, 0, this.CurrentVelocity.Z),
DeltaTime,
this.Friction
);
}
/**
* Apply gravity to vertical velocity
* @param DeltaTime - Frame delta time
* @category Movement Processing
*/
private ApplyGravity(DeltaTime: Float): void {
if (!this.IsGrounded) {
const ApplyGravityForce = (
velocityZ: Float,
gravity: Float,
deltaTime: Float
): Float => velocityZ - gravity * deltaTime;
// Apply gravity when airborne
this.CurrentVelocity = new Vector(
this.CurrentVelocity.X,
this.CurrentVelocity.Y,
ApplyGravityForce(this.CurrentVelocity.Z, this.Gravity, DeltaTime)
);
} else {
// Zero out vertical velocity when grounded
this.CurrentVelocity = new Vector(
this.CurrentVelocity.X,
this.CurrentVelocity.Y,
0
);
}
}
/**
* Update movement state based on current conditions
* @category Movement Processing
*/
private UpdateMovementState(): void {
if (!this.IsGrounded) {
this.MovementState = E_MovementState.Airborne;
} else if (this.InputMagnitude > 0.01) {
this.MovementState = E_MovementState.Walking;
} else {
this.MovementState = E_MovementState.Idle;
}
}
/**
* Update current speed for debug display
* @category Movement Processing
*/
private UpdateCurrentSpeed(): void {
// Calculate horizontal speed only (ignore vertical component)
this.CurrentSpeed = MathLibrary.VectorLength(
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0)
);
}
/**
* Calculate target rotation based on movement direction
* Determines what rotation character should have based on movement
* @param MovementDirection - Camera-relative movement direction from Blueprint
* @returns Target rotation for character
* @pure true
* @category Character Rotation
*/
public CalculateTargetRotation(MovementDirection: Vector): Rotator {
const TargetYaw = (
movementDirectionX: Float,
movementDirectionY: Float
): Float =>
MathLibrary.RadiansToDegrees(
MathLibrary.Atan2(movementDirectionY, movementDirectionX)
);
if (MathLibrary.VectorLength(MovementDirection) < 0.01) {
// No movement, maintain current target rotation
return this.TargetRotation;
}
// Character should remain level (pitch = 0, roll = 0, only yaw changes)
return new Rotator(
0,
TargetYaw(MovementDirection.X, MovementDirection.Y),
0
);
}
/**
* Update character rotation towards target
* Uses instant rotation approach similar to Zelda: BotW
* @param DeltaTime - Time since last frame
* @category Character Rotation
*/
public UpdateCharacterRotation(DeltaTime: Float): void {
if (this.ShouldRotateToMovement) {
const ShouldRotate = (): boolean =>
this.CurrentSpeed >= this.MinSpeedForRotation;
if (ShouldRotate()) {
let yawDifference = this.CurrentRotation.yaw - this.TargetRotation.yaw;
if (yawDifference > 180) {
yawDifference = yawDifference - 360;
} else if (yawDifference < -180) {
yawDifference = yawDifference + 360;
}
const NewYaw = (
currentYaw: Float,
clampedRotationAmount: Float,
coef: Float
): Float => currentYaw + clampedRotationAmount * coef;
const ClampedRotationAmount = (deltaTime: Float): Float =>
MathLibrary.Min(
this.RotationSpeed * deltaTime,
MathLibrary.abs(yawDifference)
);
this.RotationDelta = MathLibrary.abs(yawDifference);
if (this.RotationDelta > 1) {
this.IsCharacterRotating = true;
this.CurrentRotation = new Rotator(
0,
NewYaw(
this.CurrentRotation.yaw,
ClampedRotationAmount(DeltaTime),
yawDifference > 0 ? -1 : 1
),
0
);
} else {
this.IsCharacterRotating = false;
this.CurrentRotation = this.TargetRotation;
}
} else {
this.IsCharacterRotating = false;
this.RotationDelta = 0.0;
}
}
}
/**
* Initialize movement system with angle conversion
* Converts degree thresholds to radians for runtime performance
* @category System
*/
public InitializeMovementSystem(
DebugHUDComponentRef: AC_DebugHUD | null
): void {
this.DebugHUDComponent = DebugHUDComponentRef;
this.IsInitialized = true;
this.AngleThresholdsRads = {
Walkable: MathLibrary.DegreesToRadians(
this.AngleThresholdsDegrees.Walkable
),
SteepSlope: MathLibrary.DegreesToRadians(
this.AngleThresholdsDegrees.SteepSlope
),
Wall: MathLibrary.DegreesToRadians(this.AngleThresholdsDegrees.Wall),
};
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
this.DebugHUDComponent.AddDebugPage(
this.DebugPageID,
'Movement Info',
60
);
}
}
/**
* Update debug HUD with current movement info
* @category Debug
*/
public UpdateDebugPage(): void {
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
if (
this.DebugHUDComponent.ShouldUpdatePage(
this.DebugPageID,
SystemLibrary.GetGameTimeInSeconds()
)
) {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
// Constants
`Max Speed: ${this.MaxSpeed}\n` +
`Acceleration: ${this.Acceleration}\n` +
`Friction: ${this.Friction}\n` +
`Gravity: ${this.Gravity}\n` +
`Initialized: ${this.IsInitialized}\n` +
`\n` +
// Current State
`Current Velocity: ${StringLibrary.ConvVectorToString(this.CurrentVelocity)}\n` +
`Speed: ${this.CurrentSpeed}\n` +
`Is Grounded: ${this.IsGrounded}\n` +
`Surface Type: ${this.CurrentSurface}\n` +
`Movement State: ${this.MovementState}\n` +
`Input Magnitude: ${this.InputMagnitude}` +
`\n` +
// Rotation
`Current Yaw: ${this.CurrentRotation.yaw}\n` +
`Target Yaw: ${this.TargetRotation.yaw}\` +
`Rotation Delta: ${this.RotationDelta}\` +
`Is Rotating: ${this.IsCharacterRotating}\n` +
`Rotation Speed: ${this.RotationSpeed}\` +
`Min Speed: ${this.MinSpeedForRotation}`
);
}
}
}
/**
* Get movement configuration data for testing
* Provides read-only access to private movement constant
* @returns Object containing MaxSpeed configuration value
* @category Testing
* @pure true
*/
public GetTestData(): {
MaxSpeed: Float;
} {
return {
MaxSpeed: this.MaxSpeed,
};
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Maximum horizontal movement speed in UE units per second
* Character cannot exceed this speed through ground movement
* Used as target velocity cap in ProcessGroundMovement
* @default 600.0
* @category Movement Config
* @instanceEditable true
*/
private readonly MaxSpeed: Float = 600.0;
/**
* Speed of velocity interpolation towards target velocity
* Higher values = faster acceleration, more responsive feel
* Used with VInterpTo for smooth acceleration curves
* Value represents interpolation speed, not actual acceleration rate
* @default 10.0
* @category Movement Config
* @instanceEditable true
*/
private readonly Acceleration: Float = 10.0;
/**
* Speed of velocity interpolation towards zero when no input
* Higher values = faster stopping, less sliding
* Used with VInterpTo for smooth deceleration curves
* Should typically be <= Acceleration for natural feel
* @default 8.0
* @category Movement Config
* @instanceEditable true
*/
private readonly Friction: Float = 8.0;
/**
* Gravitational acceleration in UE units per second squared
* Applied to vertical velocity when character is airborne
* Standard Earth gravity ≈ 980 cm/s² in UE units
* Only affects Z-axis velocity, horizontal movement unaffected
* @default 980.0
* @category Movement Config
* @instanceEditable true
*/
private readonly Gravity: Float = 980.0;
/**
* Surface classification angle thresholds in degrees
* Walkable ≤50°, SteepSlope ≤85°, Wall ≤95°, Ceiling >95°
* @category Movement Config
* @instanceEditable true
*/
public readonly AngleThresholdsDegrees: S_AngleThresholds = {
Walkable: 50.0,
SteepSlope: 85.0,
Wall: 95.0,
};
/**
* Runtime cached angle thresholds in radians
* Converted from degrees during initialization for performance
* @category Internal Cache
*/
private AngleThresholdsRads: S_AngleThresholds = {
Walkable: 0.0,
SteepSlope: 0.0,
Wall: 0.0,
};
/**
* Flag indicating if movement system has been initialized
* Ensures angle thresholds are converted before use
* @category Debug
*/
public IsInitialized = false;
/**
* Debug page identifier for organizing debug output
* Used by debug HUD to categorize information
* @category Debug
*/
public readonly DebugPageID: string = 'MovementInfo';
/**
* Current character velocity in world space
* Updated every frame by movement calculations
* @category Movement State
*/
public CurrentVelocity: Vector = new Vector(0, 0, 0);
/**
* Ground contact state - true when character is on walkable surface
* Used for gravity application and movement restrictions
* @category Movement State
*/
public IsGrounded: boolean = true;
/**
* Type of surface currently under character
* Determines available movement options
* @category Movement State
*/
public CurrentSurface: E_SurfaceType = E_SurfaceType.Walkable;
/**
* Current movement state of character
* Used for animation and game logic decisions
* @category Movement State
*/
public MovementState: E_MovementState = E_MovementState.Idle;
/**
* Magnitude of current movement input (0-1)
* Used for determining if character should be moving
* @category Movement State
*/
public InputMagnitude: Float = 0.0;
/**
* Current movement speed (magnitude of horizontal velocity)
* Calculated from CurrentVelocity for debug display
* @category Movement State
*/
public CurrentSpeed: Float = 0.0;
/**
* Rotation speed in degrees per second
* Controls how fast character rotates towards movement direction
* @category Character Rotation Config
* @instanceEditable true
*/
public RotationSpeed: Float = 720.0;
/**
* Should character rotate when moving
* Allows disabling rotation system if needed
* @category Character Rotation Config
* @instanceEditable true
*/
public ShouldRotateToMovement: boolean = true;
/**
* Minimum movement speed to trigger rotation
* Prevents character from rotating during very slow movement
* @category Character Rotation Config
* @instanceEditable true
*/
public MinSpeedForRotation: Float = 50.0;
/**
* Current character rotation
* Used for smooth interpolation to target rotation
* @category Character Rotation State
*/
public CurrentRotation: Rotator = new Rotator(0, 0, 0);
/**
* Target character rotation
* Calculated based on movement direction and camera orientation
* @category Character Rotation State
*/
public TargetRotation: Rotator = new Rotator(0, 0, 0);
/**
* Whether character is currently rotating
* Used for animation and debug purposes
* @category Character Rotation State
*/
public IsCharacterRotating: boolean = false;
/**
* Angular difference between current and target rotation
* Used for determining rotation completion and debug display
* @category Character Rotation State
*/
public RotationDelta: Float = 0.0;
/**
* Reference to debug HUD component for displaying camera info
* Optional, used for debugging purposes
* @category Components
*/
public DebugHUDComponent: AC_DebugHUD | null = null;
}