tengri/Content/Movement/Collision/BFL_CollisionResolver.ts

326 lines
12 KiB
TypeScript

// 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();