222 lines
8.4 KiB
TypeScript
222 lines
8.4 KiB
TypeScript
// Movement/Collision/BFL_GroundProbe.ts
|
|
|
|
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
|
import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts';
|
|
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
|
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.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 { MathLibrary } from '#root/UE/MathLibrary.ts';
|
|
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
|
import { Vector } from '#root/UE/Vector.ts';
|
|
|
|
class BFL_GroundProbeClass extends BlueprintFunctionLibrary {
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
// GROUND DETECTION
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Check if character is standing on walkable ground
|
|
* Performs line trace downward from capsule bottom
|
|
* Validates surface type using SurfaceClassifier
|
|
*
|
|
* @param CharacterLocation - Current character world location
|
|
* @param CapsuleComponent - Character's capsule component for trace setup
|
|
* @param AngleThresholdsRads - Surface angle thresholds in radians
|
|
* @param Config - Movement configuration with trace distance
|
|
* @param IsShowVisualDebug - Whether to draw debug trace in world
|
|
* @returns HitResult with ground information, or empty hit if not grounded
|
|
*
|
|
* @example
|
|
* const groundHit = GroundProbe.CheckGround(
|
|
* characterLocation,
|
|
* capsuleComponent,
|
|
* angleThresholdsRads,
|
|
* config
|
|
* );
|
|
* if (groundHit.BlockingHit) {
|
|
* // Character is on walkable ground
|
|
* }
|
|
*
|
|
* @impure true - performs world trace
|
|
* @category Ground Detection
|
|
*/
|
|
public CheckGround(
|
|
CharacterLocation: Vector = new Vector(0, 0, 0),
|
|
CapsuleComponent: CapsuleComponent | null = null,
|
|
AngleThresholdsRads: S_AngleThresholds = {
|
|
Walkable: 0,
|
|
SteepSlope: 0,
|
|
Wall: 0,
|
|
},
|
|
Config: DA_MovementConfig = new DA_MovementConfig(),
|
|
IsShowVisualDebug: boolean = false
|
|
): HitResult {
|
|
if (SystemLibrary.IsValid(CapsuleComponent)) {
|
|
const CalculateEndLocation = (
|
|
currentZ: Float,
|
|
halfHeight: Float,
|
|
groundTraceDistance: Float
|
|
): Float => currentZ - halfHeight - groundTraceDistance;
|
|
|
|
const { OutHit: groundHit, ReturnValue } =
|
|
SystemLibrary.LineTraceByChannel(
|
|
CharacterLocation,
|
|
new Vector(
|
|
CharacterLocation.X,
|
|
CharacterLocation.Y,
|
|
CalculateEndLocation(
|
|
CharacterLocation.Z,
|
|
CapsuleComponent.GetScaledCapsuleHalfHeight(),
|
|
Config.GroundTraceDistance
|
|
)
|
|
),
|
|
ETraceTypeQuery.Visibility,
|
|
false,
|
|
[],
|
|
IsShowVisualDebug ? EDrawDebugTrace.ForDuration : EDrawDebugTrace.None
|
|
);
|
|
|
|
// Check if trace hit something
|
|
if (!ReturnValue) {
|
|
return new HitResult();
|
|
}
|
|
|
|
if (
|
|
BFL_SurfaceClassifier.IsWalkable(
|
|
BFL_SurfaceClassifier.Classify(
|
|
groundHit.ImpactNormal,
|
|
AngleThresholdsRads
|
|
)
|
|
)
|
|
) {
|
|
return groundHit;
|
|
} else {
|
|
return new HitResult();
|
|
}
|
|
} else {
|
|
return new HitResult();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
// GROUND SNAPPING
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Calculate snapped location to keep character on ground
|
|
* Prevents character from floating slightly above ground
|
|
* Only snaps if within reasonable distance threshold
|
|
*
|
|
* @param CurrentLocation - Current character location
|
|
* @param GroundHit - Ground hit result from CheckGround
|
|
* @param CapsuleComponent - Character's capsule component
|
|
* @param SnapThreshold - Maximum distance to snap (default: 10 cm)
|
|
* @returns Snapped location or original location if too far
|
|
*
|
|
* @example
|
|
* const snappedLocation = GroundProbe.CalculateSnapLocation(
|
|
* currentLocation,
|
|
* groundHit,
|
|
* capsuleComponent,
|
|
* 10.0
|
|
* );
|
|
* character.SetActorLocation(snappedLocation);
|
|
*
|
|
* @pure true - only calculations, no side effects
|
|
* @category Ground Snapping
|
|
*/
|
|
public CalculateSnapLocation(
|
|
CurrentLocation: Vector,
|
|
GroundHit: HitResult,
|
|
CapsuleComponent: CapsuleComponent | null,
|
|
SnapThreshold: Float = 10.0
|
|
): Vector {
|
|
if (GroundHit.BlockingHit) {
|
|
if (SystemLibrary.IsValid(CapsuleComponent)) {
|
|
const correctZ =
|
|
GroundHit.Location.Z + CapsuleComponent.GetScaledCapsuleHalfHeight();
|
|
|
|
const CalculateZDifference = (currentLocZ: Float): Float =>
|
|
MathLibrary.abs(currentLocZ - correctZ);
|
|
|
|
const zDifference = CalculateZDifference(CurrentLocation.Z);
|
|
|
|
const ShouldSnap = (groundTraceDistance: Float): boolean =>
|
|
zDifference > 0.1 && zDifference < groundTraceDistance;
|
|
|
|
if (ShouldSnap(SnapThreshold)) {
|
|
return new Vector(CurrentLocation.X, CurrentLocation.Y, correctZ);
|
|
} else {
|
|
return CurrentLocation;
|
|
}
|
|
} else {
|
|
return CurrentLocation;
|
|
}
|
|
} else {
|
|
return CurrentLocation;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if ground snapping should be applied
|
|
* Helper method to determine if conditions are right for snapping
|
|
*
|
|
* @param CurrentVelocityZ - Current vertical velocity
|
|
* @param GroundHit - Ground hit result
|
|
* @param IsGrounded - Whether character is considered grounded
|
|
* @returns True if snapping should be applied
|
|
*
|
|
* @example
|
|
* if (GroundProbe.ShouldSnapToGround(velocity.Z, groundHit, isGrounded)) {
|
|
* const snappedLoc = GroundProbe.CalculateSnapLocation(...);
|
|
* character.SetActorLocation(snappedLoc);
|
|
* }
|
|
*
|
|
* @pure true
|
|
* @category Ground Snapping
|
|
*/
|
|
public ShouldSnapToGround(
|
|
CurrentVelocityZ: Float,
|
|
GroundHit: HitResult,
|
|
IsGrounded: boolean
|
|
): boolean {
|
|
return IsGrounded && GroundHit.BlockingHit && CurrentVelocityZ <= 0;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
// UTILITIES
|
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Get surface type from ground hit
|
|
* Convenience method combining trace result with classification
|
|
*
|
|
* @param GroundHit - Ground hit result
|
|
* @param AngleThresholdsRads - Surface angle thresholds in radians
|
|
* @returns Surface type classification
|
|
*
|
|
* @pure true
|
|
* @category Utilities
|
|
*/
|
|
public GetSurfaceType(
|
|
GroundHit: HitResult,
|
|
AngleThresholdsRads: S_AngleThresholds
|
|
): E_SurfaceType {
|
|
if (!GroundHit.BlockingHit) {
|
|
return BFL_SurfaceClassifier.Classify(
|
|
GroundHit.ImpactNormal,
|
|
AngleThresholdsRads
|
|
);
|
|
} else {
|
|
return E_SurfaceType.None;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const BFL_GroundProbe = new BFL_GroundProbeClass();
|