// 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 { CapsuleComponent } from '#root/UE/CapsuleComponent.ts'; import { EDrawDebugTrace } from '#root/UE/EDrawDebugTrace.ts'; import { ETraceTypeQuery } from '#root/UE/ETraceTypeQuery.ts'; import type { Float } from '#root/UE/Float.ts'; import { HitResult } from '#root/UE/HitResult.ts'; import type { Integer } from '#root/UE/Integer.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 // ════════════════════════════════════════════════════════════════════════════════════════ // ──────────────────────────────────────────────────────────────────────────────────────── // Surface Detection // ──────────────────────────────────────────────────────────────────────────────────────── /** * Classify surface type based on normal vector * @param SurfaceNormal - Normalized surface normal vector * @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): 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(this.AngleThresholdsRads.Walkable)) { return E_SurfaceType.Walkable; } else if (IsSteepSlopeAngle(this.AngleThresholdsRads.SteepSlope)) { return E_SurfaceType.SteepSlope; } else if (IsWallAngle(this.AngleThresholdsRads.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; } // ──────────────────────────────────────────────────────────────────────────────────────── // Movement Processing // ──────────────────────────────────────────────────────────────────────────────────────── /** * 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); this.LastGroundHit = this.CheckGround(); this.IsGrounded = this.LastGroundHit.BlockingHit; // Only process movement on walkable surfaces if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) { this.ProcessGroundMovement(InputVector, DeltaTime); } else { this.ApplyFriction(DeltaTime); } this.ApplyGravity(); this.UpdateMovementState(); this.UpdateCurrentSpeed(); this.ApplyMovementWithSweep(DeltaTime); } } /** * 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): Vector => new Vector( MathLibrary.Normal(inputVector).X * this.MaxSpeed, MathLibrary.Normal(inputVector).Y * this.MaxSpeed, MathLibrary.Normal(inputVector).Z * this.MaxSpeed ); this.CurrentVelocity = MathLibrary.VInterpTo( new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0), CalculateTargetVelocity(InputVector), 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 * @category Movement Processing */ private ApplyGravity(): void { if (!this.IsGrounded) { const ApplyGravityForce = (velocityZ: Float): Float => velocityZ - this.Gravity; // Apply gravity when airborne this.CurrentVelocity = new Vector( this.CurrentVelocity.X, this.CurrentVelocity.Y, ApplyGravityForce(this.CurrentVelocity.Z) ); } 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) ); } // ──────────────────────────────────────────────────────────────────────────────────────── // Movement Application // ──────────────────────────────────────────────────────────────────────────────────────── /** * Apply movement with deterministic sweep collision detection * Replaces direct position update with collision-safe movement * @param DeltaTime - Frame delta time * @category Movement Application * @pure false - modifies actor position */ private ApplyMovementWithSweep(DeltaTime: Float): void { // Get current actor location const currentLocation = this.GetOwner().GetActorLocation(); // Calculate desired movement delta const desiredDelta = new Vector( this.CurrentVelocity.X * DeltaTime, this.CurrentVelocity.Y * DeltaTime, this.CurrentVelocity.Z * DeltaTime ); // Perform swept movement const SweepResult = this.PerformDeterministicSweep( currentLocation, desiredDelta, DeltaTime ); // Apply final position if (SweepResult.BlockingHit) { // Hit something - use safe hit location this.GetOwner().SetActorLocation(SweepResult.Location); // Handle collision response (slide along surface) const CalculateRemainingDelta = (sweepResultLocation: Vector): Vector => new Vector( desiredDelta.X - (sweepResultLocation.X - currentLocation.X), desiredDelta.Y - (sweepResultLocation.Y - currentLocation.Y), desiredDelta.Z - (sweepResultLocation.Z - currentLocation.Z) ); const SlideVector = this.HandleSweepCollision( SweepResult, CalculateRemainingDelta(SweepResult.Location) ); // Apply slide movement if significant if (MathLibrary.VectorLength(SlideVector) > 0.01) { this.GetOwner().SetActorLocation( new Vector( SweepResult.Location.X + SlideVector.X, SweepResult.Location.Y + SlideVector.Y, SweepResult.Location.Z + SlideVector.Z ) ); } } else { // No collision - use final sweep location this.GetOwner().SetActorLocation(SweepResult.Location); } const ShouldSnapToGround = ( isGroundHit: boolean, currentVelocityZ: Float, isCapsuleValid: boolean ): boolean => this.IsGrounded && isGroundHit && currentVelocityZ <= 0 && isCapsuleValid; if ( ShouldSnapToGround( this.LastGroundHit.BlockingHit, this.CurrentVelocity.Z, SystemLibrary.IsValid(this.CapsuleComponent) ) ) { const correctZ = this.LastGroundHit.Location.Z + this.CapsuleComponent!.GetScaledCapsuleHalfHeight(); const CalculateZDifference = (currentLocZ: Float): Float => currentLocZ - correctZ; const zDifference = CalculateZDifference(currentLocation.Z); const IsWithinSnapRange = (): boolean => zDifference > 0.1 && zDifference < this.GroundTraceDistance; if (IsWithinSnapRange()) { this.GetOwner().SetActorLocation( new Vector(currentLocation.X, currentLocation.Y, correctZ) ); } } } // ──────────────────────────────────────────────────────────────────────────────────────── // Character Rotation // ──────────────────────────────────────────────────────────────────────────────────────── /** * 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 */ private 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 */ private 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; } } } // ──────────────────────────────────────────────────────────────────────────────────────── // Collision State // ──────────────────────────────────────────────────────────────────────────────────────── /** * Reset collision counter at start of frame * Called before movement processing * @category Collision State * @pure false - modifies SweepCollisionCount */ private ResetCollisionCounter(): void { this.SweepCollisionCount = 0; } // ──────────────────────────────────────────────────────────────────────────────────────── // Collision Detection // ──────────────────────────────────────────────────────────────────────────────────────── /** * Calculate adaptive step size based on velocity * Fast movement = smaller steps, slow movement = larger steps * @param Velocity - Current movement velocity * @param DeltaTime - Frame delta time * @returns Step size clamped between MinStepSize and MaxStepSize * @category Collision Detection * @pure true - pure calculation based on inputs */ private CalculateAdaptiveStepSize(Velocity: Vector, DeltaTime: Float): Float { const CalculateFrameDistance = ( VelocityX: Float, VelocityY: Float, deltaTime: Float ): Float => MathLibrary.VectorLength(new Vector(VelocityX, VelocityY)) * deltaTime; const frameDistance = CalculateFrameDistance( Velocity.X, Velocity.Y, DeltaTime ); const IsMovingTooSlow = (): boolean => frameDistance < this.MinStepSize; if (IsMovingTooSlow()) { return this.MaxStepSize; } else { const CalculateClampedStepSize = (): Float => MathLibrary.ClampFloat( frameDistance * 0.5, this.MinStepSize, this.MaxStepSize ); return CalculateClampedStepSize(); } } /** * Perform deterministic sweep collision detection with stepped checks * Prevents tunneling by breaking movement into smaller steps * @param StartLocation - Starting position for sweep * @param DesiredDelta - Desired movement vector * @param DeltaTime - Frame delta time for adaptive stepping * @returns HitResult with collision information (bBlockingHit, Location, Normal, etc.) * @category Collision Detection */ private PerformDeterministicSweep( StartLocation: Vector, DesiredDelta: Vector, DeltaTime: Float ): HitResult { // Reset collision counter for this frame this.ResetCollisionCounter(); // Calculate total distance to travel const totalDistance = MathLibrary.VectorLength(DesiredDelta); const ShouldPerformSweep = (isValid: boolean): boolean => isValid && totalDistance >= 0.01; if (ShouldPerformSweep(SystemLibrary.IsValid(this.CapsuleComponent))) { // Calculate adaptive step size based on velocity const stepSize = this.CalculateAdaptiveStepSize( this.CurrentVelocity, DeltaTime ); // Current position during sweep let currentLocation = StartLocation; let remainingDistance = totalDistance; const CalculateNumSteps = (): Integer => MathLibrary.Min( MathLibrary.Ceil(totalDistance / stepSize), this.MaxCollisionChecks ); // Perform stepped sweep for (let i = 0; i < CalculateNumSteps(); i++) { this.SweepCollisionCount++; const CalculateStepSize = (): Float => MathLibrary.Min(stepSize, remainingDistance); // Calculate step distance (last step might be shorter) const currentStepSize = CalculateStepSize(); const CalculateStepTarget = (desiredDelta: Vector): Vector => { const normalizedDelta = MathLibrary.Normal(desiredDelta); return new Vector( currentLocation.X + normalizedDelta.X * currentStepSize, currentLocation.Y + normalizedDelta.Y * currentStepSize, currentLocation.Z + normalizedDelta.Z * currentStepSize ); }; // Calculate target position for this step const targetLocation = CalculateStepTarget(DesiredDelta); const { OutHit, ReturnValue } = SystemLibrary.CapsuleTraceByChannel( currentLocation, targetLocation, this.CapsuleComponent!.GetScaledCapsuleRadius(), this.CapsuleComponent!.GetScaledCapsuleHalfHeight(), ETraceTypeQuery.Visibility, false, [], EDrawDebugTrace.ForDuration ); if (ReturnValue) { return OutHit; } else { currentLocation = targetLocation; remainingDistance = remainingDistance - currentStepSize; if (remainingDistance <= 0.01) { break; } } } const NoHit = new HitResult(); NoHit.Location = currentLocation; return NoHit; } else { // If no movement, return empty hit const NoHit = new HitResult(); NoHit.Location = StartLocation; return NoHit; } } // ──────────────────────────────────────────────────────────────────────────────────────── // Collision Response // ──────────────────────────────────────────────────────────────────────────────────────── /** * Handle collision response after sweep hit * Calculates slide vector along collision surface * @param HitResult - Collision information from sweep * @param RemainingDelta - Remaining movement after collision * @returns Adjusted movement vector sliding along surface * @category Collision Response * @pure true */ private HandleSweepCollision( HitResult: HitResult, RemainingDelta: Vector ): Vector { if (HitResult.BlockingHit) { const ProjectOntoSurface = ( hitNormal: Vector, remainingDelta: Vector ): Vector => { // Project remaining movement onto collision surface const dotProduct = MathLibrary.Dot(hitNormal, remainingDelta); return new Vector( remainingDelta.X - dotProduct * hitNormal.X, remainingDelta.Y - dotProduct * hitNormal.Y, remainingDelta.Z - dotProduct * hitNormal.Z ); }; return ProjectOntoSurface(HitResult.ImpactNormal, RemainingDelta); } else { // If no collision, return original delta return RemainingDelta; } } // ──────────────────────────────────────────────────────────────────────────────────────── // Ground Detection // ──────────────────────────────────────────────────────────────────────────────────────── /** * Check if character is standing on walkable ground * @returns HitResult with ground information, or null if not grounded * @category Ground Detection */ private CheckGround(): HitResult { if (SystemLibrary.IsValid(this.CapsuleComponent)) { const StartLocation = new Vector( this.GetOwner().GetActorLocation().X, this.GetOwner().GetActorLocation().Y, this.GetOwner().GetActorLocation().Z - this.CapsuleComponent.GetScaledCapsuleHalfHeight() ); const EndLocation = new Vector( StartLocation.X, StartLocation.Y, StartLocation.Z - this.GroundTraceDistance ); const { OutHit: groundHit, ReturnValue } = SystemLibrary.LineTraceByChannel( StartLocation, EndLocation, ETraceTypeQuery.Visibility, false, [], EDrawDebugTrace.ForDuration ); if (ReturnValue) { if ( this.ClassifySurface(groundHit.ImpactNormal) === E_SurfaceType.Walkable ) { return groundHit; } else { return new HitResult(); } } else { return new HitResult(); } } else { return new HitResult(); } } // ──────────────────────────────────────────────────────────────────────────────────────── // System // ──────────────────────────────────────────────────────────────────────────────────────── /** * Initialize movement system with angle conversion * Converts degree thresholds to radians for runtime performance * @category System */ public InitializeMovementSystem( CapsuleComponentRef: CapsuleComponent | null = null, DebugHUDComponentRef: AC_DebugHUD | null = null ): void { this.CapsuleComponent = CapsuleComponentRef; 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 ); } } // ──────────────────────────────────────────────────────────────────────────────────────── // Debug // ──────────────────────────────────────────────────────────────────────────────────────── /** * 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}\n°` + `Rotation Delta: ${this.RotationDelta}\n°` + `Is Rotating: ${this.IsCharacterRotating}\n` + `Rotation Speed: ${this.RotationSpeed}\n°` + `Min Speed: ${this.MinSpeedForRotation}` + `\n` + // Position `Location: ${StringLibrary.ConvVectorToString(this.GetOwner().GetActorLocation())}` + `\n` + // Sweep Collision `Collision Checks: ${this.SweepCollisionCount}/${this.MaxCollisionChecks}\n` + `Ground Distance: ${this.GroundTraceDistance} cm` ); } } } // ──────────────────────────────────────────────────────────────────────────────────────── // Public Interface // ──────────────────────────────────────────────────────────────────────────────────────── /** * Get maximum horizontal movement speed * @returns Maximum speed in UE units per second * @category Public Interface * @pure true */ public GetMaxSpeed(): Float { return this.MaxSpeed; } /** * Get current character velocity in world space * @returns Current velocity vector (cm/s) * @category Public Interface * @pure true */ public GetCurrentVelocity(): Vector { return this.CurrentVelocity; } /** * Get current movement state * @returns Current state (Idle/Walking/Airborne) * @category Public Interface * @pure true */ public GetMovementState(): E_MovementState { return this.MovementState; } /** * Get current horizontal movement speed * @returns Speed magnitude in UE units per second (ignores Z component) * @category Public Interface * @pure true */ public GetCurrentSpeed(): Float { return this.CurrentSpeed; } /** * Get current character rotation * @returns Current yaw rotation (pitch and roll are always 0) * @category Public Interface * @pure true */ public GetCurrentRotation(): Rotator { return this.CurrentRotation; } /** * Check if movement system has been initialized * @returns True if InitializeMovementSystem() has been called * @category Public Interface * @pure true */ public GetIsInitialized(): boolean { return this.IsInitialized; } // ════════════════════════════════════════════════════════════════════════════════════════ // VARIABLES // ════════════════════════════════════════════════════════════════════════════════════════ // ──────────────────────────────────────────────────────────────────────────────────────── // Movement Config // ──────────────────────────────────────────────────────────────────────────────────────── /** * 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 = 800.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, }; // ──────────────────────────────────────────────────────────────────────────────────────── // Movement State // ──────────────────────────────────────────────────────────────────────────────────────── /** * Current character velocity in world space * Updated every frame by movement calculations * @category Movement State */ private 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 */ private IsGrounded: boolean = true; /** * Type of surface currently under character * Determines available movement options * @category Movement State */ private CurrentSurface: E_SurfaceType = E_SurfaceType.Walkable; /** * Current movement state of character * Used for animation and game logic decisions * @category Movement State */ private MovementState: E_MovementState = E_MovementState.Idle; /** * Magnitude of current movement input (0-1) * Used for determining if character should be moving * @category Movement State */ private InputMagnitude: Float = 0.0; /** * Current movement speed (magnitude of horizontal velocity) * Calculated from CurrentVelocity for debug display * @category Movement State */ private CurrentSpeed: Float = 0.0; // ──────────────────────────────────────────────────────────────────────────────────────── // Character Rotation Config // ──────────────────────────────────────────────────────────────────────────────────────── /** * Rotation speed in degrees per second * Controls how fast character rotates towards movement direction * @category Character Rotation Config * @instanceEditable true */ private readonly RotationSpeed: Float = 720.0; /** * Should character rotate when moving * Allows disabling rotation system if needed * @category Character Rotation Config * @instanceEditable true */ private readonly ShouldRotateToMovement: boolean = true; /** * Minimum movement speed to trigger rotation * Prevents character from rotating during very slow movement * @category Character Rotation Config * @instanceEditable true */ private readonly MinSpeedForRotation: Float = 50.0; // ──────────────────────────────────────────────────────────────────────────────────────── // Character Rotation State // ──────────────────────────────────────────────────────────────────────────────────────── /** * Current character rotation * Used for smooth interpolation to target rotation * @category Character Rotation State */ private CurrentRotation: Rotator = new Rotator(0, 0, 0); /** * Target character rotation * Calculated based on movement direction and camera orientation * @category Character Rotation State */ private TargetRotation: Rotator = new Rotator(0, 0, 0); /** * Whether character is currently rotating * Used for animation and debug purposes * @category Character Rotation State */ private IsCharacterRotating: boolean = false; /** * Angular difference between current and target rotation * Used for determining rotation completion and debug display * @category Character Rotation State */ private RotationDelta: Float = 0.0; // ──────────────────────────────────────────────────────────────────────────────────────── // Collision Config // ──────────────────────────────────────────────────────────────────────────────────────── /** * Maximum step size for sweep collision detection * @category Collision Config * @instanceEditable true */ private readonly MaxStepSize: Float = 50.0; /** * Minimum step size for sweep collision detection * @category Collision Config * @instanceEditable true */ private readonly MinStepSize: Float = 1.0; /** * Maximum collision checks allowed per frame * @category Collision Config * @instanceEditable true */ private readonly MaxCollisionChecks: number = 25; // ──────────────────────────────────────────────────────────────────────────────────────── // Collision State // ──────────────────────────────────────────────────────────────────────────────────────── /** * Current frame collision check counter * @category Collision State */ private SweepCollisionCount: number = 0; // ──────────────────────────────────────────────────────────────────────────────────────── // Ground Detection Config // ──────────────────────────────────────────────────────────────────────────────────────── /** * Distance to trace downward for ground detection * Larger values detect ground earlier but may cause "magnet" effect * @category Ground Detection Config * @instanceEditable true * @default 5.0 */ private readonly GroundTraceDistance: Float = 5.0; // ──────────────────────────────────────────────────────────────────────────────────────── // Ground Detection State // ──────────────────────────────────────────────────────────────────────────────────────── /** * Last ground hit result for ground snapping * @category Ground Detection State */ private LastGroundHit: HitResult = new HitResult(); // ──────────────────────────────────────────────────────────────────────────────────────── // InternalCache // ──────────────────────────────────────────────────────────────────────────────────────── /** * 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, }; // ──────────────────────────────────────────────────────────────────────────────────────── // Debug // ──────────────────────────────────────────────────────────────────────────────────────── /** * Flag indicating if movement system has been initialized * Ensures angle thresholds are converted before use * @category Debug */ private IsInitialized = false; /** * Debug page identifier for organizing debug output * Used by debug HUD to categorize information * @category Debug * @instanceEditable true */ private readonly DebugPageID: string = 'MovementInfo'; // ──────────────────────────────────────────────────────────────────────────────────────── // Components // ──────────────────────────────────────────────────────────────────────────────────────── /** * Reference to debug HUD component for displaying camera info * Optional, used for debugging purposes * @category Components */ private DebugHUDComponent: AC_DebugHUD | null = null; /** * Reference to character's capsule component for collision detection * @category Components */ private CapsuleComponent: CapsuleComponent | null = null; }