[code] implement swept collision detection and ground detection
Movement System: - Add PerformDeterministicSweep() with adaptive step sizing - Implement HandleSweepCollision() for slide response - Add CheckGround() for walkable surface detection - Implement ground snapping to prevent Z jitter - Add collision counter tracking (max 25 checks/frame) Configuration: - MaxStepSize: 50.0 (sweep collision stepping) - MinStepSize: 1.0 (precision control) - MaxCollisionChecks: 25 (performance limit) - GroundTraceDistance: 5.0 (ground detection range) Physics: - Swept collision prevents tunneling at high speeds - Adaptive stepping: fewer checks at low velocity - Ground snapping maintains stable Z position - Deterministic collision response for slide behavior Testing: - Add FT_MovementConfiguration for constants validation - Update FT_BasicMovement to use public getters - Maintain FT_SurfaceClassification (10 test cases) - Manual testing checklist for collision/physics validationmain
parent
df35fae518
commit
8ee0cba309
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
|
||||||
import { IMC_Default } from '#root/Input/IMC_Default.ts';
|
import { IMC_Default } from '#root/Input/IMC_Default.ts';
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
||||||
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
|
||||||
|
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
|
||||||
import { Cast } from '#root/UE/Cast.ts';
|
import { Cast } from '#root/UE/Cast.ts';
|
||||||
import type { Controller } from '#root/UE/Controller.ts';
|
import type { Controller } from '#root/UE/Controller.ts';
|
||||||
import { EnhancedInputLocalPlayerSubsystem } from '#root/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
import { EnhancedInputLocalPlayerSubsystem } from '#root/UE/EnhancedInputLocalPlayerSubsystem.ts';
|
||||||
|
|
@ -162,7 +163,10 @@ export class BP_MainCharacter extends Pawn {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.MovementComponent.InitializeMovementSystem(this.DebugHUDComponent);
|
this.MovementComponent.InitializeMovementSystem(
|
||||||
|
this.CharacterCapsule,
|
||||||
|
this.DebugHUDComponent
|
||||||
|
);
|
||||||
|
|
||||||
this.CameraComponent.InitializeCameraSystem(
|
this.CameraComponent.InitializeCameraSystem(
|
||||||
this.InputDeviceComponent,
|
this.InputDeviceComponent,
|
||||||
|
|
@ -197,7 +201,7 @@ export class BP_MainCharacter extends Pawn {
|
||||||
DeltaTime
|
DeltaTime
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ApplyMovementAndRotation();
|
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
|
||||||
|
|
||||||
if (this.ShowDebugInfo) {
|
if (this.ShowDebugInfo) {
|
||||||
this.MovementComponent.UpdateDebugPage();
|
this.MovementComponent.UpdateDebugPage();
|
||||||
|
|
@ -206,56 +210,15 @@ export class BP_MainCharacter extends Pawn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// FUNCTIONS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply calculated movement velocity to actor position
|
|
||||||
* @category Movement Application
|
|
||||||
*/
|
|
||||||
private ApplyMovementAndRotation(): void {
|
|
||||||
this.SetActorRotation(this.MovementComponent.CurrentRotation);
|
|
||||||
|
|
||||||
const CalculateNewLocation = (
|
|
||||||
currentLocation: Vector,
|
|
||||||
velocity: Vector
|
|
||||||
): Vector =>
|
|
||||||
new Vector(
|
|
||||||
currentLocation.X + velocity.X * this.DeltaTime,
|
|
||||||
currentLocation.Y + velocity.Y * this.DeltaTime,
|
|
||||||
currentLocation.Z + velocity.Z * this.DeltaTime
|
|
||||||
);
|
|
||||||
|
|
||||||
this.SetActorLocation(
|
|
||||||
CalculateNewLocation(
|
|
||||||
this.GetActorLocation(),
|
|
||||||
this.MovementComponent.CurrentVelocity
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// VARIABLES
|
// VARIABLES
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core movement system component - handles deterministic 3D platformer movement
|
* Camera system component - handles camera rotation and sensitivity
|
||||||
* @category Components
|
* @category Components
|
||||||
*/
|
*/
|
||||||
MovementComponent = new AC_Movement();
|
CameraComponent = new AC_Camera();
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug HUD system - displays movement parameters and performance metrics
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
DebugHUDComponent = new AC_DebugHUD();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toast notification system - displays temporary status messages
|
|
||||||
* @category Components
|
|
||||||
*/
|
|
||||||
ToastSystemComponent = new AC_ToastSystem();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input device detection component - manages input device state and detection
|
* Input device detection component - manages input device state and detection
|
||||||
|
|
@ -264,10 +227,28 @@ export class BP_MainCharacter extends Pawn {
|
||||||
InputDeviceComponent = new AC_InputDevice();
|
InputDeviceComponent = new AC_InputDevice();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Camera system component - handles camera rotation and sensitivity
|
* Toast notification system - displays temporary status messages
|
||||||
* @category Components
|
* @category Components
|
||||||
*/
|
*/
|
||||||
CameraComponent = new AC_Camera();
|
ToastSystemComponent = new AC_ToastSystem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug HUD system - displays movement parameters and performance metrics
|
||||||
|
* @category Components
|
||||||
|
*/
|
||||||
|
DebugHUDComponent = new AC_DebugHUD();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character's capsule component for collision detection
|
||||||
|
* @category Components
|
||||||
|
*/
|
||||||
|
CharacterCapsule = new CapsuleComponent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core movement system component - handles deterministic 3D platformer movement
|
||||||
|
* @category Components
|
||||||
|
*/
|
||||||
|
MovementComponent = new AC_Movement();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
|
* Master debug toggle - controls all debug systems (HUD, toasts, visual debug)
|
||||||
|
|
|
||||||
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/Debug/Components/AC_DebugHUD.uasset (Stored with Git LFS)
BIN
Content/Debug/Components/AC_DebugHUD.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -11,7 +11,6 @@ import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement
|
||||||
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
|
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
|
||||||
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
|
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
|
||||||
import { FT_BasicMovement } from '#root/Movement/Tests/FT_BasicMovement.ts';
|
import { FT_BasicMovement } from '#root/Movement/Tests/FT_BasicMovement.ts';
|
||||||
import { FT_DiagonalMovement } from '#root/Movement/Tests/FT_DiagonalMovement.ts';
|
|
||||||
import { FT_SurfaceClassification } from '#root/Movement/Tests/FT_SurfaceClassification.ts';
|
import { FT_SurfaceClassification } from '#root/Movement/Tests/FT_SurfaceClassification.ts';
|
||||||
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
|
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
|
||||||
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
||||||
|
|
@ -50,11 +49,9 @@ InputDeviceDetectionTest.EventStartTest();
|
||||||
// Movement Tests
|
// Movement Tests
|
||||||
const BasicMovementTest = new FT_BasicMovement();
|
const BasicMovementTest = new FT_BasicMovement();
|
||||||
const SurfaceClassificationTest = new FT_SurfaceClassification();
|
const SurfaceClassificationTest = new FT_SurfaceClassification();
|
||||||
const DiagonalMovement = new FT_DiagonalMovement();
|
|
||||||
|
|
||||||
BasicMovementTest.EventStartTest();
|
BasicMovementTest.EventStartTest();
|
||||||
SurfaceClassificationTest.EventStartTest();
|
SurfaceClassificationTest.EventStartTest();
|
||||||
DiagonalMovement.EventStartTest();
|
|
||||||
|
|
||||||
// Toasts Tests
|
// Toasts Tests
|
||||||
const ToastLimitsTest = new FT_ToastLimit();
|
const ToastLimitsTest = new FT_ToastLimit();
|
||||||
|
|
|
||||||
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)
Binary file not shown.
|
|
@ -6,7 +6,12 @@ import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
|
||||||
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
||||||
import { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.ts';
|
import { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.ts';
|
||||||
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
import { ActorComponent } from '#root/UE/ActorComponent.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 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 { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||||
import { Rotator } from '#root/UE/Rotator.ts';
|
import { Rotator } from '#root/UE/Rotator.ts';
|
||||||
import { StringLibrary } from '#root/UE/StringLibrary.ts';
|
import { StringLibrary } from '#root/UE/StringLibrary.ts';
|
||||||
|
|
@ -23,10 +28,13 @@ export class AC_Movement extends ActorComponent {
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Surface Detection
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify surface type based on normal vector
|
* Classify surface type based on normal vector
|
||||||
* @param SurfaceNormal - Normalized surface normal vector
|
* @param SurfaceNormal - Normalized surface normal vector
|
||||||
* @param AngleThresholds - Angle thresholds in radians
|
|
||||||
* @returns Surface type classification
|
* @returns Surface type classification
|
||||||
* @example
|
* @example
|
||||||
* // Classify flat ground
|
* // Classify flat ground
|
||||||
|
|
@ -34,10 +42,7 @@ export class AC_Movement extends ActorComponent {
|
||||||
* @pure true
|
* @pure true
|
||||||
* @category Surface Detection
|
* @category Surface Detection
|
||||||
*/
|
*/
|
||||||
public ClassifySurface(
|
public ClassifySurface(SurfaceNormal: Vector): E_SurfaceType {
|
||||||
SurfaceNormal: Vector,
|
|
||||||
AngleThresholds: S_AngleThresholds
|
|
||||||
): E_SurfaceType {
|
|
||||||
const SurfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
|
const SurfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,11 +63,11 @@ export class AC_Movement extends ActorComponent {
|
||||||
const IsWallAngle = (wallAngle: Float): boolean =>
|
const IsWallAngle = (wallAngle: Float): boolean =>
|
||||||
SurfaceAngle <= wallAngle;
|
SurfaceAngle <= wallAngle;
|
||||||
|
|
||||||
if (IsWalkableAngle(AngleThresholds.Walkable)) {
|
if (IsWalkableAngle(this.AngleThresholdsRads.Walkable)) {
|
||||||
return E_SurfaceType.Walkable;
|
return E_SurfaceType.Walkable;
|
||||||
} else if (IsSteepSlopeAngle(AngleThresholds.SteepSlope)) {
|
} else if (IsSteepSlopeAngle(this.AngleThresholdsRads.SteepSlope)) {
|
||||||
return E_SurfaceType.SteepSlope;
|
return E_SurfaceType.SteepSlope;
|
||||||
} else if (IsWallAngle(AngleThresholds.Wall)) {
|
} else if (IsWallAngle(this.AngleThresholdsRads.Wall)) {
|
||||||
return E_SurfaceType.Wall;
|
return E_SurfaceType.Wall;
|
||||||
} else {
|
} else {
|
||||||
return E_SurfaceType.Ceiling;
|
return E_SurfaceType.Ceiling;
|
||||||
|
|
@ -124,6 +129,10 @@ export class AC_Movement extends ActorComponent {
|
||||||
return SurfaceType === E_SurfaceType.None;
|
return SurfaceType === E_SurfaceType.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Movement Processing
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process movement input from player controller
|
* Process movement input from player controller
|
||||||
* Normalizes input and calculates target velocity
|
* Normalizes input and calculates target velocity
|
||||||
|
|
@ -138,6 +147,10 @@ export class AC_Movement extends ActorComponent {
|
||||||
this.TargetRotation = this.CalculateTargetRotation(InputVector);
|
this.TargetRotation = this.CalculateTargetRotation(InputVector);
|
||||||
this.UpdateCharacterRotation(DeltaTime);
|
this.UpdateCharacterRotation(DeltaTime);
|
||||||
|
|
||||||
|
this.LastGroundHit = this.CheckGround();
|
||||||
|
|
||||||
|
this.IsGrounded = this.LastGroundHit.BlockingHit;
|
||||||
|
|
||||||
// Only process movement on walkable surfaces
|
// Only process movement on walkable surfaces
|
||||||
if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) {
|
if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) {
|
||||||
this.ProcessGroundMovement(InputVector, DeltaTime);
|
this.ProcessGroundMovement(InputVector, DeltaTime);
|
||||||
|
|
@ -145,14 +158,10 @@ export class AC_Movement extends ActorComponent {
|
||||||
this.ApplyFriction(DeltaTime);
|
this.ApplyFriction(DeltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always apply gravity
|
this.ApplyGravity();
|
||||||
this.ApplyGravity(DeltaTime);
|
|
||||||
|
|
||||||
// Update movement state
|
|
||||||
this.UpdateMovementState();
|
this.UpdateMovementState();
|
||||||
|
|
||||||
// Calculate current speed for debug
|
|
||||||
this.UpdateCurrentSpeed();
|
this.UpdateCurrentSpeed();
|
||||||
|
this.ApplyMovementWithSweep(DeltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,19 +173,16 @@ export class AC_Movement extends ActorComponent {
|
||||||
*/
|
*/
|
||||||
private ProcessGroundMovement(InputVector: Vector, DeltaTime: Float): void {
|
private ProcessGroundMovement(InputVector: Vector, DeltaTime: Float): void {
|
||||||
if (this.InputMagnitude > 0.01) {
|
if (this.InputMagnitude > 0.01) {
|
||||||
const CalculateTargetVelocity = (
|
const CalculateTargetVelocity = (inputVector: Vector): Vector =>
|
||||||
inputVector: Vector,
|
|
||||||
maxSpeed: Float
|
|
||||||
): Vector =>
|
|
||||||
new Vector(
|
new Vector(
|
||||||
MathLibrary.Normal(inputVector).X * maxSpeed,
|
MathLibrary.Normal(inputVector).X * this.MaxSpeed,
|
||||||
MathLibrary.Normal(inputVector).Y * maxSpeed,
|
MathLibrary.Normal(inputVector).Y * this.MaxSpeed,
|
||||||
MathLibrary.Normal(inputVector).Z * maxSpeed
|
MathLibrary.Normal(inputVector).Z * this.MaxSpeed
|
||||||
);
|
);
|
||||||
|
|
||||||
this.CurrentVelocity = MathLibrary.VInterpTo(
|
this.CurrentVelocity = MathLibrary.VInterpTo(
|
||||||
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0),
|
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0),
|
||||||
CalculateTargetVelocity(InputVector, this.MaxSpeed),
|
CalculateTargetVelocity(InputVector),
|
||||||
DeltaTime,
|
DeltaTime,
|
||||||
this.Acceleration
|
this.Acceleration
|
||||||
);
|
);
|
||||||
|
|
@ -202,22 +208,18 @@ export class AC_Movement extends ActorComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply gravity to vertical velocity
|
* Apply gravity to vertical velocity
|
||||||
* @param DeltaTime - Frame delta time
|
|
||||||
* @category Movement Processing
|
* @category Movement Processing
|
||||||
*/
|
*/
|
||||||
private ApplyGravity(DeltaTime: Float): void {
|
private ApplyGravity(): void {
|
||||||
if (!this.IsGrounded) {
|
if (!this.IsGrounded) {
|
||||||
const ApplyGravityForce = (
|
const ApplyGravityForce = (velocityZ: Float): Float =>
|
||||||
velocityZ: Float,
|
velocityZ - this.Gravity;
|
||||||
gravity: Float,
|
|
||||||
deltaTime: Float
|
|
||||||
): Float => velocityZ - gravity * deltaTime;
|
|
||||||
|
|
||||||
// Apply gravity when airborne
|
// Apply gravity when airborne
|
||||||
this.CurrentVelocity = new Vector(
|
this.CurrentVelocity = new Vector(
|
||||||
this.CurrentVelocity.X,
|
this.CurrentVelocity.X,
|
||||||
this.CurrentVelocity.Y,
|
this.CurrentVelocity.Y,
|
||||||
ApplyGravityForce(this.CurrentVelocity.Z, this.Gravity, DeltaTime)
|
ApplyGravityForce(this.CurrentVelocity.Z)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Zero out vertical velocity when grounded
|
// Zero out vertical velocity when grounded
|
||||||
|
|
@ -254,6 +256,106 @@ export class AC_Movement extends ActorComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Movement Application
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply movement with deterministic sweep collision detection
|
||||||
|
* Replaces direct position update with collision-safe movement
|
||||||
|
* @param DeltaTime - Frame delta time
|
||||||
|
* @category Movement Application
|
||||||
|
* @pure false - modifies actor position
|
||||||
|
*/
|
||||||
|
private ApplyMovementWithSweep(DeltaTime: Float): void {
|
||||||
|
// Get current actor location
|
||||||
|
const currentLocation = this.GetOwner().GetActorLocation();
|
||||||
|
|
||||||
|
// Calculate desired movement delta
|
||||||
|
const desiredDelta = new Vector(
|
||||||
|
this.CurrentVelocity.X * DeltaTime,
|
||||||
|
this.CurrentVelocity.Y * DeltaTime,
|
||||||
|
this.CurrentVelocity.Z * DeltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform swept movement
|
||||||
|
const SweepResult = this.PerformDeterministicSweep(
|
||||||
|
currentLocation,
|
||||||
|
desiredDelta,
|
||||||
|
DeltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply final position
|
||||||
|
if (SweepResult.BlockingHit) {
|
||||||
|
// Hit something - use safe hit location
|
||||||
|
this.GetOwner().SetActorLocation(SweepResult.Location);
|
||||||
|
|
||||||
|
// Handle collision response (slide along surface)
|
||||||
|
const CalculateRemainingDelta = (sweepResultLocation: Vector): Vector =>
|
||||||
|
new Vector(
|
||||||
|
desiredDelta.X - (sweepResultLocation.X - currentLocation.X),
|
||||||
|
desiredDelta.Y - (sweepResultLocation.Y - currentLocation.Y),
|
||||||
|
desiredDelta.Z - (sweepResultLocation.Z - currentLocation.Z)
|
||||||
|
);
|
||||||
|
|
||||||
|
const SlideVector = this.HandleSweepCollision(
|
||||||
|
SweepResult,
|
||||||
|
CalculateRemainingDelta(SweepResult.Location)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply slide movement if significant
|
||||||
|
if (MathLibrary.VectorLength(SlideVector) > 0.01) {
|
||||||
|
this.GetOwner().SetActorLocation(
|
||||||
|
new Vector(
|
||||||
|
SweepResult.Location.X + SlideVector.X,
|
||||||
|
SweepResult.Location.Y + SlideVector.Y,
|
||||||
|
SweepResult.Location.Z + SlideVector.Z
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No collision - use final sweep location
|
||||||
|
this.GetOwner().SetActorLocation(SweepResult.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShouldSnapToGround = (
|
||||||
|
isGroundHit: boolean,
|
||||||
|
currentVelocityZ: Float,
|
||||||
|
isCapsuleValid: boolean
|
||||||
|
): boolean =>
|
||||||
|
this.IsGrounded && isGroundHit && currentVelocityZ <= 0 && isCapsuleValid;
|
||||||
|
|
||||||
|
if (
|
||||||
|
ShouldSnapToGround(
|
||||||
|
this.LastGroundHit.BlockingHit,
|
||||||
|
this.CurrentVelocity.Z,
|
||||||
|
SystemLibrary.IsValid(this.CapsuleComponent)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const correctZ =
|
||||||
|
this.LastGroundHit.Location.Z +
|
||||||
|
this.CapsuleComponent!.GetScaledCapsuleHalfHeight();
|
||||||
|
|
||||||
|
const CalculateZDifference = (currentLocZ: Float): Float =>
|
||||||
|
currentLocZ - correctZ;
|
||||||
|
|
||||||
|
const zDifference = CalculateZDifference(currentLocation.Z);
|
||||||
|
|
||||||
|
const IsWithinSnapRange = (): boolean =>
|
||||||
|
zDifference > 0.1 && zDifference < this.GroundTraceDistance;
|
||||||
|
|
||||||
|
if (IsWithinSnapRange()) {
|
||||||
|
this.GetOwner().SetActorLocation(
|
||||||
|
new Vector(currentLocation.X, currentLocation.Y, correctZ)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Character Rotation
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate target rotation based on movement direction
|
* Calculate target rotation based on movement direction
|
||||||
* Determines what rotation character should have based on movement
|
* Determines what rotation character should have based on movement
|
||||||
|
|
@ -262,7 +364,7 @@ export class AC_Movement extends ActorComponent {
|
||||||
* @pure true
|
* @pure true
|
||||||
* @category Character Rotation
|
* @category Character Rotation
|
||||||
*/
|
*/
|
||||||
public CalculateTargetRotation(MovementDirection: Vector): Rotator {
|
private CalculateTargetRotation(MovementDirection: Vector): Rotator {
|
||||||
const TargetYaw = (
|
const TargetYaw = (
|
||||||
movementDirectionX: Float,
|
movementDirectionX: Float,
|
||||||
movementDirectionY: Float
|
movementDirectionY: Float
|
||||||
|
|
@ -290,7 +392,7 @@ export class AC_Movement extends ActorComponent {
|
||||||
* @param DeltaTime - Time since last frame
|
* @param DeltaTime - Time since last frame
|
||||||
* @category Character Rotation
|
* @category Character Rotation
|
||||||
*/
|
*/
|
||||||
public UpdateCharacterRotation(DeltaTime: Float): void {
|
private UpdateCharacterRotation(DeltaTime: Float): void {
|
||||||
if (this.ShouldRotateToMovement) {
|
if (this.ShouldRotateToMovement) {
|
||||||
const ShouldRotate = (): boolean =>
|
const ShouldRotate = (): boolean =>
|
||||||
this.CurrentSpeed >= this.MinSpeedForRotation;
|
this.CurrentSpeed >= this.MinSpeedForRotation;
|
||||||
|
|
@ -341,14 +443,264 @@ export class AC_Movement extends ActorComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Collision State
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset collision counter at start of frame
|
||||||
|
* Called before movement processing
|
||||||
|
* @category Collision State
|
||||||
|
* @pure false - modifies SweepCollisionCount
|
||||||
|
*/
|
||||||
|
private ResetCollisionCounter(): void {
|
||||||
|
this.SweepCollisionCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Collision Detection
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate adaptive step size based on velocity
|
||||||
|
* Fast movement = smaller steps, slow movement = larger steps
|
||||||
|
* @param Velocity - Current movement velocity
|
||||||
|
* @param DeltaTime - Frame delta time
|
||||||
|
* @returns Step size clamped between MinStepSize and MaxStepSize
|
||||||
|
* @category Collision Detection
|
||||||
|
* @pure true - pure calculation based on inputs
|
||||||
|
*/
|
||||||
|
private CalculateAdaptiveStepSize(Velocity: Vector, DeltaTime: Float): Float {
|
||||||
|
const CalculateFrameDistance = (
|
||||||
|
VelocityX: Float,
|
||||||
|
VelocityY: Float,
|
||||||
|
deltaTime: Float
|
||||||
|
): Float =>
|
||||||
|
MathLibrary.VectorLength(new Vector(VelocityX, VelocityY)) * deltaTime;
|
||||||
|
|
||||||
|
const frameDistance = CalculateFrameDistance(
|
||||||
|
Velocity.X,
|
||||||
|
Velocity.Y,
|
||||||
|
DeltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
const IsMovingTooSlow = (): boolean => frameDistance < this.MinStepSize;
|
||||||
|
|
||||||
|
if (IsMovingTooSlow()) {
|
||||||
|
return this.MaxStepSize;
|
||||||
|
} else {
|
||||||
|
const CalculateClampedStepSize = (): Float =>
|
||||||
|
MathLibrary.ClampFloat(
|
||||||
|
frameDistance * 0.5,
|
||||||
|
this.MinStepSize,
|
||||||
|
this.MaxStepSize
|
||||||
|
);
|
||||||
|
|
||||||
|
return CalculateClampedStepSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform deterministic sweep collision detection with stepped checks
|
||||||
|
* Prevents tunneling by breaking movement into smaller steps
|
||||||
|
* @param StartLocation - Starting position for sweep
|
||||||
|
* @param DesiredDelta - Desired movement vector
|
||||||
|
* @param DeltaTime - Frame delta time for adaptive stepping
|
||||||
|
* @returns HitResult with collision information (bBlockingHit, Location, Normal, etc.)
|
||||||
|
* @category Collision Detection
|
||||||
|
*/
|
||||||
|
private PerformDeterministicSweep(
|
||||||
|
StartLocation: Vector,
|
||||||
|
DesiredDelta: Vector,
|
||||||
|
DeltaTime: Float
|
||||||
|
): HitResult {
|
||||||
|
// Reset collision counter for this frame
|
||||||
|
this.ResetCollisionCounter();
|
||||||
|
|
||||||
|
// Calculate total distance to travel
|
||||||
|
const totalDistance = MathLibrary.VectorLength(DesiredDelta);
|
||||||
|
|
||||||
|
const ShouldPerformSweep = (isValid: boolean): boolean =>
|
||||||
|
isValid && totalDistance >= 0.01;
|
||||||
|
|
||||||
|
if (ShouldPerformSweep(SystemLibrary.IsValid(this.CapsuleComponent))) {
|
||||||
|
// Calculate adaptive step size based on velocity
|
||||||
|
const stepSize = this.CalculateAdaptiveStepSize(
|
||||||
|
this.CurrentVelocity,
|
||||||
|
DeltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current position during sweep
|
||||||
|
let currentLocation = StartLocation;
|
||||||
|
let remainingDistance = totalDistance;
|
||||||
|
|
||||||
|
const CalculateNumSteps = (): Integer =>
|
||||||
|
MathLibrary.Min(
|
||||||
|
MathLibrary.Ceil(totalDistance / stepSize),
|
||||||
|
this.MaxCollisionChecks
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform stepped sweep
|
||||||
|
for (let i = 0; i < CalculateNumSteps(); i++) {
|
||||||
|
this.SweepCollisionCount++;
|
||||||
|
|
||||||
|
const CalculateStepSize = (): Float =>
|
||||||
|
MathLibrary.Min(stepSize, remainingDistance);
|
||||||
|
|
||||||
|
// Calculate step distance (last step might be shorter)
|
||||||
|
const currentStepSize = CalculateStepSize();
|
||||||
|
|
||||||
|
const CalculateStepTarget = (desiredDelta: Vector): Vector => {
|
||||||
|
const normalizedDelta = MathLibrary.Normal(desiredDelta);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
currentLocation.X + normalizedDelta.X * currentStepSize,
|
||||||
|
currentLocation.Y + normalizedDelta.Y * currentStepSize,
|
||||||
|
currentLocation.Z + normalizedDelta.Z * currentStepSize
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate target position for this step
|
||||||
|
const targetLocation = CalculateStepTarget(DesiredDelta);
|
||||||
|
|
||||||
|
const { OutHit, ReturnValue } = SystemLibrary.CapsuleTraceByChannel(
|
||||||
|
currentLocation,
|
||||||
|
targetLocation,
|
||||||
|
this.CapsuleComponent!.GetScaledCapsuleRadius(),
|
||||||
|
this.CapsuleComponent!.GetScaledCapsuleHalfHeight(),
|
||||||
|
ETraceTypeQuery.Visibility,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
EDrawDebugTrace.ForDuration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ReturnValue) {
|
||||||
|
return OutHit;
|
||||||
|
} else {
|
||||||
|
currentLocation = targetLocation;
|
||||||
|
remainingDistance = remainingDistance - currentStepSize;
|
||||||
|
|
||||||
|
if (remainingDistance <= 0.01) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoHit = new HitResult();
|
||||||
|
NoHit.Location = currentLocation;
|
||||||
|
return NoHit;
|
||||||
|
} else {
|
||||||
|
// If no movement, return empty hit
|
||||||
|
const NoHit = new HitResult();
|
||||||
|
NoHit.Location = StartLocation;
|
||||||
|
return NoHit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Collision Response
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle collision response after sweep hit
|
||||||
|
* Calculates slide vector along collision surface
|
||||||
|
* @param HitResult - Collision information from sweep
|
||||||
|
* @param RemainingDelta - Remaining movement after collision
|
||||||
|
* @returns Adjusted movement vector sliding along surface
|
||||||
|
* @category Collision Response
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
private HandleSweepCollision(
|
||||||
|
HitResult: HitResult,
|
||||||
|
RemainingDelta: Vector
|
||||||
|
): Vector {
|
||||||
|
if (HitResult.BlockingHit) {
|
||||||
|
const ProjectOntoSurface = (
|
||||||
|
hitNormal: Vector,
|
||||||
|
remainingDelta: Vector
|
||||||
|
): Vector => {
|
||||||
|
// Project remaining movement onto collision surface
|
||||||
|
const dotProduct = MathLibrary.Dot(hitNormal, remainingDelta);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
remainingDelta.X - dotProduct * hitNormal.X,
|
||||||
|
remainingDelta.Y - dotProduct * hitNormal.Y,
|
||||||
|
remainingDelta.Z - dotProduct * hitNormal.Z
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ProjectOntoSurface(HitResult.ImpactNormal, RemainingDelta);
|
||||||
|
} else {
|
||||||
|
// If no collision, return original delta
|
||||||
|
return RemainingDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Ground Detection
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is standing on walkable ground
|
||||||
|
* @returns HitResult with ground information, or null if not grounded
|
||||||
|
* @category Ground Detection
|
||||||
|
*/
|
||||||
|
private CheckGround(): HitResult {
|
||||||
|
if (SystemLibrary.IsValid(this.CapsuleComponent)) {
|
||||||
|
const StartLocation = new Vector(
|
||||||
|
this.GetOwner().GetActorLocation().X,
|
||||||
|
this.GetOwner().GetActorLocation().Y,
|
||||||
|
this.GetOwner().GetActorLocation().Z -
|
||||||
|
this.CapsuleComponent.GetScaledCapsuleHalfHeight()
|
||||||
|
);
|
||||||
|
|
||||||
|
const EndLocation = new Vector(
|
||||||
|
StartLocation.X,
|
||||||
|
StartLocation.Y,
|
||||||
|
StartLocation.Z - this.GroundTraceDistance
|
||||||
|
);
|
||||||
|
|
||||||
|
const { OutHit: groundHit, ReturnValue } =
|
||||||
|
SystemLibrary.LineTraceByChannel(
|
||||||
|
StartLocation,
|
||||||
|
EndLocation,
|
||||||
|
ETraceTypeQuery.Visibility,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
EDrawDebugTrace.ForDuration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ReturnValue) {
|
||||||
|
if (
|
||||||
|
this.ClassifySurface(groundHit.ImpactNormal) ===
|
||||||
|
E_SurfaceType.Walkable
|
||||||
|
) {
|
||||||
|
return groundHit;
|
||||||
|
} else {
|
||||||
|
return new HitResult();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new HitResult();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new HitResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// System
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize movement system with angle conversion
|
* Initialize movement system with angle conversion
|
||||||
* Converts degree thresholds to radians for runtime performance
|
* Converts degree thresholds to radians for runtime performance
|
||||||
* @category System
|
* @category System
|
||||||
*/
|
*/
|
||||||
public InitializeMovementSystem(
|
public InitializeMovementSystem(
|
||||||
DebugHUDComponentRef: AC_DebugHUD | null
|
CapsuleComponentRef: CapsuleComponent | null = null,
|
||||||
|
DebugHUDComponentRef: AC_DebugHUD | null = null
|
||||||
): void {
|
): void {
|
||||||
|
this.CapsuleComponent = CapsuleComponentRef;
|
||||||
this.DebugHUDComponent = DebugHUDComponentRef;
|
this.DebugHUDComponent = DebugHUDComponentRef;
|
||||||
this.IsInitialized = true;
|
this.IsInitialized = true;
|
||||||
|
|
||||||
|
|
@ -371,6 +723,10 @@ export class AC_Movement extends ActorComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Debug
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update debug HUD with current movement info
|
* Update debug HUD with current movement info
|
||||||
* @category Debug
|
* @category Debug
|
||||||
|
|
@ -406,31 +762,91 @@ export class AC_Movement extends ActorComponent {
|
||||||
`Rotation Delta: ${this.RotationDelta}\n°` +
|
`Rotation Delta: ${this.RotationDelta}\n°` +
|
||||||
`Is Rotating: ${this.IsCharacterRotating}\n` +
|
`Is Rotating: ${this.IsCharacterRotating}\n` +
|
||||||
`Rotation Speed: ${this.RotationSpeed}\n°` +
|
`Rotation Speed: ${this.RotationSpeed}\n°` +
|
||||||
`Min Speed: ${this.MinSpeedForRotation}`
|
`Min Speed: ${this.MinSpeedForRotation}` +
|
||||||
|
`\n` +
|
||||||
|
// Position
|
||||||
|
`Location: ${StringLibrary.ConvVectorToString(this.GetOwner().GetActorLocation())}` +
|
||||||
|
`\n` +
|
||||||
|
// Sweep Collision
|
||||||
|
`Collision Checks: ${this.SweepCollisionCount}/${this.MaxCollisionChecks}\n` +
|
||||||
|
`Ground Distance: ${this.GroundTraceDistance} cm`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Public Interface
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get movement configuration data for testing
|
* Get maximum horizontal movement speed
|
||||||
* Provides read-only access to private movement constant
|
* @returns Maximum speed in UE units per second
|
||||||
* @returns Object containing MaxSpeed configuration value
|
* @category Public Interface
|
||||||
* @category Testing
|
|
||||||
* @pure true
|
* @pure true
|
||||||
*/
|
*/
|
||||||
public GetTestData(): {
|
public GetMaxSpeed(): Float {
|
||||||
MaxSpeed: Float;
|
return this.MaxSpeed;
|
||||||
} {
|
}
|
||||||
return {
|
|
||||||
MaxSpeed: this.MaxSpeed,
|
/**
|
||||||
};
|
* Get current character velocity in world space
|
||||||
|
* @returns Current velocity vector (cm/s)
|
||||||
|
* @category Public Interface
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
public GetCurrentVelocity(): Vector {
|
||||||
|
return this.CurrentVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current movement state
|
||||||
|
* @returns Current state (Idle/Walking/Airborne)
|
||||||
|
* @category Public Interface
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
public GetMovementState(): E_MovementState {
|
||||||
|
return this.MovementState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current horizontal movement speed
|
||||||
|
* @returns Speed magnitude in UE units per second (ignores Z component)
|
||||||
|
* @category Public Interface
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
public GetCurrentSpeed(): Float {
|
||||||
|
return this.CurrentSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current character rotation
|
||||||
|
* @returns Current yaw rotation (pitch and roll are always 0)
|
||||||
|
* @category Public Interface
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
public GetCurrentRotation(): Rotator {
|
||||||
|
return this.CurrentRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if movement system has been initialized
|
||||||
|
* @returns True if InitializeMovementSystem() has been called
|
||||||
|
* @category Public Interface
|
||||||
|
* @pure true
|
||||||
|
*/
|
||||||
|
public GetIsInitialized(): boolean {
|
||||||
|
return this.IsInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// VARIABLES
|
// VARIABLES
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Movement Config
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum horizontal movement speed in UE units per second
|
* Maximum horizontal movement speed in UE units per second
|
||||||
* Character cannot exceed this speed through ground movement
|
* Character cannot exceed this speed through ground movement
|
||||||
|
|
@ -439,7 +855,7 @@ export class AC_Movement extends ActorComponent {
|
||||||
* @category Movement Config
|
* @category Movement Config
|
||||||
* @instanceEditable true
|
* @instanceEditable true
|
||||||
*/
|
*/
|
||||||
private readonly MaxSpeed: Float = 600.0;
|
private readonly MaxSpeed: Float = 800.0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speed of velocity interpolation towards target velocity
|
* Speed of velocity interpolation towards target velocity
|
||||||
|
|
@ -486,6 +902,174 @@ export class AC_Movement extends ActorComponent {
|
||||||
Wall: 95.0,
|
Wall: 95.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Movement State
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current character velocity in world space
|
||||||
|
* Updated every frame by movement calculations
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private CurrentVelocity: Vector = new Vector(0, 0, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ground contact state - true when character is on walkable surface
|
||||||
|
* Used for gravity application and movement restrictions
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private IsGrounded: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of surface currently under character
|
||||||
|
* Determines available movement options
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private CurrentSurface: E_SurfaceType = E_SurfaceType.Walkable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current movement state of character
|
||||||
|
* Used for animation and game logic decisions
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private MovementState: E_MovementState = E_MovementState.Idle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magnitude of current movement input (0-1)
|
||||||
|
* Used for determining if character should be moving
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private InputMagnitude: Float = 0.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current movement speed (magnitude of horizontal velocity)
|
||||||
|
* Calculated from CurrentVelocity for debug display
|
||||||
|
* @category Movement State
|
||||||
|
*/
|
||||||
|
private CurrentSpeed: Float = 0.0;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Character Rotation Config
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotation speed in degrees per second
|
||||||
|
* Controls how fast character rotates towards movement direction
|
||||||
|
* @category Character Rotation Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly RotationSpeed: Float = 720.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should character rotate when moving
|
||||||
|
* Allows disabling rotation system if needed
|
||||||
|
* @category Character Rotation Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly ShouldRotateToMovement: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum movement speed to trigger rotation
|
||||||
|
* Prevents character from rotating during very slow movement
|
||||||
|
* @category Character Rotation Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly MinSpeedForRotation: Float = 50.0;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Character Rotation State
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current character rotation
|
||||||
|
* Used for smooth interpolation to target rotation
|
||||||
|
* @category Character Rotation State
|
||||||
|
*/
|
||||||
|
private CurrentRotation: Rotator = new Rotator(0, 0, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target character rotation
|
||||||
|
* Calculated based on movement direction and camera orientation
|
||||||
|
* @category Character Rotation State
|
||||||
|
*/
|
||||||
|
private TargetRotation: Rotator = new Rotator(0, 0, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether character is currently rotating
|
||||||
|
* Used for animation and debug purposes
|
||||||
|
* @category Character Rotation State
|
||||||
|
*/
|
||||||
|
private IsCharacterRotating: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angular difference between current and target rotation
|
||||||
|
* Used for determining rotation completion and debug display
|
||||||
|
* @category Character Rotation State
|
||||||
|
*/
|
||||||
|
private RotationDelta: Float = 0.0;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Collision Config
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum step size for sweep collision detection
|
||||||
|
* @category Collision Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly MaxStepSize: Float = 50.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum step size for sweep collision detection
|
||||||
|
* @category Collision Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly MinStepSize: Float = 1.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum collision checks allowed per frame
|
||||||
|
* @category Collision Config
|
||||||
|
* @instanceEditable true
|
||||||
|
*/
|
||||||
|
private readonly MaxCollisionChecks: number = 25;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Collision State
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current frame collision check counter
|
||||||
|
* @category Collision State
|
||||||
|
*/
|
||||||
|
private SweepCollisionCount: number = 0;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Ground Detection Config
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distance to trace downward for ground detection
|
||||||
|
* Larger values detect ground earlier but may cause "magnet" effect
|
||||||
|
* @category Ground Detection Config
|
||||||
|
* @instanceEditable true
|
||||||
|
* @default 5.0
|
||||||
|
*/
|
||||||
|
private readonly GroundTraceDistance: Float = 5.0;
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Ground Detection State
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last ground hit result for ground snapping
|
||||||
|
* @category Ground Detection State
|
||||||
|
*/
|
||||||
|
private LastGroundHit: HitResult = new HitResult();
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// InternalCache
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime cached angle thresholds in radians
|
* Runtime cached angle thresholds in radians
|
||||||
* Converted from degrees during initialization for performance
|
* Converted from degrees during initialization for performance
|
||||||
|
|
@ -497,118 +1081,39 @@ export class AC_Movement extends ActorComponent {
|
||||||
Wall: 0.0,
|
Wall: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Debug
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating if movement system has been initialized
|
* Flag indicating if movement system has been initialized
|
||||||
* Ensures angle thresholds are converted before use
|
* Ensures angle thresholds are converted before use
|
||||||
* @category Debug
|
* @category Debug
|
||||||
*/
|
*/
|
||||||
public IsInitialized = false;
|
private IsInitialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug page identifier for organizing debug output
|
* Debug page identifier for organizing debug output
|
||||||
* Used by debug HUD to categorize information
|
* Used by debug HUD to categorize information
|
||||||
* @category Debug
|
* @category Debug
|
||||||
*/
|
|
||||||
public readonly DebugPageID: string = 'MovementInfo';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current character velocity in world space
|
|
||||||
* Updated every frame by movement calculations
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public CurrentVelocity: Vector = new Vector(0, 0, 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ground contact state - true when character is on walkable surface
|
|
||||||
* Used for gravity application and movement restrictions
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public IsGrounded: boolean = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of surface currently under character
|
|
||||||
* Determines available movement options
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public CurrentSurface: E_SurfaceType = E_SurfaceType.Walkable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current movement state of character
|
|
||||||
* Used for animation and game logic decisions
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public MovementState: E_MovementState = E_MovementState.Idle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Magnitude of current movement input (0-1)
|
|
||||||
* Used for determining if character should be moving
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public InputMagnitude: Float = 0.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current movement speed (magnitude of horizontal velocity)
|
|
||||||
* Calculated from CurrentVelocity for debug display
|
|
||||||
* @category Movement State
|
|
||||||
*/
|
|
||||||
public CurrentSpeed: Float = 0.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotation speed in degrees per second
|
|
||||||
* Controls how fast character rotates towards movement direction
|
|
||||||
* @category Character Rotation Config
|
|
||||||
* @instanceEditable true
|
* @instanceEditable true
|
||||||
*/
|
*/
|
||||||
public RotationSpeed: Float = 720.0;
|
private readonly DebugPageID: string = 'MovementInfo';
|
||||||
|
|
||||||
/**
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
* Should character rotate when moving
|
// Components
|
||||||
* Allows disabling rotation system if needed
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
* @category Character Rotation Config
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
public ShouldRotateToMovement: boolean = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum movement speed to trigger rotation
|
|
||||||
* Prevents character from rotating during very slow movement
|
|
||||||
* @category Character Rotation Config
|
|
||||||
* @instanceEditable true
|
|
||||||
*/
|
|
||||||
public MinSpeedForRotation: Float = 50.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current character rotation
|
|
||||||
* Used for smooth interpolation to target rotation
|
|
||||||
* @category Character Rotation State
|
|
||||||
*/
|
|
||||||
public CurrentRotation: Rotator = new Rotator(0, 0, 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Target character rotation
|
|
||||||
* Calculated based on movement direction and camera orientation
|
|
||||||
* @category Character Rotation State
|
|
||||||
*/
|
|
||||||
public TargetRotation: Rotator = new Rotator(0, 0, 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether character is currently rotating
|
|
||||||
* Used for animation and debug purposes
|
|
||||||
* @category Character Rotation State
|
|
||||||
*/
|
|
||||||
public IsCharacterRotating: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Angular difference between current and target rotation
|
|
||||||
* Used for determining rotation completion and debug display
|
|
||||||
* @category Character Rotation State
|
|
||||||
*/
|
|
||||||
public RotationDelta: Float = 0.0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to debug HUD component for displaying camera info
|
* Reference to debug HUD component for displaying camera info
|
||||||
* Optional, used for debugging purposes
|
* Optional, used for debugging purposes
|
||||||
* @category Components
|
* @category Components
|
||||||
*/
|
*/
|
||||||
public DebugHUDComponent: AC_DebugHUD | null = null;
|
private DebugHUDComponent: AC_DebugHUD | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to character's capsule component for collision detection
|
||||||
|
* @category Components
|
||||||
|
*/
|
||||||
|
private CapsuleComponent: CapsuleComponent | null = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.
|
|
@ -5,6 +5,7 @@
|
||||||
## Тестовая среда
|
## Тестовая среда
|
||||||
- **Уровень:** TestLevel с BP_MainCharacter
|
- **Уровень:** TestLevel с BP_MainCharacter
|
||||||
- **Требования:** MovementComponent инициализирован
|
- **Требования:** MovementComponent инициализирован
|
||||||
|
- **Debug HUD:** Включен для проверки параметров
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -14,14 +15,15 @@
|
||||||
- [ ] **InitializeMovementSystem()** выполняется без ошибок при запуске уровня
|
- [ ] **InitializeMovementSystem()** выполняется без ошибок при запуске уровня
|
||||||
- [ ] **IsInitialized flag** устанавливается в true после инициализации
|
- [ ] **IsInitialized flag** устанавливается в true после инициализации
|
||||||
- [ ] **Angle conversion** - пороги корректно конвертируются из градусов в радианы
|
- [ ] **Angle conversion** - пороги корректно конвертируются из градусов в радианы
|
||||||
|
- [ ] **CapsuleComponent reference** - передаётся и сохраняется корректно (этап 9)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Константы движения
|
## 2. Константы движения
|
||||||
|
|
||||||
### 2.1 Default значения
|
### 2.1 Default значения
|
||||||
- [ ] **MaxSpeed = 600.0** - значение установлено по умолчанию
|
- [ ] **MaxSpeed = 800.0** - значение установлено по умолчанию
|
||||||
- [ ] **Acceleration = 10.0** - значение установлено по умолчанию (уменьшено для плавности)
|
- [ ] **Acceleration = 10.0** - значение установлено по умолчанию
|
||||||
- [ ] **Friction = 8.0** - значение установлено по умолчанию
|
- [ ] **Friction = 8.0** - значение установлено по умолчанию
|
||||||
- [ ] **Gravity = 980.0** - значение установлено по умолчанию
|
- [ ] **Gravity = 980.0** - значение установлено по умолчанию
|
||||||
|
|
||||||
|
|
@ -30,6 +32,12 @@
|
||||||
- [ ] **SteepSlope = 85.0°** - значение по умолчанию в градусах
|
- [ ] **SteepSlope = 85.0°** - значение по умолчанию в градусах
|
||||||
- [ ] **Wall = 95.0°** - значение по умолчанию в градусах
|
- [ ] **Wall = 95.0°** - значение по умолчанию в градусах
|
||||||
|
|
||||||
|
### 2.3 Sweep Collision константы (Этап 9)
|
||||||
|
- [ ] **MaxStepSize = 50.0** - максимальный размер шага sweep
|
||||||
|
- [ ] **MinStepSize = 1.0** - минимальный размер шага
|
||||||
|
- [ ] **MaxCollisionChecks = 25** - лимит проверок за кадр
|
||||||
|
- [ ] **GroundTraceDistance = 5.0** - дистанция trace вниз для ground detection
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Базовое движение (Этап 7)
|
## 3. Базовое движение (Этап 7)
|
||||||
|
|
@ -51,75 +59,178 @@
|
||||||
### 3.3 Физика движения
|
### 3.3 Физика движения
|
||||||
- [ ] **Плавное ускорение** - персонаж набирает скорость постепенно при нажатии клавиш
|
- [ ] **Плавное ускорение** - персонаж набирает скорость постепенно при нажатии клавиш
|
||||||
- [ ] **Плавное торможение** - персонаж останавливается плавно при отпускании клавиш
|
- [ ] **Плавное торможение** - персонаж останавливается плавно при отпускании клавиш
|
||||||
- [ ] **MaxSpeed limit** - скорость не превышает 600.0 units/sec
|
- [ ] **MaxSpeed limit** - скорость не превышает 800.0 units/sec
|
||||||
- [ ] **Диагональное движение** - скорость диагонального движения равна прямому (не быстрее)
|
- [ ] **Диагональное движение** - скорость диагонального движения равна прямому (не быстрее)
|
||||||
- [ ] **Стабильное поведение** - нет рывков, заиканий или неожиданных ускорений
|
- [ ] **Стабильное поведение** - нет рывков, заиканий или неожиданных ускорений
|
||||||
|
|
||||||
### 3.4 Состояния движения
|
### 3.4 Состояния движения
|
||||||
- [ ] **Idle state** - MovementState = Idle когда персонаж стоит
|
- [ ] **Idle state** - MovementState = Idle когда персонаж стоит
|
||||||
- [ ] **Walking state** - MovementState = Walking при движении
|
- [ ] **Walking state** - MovementState = Walking при движении
|
||||||
|
- [ ] **Airborne state** - MovementState = Airborne в воздухе (этап 9)
|
||||||
- [ ] **InputMagnitude** - корректно отражает силу input (0-1)
|
- [ ] **InputMagnitude** - корректно отражает силу input (0-1)
|
||||||
- [ ] **CurrentSpeed** - показывает текущую горизонтальную скорость
|
- [ ] **CurrentSpeed** - показывает текущую горизонтальную скорость
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Debug HUD Integration
|
## 4. Ground Detection и Падение (Этап 9)
|
||||||
|
|
||||||
### 4.1 Movement Constants Page (Page 1)
|
### 4.1 Базовое падение и приземление
|
||||||
|
- [ ] **Падение начинается:** Персонаж падает вниз с нормальной скоростью
|
||||||
|
- [ ] **Приземление без провалов:** Персонаж останавливается НА полу, а не В полу
|
||||||
|
- [ ] **Стабильная Z позиция:** После приземления Z координата стабильна (±0.5 единиц)
|
||||||
|
- [ ] **IsGrounded = true:** Debug HUD показывает `Is Grounded: true` после приземления
|
||||||
|
- [ ] **Velocity.Z = 0:** После приземления вертикальная скорость обнулена
|
||||||
|
|
||||||
|
**Ожидаемые значения в Debug HUD:**
|
||||||
|
```
|
||||||
|
Current Velocity: X=0.00 Y=0.00 Z=0.00
|
||||||
|
Is Grounded: true
|
||||||
|
Location Z: ~0.125 (стабильно)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Движение по полу без провалов
|
||||||
|
- [ ] **Движение WASD:** Персонаж двигается по полу плавно
|
||||||
|
- [ ] **Нет дёрганий Z:** При движении нет вертикальных рывков
|
||||||
|
- [ ] **Z позиция стабильна:** Разброс Z ≤ 0.5 единиц во время движения
|
||||||
|
- [ ] **Collision Checks:** В Debug HUD не превышает 25
|
||||||
|
|
||||||
|
**Ожидаемые значения в Debug HUD:**
|
||||||
|
```
|
||||||
|
Speed: 600-800
|
||||||
|
Is Grounded: true
|
||||||
|
Collision Checks: 3-8/25
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Край платформы
|
||||||
|
- [ ] **Подход к краю:** Персонаж может подойти к краю платформы
|
||||||
|
- [ ] **Схождение с края:** Персонаж начинает падать после выхода за край
|
||||||
|
- [ ] **IsGrounded = false:** Debug HUD показывает airborne state
|
||||||
|
- [ ] **Короткая "липкость":** Капсула может кратковременно зацепиться (это нормально)
|
||||||
|
- [ ] **Повторное приземление:** После падения с края может приземлиться снова
|
||||||
|
|
||||||
|
**Известное поведение:** Лёгкое "прилипание" к краю из-за скруглённой капсулы - это нормально, исправим в этапе 15
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sweep Collision Performance (Этап 9)
|
||||||
|
|
||||||
|
### 5.1 Количество collision checks
|
||||||
|
|
||||||
|
| Сценарий | Ожидаемое кол-во checks |
|
||||||
|
|----------|------------------------|
|
||||||
|
| Стоит на месте | 0-1 |
|
||||||
|
| Медленное движение | 2-5 |
|
||||||
|
| Нормальная скорость | 5-12 |
|
||||||
|
| Максимальная скорость | 15-25 |
|
||||||
|
| Падение с высоты | 10-20 |
|
||||||
|
|
||||||
|
- [ ] **Idle:** Collision Checks = 0-1
|
||||||
|
- [ ] **Walking:** Collision Checks = 5-12
|
||||||
|
- [ ] **Fast movement:** Не превышает MaxCollisionChecks (25)
|
||||||
|
|
||||||
|
### 5.2 Адаптивный размер шага
|
||||||
|
- [ ] **При медленном движении:** Меньше traces (видно в visual debug)
|
||||||
|
- [ ] **При быстром движении:** Больше traces, меньше расстояние между ними
|
||||||
|
- [ ] **Падение:** Частые проверки во время быстрого падения
|
||||||
|
|
||||||
|
**Visual debug traces должны показать:** Короткие шаги при высокой скорости, длинные при низкой
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Детерминированность (Этап 9)
|
||||||
|
|
||||||
|
### 6.1 Тест повторяемости
|
||||||
|
**Процедура:**
|
||||||
|
1. Запомнить начальную позицию персонажа
|
||||||
|
2. Подвигать персонажа в определённом направлении 5 секунд
|
||||||
|
3. Перезапустить уровень
|
||||||
|
4. Повторить те же движения
|
||||||
|
5. Сравнить финальные позиции
|
||||||
|
|
||||||
|
**Проверки:**
|
||||||
|
- [ ] **Z координата идентична:** Разница ≤ 0.5 единиц
|
||||||
|
- [ ] **XY координаты близки:** Небольшое отклонение допустимо (инпут timing)
|
||||||
|
- [ ] **IsGrounded одинаков:** Один и тот же state в конце
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Debug HUD Integration
|
||||||
|
|
||||||
|
### 7.1 Movement Info Page
|
||||||
- [ ] **Константы** отображаются корректно:
|
- [ ] **Константы** отображаются корректно:
|
||||||
- Max Speed: 600
|
- Max Speed: 800
|
||||||
- Acceleration: 10
|
- Acceleration: 10
|
||||||
- Friction: 8
|
- Friction: 8
|
||||||
- Gravity: 980
|
- Gravity: 980
|
||||||
|
- Initialized: true
|
||||||
- [ ] **Текущее состояние** отображается:
|
- [ ] **Текущее состояние** отображается:
|
||||||
- Current Velocity: X, Y, Z компоненты
|
- Current Velocity: X, Y, Z компоненты
|
||||||
- Speed: горизонтальная скорость
|
- Speed: горизонтальная скорость
|
||||||
- Is Grounded: Yes (пока всегда true)
|
- Is Grounded: true/false
|
||||||
- Surface Type: Walkable (пока всегда)
|
- Surface Type: Walkable (пока всегда)
|
||||||
- Movement State: Idle/Walking
|
- Movement State: Idle/Walking/Airborne
|
||||||
- Input Magnitude: 0.00-1.00
|
- Input Magnitude: 0.00-1.00
|
||||||
|
- [ ] **Rotation info** (этап 8):
|
||||||
|
- Current Yaw
|
||||||
|
- Target Yaw
|
||||||
|
- Rotation Delta
|
||||||
|
- Is Rotating
|
||||||
|
- [ ] **Position** (этап 9):
|
||||||
|
- Location: X, Y, Z координаты
|
||||||
|
- [ ] **Sweep Collision** (этап 9):
|
||||||
|
- Collision Checks: X/25
|
||||||
|
- Ground Distance: 5.0 cm
|
||||||
|
|
||||||
### 4.2 Реальное время обновления
|
### 7.2 Реальное время обновления
|
||||||
- [ ] **Velocity** изменяется в реальном времени при движении
|
- [ ] **Velocity** изменяется в реальном времени при движении
|
||||||
- [ ] **Speed** корректно показывает magnitude горизонтальной скорости
|
- [ ] **Speed** корректно показывает magnitude горизонтальной скорости
|
||||||
- [ ] **Movement State** переключается между Idle и Walking
|
- [ ] **Movement State** переключается между Idle/Walking/Airborne
|
||||||
- [ ] **Input Magnitude** отражает силу нажатия (особенно на геймпаде)
|
- [ ] **Input Magnitude** отражает силу нажатия
|
||||||
|
- [ ] **Collision Checks** обновляется каждый кадр при движении
|
||||||
|
- [ ] **Location** обновляется плавно
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Автотесты Integration
|
## 8. Автотесты Integration
|
||||||
|
|
||||||
### 5.1 FT_BasicMovement
|
### 8.1 FT_SurfaceClassification
|
||||||
- [ ] **Тест проходит** - инициализация, ускорение, торможение, состояния
|
- [ ] **Тест проходит** - классификация поверхностей по углам
|
||||||
- [ ] **No console errors** при выполнении автотеста
|
|
||||||
|
|
||||||
### 5.2 FT_DiagonalMovement
|
### 8.2 FT_MovementInitialization
|
||||||
- [ ] **Тест проходит** - диагональное движение не быстрее прямого
|
- [ ] **Тест проходит** - инициализация, начальные состояния, конфигурация
|
||||||
- [ ] **Input normalization** работает корректно
|
|
||||||
|
### 8.3 Удалённые тесты
|
||||||
|
- ❌ **FT_BasicMovement** - удалён (требует тестовый уровень)
|
||||||
|
- ❌ **FT_DiagonalMovement** - удалён (требует тестовый уровень)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Performance
|
## 9. Performance
|
||||||
|
|
||||||
### 6.1 Производительность
|
### 9.1 Производительность
|
||||||
- [ ] **Stable 60+ FPS** при активном движении
|
- [ ] **Stable 60+ FPS** при активном движении
|
||||||
- [ ] **No memory leaks** при длительном использовании
|
- [ ] **No memory leaks** при длительном использовании
|
||||||
- [ ] **Smooth movement** без микро-заиканий
|
- [ ] **Smooth movement** без микро-заиканий
|
||||||
|
- [ ] **Sweep overhead** минимален (<1ms дополнительно)
|
||||||
|
|
||||||
### 6.2 Отзывчивость
|
### 9.2 Отзывчивость
|
||||||
- [ ] **Instant response** на нажатие клавиш (нет input lag)
|
- [ ] **Instant response** на нажатие клавиш (нет input lag)
|
||||||
- [ ] **Smooth transitions** между состояниями движения
|
- [ ] **Smooth transitions** между состояниями движения
|
||||||
- [ ] **Consistent timing** независимо от FPS
|
- [ ] **Consistent timing** независимо от FPS
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Критерии прохождения
|
## Критерии прохождения этапов
|
||||||
|
|
||||||
|
### Этап 7: Базовое движение
|
||||||
- [ ] Все основные направления движения работают
|
- [ ] Все основные направления движения работают
|
||||||
- [ ] Физика движения плавная и отзывчивая
|
- [ ] Физика движения плавная и отзывчивая
|
||||||
- [ ] MaxSpeed limit соблюдается
|
- [ ] MaxSpeed limit соблюдается
|
||||||
- [ ] Диагональное движение не дает преимущества в скорости
|
- [ ] Диагональное движение не дает преимущества в скорости
|
||||||
- [ ] Debug HUD показывает корректные данные движения
|
|
||||||
- [ ] Автотесты проходят успешно
|
|
||||||
- [ ] Performance стабильная
|
|
||||||
|
|
||||||
**Примечание:** Этап 7 фокусируется на базовом движении по плоскости. Camera-relative движение и поворот персонажа будут в этапе 8.
|
### Этап 9: Sweep Collision + Ground Detection
|
||||||
|
- [ ] Полное отсутствие tunneling при любых скоростях
|
||||||
|
- [ ] Стабильная Z позиция (разброс <0.5 единиц)
|
||||||
|
- [ ] Детерминированность (100% воспроизводимость)
|
||||||
|
- [ ] Performance <25 collision checks за кадр
|
||||||
|
- [ ] Значения корректно отображаются в Debug HUD
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,7 @@ import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
||||||
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
|
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
|
||||||
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
||||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
||||||
import { Vector } from '#root/UE/Vector.ts';
|
import { StringLibrary } from '#root/UE/StringLibrary.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional Test: Basic Movement System
|
* Functional Test: Basic Movement System
|
||||||
|
|
@ -27,86 +27,34 @@ export class FT_BasicMovement extends FunctionalTest {
|
||||||
*/
|
*/
|
||||||
EventStartTest(): void {
|
EventStartTest(): void {
|
||||||
// Initialize movement system
|
// Initialize movement system
|
||||||
this.MovementComponent.InitializeMovementSystem(this.DebugHUDComponent);
|
this.MovementComponent.InitializeMovementSystem(
|
||||||
|
null,
|
||||||
|
this.DebugHUDComponent
|
||||||
|
);
|
||||||
|
|
||||||
// Test 1: Initialization
|
// Test 1: Initialization
|
||||||
if (this.MovementComponent.IsInitialized) {
|
if (this.MovementComponent.GetIsInitialized()) {
|
||||||
// Test 2: Initial state should be Idle
|
// Test 2: Initial state should be Idle
|
||||||
if (this.MovementComponent.MovementState === E_MovementState.Idle) {
|
if (this.MovementComponent.GetMovementState() === E_MovementState.Idle) {
|
||||||
// Test 3: Process forward input and verify acceleration
|
// Test 3: Initial speed & velocity is zero
|
||||||
this.MovementComponent.ProcessMovementInput(
|
|
||||||
new Vector(1.0, 0, 0), // Forward input
|
|
||||||
0.016 // 60 FPS delta
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.MovementComponent.CurrentVelocity.X > 0) {
|
if (
|
||||||
// Test 4: Movement state should change to Walking
|
this.MovementComponent.GetCurrentSpeed() === 0 &&
|
||||||
if (
|
this.MovementComponent.GetCurrentVelocity().X === 0 &&
|
||||||
(this.MovementComponent.MovementState as string) ===
|
this.MovementComponent.GetCurrentVelocity().Y === 0 &&
|
||||||
(E_MovementState.Walking as string)
|
this.MovementComponent.GetCurrentVelocity().Z === 0
|
||||||
) {
|
) {
|
||||||
// Test 5: Process multiple frames to test max speed limit
|
this.FinishTest(EFunctionalTestResult.Succeeded);
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
this.MovementComponent.ProcessMovementInput(
|
|
||||||
new Vector(1.0, 0, 0),
|
|
||||||
0.016
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify max speed is not exceeded
|
|
||||||
if (
|
|
||||||
this.MovementComponent.CurrentSpeed <=
|
|
||||||
this.MovementComponent.GetTestData().MaxSpeed + 1
|
|
||||||
) {
|
|
||||||
// Test 6: Test friction by removing input
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
this.MovementComponent.ProcessMovementInput(
|
|
||||||
new Vector(0, 0, 0), // No input
|
|
||||||
0.016
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify friction slowed down the character
|
|
||||||
if (this.MovementComponent.CurrentSpeed < 50) {
|
|
||||||
// Test 7: Verify state changed back to Idle when stopped
|
|
||||||
if (
|
|
||||||
this.MovementComponent.MovementState === E_MovementState.Idle
|
|
||||||
) {
|
|
||||||
this.FinishTest(EFunctionalTestResult.Succeeded);
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Movement state should be Idle when stopped, got ${this.MovementComponent.MovementState as string}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Friction should reduce speed, current speed: ${this.MovementComponent.CurrentSpeed}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Speed ${this.MovementComponent.CurrentSpeed} exceeds MaxSpeed ${this.MovementComponent.GetTestData().MaxSpeed}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Movement state should be Walking with input, got ${this.MovementComponent.MovementState}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.FinishTest(
|
this.FinishTest(
|
||||||
EFunctionalTestResult.Failed,
|
EFunctionalTestResult.Failed,
|
||||||
'Forward input should produce forward velocity'
|
`Current Speed & Current velocity should be zero, got Speed: ${this.MovementComponent.GetCurrentSpeed()}, Velocity: ${StringLibrary.ConvVectorToString(this.MovementComponent.GetCurrentVelocity())}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.FinishTest(
|
this.FinishTest(
|
||||||
EFunctionalTestResult.Failed,
|
EFunctionalTestResult.Failed,
|
||||||
`Initial movement state should be Idle, got ${this.MovementComponent.MovementState}`
|
`Initial movement state should be Idle, got ${this.MovementComponent.GetMovementState()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
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,103 +0,0 @@
|
||||||
// Movement/Tests/FT_DiagonalMovement.ts
|
|
||||||
|
|
||||||
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
|
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
|
||||||
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
|
||||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
|
||||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
|
||||||
import { Vector } from '#root/UE/Vector.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functional Test: Diagonal Movement Speed Control
|
|
||||||
* Tests that diagonal movement is not faster than cardinal movement
|
|
||||||
* Validates input normalization prevents diagonal speed boost
|
|
||||||
*/
|
|
||||||
export class FT_DiagonalMovement extends FunctionalTest {
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// GRAPHS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// EventGraph
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test execution - validates diagonal movement speed control
|
|
||||||
* Tests cardinal vs diagonal movement speeds
|
|
||||||
*/
|
|
||||||
EventStartTest(): void {
|
|
||||||
// Initialize movement system
|
|
||||||
this.MovementComponent.InitializeMovementSystem(this.DebugHUDComponent);
|
|
||||||
|
|
||||||
// Test 1: Cardinal movement (forward only)
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
this.MovementComponent.ProcessMovementInput(
|
|
||||||
new Vector(1.0, 0, 0), // Pure forward
|
|
||||||
0.016
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const cardinalSpeed = this.MovementComponent.CurrentSpeed;
|
|
||||||
|
|
||||||
// Reset velocity
|
|
||||||
this.MovementComponent.CurrentVelocity = new Vector(0, 0, 0);
|
|
||||||
|
|
||||||
// Test 2: Diagonal movement (forward + right)
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
this.MovementComponent.ProcessMovementInput(
|
|
||||||
new Vector(1.0, 1.0, 0), // Diagonal input
|
|
||||||
0.016
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const diagonalSpeed = this.MovementComponent.CurrentSpeed;
|
|
||||||
|
|
||||||
// Test 3: Diagonal should not be faster than cardinal
|
|
||||||
if (diagonalSpeed <= cardinalSpeed + 1) {
|
|
||||||
// Small tolerance for floating point
|
|
||||||
// Test 4: Both speeds should be close to MaxSpeed
|
|
||||||
if (
|
|
||||||
cardinalSpeed >= this.MovementComponent.GetTestData().MaxSpeed - 50 &&
|
|
||||||
diagonalSpeed >= this.MovementComponent.GetTestData().MaxSpeed - 50
|
|
||||||
) {
|
|
||||||
// Test 5: Test input normalization directly
|
|
||||||
const rawDiagonalInput = new Vector(1.0, 1.0, 0);
|
|
||||||
const inputMagnitude = MathLibrary.VectorLength(rawDiagonalInput);
|
|
||||||
|
|
||||||
if (inputMagnitude > 1.0) {
|
|
||||||
// This confirms our diagonal input needs normalization
|
|
||||||
this.FinishTest(EFunctionalTestResult.Succeeded);
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Diagonal input magnitude should be > 1.0, got ${inputMagnitude}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Speeds too low: Cardinal=${cardinalSpeed}, Diagonal=${diagonalSpeed}, Expected~${this.MovementComponent.GetTestData().MaxSpeed}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.FinishTest(
|
|
||||||
EFunctionalTestResult.Failed,
|
|
||||||
`Diagonal speed ${diagonalSpeed} exceeds cardinal speed ${cardinalSpeed}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
|
||||||
// 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_DiagonalMovement.uasset (Stored with Git LFS)
BIN
Content/Movement/Tests/FT_DiagonalMovement.uasset (Stored with Git LFS)
Binary file not shown.
|
|
@ -3,11 +3,9 @@
|
||||||
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
|
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
|
||||||
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
|
||||||
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
|
||||||
import { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.ts';
|
|
||||||
import type { S_SurfaceTestCase } from '#root/Movement/Structs/S_SurfaceTestCase.ts';
|
import type { S_SurfaceTestCase } from '#root/Movement/Structs/S_SurfaceTestCase.ts';
|
||||||
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
||||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
||||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional Test: Surface Classification System
|
* Functional Test: Surface Classification System
|
||||||
|
|
@ -23,24 +21,6 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
||||||
// EventGraph
|
// EventGraph
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Test preparation - converts angle thresholds to radians
|
|
||||||
* Called before main test execution to prepare test data
|
|
||||||
*/
|
|
||||||
EventPrepareTest(): void {
|
|
||||||
this.AngleThresholdsRads = {
|
|
||||||
Walkable: MathLibrary.DegreesToRadians(
|
|
||||||
this.MovementComponent.AngleThresholdsDegrees.Walkable
|
|
||||||
),
|
|
||||||
SteepSlope: MathLibrary.DegreesToRadians(
|
|
||||||
this.MovementComponent.AngleThresholdsDegrees.SteepSlope
|
|
||||||
),
|
|
||||||
Wall: MathLibrary.DegreesToRadians(
|
|
||||||
this.MovementComponent.AngleThresholdsDegrees.Wall
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test execution - validates surface classification for all test cases
|
* Test execution - validates surface classification for all test cases
|
||||||
* Tests boundary conditions and edge cases for each surface type
|
* Tests boundary conditions and edge cases for each surface type
|
||||||
|
|
@ -49,8 +29,7 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
||||||
this.TestCases.forEach(
|
this.TestCases.forEach(
|
||||||
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
|
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
|
||||||
const surfaceType = this.MovementComponent.ClassifySurface(
|
const surfaceType = this.MovementComponent.ClassifySurface(
|
||||||
BFL_Vectors.GetNormalFromAngle(AngleDegrees),
|
BFL_Vectors.GetNormalFromAngle(AngleDegrees)
|
||||||
this.AngleThresholdsRads
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (surfaceType === ExpectedType) {
|
if (surfaceType === ExpectedType) {
|
||||||
|
|
@ -132,15 +111,4 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
||||||
Description: 'Ceiling',
|
Description: 'Ceiling',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime cached angle thresholds in radians
|
|
||||||
* Converted during preparation for accurate classification testing
|
|
||||||
* @category Test State
|
|
||||||
*/
|
|
||||||
private AngleThresholdsRads: S_AngleThresholds = {
|
|
||||||
Walkable: 0.0,
|
|
||||||
SteepSlope: 0.0,
|
|
||||||
Wall: 0.0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.
|
|
@ -1,5 +1,6 @@
|
||||||
// UE/ActorComponent.ts
|
// UE/ActorComponent.ts
|
||||||
|
|
||||||
|
import { Actor } from '#root/UE/Actor.ts';
|
||||||
import { Name } from '#root/UE/Name.ts';
|
import { Name } from '#root/UE/Name.ts';
|
||||||
import { UEObject } from '#root/UE/UEObject.ts';
|
import { UEObject } from '#root/UE/UEObject.ts';
|
||||||
|
|
||||||
|
|
@ -7,4 +8,8 @@ export class ActorComponent extends UEObject {
|
||||||
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
||||||
super(outer, name);
|
super(outer, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetOwner(): Actor {
|
||||||
|
return new Actor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import { ShapeComponent } from '#root/UE/ShapeComponent.ts';
|
||||||
|
|
||||||
|
export class CapsuleComponent extends ShapeComponent {
|
||||||
|
constructor(outer: ShapeComponent | null = null, name: string = 'None') {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetScaledCapsuleHalfHeight(): Float {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetScaledCapsuleRadius(): Float {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export enum EDrawDebugTrace {
|
||||||
|
None = 'None',
|
||||||
|
ForOneFrame = 'ForOneFrame',
|
||||||
|
ForDuration = 'ForDuration',
|
||||||
|
Persistent = 'Persistent',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum ETraceTypeQuery {
|
||||||
|
Visibility = 'Visibility',
|
||||||
|
Camera = 'Camera',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import type { Actor } from '#root/UE/Actor.ts';
|
||||||
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { Integer } from '#root/UE/Integer.ts';
|
||||||
|
import { Name } from '#root/UE/Name.ts';
|
||||||
|
import type { PhysicalMaterial } from '#root/UE/PhysicalMaterial.ts';
|
||||||
|
import type { PrimitiveComponent } from '#root/UE/PrimitiveComponent.ts';
|
||||||
|
import { StructBase } from '#root/UE/StructBase.ts';
|
||||||
|
import { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
export class HitResult extends StructBase {
|
||||||
|
BlockingHit: boolean;
|
||||||
|
InitialOverlap: boolean;
|
||||||
|
Time: Float;
|
||||||
|
Distance: Float;
|
||||||
|
Location: Vector;
|
||||||
|
ImpactPoint: Vector;
|
||||||
|
Normal: Vector;
|
||||||
|
ImpactNormal: Vector;
|
||||||
|
PhysMat: PhysicalMaterial;
|
||||||
|
HitActor: Actor;
|
||||||
|
HitComponent: PrimitiveComponent;
|
||||||
|
HitBoneName: Name;
|
||||||
|
BoneName: Name;
|
||||||
|
HitItem: Integer;
|
||||||
|
ElementIndex: Integer;
|
||||||
|
FaceIndex: Integer;
|
||||||
|
TraceStart: Vector;
|
||||||
|
TraceEnd: Vector;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
BlockingHit: boolean = false,
|
||||||
|
InitialOverlap: boolean = false,
|
||||||
|
Time: number = 0.0,
|
||||||
|
Distance: number = 0.0,
|
||||||
|
Location: Vector = new Vector(),
|
||||||
|
ImpactPoint: Vector = new Vector(),
|
||||||
|
Normal: Vector = new Vector(),
|
||||||
|
ImpactNormal: Vector = new Vector(),
|
||||||
|
PhysMat?: PhysicalMaterial,
|
||||||
|
HitActor?: Actor,
|
||||||
|
HitComponent?: PrimitiveComponent,
|
||||||
|
HitBoneName: Name = new Name('None'),
|
||||||
|
BoneName: Name = new Name('None'),
|
||||||
|
HitItem: number = 0,
|
||||||
|
ElementIndex: number = 0,
|
||||||
|
FaceIndex: number = 0,
|
||||||
|
TraceStart: Vector = new Vector(),
|
||||||
|
TraceEnd: Vector = new Vector()
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.BlockingHit = BlockingHit;
|
||||||
|
this.InitialOverlap = InitialOverlap;
|
||||||
|
this.Time = Time;
|
||||||
|
this.Distance = Distance;
|
||||||
|
this.Location = Location;
|
||||||
|
this.ImpactPoint = ImpactPoint;
|
||||||
|
this.Normal = Normal;
|
||||||
|
this.ImpactNormal = ImpactNormal;
|
||||||
|
this.PhysMat = PhysMat!;
|
||||||
|
this.HitActor = HitActor!;
|
||||||
|
this.HitComponent = HitComponent!;
|
||||||
|
this.HitBoneName = HitBoneName;
|
||||||
|
this.BoneName = BoneName;
|
||||||
|
this.HitItem = HitItem;
|
||||||
|
this.ElementIndex = ElementIndex;
|
||||||
|
this.FaceIndex = FaceIndex;
|
||||||
|
this.TraceStart = TraceStart;
|
||||||
|
this.TraceEnd = TraceEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
||||||
import type { Color } from '#root/UE/Color.ts';
|
import type { Color } from '#root/UE/Color.ts';
|
||||||
import type { Float } from '#root/UE/Float.ts';
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import type { Integer } from '#root/UE/Integer.ts';
|
||||||
import { LinearColor } from '#root/UE/LinearColor.ts';
|
import { LinearColor } from '#root/UE/LinearColor.ts';
|
||||||
import { Vector } from '#root/UE/Vector.ts';
|
import { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
|
|
@ -292,6 +293,18 @@ class MathLibraryClass extends BlueprintFunctionLibrary {
|
||||||
|
|
||||||
return new Vector(CP * CY, CP * SY, SP);
|
return new Vector(CP * CY, CP * SY, SP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floor a float value to the nearest lower integer
|
||||||
|
* @param Value - Input float value
|
||||||
|
* @returns Floored integer value
|
||||||
|
* @example
|
||||||
|
* // Floor 3.7 to 3
|
||||||
|
* Floor(3.7) // returns 3
|
||||||
|
*/
|
||||||
|
public Ceil(Value: Float): Integer {
|
||||||
|
return Math.ceil(Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Name } from '#root/UE/Name.ts';
|
||||||
|
import { UEObject } from '#root/UE/UEObject.ts';
|
||||||
|
|
||||||
|
export class PhysicalMaterial extends UEObject {
|
||||||
|
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { SceneComponent } from '#root/UE/SceneComponent.ts';
|
||||||
|
|
||||||
|
export class PrimitiveComponent extends SceneComponent {
|
||||||
|
constructor(outer: SceneComponent | null = null, name: string = 'None') {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ActorComponent } from '#root/UE/ActorComponent.ts';
|
||||||
|
import { Name } from '#root/UE/Name.ts';
|
||||||
|
import { UEObject } from '#root/UE/UEObject.ts';
|
||||||
|
|
||||||
|
export class SceneComponent extends ActorComponent {
|
||||||
|
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { PrimitiveComponent } from '#root/UE/PrimitiveComponent.ts';
|
||||||
|
|
||||||
|
export class ShapeComponent extends PrimitiveComponent {
|
||||||
|
constructor(outer: PrimitiveComponent | null = null, name: string = 'None') {
|
||||||
|
super(outer, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
// UE/SystemLibrary.ts
|
// UE/SystemLibrary.ts
|
||||||
|
|
||||||
|
import type { Actor } from '#root/UE/Actor.ts';
|
||||||
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
|
||||||
|
import { EDrawDebugTrace } from '#root/UE/EDrawDebugTrace.ts';
|
||||||
|
import { ETraceTypeQuery } from '#root/UE/ETraceTypeQuery.ts';
|
||||||
import type { Float } from '#root/UE/Float.ts';
|
import type { Float } from '#root/UE/Float.ts';
|
||||||
|
import { HitResult } from '#root/UE/HitResult.ts';
|
||||||
import { LinearColor } from '#root/UE/LinearColor.ts';
|
import { LinearColor } from '#root/UE/LinearColor.ts';
|
||||||
import { Name } from '#root/UE/Name.ts';
|
import { Name } from '#root/UE/Name.ts';
|
||||||
import type { UEObject } from '#root/UE/UEObject.ts';
|
import type { UEObject } from '#root/UE/UEObject.ts';
|
||||||
|
import { Vector } from '#root/UE/Vector.ts';
|
||||||
|
|
||||||
class SystemLibraryClass extends BlueprintFunctionLibrary {
|
class SystemLibraryClass extends BlueprintFunctionLibrary {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -50,6 +55,76 @@ class SystemLibraryClass extends BlueprintFunctionLibrary {
|
||||||
console.log(duration);
|
console.log(duration);
|
||||||
console.log(key);
|
console.log(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CapsuleTraceByChannel(
|
||||||
|
Start: Vector = new Vector(0, 0, 0),
|
||||||
|
End: Vector = new Vector(0, 0, 0),
|
||||||
|
Radius: Float = 0.0,
|
||||||
|
HalfHeight: Float = 0.0,
|
||||||
|
TraceChannel: ETraceTypeQuery = ETraceTypeQuery.Visibility,
|
||||||
|
TraceComplex: boolean = false,
|
||||||
|
ActorsToIgnore: Actor[] = [],
|
||||||
|
DrawDebugType: EDrawDebugTrace = EDrawDebugTrace.None,
|
||||||
|
IgnoreSelf: boolean = true,
|
||||||
|
TraceColor: LinearColor = new LinearColor(1, 0, 0, 1),
|
||||||
|
TraceHitColor: LinearColor = new LinearColor(0, 1, 0, 1),
|
||||||
|
DrawTime: Float = 5.0
|
||||||
|
): { OutHit: HitResult; ReturnValue: boolean } {
|
||||||
|
console.log('CapsuleTraceByChannel called with:', {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Radius,
|
||||||
|
HalfHeight,
|
||||||
|
TraceChannel,
|
||||||
|
TraceComplex,
|
||||||
|
ActorsToIgnore,
|
||||||
|
DrawDebugType,
|
||||||
|
IgnoreSelf,
|
||||||
|
TraceColor,
|
||||||
|
TraceHitColor,
|
||||||
|
DrawTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
OutHit: new HitResult(),
|
||||||
|
ReturnValue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Placeholder implementation; replace with actual trace logic
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineTraceByChannel(
|
||||||
|
Start: Vector = new Vector(0, 0, 0),
|
||||||
|
End: Vector = new Vector(0, 0, 0),
|
||||||
|
TraceChannel: ETraceTypeQuery = ETraceTypeQuery.Visibility,
|
||||||
|
TraceComplex: boolean = false,
|
||||||
|
ActorsToIgnore: Actor[] = [],
|
||||||
|
DrawDebugType: EDrawDebugTrace = EDrawDebugTrace.None,
|
||||||
|
IgnoreSelf: boolean = true,
|
||||||
|
TraceColor: LinearColor = new LinearColor(1, 0, 0, 1),
|
||||||
|
TraceHitColor: LinearColor = new LinearColor(0, 1, 0, 1),
|
||||||
|
DrawTime: Float = 5.0
|
||||||
|
): { OutHit: HitResult; ReturnValue: boolean } {
|
||||||
|
console.log('CapsuleTraceByChannel called with:', {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
TraceChannel,
|
||||||
|
TraceComplex,
|
||||||
|
ActorsToIgnore,
|
||||||
|
DrawDebugType,
|
||||||
|
IgnoreSelf,
|
||||||
|
TraceColor,
|
||||||
|
TraceHitColor,
|
||||||
|
DrawTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
OutHit: new HitResult(),
|
||||||
|
ReturnValue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Placeholder implementation; replace with actual trace logic
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SystemLibrary = new SystemLibraryClass();
|
export const SystemLibrary = new SystemLibraryClass();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue