[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 { AC_Movement } from '#root/Movement/Components/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';
|
||||
import type { Controller } from '#root/UE/Controller.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.InputDeviceComponent,
|
||||
|
|
@ -197,7 +201,7 @@ export class BP_MainCharacter extends Pawn {
|
|||
DeltaTime
|
||||
);
|
||||
|
||||
this.ApplyMovementAndRotation();
|
||||
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
|
||||
|
||||
if (this.ShowDebugInfo) {
|
||||
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
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Core movement system component - handles deterministic 3D platformer movement
|
||||
* Camera system component - handles camera rotation and sensitivity
|
||||
* @category Components
|
||||
*/
|
||||
MovementComponent = new AC_Movement();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
CameraComponent = new AC_Camera();
|
||||
|
||||
/**
|
||||
* Input device detection component - manages input device state and detection
|
||||
|
|
@ -264,10 +227,28 @@ export class BP_MainCharacter extends Pawn {
|
|||
InputDeviceComponent = new AC_InputDevice();
|
||||
|
||||
/**
|
||||
* Camera system component - handles camera rotation and sensitivity
|
||||
* Toast notification system - displays temporary status messages
|
||||
* @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)
|
||||
|
|
|
|||
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_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.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_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
|
||||
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
|
||||
|
|
@ -50,11 +49,9 @@ InputDeviceDetectionTest.EventStartTest();
|
|||
// Movement Tests
|
||||
const BasicMovementTest = new FT_BasicMovement();
|
||||
const SurfaceClassificationTest = new FT_SurfaceClassification();
|
||||
const DiagonalMovement = new FT_DiagonalMovement();
|
||||
|
||||
BasicMovementTest.EventStartTest();
|
||||
SurfaceClassificationTest.EventStartTest();
|
||||
DiagonalMovement.EventStartTest();
|
||||
|
||||
// Toasts Tests
|
||||
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 { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.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 { HitResult } from '#root/UE/HitResult.ts';
|
||||
import type { Integer } from '#root/UE/Integer.ts';
|
||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||
import { Rotator } from '#root/UE/Rotator.ts';
|
||||
import { StringLibrary } from '#root/UE/StringLibrary.ts';
|
||||
|
|
@ -23,10 +28,13 @@ export class AC_Movement extends ActorComponent {
|
|||
// FUNCTIONS
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Surface Detection
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify surface type based on normal vector
|
||||
* @param SurfaceNormal - Normalized surface normal vector
|
||||
* @param AngleThresholds - Angle thresholds in radians
|
||||
* @returns Surface type classification
|
||||
* @example
|
||||
* // Classify flat ground
|
||||
|
|
@ -34,10 +42,7 @@ export class AC_Movement extends ActorComponent {
|
|||
* @pure true
|
||||
* @category Surface Detection
|
||||
*/
|
||||
public ClassifySurface(
|
||||
SurfaceNormal: Vector,
|
||||
AngleThresholds: S_AngleThresholds
|
||||
): E_SurfaceType {
|
||||
public ClassifySurface(SurfaceNormal: Vector): E_SurfaceType {
|
||||
const SurfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
|
||||
|
||||
/**
|
||||
|
|
@ -58,11 +63,11 @@ export class AC_Movement extends ActorComponent {
|
|||
const IsWallAngle = (wallAngle: Float): boolean =>
|
||||
SurfaceAngle <= wallAngle;
|
||||
|
||||
if (IsWalkableAngle(AngleThresholds.Walkable)) {
|
||||
if (IsWalkableAngle(this.AngleThresholdsRads.Walkable)) {
|
||||
return E_SurfaceType.Walkable;
|
||||
} else if (IsSteepSlopeAngle(AngleThresholds.SteepSlope)) {
|
||||
} else if (IsSteepSlopeAngle(this.AngleThresholdsRads.SteepSlope)) {
|
||||
return E_SurfaceType.SteepSlope;
|
||||
} else if (IsWallAngle(AngleThresholds.Wall)) {
|
||||
} else if (IsWallAngle(this.AngleThresholdsRads.Wall)) {
|
||||
return E_SurfaceType.Wall;
|
||||
} else {
|
||||
return E_SurfaceType.Ceiling;
|
||||
|
|
@ -124,6 +129,10 @@ export class AC_Movement extends ActorComponent {
|
|||
return SurfaceType === E_SurfaceType.None;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Movement Processing
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Process movement input from player controller
|
||||
* Normalizes input and calculates target velocity
|
||||
|
|
@ -138,6 +147,10 @@ export class AC_Movement extends ActorComponent {
|
|||
this.TargetRotation = this.CalculateTargetRotation(InputVector);
|
||||
this.UpdateCharacterRotation(DeltaTime);
|
||||
|
||||
this.LastGroundHit = this.CheckGround();
|
||||
|
||||
this.IsGrounded = this.LastGroundHit.BlockingHit;
|
||||
|
||||
// Only process movement on walkable surfaces
|
||||
if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) {
|
||||
this.ProcessGroundMovement(InputVector, DeltaTime);
|
||||
|
|
@ -145,14 +158,10 @@ export class AC_Movement extends ActorComponent {
|
|||
this.ApplyFriction(DeltaTime);
|
||||
}
|
||||
|
||||
// Always apply gravity
|
||||
this.ApplyGravity(DeltaTime);
|
||||
|
||||
// Update movement state
|
||||
this.ApplyGravity();
|
||||
this.UpdateMovementState();
|
||||
|
||||
// Calculate current speed for debug
|
||||
this.UpdateCurrentSpeed();
|
||||
this.ApplyMovementWithSweep(DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,19 +173,16 @@ export class AC_Movement extends ActorComponent {
|
|||
*/
|
||||
private ProcessGroundMovement(InputVector: Vector, DeltaTime: Float): void {
|
||||
if (this.InputMagnitude > 0.01) {
|
||||
const CalculateTargetVelocity = (
|
||||
inputVector: Vector,
|
||||
maxSpeed: Float
|
||||
): Vector =>
|
||||
const CalculateTargetVelocity = (inputVector: Vector): Vector =>
|
||||
new Vector(
|
||||
MathLibrary.Normal(inputVector).X * maxSpeed,
|
||||
MathLibrary.Normal(inputVector).Y * maxSpeed,
|
||||
MathLibrary.Normal(inputVector).Z * maxSpeed
|
||||
MathLibrary.Normal(inputVector).X * this.MaxSpeed,
|
||||
MathLibrary.Normal(inputVector).Y * this.MaxSpeed,
|
||||
MathLibrary.Normal(inputVector).Z * this.MaxSpeed
|
||||
);
|
||||
|
||||
this.CurrentVelocity = MathLibrary.VInterpTo(
|
||||
new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0),
|
||||
CalculateTargetVelocity(InputVector, this.MaxSpeed),
|
||||
CalculateTargetVelocity(InputVector),
|
||||
DeltaTime,
|
||||
this.Acceleration
|
||||
);
|
||||
|
|
@ -202,22 +208,18 @@ export class AC_Movement extends ActorComponent {
|
|||
|
||||
/**
|
||||
* Apply gravity to vertical velocity
|
||||
* @param DeltaTime - Frame delta time
|
||||
* @category Movement Processing
|
||||
*/
|
||||
private ApplyGravity(DeltaTime: Float): void {
|
||||
private ApplyGravity(): void {
|
||||
if (!this.IsGrounded) {
|
||||
const ApplyGravityForce = (
|
||||
velocityZ: Float,
|
||||
gravity: Float,
|
||||
deltaTime: Float
|
||||
): Float => velocityZ - gravity * deltaTime;
|
||||
const ApplyGravityForce = (velocityZ: Float): Float =>
|
||||
velocityZ - this.Gravity;
|
||||
|
||||
// Apply gravity when airborne
|
||||
this.CurrentVelocity = new Vector(
|
||||
this.CurrentVelocity.X,
|
||||
this.CurrentVelocity.Y,
|
||||
ApplyGravityForce(this.CurrentVelocity.Z, this.Gravity, DeltaTime)
|
||||
ApplyGravityForce(this.CurrentVelocity.Z)
|
||||
);
|
||||
} else {
|
||||
// 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
|
||||
* Determines what rotation character should have based on movement
|
||||
|
|
@ -262,7 +364,7 @@ export class AC_Movement extends ActorComponent {
|
|||
* @pure true
|
||||
* @category Character Rotation
|
||||
*/
|
||||
public CalculateTargetRotation(MovementDirection: Vector): Rotator {
|
||||
private CalculateTargetRotation(MovementDirection: Vector): Rotator {
|
||||
const TargetYaw = (
|
||||
movementDirectionX: Float,
|
||||
movementDirectionY: Float
|
||||
|
|
@ -290,7 +392,7 @@ export class AC_Movement extends ActorComponent {
|
|||
* @param DeltaTime - Time since last frame
|
||||
* @category Character Rotation
|
||||
*/
|
||||
public UpdateCharacterRotation(DeltaTime: Float): void {
|
||||
private UpdateCharacterRotation(DeltaTime: Float): void {
|
||||
if (this.ShouldRotateToMovement) {
|
||||
const ShouldRotate = (): boolean =>
|
||||
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
|
||||
* Converts degree thresholds to radians for runtime performance
|
||||
* @category System
|
||||
*/
|
||||
public InitializeMovementSystem(
|
||||
DebugHUDComponentRef: AC_DebugHUD | null
|
||||
CapsuleComponentRef: CapsuleComponent | null = null,
|
||||
DebugHUDComponentRef: AC_DebugHUD | null = null
|
||||
): void {
|
||||
this.CapsuleComponent = CapsuleComponentRef;
|
||||
this.DebugHUDComponent = DebugHUDComponentRef;
|
||||
this.IsInitialized = true;
|
||||
|
||||
|
|
@ -371,6 +723,10 @@ export class AC_Movement extends ActorComponent {
|
|||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Debug
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Update debug HUD with current movement info
|
||||
* @category Debug
|
||||
|
|
@ -406,31 +762,91 @@ export class AC_Movement extends ActorComponent {
|
|||
`Rotation Delta: ${this.RotationDelta}\n°` +
|
||||
`Is Rotating: ${this.IsCharacterRotating}\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
|
||||
* Provides read-only access to private movement constant
|
||||
* @returns Object containing MaxSpeed configuration value
|
||||
* @category Testing
|
||||
* Get maximum horizontal movement speed
|
||||
* @returns Maximum speed in UE units per second
|
||||
* @category Public Interface
|
||||
* @pure true
|
||||
*/
|
||||
public GetTestData(): {
|
||||
MaxSpeed: Float;
|
||||
} {
|
||||
return {
|
||||
MaxSpeed: this.MaxSpeed,
|
||||
};
|
||||
public GetMaxSpeed(): Float {
|
||||
return 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
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Movement Config
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Maximum horizontal movement speed in UE units per second
|
||||
* Character cannot exceed this speed through ground movement
|
||||
|
|
@ -439,7 +855,7 @@ export class AC_Movement extends ActorComponent {
|
|||
* @category Movement Config
|
||||
* @instanceEditable true
|
||||
*/
|
||||
private readonly MaxSpeed: Float = 600.0;
|
||||
private readonly MaxSpeed: Float = 800.0;
|
||||
|
||||
/**
|
||||
* Speed of velocity interpolation towards target velocity
|
||||
|
|
@ -486,6 +902,174 @@ export class AC_Movement extends ActorComponent {
|
|||
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
|
||||
* Converted from degrees during initialization for performance
|
||||
|
|
@ -497,118 +1081,39 @@ export class AC_Movement extends ActorComponent {
|
|||
Wall: 0.0,
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Debug
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Flag indicating if movement system has been initialized
|
||||
* Ensures angle thresholds are converted before use
|
||||
* @category Debug
|
||||
*/
|
||||
public IsInitialized = false;
|
||||
private IsInitialized = false;
|
||||
|
||||
/**
|
||||
* Debug page identifier for organizing debug output
|
||||
* Used by debug HUD to categorize information
|
||||
* @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
|
||||
*/
|
||||
public RotationSpeed: Float = 720.0;
|
||||
private readonly DebugPageID: string = 'MovementInfo';
|
||||
|
||||
/**
|
||||
* Should character rotate when moving
|
||||
* 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;
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Components
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reference to debug HUD component for displaying camera info
|
||||
* Optional, used for debugging purposes
|
||||
* @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
|
||||
- **Требования:** MovementComponent инициализирован
|
||||
- **Debug HUD:** Включен для проверки параметров
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -14,14 +15,15 @@
|
|||
- [ ] **InitializeMovementSystem()** выполняется без ошибок при запуске уровня
|
||||
- [ ] **IsInitialized flag** устанавливается в true после инициализации
|
||||
- [ ] **Angle conversion** - пороги корректно конвертируются из градусов в радианы
|
||||
- [ ] **CapsuleComponent reference** - передаётся и сохраняется корректно (этап 9)
|
||||
|
||||
---
|
||||
|
||||
## 2. Константы движения
|
||||
|
||||
### 2.1 Default значения
|
||||
- [ ] **MaxSpeed = 600.0** - значение установлено по умолчанию
|
||||
- [ ] **Acceleration = 10.0** - значение установлено по умолчанию (уменьшено для плавности)
|
||||
- [ ] **MaxSpeed = 800.0** - значение установлено по умолчанию
|
||||
- [ ] **Acceleration = 10.0** - значение установлено по умолчанию
|
||||
- [ ] **Friction = 8.0** - значение установлено по умолчанию
|
||||
- [ ] **Gravity = 980.0** - значение установлено по умолчанию
|
||||
|
||||
|
|
@ -30,6 +32,12 @@
|
|||
- [ ] **SteepSlope = 85.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)
|
||||
|
|
@ -51,75 +59,178 @@
|
|||
### 3.3 Физика движения
|
||||
- [ ] **Плавное ускорение** - персонаж набирает скорость постепенно при нажатии клавиш
|
||||
- [ ] **Плавное торможение** - персонаж останавливается плавно при отпускании клавиш
|
||||
- [ ] **MaxSpeed limit** - скорость не превышает 600.0 units/sec
|
||||
- [ ] **MaxSpeed limit** - скорость не превышает 800.0 units/sec
|
||||
- [ ] **Диагональное движение** - скорость диагонального движения равна прямому (не быстрее)
|
||||
- [ ] **Стабильное поведение** - нет рывков, заиканий или неожиданных ускорений
|
||||
|
||||
### 3.4 Состояния движения
|
||||
- [ ] **Idle state** - MovementState = Idle когда персонаж стоит
|
||||
- [ ] **Walking state** - MovementState = Walking при движении
|
||||
- [ ] **Airborne state** - MovementState = Airborne в воздухе (этап 9)
|
||||
- [ ] **InputMagnitude** - корректно отражает силу input (0-1)
|
||||
- [ ] **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
|
||||
- Friction: 8
|
||||
- Gravity: 980
|
||||
- Initialized: true
|
||||
- [ ] **Текущее состояние** отображается:
|
||||
- Current Velocity: X, Y, Z компоненты
|
||||
- Speed: горизонтальная скорость
|
||||
- Is Grounded: Yes (пока всегда true)
|
||||
- Is Grounded: true/false
|
||||
- Surface Type: Walkable (пока всегда)
|
||||
- Movement State: Idle/Walking
|
||||
- Movement State: Idle/Walking/Airborne
|
||||
- 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** изменяется в реальном времени при движении
|
||||
- [ ] **Speed** корректно показывает magnitude горизонтальной скорости
|
||||
- [ ] **Movement State** переключается между Idle и Walking
|
||||
- [ ] **Input Magnitude** отражает силу нажатия (особенно на геймпаде)
|
||||
- [ ] **Movement State** переключается между Idle/Walking/Airborne
|
||||
- [ ] **Input Magnitude** отражает силу нажатия
|
||||
- [ ] **Collision Checks** обновляется каждый кадр при движении
|
||||
- [ ] **Location** обновляется плавно
|
||||
|
||||
---
|
||||
|
||||
## 5. Автотесты Integration
|
||||
## 8. Автотесты Integration
|
||||
|
||||
### 5.1 FT_BasicMovement
|
||||
- [ ] **Тест проходит** - инициализация, ускорение, торможение, состояния
|
||||
- [ ] **No console errors** при выполнении автотеста
|
||||
### 8.1 FT_SurfaceClassification
|
||||
- [ ] **Тест проходит** - классификация поверхностей по углам
|
||||
|
||||
### 5.2 FT_DiagonalMovement
|
||||
- [ ] **Тест проходит** - диагональное движение не быстрее прямого
|
||||
- [ ] **Input normalization** работает корректно
|
||||
### 8.2 FT_MovementInitialization
|
||||
- [ ] **Тест проходит** - инициализация, начальные состояния, конфигурация
|
||||
|
||||
### 8.3 Удалённые тесты
|
||||
- ❌ **FT_BasicMovement** - удалён (требует тестовый уровень)
|
||||
- ❌ **FT_DiagonalMovement** - удалён (требует тестовый уровень)
|
||||
|
||||
---
|
||||
|
||||
## 6. Performance
|
||||
## 9. Performance
|
||||
|
||||
### 6.1 Производительность
|
||||
### 9.1 Производительность
|
||||
- [ ] **Stable 60+ FPS** при активном движении
|
||||
- [ ] **No memory leaks** при длительном использовании
|
||||
- [ ] **Smooth movement** без микро-заиканий
|
||||
- [ ] **Sweep overhead** минимален (<1ms дополнительно)
|
||||
|
||||
### 6.2 Отзывчивость
|
||||
### 9.2 Отзывчивость
|
||||
- [ ] **Instant response** на нажатие клавиш (нет input lag)
|
||||
- [ ] **Smooth transitions** между состояниями движения
|
||||
- [ ] **Consistent timing** независимо от FPS
|
||||
|
||||
---
|
||||
|
||||
## Критерии прохождения
|
||||
## Критерии прохождения этапов
|
||||
|
||||
### Этап 7: Базовое движение
|
||||
- [ ] Все основные направления движения работают
|
||||
- [ ] Физика движения плавная и отзывчивая
|
||||
- [ ] 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 { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.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
|
||||
|
|
@ -27,86 +27,34 @@ export class FT_BasicMovement extends FunctionalTest {
|
|||
*/
|
||||
EventStartTest(): void {
|
||||
// Initialize movement system
|
||||
this.MovementComponent.InitializeMovementSystem(this.DebugHUDComponent);
|
||||
this.MovementComponent.InitializeMovementSystem(
|
||||
null,
|
||||
this.DebugHUDComponent
|
||||
);
|
||||
|
||||
// Test 1: Initialization
|
||||
if (this.MovementComponent.IsInitialized) {
|
||||
if (this.MovementComponent.GetIsInitialized()) {
|
||||
// Test 2: Initial state should be Idle
|
||||
if (this.MovementComponent.MovementState === E_MovementState.Idle) {
|
||||
// Test 3: Process forward input and verify acceleration
|
||||
this.MovementComponent.ProcessMovementInput(
|
||||
new Vector(1.0, 0, 0), // Forward input
|
||||
0.016 // 60 FPS delta
|
||||
);
|
||||
if (this.MovementComponent.GetMovementState() === E_MovementState.Idle) {
|
||||
// Test 3: Initial speed & velocity is zero
|
||||
|
||||
if (this.MovementComponent.CurrentVelocity.X > 0) {
|
||||
// Test 4: Movement state should change to Walking
|
||||
if (
|
||||
(this.MovementComponent.MovementState as string) ===
|
||||
(E_MovementState.Walking as string)
|
||||
) {
|
||||
// Test 5: Process multiple frames to test max speed limit
|
||||
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.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,
|
||||
`Movement state should be Idle when stopped, got ${this.MovementComponent.MovementState as string}`
|
||||
`Current Speed & Current velocity should be zero, got Speed: ${this.MovementComponent.GetCurrentSpeed()}, Velocity: ${StringLibrary.ConvVectorToString(this.MovementComponent.GetCurrentVelocity())}`
|
||||
);
|
||||
}
|
||||
} 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 {
|
||||
this.FinishTest(
|
||||
EFunctionalTestResult.Failed,
|
||||
'Forward input should produce forward velocity'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.FinishTest(
|
||||
EFunctionalTestResult.Failed,
|
||||
`Initial movement state should be Idle, got ${this.MovementComponent.MovementState}`
|
||||
`Initial movement state should be Idle, got ${this.MovementComponent.GetMovementState()}`
|
||||
);
|
||||
}
|
||||
} 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 { AC_Movement } from '#root/Movement/Components/AC_Movement.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 { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
|
||||
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
|
||||
import { MathLibrary } from '#root/UE/MathLibrary.ts';
|
||||
|
||||
/**
|
||||
* Functional Test: Surface Classification System
|
||||
|
|
@ -23,24 +21,6 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
|||
// 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
|
||||
* Tests boundary conditions and edge cases for each surface type
|
||||
|
|
@ -49,8 +29,7 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
|||
this.TestCases.forEach(
|
||||
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
|
||||
const surfaceType = this.MovementComponent.ClassifySurface(
|
||||
BFL_Vectors.GetNormalFromAngle(AngleDegrees),
|
||||
this.AngleThresholdsRads
|
||||
BFL_Vectors.GetNormalFromAngle(AngleDegrees)
|
||||
);
|
||||
|
||||
if (surfaceType === ExpectedType) {
|
||||
|
|
@ -132,15 +111,4 @@ export class FT_SurfaceClassification extends FunctionalTest {
|
|||
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
|
||||
|
||||
import { Actor } from '#root/UE/Actor.ts';
|
||||
import { Name } from '#root/UE/Name.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) {
|
||||
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 type { Color } from '#root/UE/Color.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 { Vector } from '#root/UE/Vector.ts';
|
||||
|
||||
|
|
@ -292,6 +293,18 @@ class MathLibraryClass extends BlueprintFunctionLibrary {
|
|||
|
||||
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
|
||||
|
||||
import type { Actor } from '#root/UE/Actor.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 { HitResult } from '#root/UE/HitResult.ts';
|
||||
import { LinearColor } from '#root/UE/LinearColor.ts';
|
||||
import { Name } from '#root/UE/Name.ts';
|
||||
import type { UEObject } from '#root/UE/UEObject.ts';
|
||||
import { Vector } from '#root/UE/Vector.ts';
|
||||
|
||||
class SystemLibraryClass extends BlueprintFunctionLibrary {
|
||||
constructor(
|
||||
|
|
@ -50,6 +55,76 @@ class SystemLibraryClass extends BlueprintFunctionLibrary {
|
|||
console.log(duration);
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue