326 lines
12 KiB
TypeScript
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();
|