// Movement/Collision/BFL_CollisionResolver.ts import type { S_SweepResult } from '#root/Movement/Collision/S_SweepResult.ts'; import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts'; import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.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 { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { Vector } from '#root/UE/Vector.ts'; /** * Collision Resolution System * * Handles swept collision detection and surface sliding * Prevents tunneling through geometry with adaptive stepping * Provides deterministic collision response * * @category Movement Collision * @impure Uses SystemLibrary traces (reads world state) */ class BFL_CollisionResolverClass extends BlueprintFunctionLibrary { // ════════════════════════════════════════════════════════════════════════════════════════ // SWEEP COLLISION // ════════════════════════════════════════════════════════════════════════════════════════ /** * Perform deterministic swept collision detection * Breaks movement into adaptive steps to prevent tunneling * Handles multiple collision iterations for smooth sliding * * @param StartLocation - Starting position for sweep * @param DesiredDelta - Desired movement vector * @param CapsuleComponent - Capsule for collision shape * @param Config - Movement configuration with step sizes and iteration limits * @param DeltaTime - Frame delta time for adaptive step calculation * @param IsShowVisualDebug - Whether to draw debug traces in world * @returns SweepResult with final location and collision info * * @example * const result = CollisionResolver.PerformSweep( * characterLocation, * new Vector(100, 0, 0), // Move 1 meter forward * capsuleComponent, * config, * 0.016 * ); * character.SetActorLocation(result.Location); * * @impure true - performs world traces * @category Sweep Collision */ public PerformSweep( StartLocation: Vector = new Vector(0, 0, 0), DesiredDelta: Vector = new Vector(0, 0, 0), CapsuleComponent: CapsuleComponent | null = null, Config: DA_MovementConfig = new DA_MovementConfig(), DeltaTime: Float = 0, IsShowVisualDebug: boolean = false ): S_SweepResult { // Validate capsule component if (SystemLibrary.IsValid(CapsuleComponent)) { // Calculate total distance to travel const totalDistance = MathLibrary.VectorLength(DesiredDelta); // Early exit if movement is negligible if (totalDistance >= 0.01) { // Calculate adaptive step size based on velocity const stepSize = this.CalculateStepSize( new Vector( DesiredDelta.X / DeltaTime, DesiredDelta.Y / DeltaTime, DesiredDelta.Z / DeltaTime ), DeltaTime, Config ); // Perform stepped sweep let currentLocation = StartLocation; let remainingDistance = totalDistance; let collisionCount = 0; let lastHit = new HitResult(); // Calculate number of steps (capped by max collision checks) const CalculateNumSteps = (maxCollisionChecks: Integer): Integer => MathLibrary.Min( MathLibrary.Ceil(totalDistance / stepSize), maxCollisionChecks ); for (let i = 0; i < CalculateNumSteps(Config.MaxCollisionChecks); i++) { collisionCount++; // Calculate step distance (last step might be shorter) const currentStepSize = MathLibrary.Min(stepSize, remainingDistance); const MathExpression = (desiredDelta: Vector): Vector => new Vector( currentLocation.X + MathLibrary.Normal(desiredDelta).X * currentStepSize, currentLocation.Y + MathLibrary.Normal(desiredDelta).Y * currentStepSize, currentLocation.Z + MathLibrary.Normal(desiredDelta).Z * currentStepSize ); // Calculate target position for this step const targetLocation = MathExpression(DesiredDelta); // Perform capsule trace for this step const { OutHit, ReturnValue } = SystemLibrary.CapsuleTraceByChannel( currentLocation, targetLocation, CapsuleComponent.GetScaledCapsuleRadius(), CapsuleComponent.GetScaledCapsuleHalfHeight(), ETraceTypeQuery.Visibility, false, [], IsShowVisualDebug ? EDrawDebugTrace.ForDuration : EDrawDebugTrace.None ); // Check if trace hit something if (ReturnValue) { // Collision detected - return hit location lastHit = OutHit; return { Location: lastHit.Location, Hit: lastHit, Blocked: true, CollisionCount: collisionCount, }; } else { // No collision - update position and continue currentLocation = targetLocation; remainingDistance = remainingDistance - currentStepSize; // Check if reached destination if (remainingDistance <= 0.01) { break; } } } // Reached destination without blocking hit return { Location: currentLocation, Hit: lastHit, Blocked: false, CollisionCount: collisionCount, }; } else { return { Location: StartLocation, Hit: new HitResult(), Blocked: false, CollisionCount: 0, }; } } else { return { Location: StartLocation, Hit: new HitResult(), Blocked: false, CollisionCount: 0, }; } } // ════════════════════════════════════════════════════════════════════════════════════════ // SURFACE SLIDING // ════════════════════════════════════════════════════════════════════════════════════════ /** * Project movement vector onto collision surface for sliding * Removes component of movement that goes into surface * Allows character to slide smoothly along walls * * @param MovementDelta - Desired movement vector * @param SurfaceNormal - Normal of surface that was hit * @returns Projected movement vector parallel to surface * * @example * // Character hits wall at 45° angle * const slideVector = CollisionResolver.ProjectOntoSurface( * new Vector(100, 100, 0), // Moving diagonally * new Vector(-0.707, 0, 0.707) // Wall normal (45° wall) * ); * // Returns vector parallel to wall surface * * @pure true * @category Surface Sliding */ public ProjectOntoSurface( MovementDelta: Vector, SurfaceNormal: Vector ): Vector { // Project by removing normal component // Formula: V' = V - (V·N)N const MathExpression = ( movementDelta: Vector, surfaceNormal: Vector ): Vector => new Vector( MovementDelta.X - SurfaceNormal.X * MathLibrary.Dot(movementDelta, surfaceNormal), MovementDelta.Y - SurfaceNormal.Y * MathLibrary.Dot(movementDelta, surfaceNormal), MovementDelta.Z - SurfaceNormal.Z * MathLibrary.Dot(movementDelta, surfaceNormal) ); return MathExpression(MovementDelta, SurfaceNormal); } /** * Calculate sliding vector after collision * Combines sweep result with projection for smooth sliding * * @param SweepResult - Result from PerformSweep * @param OriginalDelta - Original desired movement * @param StartLocation - Starting location before sweep * @returns Vector to apply for sliding movement * * @example * const slideVector = CollisionResolver.CalculateSlideVector( * sweepResult, * desiredDelta, * startLocation * ); * if (slideVector.Length() > 0.01) { * character.SetActorLocation(sweepResult.Location + slideVector); * } * * @pure true * @category Surface Sliding */ public CalculateSlideVector( SweepResult: S_SweepResult, OriginalDelta: Vector, StartLocation: Vector ): Vector { if (SweepResult.Blocked) { const MathExpression = ( sweepLocation: Vector, startLocation: Vector, originalDelta: Vector ): Vector => new Vector( originalDelta.X - (sweepLocation.X - startLocation.X), originalDelta.Y - (sweepLocation.Y - startLocation.Y), originalDelta.Z - (sweepLocation.Z - startLocation.Z) ); // Project remaining movement onto collision surface return this.ProjectOntoSurface( MathExpression(SweepResult.Location, StartLocation, OriginalDelta), SweepResult.Hit.ImpactNormal ); } else { // No sliding if no collision return new Vector(0, 0, 0); } } // ════════════════════════════════════════════════════════════════════════════════════════ // ADAPTIVE STEPPING // ════════════════════════════════════════════════════════════════════════════════════════ /** * Calculate adaptive step size based on velocity * Fast movement = smaller steps (more precise) * Slow movement = larger steps (more performant) * * @param Velocity - Current movement velocity * @param DeltaTime - Frame delta time * @param Config - Movement configuration with min/max step sizes * @returns Step size in cm, clamped between min and max * * @example * const stepSize = CollisionResolver.CalculateStepSize( * new Vector(1000, 0, 0), // Fast movement * 0.016, * config * ); * // Returns small step size for precise collision detection * * @pure true * @category Adaptive Stepping */ public CalculateStepSize( Velocity: Vector = new Vector(0, 0, 0), DeltaTime: Float = 0, Config: DA_MovementConfig = new DA_MovementConfig() ): Float { // Calculate distance traveled this frame const frameDistance = MathLibrary.VectorLength( new Vector(Velocity.X, Velocity.Y, 0) // Horizontal distance only ) * DeltaTime; // If moving very slowly, use max step size if (frameDistance < Config.MinStepSize) { return Config.MaxStepSize; } // Clamp between min and max return MathLibrary.ClampFloat( // Calculate adaptive step size (half of frame distance) // This ensures at least 2 checks per frame frameDistance * 0.5, Config.MinStepSize, Config.MaxStepSize ); } } export const BFL_CollisionResolver = new BFL_CollisionResolverClass();