[code] AC_Movement refactoring
parent
8ee0cba309
commit
9539f48a06
10
.eslintrc.js
10
.eslintrc.js
|
|
@ -108,13 +108,13 @@ module.exports = {
|
||||||
{
|
{
|
||||||
'selector': 'variable',
|
'selector': 'variable',
|
||||||
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
||||||
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'U', 'A', 'T', 'F']
|
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'selector': 'variable',
|
'selector': 'variable',
|
||||||
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
||||||
'filter': {
|
'filter': {
|
||||||
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
|
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
|
||||||
'match': true
|
'match': true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -122,14 +122,14 @@ module.exports = {
|
||||||
{
|
{
|
||||||
'selector': 'class',
|
'selector': 'class',
|
||||||
'format': ['PascalCase'],
|
'format': ['PascalCase'],
|
||||||
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'U', 'A', 'T', 'F']
|
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
|
||||||
},
|
},
|
||||||
// Regular classes without prefix
|
// Regular classes without prefix
|
||||||
{
|
{
|
||||||
'selector': 'class',
|
'selector': 'class',
|
||||||
'format': ['PascalCase'],
|
'format': ['PascalCase'],
|
||||||
'filter': {
|
'filter': {
|
||||||
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
|
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
|
||||||
'match': true
|
'match': true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -137,7 +137,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
'selector': 'interface',
|
'selector': 'interface',
|
||||||
'format': ['PascalCase'],
|
'format': ['PascalCase'],
|
||||||
'prefix': ['S_', 'I_', 'I']
|
'prefix': ['S_', 'I_', 'I', 'DA_']
|
||||||
},
|
},
|
||||||
// Enums
|
// Enums
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
|
||||||
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
||||||
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
||||||
import { IMC_Default } from '#root/Input/IMC_Default.ts';
|
import { IMC_Default } from '#root/Input/IMC_Default.ts';
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
import { AC_Movement } from '#root/Movement/AC_Movement.ts';
|
||||||
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
||||||
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||||
import { Cast } from '#root/UE/Cast.ts';
|
import { Cast } from '#root/UE/Cast.ts';
|
||||||
|
|
@ -201,8 +201,6 @@ export class BP_MainCharacter extends Pawn {
|
||||||
DeltaTime
|
DeltaTime
|
||||||
);
|
);
|
||||||
|
|
||||||
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
|
|
||||||
|
|
||||||
if (this.ShowDebugInfo) {
|
if (this.ShowDebugInfo) {
|
||||||
this.MovementComponent.UpdateDebugPage();
|
this.MovementComponent.UpdateDebugPage();
|
||||||
this.InputDeviceComponent.UpdateDebugPage();
|
this.InputDeviceComponent.UpdateDebugPage();
|
||||||
|
|
|
||||||
BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)
BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)
BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Camera/Tests/FT_CameraLimits.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraLimits.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Camera/Tests/FT_CameraRotation.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraRotation.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Camera/Tests/FT_CameraSensitivity.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraSensitivity.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Camera/Tests/FT_CameraSmoothing.uasset (Stored with Git LFS)
BIN
Content/Camera/Tests/FT_CameraSmoothing.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Debug/Components/AC_DebugHUD.uasset (Stored with Git LFS)
BIN
Content/Debug/Components/AC_DebugHUD.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Debug/Tests/FT_DebugSystem.uasset (Stored with Git LFS)
BIN
Content/Debug/Tests/FT_DebugSystem.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
||||||
// Debug/UI/WBP_DebugHUD.ts
|
// Debug/UI/WBP_DebugHUD.ts
|
||||||
|
|
||||||
import type { AC_Movement } from '#root/Movement/Components/AC_Movement.js';
|
import type { AC_Movement } from '#root/Movement/AC_Movement.ts';
|
||||||
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
|
||||||
import type { Text } from '#root/UE/Text.ts';
|
import type { Text } from '#root/UE/Text.ts';
|
||||||
import { TextBlock } from '#root/UE/TextBlock.ts';
|
import { TextBlock } from '#root/UE/TextBlock.ts';
|
||||||
|
|
|
||||||
BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)
BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -10,8 +10,6 @@ import { FT_DebugNavigation } from '#root/Debug/Tests/FT_DebugNavigation.ts';
|
||||||
import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement.ts';
|
import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement.ts';
|
||||||
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
|
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
|
||||||
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
|
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
|
||||||
import { FT_BasicMovement } from '#root/Movement/Tests/FT_BasicMovement.ts';
|
|
||||||
import { FT_SurfaceClassification } from '#root/Movement/Tests/FT_SurfaceClassification.ts';
|
|
||||||
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
|
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
|
||||||
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
||||||
import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts';
|
import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts';
|
||||||
|
|
@ -46,13 +44,6 @@ const InputDeviceDetectionTest = new FT_InputDeviceDetection();
|
||||||
|
|
||||||
InputDeviceDetectionTest.EventStartTest();
|
InputDeviceDetectionTest.EventStartTest();
|
||||||
|
|
||||||
// Movement Tests
|
|
||||||
const BasicMovementTest = new FT_BasicMovement();
|
|
||||||
const SurfaceClassificationTest = new FT_SurfaceClassification();
|
|
||||||
|
|
||||||
BasicMovementTest.EventStartTest();
|
|
||||||
SurfaceClassificationTest.EventStartTest();
|
|
||||||
|
|
||||||
// Toasts Tests
|
// Toasts Tests
|
||||||
const ToastLimitsTest = new FT_ToastLimit();
|
const ToastLimitsTest = new FT_ToastLimit();
|
||||||
const ToastsDurationHandlingTest = new FT_ToastsDurationHandling();
|
const ToastsDurationHandlingTest = new FT_ToastsDurationHandling();
|
||||||
|
|
|
||||||
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
Binary file not shown.
|
|
@ -0,0 +1,231 @@
|
||||||
|
// Movement/AC_Movement.ts
|
||||||
|
|
||||||
|
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
||||||
|
import { BFL_MovementProcessor } from '#root/Movement/Core/BFL_MovementProcessor.ts';
|
||||||
|
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
||||||
|
import { DA_MovementConfigDefault } from '#root/Movement/Core/DA_MovementConfigDefault.ts';
|
||||||
|
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
|
||||||
|
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
|
||||||
|
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
||||||
|
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
|
||||||
|
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||||
|
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import { HitResult } from '#root/UE/HitResult.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
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 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.Config.MaxSpeed}\n` +
|
||||||
|
`Acceleration: ${this.Config.Acceleration}\n` +
|
||||||
|
`Friction: ${this.Config.Friction}\n` +
|
||||||
|
`Gravity: ${this.Config.Gravity}\n` +
|
||||||
|
`Initialized: ${this.IsInitialized}\n` +
|
||||||
|
`\n` +
|
||||||
|
// Current State
|
||||||
|
`Current Velocity: ${StringLibrary.ConvVectorToString(this.CurrentMovementState.Velocity)}\n` +
|
||||||
|
`Speed: ${this.CurrentMovementState.Speed}\n` +
|
||||||
|
`Is Grounded: ${this.CurrentMovementState.IsGrounded}\n` +
|
||||||
|
`Surface Type: ${this.CurrentMovementState.SurfaceType}\n` +
|
||||||
|
`Movement State: ${this.CurrentMovementState.MovementState}\n` +
|
||||||
|
`Input Magnitude: ${this.CurrentMovementState.InputMagnitude}` +
|
||||||
|
`\n` +
|
||||||
|
// Rotation
|
||||||
|
`Current Yaw: ${this.CurrentMovementState.Rotation.yaw}\n` +
|
||||||
|
`Rotation Delta: ${this.CurrentMovementState.RotationDelta}\n°` +
|
||||||
|
`Is Rotating: ${this.CurrentMovementState.IsRotating}\n` +
|
||||||
|
`Rotation Speed: ${this.Config.RotationSpeed}\n°` +
|
||||||
|
`Min Speed: ${this.Config.MinSpeedForRotation}` +
|
||||||
|
`\n` +
|
||||||
|
// Position
|
||||||
|
`Location: ${StringLibrary.ConvVectorToString(this.GetOwner().GetActorLocation())}` +
|
||||||
|
`\n` +
|
||||||
|
// Sweep Collision
|
||||||
|
`Collision Checks: ${this.CurrentMovementState.CollisionCount}/${this.Config.MaxCollisionChecks}\n` +
|
||||||
|
`Sweep Blocked: ${this.CurrentMovementState.IsBlocked}\n` +
|
||||||
|
`Ground Distance: ${this.Config.GroundTraceDistance} cm`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Default
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Default
|
||||||
|
*/
|
||||||
|
public ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void {
|
||||||
|
if (this.IsInitialized) {
|
||||||
|
this.CurrentMovementState = BFL_MovementProcessor.ProcessMovement(
|
||||||
|
this.CurrentMovementState,
|
||||||
|
{
|
||||||
|
InputVector,
|
||||||
|
DeltaTime,
|
||||||
|
CapsuleComponent: this.CapsuleComponent,
|
||||||
|
Config: this.Config,
|
||||||
|
AngleThresholdsRads: this.AngleThresholdsRads,
|
||||||
|
},
|
||||||
|
SystemLibrary.IsValid(this.DebugHUDComponent)
|
||||||
|
? this.DebugHUDComponent.ShowVisualDebug
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
|
||||||
|
this.GetOwner().SetActorLocation(this.CurrentMovementState.Location);
|
||||||
|
this.GetOwner().SetActorRotation(this.CurrentMovementState.Rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize movement system with angle conversion
|
||||||
|
* Converts degree thresholds to radians for runtime performance
|
||||||
|
* @category Default
|
||||||
|
*/
|
||||||
|
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.Config.AngleThresholdsDegrees.Walkable
|
||||||
|
),
|
||||||
|
SteepSlope: MathLibrary.DegreesToRadians(
|
||||||
|
this.Config.AngleThresholdsDegrees.SteepSlope
|
||||||
|
),
|
||||||
|
Wall: MathLibrary.DegreesToRadians(
|
||||||
|
this.Config.AngleThresholdsDegrees.Wall
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.CurrentMovementState = BFL_MovementProcessor.CreateInitialState(
|
||||||
|
this.GetOwner().GetActorLocation(),
|
||||||
|
this.GetOwner().GetActorRotation()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
|
||||||
|
this.DebugHUDComponent.AddDebugPage(
|
||||||
|
this.DebugPageID,
|
||||||
|
'Movement Info',
|
||||||
|
60
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// VARIABLES
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Default
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default movement state
|
||||||
|
* @category Default
|
||||||
|
*/
|
||||||
|
private CurrentMovementState: S_MovementState = {
|
||||||
|
Location: new Vector(0, 0, 0),
|
||||||
|
Rotation: new Rotator(0, 0, 0),
|
||||||
|
Velocity: new Vector(0, 0, 0),
|
||||||
|
Speed: 0.0,
|
||||||
|
IsGrounded: false,
|
||||||
|
GroundHit: new HitResult(),
|
||||||
|
SurfaceType: E_SurfaceType.None,
|
||||||
|
IsBlocked: false,
|
||||||
|
CollisionCount: 0,
|
||||||
|
IsRotating: false,
|
||||||
|
RotationDelta: 0.0,
|
||||||
|
MovementState: E_MovementState.Idle,
|
||||||
|
InputMagnitude: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default movement configuration
|
||||||
|
* @category Default
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly Config: DA_MovementConfig = new DA_MovementConfigDefault();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime cached angle thresholds in radians
|
||||||
|
* Converted from degrees during initialization for performance
|
||||||
|
* @category Default
|
||||||
|
*/
|
||||||
|
private AngleThresholdsRads: S_AngleThresholds = {
|
||||||
|
Walkable: 0.0,
|
||||||
|
SteepSlope: 0.0,
|
||||||
|
Wall: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug page identifier for organizing debug output
|
||||||
|
* Used by debug HUD to categorize information
|
||||||
|
* @category Default
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly DebugPageID: string = 'MovementInfo';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicating if movement system has been initialized
|
||||||
|
* Ensures angle thresholds are converted before use
|
||||||
|
* @category Debug
|
||||||
|
*/
|
||||||
|
private IsInitialized = false;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,325 @@
|
||||||
|
// 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();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,221 @@
|
||||||
|
// 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();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Movement/Collision/S_SweepResult.ts
|
||||||
|
|
||||||
|
import type { HitResult } from '#root/UE/HitResult.ts';
|
||||||
|
import type { Integer } from '#root/UE/Integer.ts';
|
||||||
|
import type { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
export interface S_SweepResult {
|
||||||
|
Location: Vector;
|
||||||
|
Hit: HitResult;
|
||||||
|
Blocked: boolean;
|
||||||
|
CollisionCount: Integer;
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
Content/Movement/Components/AC_Movement.uasset (Stored with Git LFS)
BIN
Content/Movement/Components/AC_Movement.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -0,0 +1,261 @@
|
||||||
|
// Movement/Core/BFL_MovementProcessor.ts
|
||||||
|
|
||||||
|
import { BFL_CollisionResolver } from '#root/Movement/Collision/BFL_CollisionResolver.ts';
|
||||||
|
import { BFL_GroundProbe } from '#root/Movement/Collision/BFL_GroundProbe.ts';
|
||||||
|
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
|
||||||
|
import type { S_MovementInput } from '#root/Movement/Core/S_MovementInput.ts';
|
||||||
|
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
|
||||||
|
import { BFL_Kinematics } from '#root/Movement/Physics/BFL_Kinematics.ts';
|
||||||
|
import { BFL_RotationController } from '#root/Movement/Rotation/BFL_RotationController.ts';
|
||||||
|
import { BFL_MovementStateMachine } from '#root/Movement/State/BFL_MovementStateMachine.ts';
|
||||||
|
import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts';
|
||||||
|
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
||||||
|
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
||||||
|
import { HitResult } from '#root/UE/HitResult.ts';
|
||||||
|
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||||
|
import type { Rotator } from '#root/UE/Rotator.ts';
|
||||||
|
import { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement Processor
|
||||||
|
*
|
||||||
|
* Unified movement processing system
|
||||||
|
* Takes current state + input, returns next state
|
||||||
|
* Pure functional approach - no side effects
|
||||||
|
*
|
||||||
|
* @category Movement Processing
|
||||||
|
* @impure Only collision traces (GroundProbe, CollisionResolver)
|
||||||
|
*/
|
||||||
|
class BFL_MovementProcessorClass extends BlueprintFunctionLibrary {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// MAIN PROCESSING
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process movement for one frame
|
||||||
|
*
|
||||||
|
* Main entry point - computes complete next state from current state + input
|
||||||
|
* Orchestrates all movement subsystems in correct order
|
||||||
|
*
|
||||||
|
* @param CurrentState - Current movement state
|
||||||
|
* @param Input - Movement input data (input vector, delta time, config, etc.)
|
||||||
|
* @param IsShowVisualDebug - Whether to show debug traces in the world
|
||||||
|
* @returns New movement state after processing
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const newState = BFL_MovementProcessor.ProcessMovement(
|
||||||
|
* this.CurrentMovementState,
|
||||||
|
* {
|
||||||
|
* InputVector: inputVector,
|
||||||
|
* DeltaTime: deltaTime,
|
||||||
|
* CapsuleComponent: this.CapsuleComponent,
|
||||||
|
* Config: this.Config,
|
||||||
|
* AngleThresholdsRads: this.AngleThresholdsRads
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Apply results
|
||||||
|
* this.GetOwner().SetActorLocation(newState.Location);
|
||||||
|
* this.GetOwner().SetActorRotation(newState.Rotation);
|
||||||
|
*
|
||||||
|
* @impure true - performs collision traces
|
||||||
|
* @category Main Processing
|
||||||
|
*/
|
||||||
|
public ProcessMovement(
|
||||||
|
CurrentState: S_MovementState,
|
||||||
|
Input: S_MovementInput,
|
||||||
|
IsShowVisualDebug: boolean = false
|
||||||
|
): S_MovementState {
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 1: INPUT & ROTATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const inputMagnitude = MathLibrary.VectorLength(Input.InputVector);
|
||||||
|
|
||||||
|
const rotationResult = BFL_RotationController.UpdateRotation(
|
||||||
|
CurrentState.Rotation,
|
||||||
|
Input.InputVector,
|
||||||
|
Input.Config,
|
||||||
|
Input.DeltaTime,
|
||||||
|
CurrentState.Speed
|
||||||
|
);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 2: GROUND DETECTION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const groundHit = BFL_GroundProbe.CheckGround(
|
||||||
|
CurrentState.Location,
|
||||||
|
Input.CapsuleComponent,
|
||||||
|
Input.AngleThresholdsRads,
|
||||||
|
Input.Config
|
||||||
|
);
|
||||||
|
|
||||||
|
const isGrounded = groundHit.BlockingHit;
|
||||||
|
|
||||||
|
const surfaceType = BFL_GroundProbe.GetSurfaceType(
|
||||||
|
groundHit,
|
||||||
|
Input.AngleThresholdsRads
|
||||||
|
);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 3: PHYSICS CALCULATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
let newVelocity = CurrentState.Velocity;
|
||||||
|
|
||||||
|
// Ground movement or air friction
|
||||||
|
if (BFL_SurfaceClassifier.IsWalkable(surfaceType) && isGrounded) {
|
||||||
|
newVelocity = BFL_Kinematics.CalculateGroundVelocity(
|
||||||
|
newVelocity,
|
||||||
|
Input.InputVector,
|
||||||
|
Input.DeltaTime,
|
||||||
|
Input.Config
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newVelocity = BFL_Kinematics.CalculateFriction(
|
||||||
|
newVelocity,
|
||||||
|
Input.DeltaTime,
|
||||||
|
Input.Config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
newVelocity = BFL_Kinematics.CalculateGravity(
|
||||||
|
newVelocity,
|
||||||
|
isGrounded,
|
||||||
|
Input.Config
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSpeed = BFL_Kinematics.GetHorizontalSpeed(newVelocity);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 4: MOVEMENT APPLICATION (Sweep)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const desiredDelta = new Vector(
|
||||||
|
newVelocity.X * Input.DeltaTime,
|
||||||
|
newVelocity.Y * Input.DeltaTime,
|
||||||
|
newVelocity.Z * Input.DeltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
const sweepResult = BFL_CollisionResolver.PerformSweep(
|
||||||
|
CurrentState.Location,
|
||||||
|
desiredDelta,
|
||||||
|
Input.CapsuleComponent,
|
||||||
|
Input.Config,
|
||||||
|
Input.DeltaTime,
|
||||||
|
IsShowVisualDebug
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalLocation = sweepResult.Location;
|
||||||
|
|
||||||
|
// Handle collision sliding
|
||||||
|
if (sweepResult.Blocked) {
|
||||||
|
const slideVector = BFL_CollisionResolver.CalculateSlideVector(
|
||||||
|
sweepResult,
|
||||||
|
desiredDelta,
|
||||||
|
CurrentState.Location
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
MathLibrary.VectorLength(slideVector) > 0.5 &&
|
||||||
|
MathLibrary.Dot(
|
||||||
|
MathLibrary.Normal(slideVector),
|
||||||
|
sweepResult.Hit.ImpactNormal
|
||||||
|
) >= -0.1
|
||||||
|
) {
|
||||||
|
finalLocation = new Vector(
|
||||||
|
sweepResult.Location.X + slideVector.X,
|
||||||
|
sweepResult.Location.Y + slideVector.Y,
|
||||||
|
sweepResult.Location.Z + slideVector.Z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 5: GROUND SNAPPING
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if (
|
||||||
|
BFL_GroundProbe.ShouldSnapToGround(newVelocity.Z, groundHit, isGrounded)
|
||||||
|
) {
|
||||||
|
finalLocation = BFL_GroundProbe.CalculateSnapLocation(
|
||||||
|
finalLocation,
|
||||||
|
groundHit,
|
||||||
|
Input.CapsuleComponent,
|
||||||
|
Input.Config.GroundTraceDistance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PHASE 6: STATE DETERMINATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const movementState = BFL_MovementStateMachine.DetermineState({
|
||||||
|
IsGrounded: isGrounded,
|
||||||
|
SurfaceType: surfaceType,
|
||||||
|
InputMagnitude: inputMagnitude,
|
||||||
|
CurrentSpeed: newSpeed,
|
||||||
|
VerticalVelocity: newVelocity.Z,
|
||||||
|
IsBlocked: sweepResult.Blocked,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// RETURN NEW STATE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
return {
|
||||||
|
Location: finalLocation,
|
||||||
|
Rotation: rotationResult.Rotation,
|
||||||
|
Velocity: newVelocity,
|
||||||
|
Speed: newSpeed,
|
||||||
|
IsGrounded: isGrounded,
|
||||||
|
GroundHit: groundHit,
|
||||||
|
SurfaceType: surfaceType,
|
||||||
|
IsBlocked: sweepResult.Blocked,
|
||||||
|
CollisionCount: sweepResult.CollisionCount,
|
||||||
|
IsRotating: rotationResult.IsRotating,
|
||||||
|
RotationDelta: rotationResult.RemainingDelta,
|
||||||
|
MovementState: movementState,
|
||||||
|
InputMagnitude: inputMagnitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// STATE UTILITIES
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create initial movement state
|
||||||
|
*
|
||||||
|
* @param Location - Starting location
|
||||||
|
* @param Rotation - Starting rotation
|
||||||
|
* @returns Initial movement state with defaults
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category State Utilities
|
||||||
|
*/
|
||||||
|
public CreateInitialState(
|
||||||
|
Location: Vector,
|
||||||
|
Rotation: Rotator
|
||||||
|
): S_MovementState {
|
||||||
|
return {
|
||||||
|
Location,
|
||||||
|
Rotation,
|
||||||
|
Velocity: new Vector(0, 0, 0),
|
||||||
|
Speed: 0.0,
|
||||||
|
IsGrounded: false,
|
||||||
|
GroundHit: new HitResult(),
|
||||||
|
SurfaceType: E_SurfaceType.None,
|
||||||
|
IsBlocked: false,
|
||||||
|
CollisionCount: 0,
|
||||||
|
IsRotating: false,
|
||||||
|
RotationDelta: 0.0,
|
||||||
|
MovementState: E_MovementState.Idle,
|
||||||
|
InputMagnitude: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BFL_MovementProcessor = new BFL_MovementProcessorClass();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Movement/Core/DA_MovementConfig.ts
|
||||||
|
|
||||||
|
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import { PrimaryDataAsset } from '#root/UE/PrimaryDataAsset.ts';
|
||||||
|
|
||||||
|
export class DA_MovementConfig extends PrimaryDataAsset {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// MOVEMENT PHYSICS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum horizontal movement speed in UE units per second
|
||||||
|
* Character cannot exceed this speed through ground movement
|
||||||
|
* Used as target velocity cap in ProcessGroundMovement
|
||||||
|
*
|
||||||
|
* @category Movement Physics
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm/s
|
||||||
|
*/
|
||||||
|
public 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
|
||||||
|
*
|
||||||
|
* @category Movement Physics
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
public 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
|
||||||
|
*
|
||||||
|
* @category Movement Physics
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
public 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
|
||||||
|
*
|
||||||
|
* @category Movement Physics
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm/s^2
|
||||||
|
*/
|
||||||
|
public readonly Gravity: Float = 980.0;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// SURFACE DETECTION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Surface classification angle thresholds in degrees
|
||||||
|
* Walkable ≤50°, SteepSlope ≤85°, Wall ≤95°, Ceiling >95°
|
||||||
|
*
|
||||||
|
* @category Surface Detection
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
public readonly AngleThresholdsDegrees: S_AngleThresholds = {
|
||||||
|
Walkable: 50,
|
||||||
|
SteepSlope: 85,
|
||||||
|
Wall: 95,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COLLISION SETTINGS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distance to trace downward for ground detection
|
||||||
|
* Should be slightly larger than capsule half-height
|
||||||
|
*
|
||||||
|
* @category Collision Settings
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm
|
||||||
|
*/
|
||||||
|
public readonly GroundTraceDistance: Float = 50.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum step size for collision sweeps
|
||||||
|
* Smaller values = more precise but more expensive
|
||||||
|
*
|
||||||
|
* @category Collision Settings
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm
|
||||||
|
*/
|
||||||
|
public readonly MinStepSize: Float = 1.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum step size for collision sweeps
|
||||||
|
* Larger values = less precise but cheaper
|
||||||
|
*
|
||||||
|
* @category Collision Settings
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm
|
||||||
|
*/
|
||||||
|
public readonly MaxStepSize: Float = 50.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum collision checks allowed per frame
|
||||||
|
* Prevents infinite loops in complex geometry
|
||||||
|
*
|
||||||
|
* @category Collision Settings
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
public readonly MaxCollisionChecks: Float = 25;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CHARACTER ROTATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character rotation speed (degrees per second)
|
||||||
|
* How fast character turns toward movement direction
|
||||||
|
*
|
||||||
|
* @category Character Rotation
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit deg/s
|
||||||
|
*/
|
||||||
|
public RotationSpeed: Float = 360.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum movement speed required to rotate character
|
||||||
|
* Prevents rotation jitter when nearly stationary
|
||||||
|
*
|
||||||
|
* @category Character Rotation
|
||||||
|
* @instanceEditable true
|
||||||
|
* @unit cm/s
|
||||||
|
*/
|
||||||
|
public MinSpeedForRotation: Float = 50.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable character rotation toward movement
|
||||||
|
* Useful for debugging or special movement modes
|
||||||
|
*
|
||||||
|
* @category Character Rotation
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
public ShouldRotateToMovement: boolean = true;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Movement/Core/DA_MovementConfigDefault.ts
|
||||||
|
|
||||||
|
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
||||||
|
|
||||||
|
export class DA_MovementConfigDefault extends DA_MovementConfig {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// MOVEMENT PHYSICS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
override MaxSpeed = 800.0;
|
||||||
|
override Acceleration = 10.0;
|
||||||
|
override Friction = 8.0;
|
||||||
|
override Gravity = 980.0;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// SURFACE DETECTION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
override AngleThresholdsDegrees = {
|
||||||
|
Walkable: 50.0,
|
||||||
|
SteepSlope: 85.0,
|
||||||
|
Wall: 95.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COLLISION SETTINGS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
override GroundTraceDistance = 50.0;
|
||||||
|
override MinStepSize = 1.0;
|
||||||
|
override MaxStepSize = 50.0;
|
||||||
|
override MaxCollisionChecks = 25;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CHARACTER ROTATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
override RotationSpeed = 360.0;
|
||||||
|
override MinSpeedForRotation = 50.0;
|
||||||
|
override ShouldRotateToMovement = true;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Movement/Core/E_MovementState.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement state enumeration
|
||||||
|
* Defines all possible character movement states
|
||||||
|
*
|
||||||
|
* @category Movement Enums
|
||||||
|
*/
|
||||||
|
export enum E_MovementState {
|
||||||
|
/**
|
||||||
|
* Character is stationary on ground
|
||||||
|
* No input, no movement
|
||||||
|
*/
|
||||||
|
Idle = 'Idle',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character is moving on ground
|
||||||
|
* Has input and horizontal velocity
|
||||||
|
*/
|
||||||
|
Walking = 'Walking',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character is in the air
|
||||||
|
* Not touching ground, affected by gravity
|
||||||
|
*/
|
||||||
|
Airborne = 'Airborne',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character is sliding down steep slope
|
||||||
|
* On non-walkable surface (steep slope)
|
||||||
|
*/
|
||||||
|
Sliding = 'Sliding',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character is blocked by collision
|
||||||
|
* Hitting wall or ceiling
|
||||||
|
*/
|
||||||
|
Blocked = 'Blocked',
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Movement/Core/S_MovementInput.ts
|
||||||
|
|
||||||
|
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
||||||
|
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
|
||||||
|
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement processing input data
|
||||||
|
* All data needed to compute next movement state
|
||||||
|
*
|
||||||
|
* @category Movement Input
|
||||||
|
*/
|
||||||
|
export interface S_MovementInput {
|
||||||
|
/**
|
||||||
|
* Player input vector (normalized XY direction)
|
||||||
|
*/
|
||||||
|
InputVector: Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame delta time (seconds)
|
||||||
|
*/
|
||||||
|
DeltaTime: Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character capsule component for collision
|
||||||
|
*/
|
||||||
|
CapsuleComponent: CapsuleComponent | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement configuration
|
||||||
|
*/
|
||||||
|
Config: DA_MovementConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angle thresholds in radians (for surface classification)
|
||||||
|
*/
|
||||||
|
AngleThresholdsRads: S_AngleThresholds;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Movement/Core/S_MovementState.ts
|
||||||
|
|
||||||
|
import type { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
|
||||||
|
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { HitResult } from '#root/UE/HitResult.ts';
|
||||||
|
import type { Rotator } from '#root/UE/Rotator.ts';
|
||||||
|
import type { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete movement state snapshot
|
||||||
|
* Immutable data structure representing full character movement state
|
||||||
|
*
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
export interface S_MovementState {
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// TRANSFORM
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character world location
|
||||||
|
*/
|
||||||
|
Location: Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character rotation (yaw only)
|
||||||
|
*/
|
||||||
|
Rotation: Rotator;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// VELOCITY & PHYSICS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current velocity vector (cm/s)
|
||||||
|
*/
|
||||||
|
Velocity: Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal speed (cm/s)
|
||||||
|
*/
|
||||||
|
Speed: Float;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// GROUND STATE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether character is on walkable ground
|
||||||
|
*/
|
||||||
|
IsGrounded: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ground trace hit result
|
||||||
|
*/
|
||||||
|
GroundHit: HitResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current surface type
|
||||||
|
*/
|
||||||
|
SurfaceType: E_SurfaceType;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// COLLISION STATE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether movement was blocked by collision
|
||||||
|
*/
|
||||||
|
IsBlocked: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of collision checks this frame
|
||||||
|
*/
|
||||||
|
CollisionCount: number;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// ROTATION STATE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether character is actively rotating
|
||||||
|
*/
|
||||||
|
IsRotating: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remaining angular distance to target (degrees)
|
||||||
|
*/
|
||||||
|
RotationDelta: Float;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// MOVEMENT STATE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current movement state (Idle, Walking, Airborne, etc.)
|
||||||
|
*/
|
||||||
|
MovementState: E_MovementState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input magnitude (0-1)
|
||||||
|
*/
|
||||||
|
InputMagnitude: Float;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -1,7 +0,0 @@
|
||||||
// Movement/Enums/E_MovementState.ts
|
|
||||||
|
|
||||||
export enum E_MovementState {
|
|
||||||
Idle = 'Idle',
|
|
||||||
Walking = 'Walking',
|
|
||||||
Airborne = 'Airborne',
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Enums/E_MovementState.uasset (Stored with Git LFS)
BIN
Content/Movement/Enums/E_MovementState.uasset (Stored with Git LFS)
Binary file not shown.
BIN
Content/Movement/Enums/E_SurfaceType.uasset (Stored with Git LFS)
BIN
Content/Movement/Enums/E_SurfaceType.uasset (Stored with Git LFS)
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,179 @@
|
||||||
|
// Movement/Physics/BFL_Kinematics.ts
|
||||||
|
|
||||||
|
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
||||||
|
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||||
|
import { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
class BFL_KinematicsClass extends BlueprintFunctionLibrary {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// GROUND MOVEMENT
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate new velocity for ground-based movement with acceleration
|
||||||
|
* Uses VInterpTo for smooth acceleration towards target velocity
|
||||||
|
* Only affects horizontal (XY) components, preserves vertical (Z)
|
||||||
|
*
|
||||||
|
* @param CurrentVelocity - Current character velocity (cm/s)
|
||||||
|
* @param InputVector - Normalized input direction from player/AI
|
||||||
|
* @param DeltaTime - Frame delta time for frame-rate independence (s)
|
||||||
|
* @param Config - Movement configuration with MaxSpeed and Acceleration
|
||||||
|
* @returns New velocity vector with updated horizontal components
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Character moving forward with input (1, 0, 0)
|
||||||
|
* const newVel = Kinematics.CalculateGroundVelocity(
|
||||||
|
* new Vector(400, 0, 0), // Current velocity
|
||||||
|
* new Vector(1, 0, 0), // Forward input
|
||||||
|
* 0.016, // 60 FPS delta
|
||||||
|
* config
|
||||||
|
* );
|
||||||
|
* // Returns: Vector(450, 0, 0) - accelerated towards MaxSpeed
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Ground Movement
|
||||||
|
*/
|
||||||
|
public CalculateGroundVelocity(
|
||||||
|
CurrentVelocity: Vector,
|
||||||
|
InputVector: Vector,
|
||||||
|
DeltaTime: Float,
|
||||||
|
Config: DA_MovementConfig
|
||||||
|
): Vector {
|
||||||
|
if (MathLibrary.VectorLength(InputVector) > 0.01) {
|
||||||
|
const CalculateTargetVelocity = (
|
||||||
|
inputVector: Vector,
|
||||||
|
maxSpeed: Float
|
||||||
|
): Vector =>
|
||||||
|
new Vector(
|
||||||
|
MathLibrary.Normal(inputVector).X * maxSpeed,
|
||||||
|
MathLibrary.Normal(inputVector).Y * maxSpeed,
|
||||||
|
MathLibrary.Normal(inputVector).Z * maxSpeed
|
||||||
|
);
|
||||||
|
|
||||||
|
return MathLibrary.VInterpTo(
|
||||||
|
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
|
||||||
|
CalculateTargetVelocity(InputVector, Config.MaxSpeed),
|
||||||
|
DeltaTime,
|
||||||
|
Config.Acceleration
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.CalculateFriction(CurrentVelocity, DeltaTime, Config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FRICTION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply friction to horizontal velocity (deceleration when no input)
|
||||||
|
* Smoothly interpolates velocity towards zero using friction rate
|
||||||
|
* Only affects horizontal (XY) components, preserves vertical (Z)
|
||||||
|
*
|
||||||
|
* @param CurrentVelocity - Current character velocity (cm/s)
|
||||||
|
* @param DeltaTime - Frame delta time (s)
|
||||||
|
* @param Config - Movement configuration with Friction rate
|
||||||
|
* @returns New velocity vector with friction applied to horizontal components
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Character sliding to stop after input released
|
||||||
|
* const newVel = Kinematics.ApplyFriction(
|
||||||
|
* new Vector(500, 0, 0), // Moving forward
|
||||||
|
* 0.016, // 60 FPS delta
|
||||||
|
* config // Friction = 8.0
|
||||||
|
* );
|
||||||
|
* // Returns: Vector(450, 0, 0) - smoothly decelerating
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Friction
|
||||||
|
*/
|
||||||
|
public CalculateFriction(
|
||||||
|
CurrentVelocity: Vector,
|
||||||
|
DeltaTime: Float,
|
||||||
|
Config: DA_MovementConfig
|
||||||
|
): Vector {
|
||||||
|
return MathLibrary.VInterpTo(
|
||||||
|
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
|
||||||
|
new Vector(0, 0, CurrentVelocity.Z),
|
||||||
|
DeltaTime,
|
||||||
|
Config.Friction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// GRAVITY
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply gravity to vertical velocity when airborne
|
||||||
|
* Only affects Z component, horizontal velocity unchanged
|
||||||
|
* Gravity is NOT applied when grounded (Z velocity set to 0)
|
||||||
|
*
|
||||||
|
* @param CurrentVelocity - Current character velocity (cm/s)
|
||||||
|
* @param IsGrounded - Whether character is on walkable surface
|
||||||
|
* @param Config - Movement configuration with Gravity force
|
||||||
|
* @returns New velocity vector with gravity applied to vertical component
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Character falling (not grounded)
|
||||||
|
* const newVel = Kinematics.ApplyGravity(
|
||||||
|
* new Vector(500, 0, -200), // Moving forward and falling
|
||||||
|
* false, // Not grounded
|
||||||
|
* config // Gravity = 980 cm/s²
|
||||||
|
* );
|
||||||
|
* // Returns: Vector(500, 0, -216.8) - falling faster
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Character on ground
|
||||||
|
* const newVel = Kinematics.ApplyGravity(
|
||||||
|
* new Vector(500, 0, -10), // Small downward velocity
|
||||||
|
* true, // Grounded
|
||||||
|
* config
|
||||||
|
* );
|
||||||
|
* // Returns: Vector(500, 0, 0) - vertical velocity zeroed
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Gravity
|
||||||
|
*/
|
||||||
|
public CalculateGravity(
|
||||||
|
CurrentVelocity: Vector,
|
||||||
|
IsGrounded: boolean,
|
||||||
|
Config: DA_MovementConfig
|
||||||
|
): Vector {
|
||||||
|
if (!IsGrounded) {
|
||||||
|
return new Vector(
|
||||||
|
CurrentVelocity.X,
|
||||||
|
CurrentVelocity.Y,
|
||||||
|
CurrentVelocity.Z - Config.Gravity
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// VELOCITY QUERIES
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get horizontal speed (magnitude of XY velocity)
|
||||||
|
* Ignores vertical component, useful for animation and debug display
|
||||||
|
*
|
||||||
|
* @param Velocity - Velocity vector to measure
|
||||||
|
* @returns Speed in cm/s (horizontal plane only)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const speed = Kinematics.GetHorizontalSpeed(new Vector(300, 400, -100));
|
||||||
|
* // Returns: 500.0 (sqrt(300² + 400²))
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Velocity Queries
|
||||||
|
*/
|
||||||
|
public GetHorizontalSpeed(Velocity: Vector): Float {
|
||||||
|
return MathLibrary.VectorLength(new Vector(Velocity.X, Velocity.Y, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BFL_Kinematics = new BFL_KinematicsClass();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,256 @@
|
||||||
|
// Movement/Rotation/BFL_RotationController.ts
|
||||||
|
|
||||||
|
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
|
||||||
|
import type { S_RotationResult } from '#root/Movement/Rotation/S_RotationResult.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { Integer } from '#root/UE/Integer.ts';
|
||||||
|
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||||
|
import { Rotator } from '#root/UE/Rotator.ts';
|
||||||
|
import type { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character Rotation Controller
|
||||||
|
*
|
||||||
|
* Pure functional module for character rotation calculations
|
||||||
|
* Handles smooth rotation toward movement direction
|
||||||
|
* All methods are deterministic and side-effect free
|
||||||
|
*
|
||||||
|
* @category Movement Rotation
|
||||||
|
* @pure All methods are pure functions
|
||||||
|
*/
|
||||||
|
class BFL_RotationControllerClass {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// TARGET CALCULATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate target yaw angle from movement direction
|
||||||
|
* Converts 2D movement vector to rotation angle
|
||||||
|
*
|
||||||
|
* @param MovementDirection - Movement direction vector (XY plane)
|
||||||
|
* @returns Target yaw angle in degrees
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Moving forward (X+)
|
||||||
|
* const yaw = RotationController.CalculateTargetYaw(new Vector(1, 0, 0));
|
||||||
|
* // Returns: 0°
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Moving right (Y+)
|
||||||
|
* const yaw = RotationController.CalculateTargetYaw(new Vector(0, 1, 0));
|
||||||
|
* // Returns: 90°
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Target Calculation
|
||||||
|
*/
|
||||||
|
public CalculateTargetYaw(MovementDirection: Vector): Float {
|
||||||
|
// Use atan2 to get angle from X/Y components
|
||||||
|
// Returns angle in degrees
|
||||||
|
return MathLibrary.Atan2Degrees(MovementDirection.Y, MovementDirection.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate target rotation from movement direction
|
||||||
|
* Creates full Rotator with only yaw set (pitch/roll = 0)
|
||||||
|
*
|
||||||
|
* @param MovementDirection - Movement direction vector
|
||||||
|
* @returns Target rotation (yaw only, pitch/roll = 0)
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Target Calculation
|
||||||
|
*/
|
||||||
|
public CalculateTargetRotation(MovementDirection: Vector): Rotator {
|
||||||
|
return new Rotator(0, this.CalculateTargetYaw(MovementDirection), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ROTATION INTERPOLATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolate rotation smoothly toward target
|
||||||
|
* Handles angle wraparound (180°/-180° boundary)
|
||||||
|
*
|
||||||
|
* @param CurrentRotation - Current character rotation
|
||||||
|
* @param TargetRotation - Desired target rotation
|
||||||
|
* @param RotationSpeed - Rotation speed in degrees/sec
|
||||||
|
* @param DeltaTime - Frame delta time
|
||||||
|
* @param MinSpeedForRotation - Minimum speed to allow rotation (default: 0)
|
||||||
|
* @param CurrentSpeed - Current movement speed for threshold check
|
||||||
|
* @returns RotationResult with new rotation and metadata
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const result = RotationController.InterpolateRotation(
|
||||||
|
* new Rotator(0, 0, 0), // Current: facing forward
|
||||||
|
* new Rotator(0, 90, 0), // Target: facing right
|
||||||
|
* 720, // 720°/sec rotation speed
|
||||||
|
* 0.016, // 60 FPS delta
|
||||||
|
* 50, // Min speed threshold
|
||||||
|
* 500 // Current speed
|
||||||
|
* );
|
||||||
|
* // Returns: Rotator smoothly interpolated toward 90°
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Rotation Interpolation
|
||||||
|
*/
|
||||||
|
public InterpolateRotation(
|
||||||
|
CurrentRotation: Rotator,
|
||||||
|
TargetRotation: Rotator,
|
||||||
|
RotationSpeed: Float,
|
||||||
|
DeltaTime: Float,
|
||||||
|
MinSpeedForRotation: Float = 0.0,
|
||||||
|
CurrentSpeed: Float = 0.0
|
||||||
|
): S_RotationResult {
|
||||||
|
// Check if character is moving fast enough to rotate
|
||||||
|
if (CurrentSpeed >= MinSpeedForRotation) {
|
||||||
|
// Calculate angular distance with wraparound handling
|
||||||
|
const angularDistance = this.GetAngularDistance(
|
||||||
|
CurrentRotation.yaw,
|
||||||
|
TargetRotation.yaw
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if rotation is not complete (within 1° tolerance)
|
||||||
|
if (MathLibrary.abs(angularDistance) <= 1.0) {
|
||||||
|
const CalculateNewYaw = (
|
||||||
|
currentRotationYaw: Float,
|
||||||
|
rotationDirection: Integer,
|
||||||
|
rotationSpeed: Float,
|
||||||
|
deltaTime: Float
|
||||||
|
): Float =>
|
||||||
|
currentRotationYaw +
|
||||||
|
MathLibrary.Min(
|
||||||
|
rotationSpeed * deltaTime,
|
||||||
|
MathLibrary.abs(angularDistance)
|
||||||
|
) *
|
||||||
|
rotationDirection;
|
||||||
|
|
||||||
|
return {
|
||||||
|
Rotation: new Rotator(
|
||||||
|
0,
|
||||||
|
CalculateNewYaw(
|
||||||
|
CurrentRotation.yaw,
|
||||||
|
angularDistance > 0 ? -1 : 1,
|
||||||
|
RotationSpeed,
|
||||||
|
DeltaTime
|
||||||
|
),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
IsRotating: true,
|
||||||
|
RemainingDelta: MathLibrary.abs(angularDistance),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
Rotation: TargetRotation,
|
||||||
|
IsRotating: false,
|
||||||
|
RemainingDelta: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
Rotation: CurrentRotation,
|
||||||
|
IsRotating: false,
|
||||||
|
RemainingDelta: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ANGLE UTILITIES
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the shortest angular distance between two angles
|
||||||
|
* Handles wraparound for shortest path
|
||||||
|
*
|
||||||
|
* @param fromAngle - Starting angle in degrees
|
||||||
|
* @param toAngle - Target angle in degrees
|
||||||
|
* @returns Signed angular distance (positive = clockwise, negative = counter-clockwise)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GetAngularDistance(10, 350) // Returns: -20 (shorter to go counter-clockwise)
|
||||||
|
* GetAngularDistance(350, 10) // Returns: 20 (shorter to go clockwise)
|
||||||
|
* GetAngularDistance(0, 180) // Returns: 180 (either direction same)
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Angle Utilities
|
||||||
|
*/
|
||||||
|
public GetAngularDistance(fromAngle: Float, toAngle: Float): Float {
|
||||||
|
// Calculate raw difference
|
||||||
|
let difference = fromAngle - toAngle;
|
||||||
|
|
||||||
|
// Normalize to the shortest path
|
||||||
|
if (difference > 180) {
|
||||||
|
difference -= 360;
|
||||||
|
} else if (difference < -180) {
|
||||||
|
difference += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difference;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONVENIENCE METHODS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update character rotation toward movement direction
|
||||||
|
* Convenience method combining target calculation and interpolation
|
||||||
|
*
|
||||||
|
* @param CurrentRotation - Current character rotation
|
||||||
|
* @param MovementDirection - Movement direction vector
|
||||||
|
* @param Config - Movement configuration with rotation settings
|
||||||
|
* @param DeltaTime - Frame delta time
|
||||||
|
* @param CurrentSpeed - Current movement speed
|
||||||
|
* @returns RotationResult with updated rotation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const result = RotationController.UpdateRotation(
|
||||||
|
* CurrentRotation,
|
||||||
|
* InputVector,
|
||||||
|
* Config,
|
||||||
|
* DeltaTime,
|
||||||
|
* CurrentSpeed
|
||||||
|
* );
|
||||||
|
* character.SetActorRotation(result.Rotation);
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Convenience Methods
|
||||||
|
*/
|
||||||
|
public UpdateRotation(
|
||||||
|
CurrentRotation: Rotator,
|
||||||
|
MovementDirection: Vector,
|
||||||
|
Config: DA_MovementConfig,
|
||||||
|
DeltaTime: Float,
|
||||||
|
CurrentSpeed: Float
|
||||||
|
): S_RotationResult {
|
||||||
|
// Rotation if enabled in config
|
||||||
|
if (Config.ShouldRotateToMovement) {
|
||||||
|
// Rotation if movement
|
||||||
|
if (MathLibrary.VectorLength(MovementDirection) >= 0.01) {
|
||||||
|
// Calculate target and interpolate;
|
||||||
|
return this.InterpolateRotation(
|
||||||
|
CurrentRotation,
|
||||||
|
this.CalculateTargetRotation(MovementDirection),
|
||||||
|
Config.RotationSpeed,
|
||||||
|
DeltaTime,
|
||||||
|
Config.MinSpeedForRotation,
|
||||||
|
CurrentSpeed
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
Rotation: CurrentRotation,
|
||||||
|
IsRotating: false,
|
||||||
|
RemainingDelta: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
Rotation: CurrentRotation,
|
||||||
|
IsRotating: false,
|
||||||
|
RemainingDelta: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BFL_RotationController = new BFL_RotationControllerClass();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Movement/Rotation/S_RotationResult.ts
|
||||||
|
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { Rotator } from '#root/UE/Rotator.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotation result data
|
||||||
|
* Contains updated rotation and metadata about rotation state
|
||||||
|
*
|
||||||
|
* @category Movement Rotation
|
||||||
|
*/
|
||||||
|
export interface S_RotationResult {
|
||||||
|
/**
|
||||||
|
* New rotation after interpolation
|
||||||
|
*/
|
||||||
|
Rotation: Rotator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether character is actively rotating
|
||||||
|
* False if rotation is complete or speed too low
|
||||||
|
*/
|
||||||
|
IsRotating: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angular distance remaining to target (degrees)
|
||||||
|
* Used for animations and debug
|
||||||
|
*/
|
||||||
|
RemainingDelta: Float;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Movement/State/BFL_MovementStateMachine.ts
|
||||||
|
|
||||||
|
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
|
||||||
|
import type { S_MovementContext } from '#root/Movement/State/S_MovementContext.ts';
|
||||||
|
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement State Machine
|
||||||
|
*
|
||||||
|
* Pure functional FSM for determining movement state
|
||||||
|
* Takes movement context and returns appropriate state
|
||||||
|
* No side effects - completely deterministic
|
||||||
|
*
|
||||||
|
* @category Movement State
|
||||||
|
* @pure All methods are pure functions
|
||||||
|
*/
|
||||||
|
class BFL_MovementStateMachineClass {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// STATE DETERMINATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine movement state based on current Context
|
||||||
|
* Main entry point for state machine logic
|
||||||
|
*
|
||||||
|
* @param Context - Current movement context
|
||||||
|
* @returns Appropriate movement state
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const state = MovementStateMachine.DetermineState({
|
||||||
|
* IsGrounded: true,
|
||||||
|
* SurfaceType: E_SurfaceType.Walkable,
|
||||||
|
* InputMagnitude: 0.8,
|
||||||
|
* CurrentSpeed: 500,
|
||||||
|
* VerticalVelocity: 0,
|
||||||
|
* IsBlocked: false
|
||||||
|
* });
|
||||||
|
* // Returns: E_MovementState.Walking
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category State Determination
|
||||||
|
*/
|
||||||
|
public DetermineState(Context: S_MovementContext): E_MovementState {
|
||||||
|
// Priority 1: Check if grounded
|
||||||
|
if (Context.IsGrounded) {
|
||||||
|
// Priority 2: Check surface type
|
||||||
|
if (Context.SurfaceType === E_SurfaceType.SteepSlope) {
|
||||||
|
return E_MovementState.Sliding;
|
||||||
|
} else if (
|
||||||
|
Context.SurfaceType === E_SurfaceType.Wall ||
|
||||||
|
Context.SurfaceType === E_SurfaceType.Ceiling ||
|
||||||
|
// Priority 3: Check if blocked by collision
|
||||||
|
Context.IsBlocked
|
||||||
|
) {
|
||||||
|
return E_MovementState.Blocked;
|
||||||
|
} else {
|
||||||
|
// Priority 4: Determine ground state based on input
|
||||||
|
return this.DetermineGroundedState(Context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.DetermineAirborneState(Context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// STATE HELPERS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine state when character is airborne
|
||||||
|
* Distinguishes between jumping, falling, etc.
|
||||||
|
*
|
||||||
|
* @param Context - Current movement context
|
||||||
|
* @returns Airborne-specific state
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category State Helpers
|
||||||
|
*/
|
||||||
|
private DetermineAirborneState(Context: S_MovementContext): E_MovementState {
|
||||||
|
// Could extend this to differentiate Jump vs Fall
|
||||||
|
// For now, just return Airborne
|
||||||
|
return E_MovementState.Airborne;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine state when character is on ground
|
||||||
|
* Distinguishes between idle, walking, running, etc.
|
||||||
|
*
|
||||||
|
* @param Context - Current movement context
|
||||||
|
* @returns Grounded-specific state
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category State Helpers
|
||||||
|
*/
|
||||||
|
private DetermineGroundedState(Context: S_MovementContext): E_MovementState {
|
||||||
|
// Check if player is providing input
|
||||||
|
if (Context.InputMagnitude > 0.01 && Context.CurrentSpeed > 1.0) {
|
||||||
|
return E_MovementState.Walking;
|
||||||
|
} else {
|
||||||
|
return E_MovementState.Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BFL_MovementStateMachine = new BFL_MovementStateMachineClass();
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Movement/State/S_MovementContext.ts
|
||||||
|
|
||||||
|
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement context data for state determination
|
||||||
|
* Contains all information needed to determine movement state
|
||||||
|
*
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
export interface S_MovementContext {
|
||||||
|
/**
|
||||||
|
* Whether character is on walkable ground
|
||||||
|
*/
|
||||||
|
IsGrounded: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of surface character is on
|
||||||
|
*/
|
||||||
|
SurfaceType: E_SurfaceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magnitude of player input (0-1)
|
||||||
|
*/
|
||||||
|
InputMagnitude: Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current horizontal movement speed (cm/s)
|
||||||
|
*/
|
||||||
|
CurrentSpeed: Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current vertical velocity (cm/s)
|
||||||
|
* Positive = moving up, Negative = falling
|
||||||
|
*/
|
||||||
|
VerticalVelocity: Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether character is blocked by collision
|
||||||
|
*/
|
||||||
|
IsBlocked: boolean;
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
Content/Movement/Structs/S_AngleThresholds.uasset (Stored with Git LFS)
BIN
Content/Movement/Structs/S_AngleThresholds.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,10 +0,0 @@
|
||||||
// Movement/Structs/S_SurfaceTestCase.ts
|
|
||||||
|
|
||||||
import type { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
|
||||||
import type { Float } from '#root/UE/Float.ts';
|
|
||||||
|
|
||||||
export interface S_SurfaceTestCase {
|
|
||||||
AngleDegrees: Float;
|
|
||||||
ExpectedType: E_SurfaceType;
|
|
||||||
Description: string;
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Structs/S_SurfaceTestCase.uasset (Stored with Git LFS)
BIN
Content/Movement/Structs/S_SurfaceTestCase.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Movement/Surface/BFL_SurfaceClassifier.ts
|
||||||
|
|
||||||
|
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.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 { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
class BFL_SurfaceClassifierClass extends BlueprintFunctionLibrary {
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CLASSIFICATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify surface type based on normal vector and angle thresholds
|
||||||
|
*
|
||||||
|
* @param SurfaceNormal - Normalized surface normal vector (from hit result)
|
||||||
|
* @param AngleThresholdsRads - Angle thresholds in radians (pre-converted for performance)
|
||||||
|
* @returns Surface type classification
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Flat ground (normal pointing up)
|
||||||
|
* const flat = SurfaceClassifier.Classify(new Vector(0, 0, 1), thresholds);
|
||||||
|
* // Returns: E_SurfaceType.Walkable
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Steep slope (50° angle)
|
||||||
|
* const steep = SurfaceClassifier.Classify(BFL_Vectors.GetNormalFromAngle(50), thresholds);
|
||||||
|
* // Returns: E_SurfaceType.SteepSlope
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Classification
|
||||||
|
*/
|
||||||
|
public Classify(
|
||||||
|
SurfaceNormal: Vector,
|
||||||
|
AngleThresholdsRads: S_AngleThresholds
|
||||||
|
): E_SurfaceType {
|
||||||
|
// Calculate angle between surface normal and up vector
|
||||||
|
const surfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
|
||||||
|
|
||||||
|
// Classify based on angle thresholds
|
||||||
|
if (surfaceAngle <= AngleThresholdsRads.Walkable) {
|
||||||
|
return E_SurfaceType.Walkable;
|
||||||
|
} else if (surfaceAngle <= AngleThresholdsRads.SteepSlope) {
|
||||||
|
return E_SurfaceType.SteepSlope;
|
||||||
|
} else if (surfaceAngle <= AngleThresholdsRads.Wall) {
|
||||||
|
return E_SurfaceType.Wall;
|
||||||
|
} else {
|
||||||
|
return E_SurfaceType.Ceiling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// TYPE CHECKS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if surface allows normal walking movement
|
||||||
|
*
|
||||||
|
* @param surfaceType - Surface type to check
|
||||||
|
* @returns True if surface is walkable
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Type Checks
|
||||||
|
*/
|
||||||
|
public IsWalkable(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 Type Checks
|
||||||
|
*/
|
||||||
|
public IsSteep(surfaceType: E_SurfaceType): boolean {
|
||||||
|
return surfaceType === E_SurfaceType.SteepSlope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if surface blocks movement (collision wall)
|
||||||
|
*
|
||||||
|
* @param surfaceType - Surface type to check
|
||||||
|
* @returns True if surface is a wall
|
||||||
|
*
|
||||||
|
* @pure true
|
||||||
|
* @category Type Checks
|
||||||
|
*/
|
||||||
|
public IsWall(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 Type Checks
|
||||||
|
*/
|
||||||
|
public IsCeiling(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 Type Checks
|
||||||
|
*/
|
||||||
|
public IsNone(surfaceType: E_SurfaceType): boolean {
|
||||||
|
return surfaceType === E_SurfaceType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BFL_SurfaceClassifier = new BFL_SurfaceClassifierClass();
|
||||||
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
||||||
// Movement/Enums/E_SurfaceType.ts
|
// Movement/Surface/E_SurfaceType.ts
|
||||||
|
|
||||||
export enum E_SurfaceType {
|
export enum E_SurfaceType {
|
||||||
None = 'None',
|
None = 'None',
|
||||||
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
||||||
// Movement/Structs/S_AngleThresholds.ts
|
// Movement/Surface/S_AngleThresholds.ts
|
||||||
|
|
||||||
import type { Float } from '#root/UE/Float.ts';
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -1,83 +0,0 @@
|
||||||
// Movement/Tests/FT_BasicMovement.ts
|
|
||||||
|
|
||||||
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
|
||||||
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
|
|
||||||
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
|
||||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
|
||||||
import { StringLibrary } from '#root/UE/StringLibrary.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functional Test: Basic Movement System
|
|
||||||
* Tests fundamental movement mechanics: acceleration, friction, max speed
|
|
||||||
* Validates movement state transitions and input processing
|
|
||||||
*/
|
|
||||||
export class FT_BasicMovement extends FunctionalTest {
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// GRAPHS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// EventGraph
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test execution - validates basic movement functionality
|
|
||||||
* Tests initialization, input processing, state management
|
|
||||||
*/
|
|
||||||
EventStartTest(): void {
|
|
||||||
// Initialize movement system
|
|
||||||
this.MovementComponent.InitializeMovementSystem(
|
|
||||||
null,
|
|
||||||
this.DebugHUDComponent
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 1: Initialization
|
|
||||||
if (this.MovementComponent.GetIsInitialized()) {
|
|
||||||
// Test 2: Initial state should be Idle
|
|
||||||
if (this.MovementComponent.GetMovementState() === E_MovementState.Idle) {
|
|
||||||
// Test 3: Initial speed & velocity is zero
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.MovementComponent.GetCurrentSpeed() === 0 &&
|
|
||||||
this.MovementComponent.GetCurrentVelocity().X === 0 &&
|
|
||||||
this.MovementComponent.GetCurrentVelocity().Y === 0 &&
|
|
||||||
this.MovementComponent.GetCurrentVelocity().Z === 0
|
|
||||||
) {
|
|
||||||
this.FinishTest(EFunctionalTestResult.Succeeded);
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Current Speed & Current velocity should be zero, got Speed: ${this.MovementComponent.GetCurrentSpeed()}, Velocity: ${StringLibrary.ConvVectorToString(this.MovementComponent.GetCurrentVelocity())}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Initial movement state should be Idle, got ${this.MovementComponent.GetMovementState()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
'Movement system failed to initialize'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// VARIABLES
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Movement system component - component under test
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
private MovementComponent = new AC_Movement();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug HUD system - displays test status and parameters
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
private DebugHUDComponent = new AC_DebugHUD();
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Tests/FT_BasicMovement.uasset (Stored with Git LFS)
BIN
Content/Movement/Tests/FT_BasicMovement.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -1,114 +0,0 @@
|
||||||
// Movement/Tests/FT_SurfaceClassification.ts
|
|
||||||
|
|
||||||
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
|
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
|
||||||
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
|
||||||
import type { S_SurfaceTestCase } from '#root/Movement/Structs/S_SurfaceTestCase.ts';
|
|
||||||
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
|
||||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functional Test: Surface Classification System
|
|
||||||
* Tests angle-based surface type detection across all boundary conditions
|
|
||||||
* Validates Walkable/SteepSlope/Wall/Ceiling classification accuracy
|
|
||||||
*/
|
|
||||||
export class FT_SurfaceClassification extends FunctionalTest {
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// GRAPHS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// EventGraph
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test execution - validates surface classification for all test cases
|
|
||||||
* Tests boundary conditions and edge cases for each surface type
|
|
||||||
*/
|
|
||||||
EventStartTest(): void {
|
|
||||||
this.TestCases.forEach(
|
|
||||||
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
|
|
||||||
const surfaceType = this.MovementComponent.ClassifySurface(
|
|
||||||
BFL_Vectors.GetNormalFromAngle(AngleDegrees)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (surfaceType === ExpectedType) {
|
|
||||||
this.FinishTest(EFunctionalTestResult.Succeeded);
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Movement Component test ${arrayIndex + 1} FAIL: ${Description} (${AngleDegrees}°) expected ${ExpectedType}, got ${surfaceType}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// VARIABLES
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Movement system component - provides surface classification functionality
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
private MovementComponent = new AC_Movement();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comprehensive test cases covering all surface type boundaries
|
|
||||||
* Tests edge cases and typical angles for each classification
|
|
||||||
* @category Test Data
|
|
||||||
*/
|
|
||||||
private TestCases: S_SurfaceTestCase[] = [
|
|
||||||
{
|
|
||||||
AngleDegrees: 0.0,
|
|
||||||
ExpectedType: E_SurfaceType.Walkable,
|
|
||||||
Description: 'Flat surface',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 25.0,
|
|
||||||
ExpectedType: E_SurfaceType.Walkable,
|
|
||||||
Description: 'Gentle slope',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 49.0,
|
|
||||||
ExpectedType: E_SurfaceType.Walkable,
|
|
||||||
Description: 'Max walkable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 51.0,
|
|
||||||
ExpectedType: E_SurfaceType.SteepSlope,
|
|
||||||
Description: 'Steep slope',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 70.0,
|
|
||||||
ExpectedType: E_SurfaceType.SteepSlope,
|
|
||||||
Description: 'Very steep',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 84.0,
|
|
||||||
ExpectedType: E_SurfaceType.SteepSlope,
|
|
||||||
Description: 'Max steep',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 90.0,
|
|
||||||
ExpectedType: E_SurfaceType.Wall,
|
|
||||||
Description: 'Vertical wall',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 94.0,
|
|
||||||
ExpectedType: E_SurfaceType.Wall,
|
|
||||||
Description: 'Max wall',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 120.0,
|
|
||||||
ExpectedType: E_SurfaceType.Ceiling,
|
|
||||||
Description: 'Overhang',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AngleDegrees: 180.0,
|
|
||||||
ExpectedType: E_SurfaceType.Ceiling,
|
|
||||||
Description: 'Ceiling',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
BIN
Content/Movement/Tests/FT_SurfaceClassification.uasset (Stored with Git LFS)
BIN
Content/Movement/Tests/FT_SurfaceClassification.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -10,6 +10,10 @@ export class Actor extends UEObject {
|
||||||
super(outer, name);
|
super(outer, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetActorLocation(): Vector {
|
||||||
|
return new Vector(); // Placeholder implementation
|
||||||
|
}
|
||||||
|
|
||||||
public SetActorLocation(
|
public SetActorLocation(
|
||||||
NewLocation: Vector = new Vector(),
|
NewLocation: Vector = new Vector(),
|
||||||
Sweep: boolean = false,
|
Sweep: boolean = false,
|
||||||
|
|
@ -19,6 +23,10 @@ export class Actor extends UEObject {
|
||||||
// Implementation for setting actor location
|
// Implementation for setting actor location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetActorRotation(): Rotator {
|
||||||
|
return new Rotator(); // Placeholder implementation
|
||||||
|
}
|
||||||
|
|
||||||
public SetActorRotation(
|
public SetActorRotation(
|
||||||
NewRotation: Rotator = new Rotator(),
|
NewRotation: Rotator = new Rotator(),
|
||||||
TeleportPhysics: boolean = false
|
TeleportPhysics: boolean = false
|
||||||
|
|
@ -26,8 +34,4 @@ export class Actor extends UEObject {
|
||||||
console.log(NewRotation, TeleportPhysics);
|
console.log(NewRotation, TeleportPhysics);
|
||||||
// Implementation for setting actor rotation
|
// Implementation for setting actor rotation
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetActorLocation(): Vector {
|
|
||||||
return new Vector(); // Placeholder implementation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,15 +76,25 @@ class MathLibraryClass extends BlueprintFunctionLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate arctangent2 of Y and X
|
* Calculate arctangent2 of Y and X in radians
|
||||||
* @param Y - Y coordinate
|
* @param Y - Y coordinate
|
||||||
* @param X - X coordinate
|
* @param X - X coordinate
|
||||||
* @returns Angle in radians (-π to π)
|
* @returns Angle in radians (-π to π)
|
||||||
*/
|
*/
|
||||||
public Atan2(Y: Float, X: Float): Float {
|
public Atan2Radians(Y: Float, X: Float): Float {
|
||||||
return Math.atan2(Y, X);
|
return Math.atan2(Y, X);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate arctangent2 of Y and X in degrees
|
||||||
|
* @param Y - Y coordinate
|
||||||
|
* @param X - X coordinate
|
||||||
|
* @returns Angle in degrees (-180 to 180)
|
||||||
|
*/
|
||||||
|
public Atan2Degrees(Y: Float, X: Float): Float {
|
||||||
|
return this.RadiansToDegrees(Math.atan2(Y, X));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate dot product of two vectors
|
* Calculate dot product of two vectors
|
||||||
* @param Vector1 - First vector
|
* @param Vector1 - First vector
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// UE/PrimaryDataAsset.ts
|
||||||
|
|
||||||
|
import { DataAsset } from '#root/UE/DataAsset.ts';
|
||||||
|
import { Name } from '#root/UE/Name.ts';
|
||||||
|
import { UEObject } from '#root/UE/UEObject.ts';
|
||||||
|
|
||||||
|
export class PrimaryDataAsset extends DataAsset {
|
||||||
|
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue