[code] AC_Movement refactoring
parent
8ee0cba309
commit
9539f48a06
10
.eslintrc.js
10
.eslintrc.js
|
|
@ -108,13 +108,13 @@ module.exports = {
|
|||
{
|
||||
'selector': 'variable',
|
||||
'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',
|
||||
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
||||
'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
|
||||
}
|
||||
},
|
||||
|
|
@ -122,14 +122,14 @@ module.exports = {
|
|||
{
|
||||
'selector': 'class',
|
||||
'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
|
||||
{
|
||||
'selector': 'class',
|
||||
'format': ['PascalCase'],
|
||||
'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
|
||||
}
|
||||
},
|
||||
|
|
@ -137,7 +137,7 @@ module.exports = {
|
|||
{
|
||||
'selector': 'interface',
|
||||
'format': ['PascalCase'],
|
||||
'prefix': ['S_', 'I_', 'I']
|
||||
'prefix': ['S_', 'I_', 'I', 'DA_']
|
||||
},
|
||||
// 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_InputDevice } from '#root/Input/Components/AC_InputDevice.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 { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||
import { Cast } from '#root/UE/Cast.ts';
|
||||
|
|
@ -201,8 +201,6 @@ export class BP_MainCharacter extends Pawn {
|
|||
DeltaTime
|
||||
);
|
||||
|
||||
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
|
||||
|
||||
if (this.ShowDebugInfo) {
|
||||
this.MovementComponent.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
|
||||
|
||||
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 type { Text } from '#root/UE/Text.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_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.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_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
||||
import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts';
|
||||
|
|
@ -46,13 +44,6 @@ const InputDeviceDetectionTest = new FT_InputDeviceDetection();
|
|||
|
||||
InputDeviceDetectionTest.EventStartTest();
|
||||
|
||||
// Movement Tests
|
||||
const BasicMovementTest = new FT_BasicMovement();
|
||||
const SurfaceClassificationTest = new FT_SurfaceClassification();
|
||||
|
||||
BasicMovementTest.EventStartTest();
|
||||
SurfaceClassificationTest.EventStartTest();
|
||||
|
||||
// Toasts Tests
|
||||
const ToastLimitsTest = new FT_ToastLimit();
|
||||
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 {
|
||||
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';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public GetActorLocation(): Vector {
|
||||
return new Vector(); // Placeholder implementation
|
||||
}
|
||||
|
||||
public SetActorLocation(
|
||||
NewLocation: Vector = new Vector(),
|
||||
Sweep: boolean = false,
|
||||
|
|
@ -19,6 +23,10 @@ export class Actor extends UEObject {
|
|||
// Implementation for setting actor location
|
||||
}
|
||||
|
||||
public GetActorRotation(): Rotator {
|
||||
return new Rotator(); // Placeholder implementation
|
||||
}
|
||||
|
||||
public SetActorRotation(
|
||||
NewRotation: Rotator = new Rotator(),
|
||||
TeleportPhysics: boolean = false
|
||||
|
|
@ -26,8 +34,4 @@ export class Actor extends UEObject {
|
|||
console.log(NewRotation, TeleportPhysics);
|
||||
// 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 X - X coordinate
|
||||
* @returns Angle in radians (-π to π)
|
||||
*/
|
||||
public Atan2(Y: Float, X: Float): Float {
|
||||
public Atan2Radians(Y: Float, X: Float): Float {
|
||||
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
|
||||
* @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