269 lines
7.2 KiB
C++
269 lines
7.2 KiB
C++
// Source/TengriPlatformer/Movement/TengriMovementComponent.cpp
|
|
|
|
#include "TengriMovementComponent.h"
|
|
#include "Components/CapsuleComponent.h"
|
|
#include "TengriPlatformer/Movement/Collision/TengriCollisionResolver.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogTengriMovement, Log, All);
|
|
|
|
// ============================================================================
|
|
// CONSTRUCTOR
|
|
// ============================================================================
|
|
|
|
UTengriMovementComponent::UTengriMovementComponent()
|
|
{
|
|
PrimaryComponentTick.bCanEverTick = true;
|
|
Velocity = FVector::ZeroVector;
|
|
bIsGrounded = false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
InitializeSystem();
|
|
}
|
|
|
|
void UTengriMovementComponent::InitializeSystem()
|
|
{
|
|
if (const AActor* Owner = GetOwner())
|
|
{
|
|
OwnerCapsule = Cast<UCapsuleComponent>(Owner->GetRootComponent());
|
|
if (!OwnerCapsule)
|
|
{
|
|
UE_LOG(LogTengriMovement, Error,
|
|
TEXT("InitializeSystem failed: Owner root component is not a CapsuleComponent"));
|
|
SetComponentTickEnabled(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (MovementConfig)
|
|
{
|
|
CachedThresholds = MovementConfig->GetThresholds();
|
|
UE_LOG(LogTengriMovement, Log,
|
|
TEXT("System initialized. WalkableZ: %.3f"), CachedThresholds.WalkableZ);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTengriMovement, Warning,
|
|
TEXT("InitializeSystem: No MovementConfig assigned"));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// BLUEPRINT API
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::SetInputVector(FVector NewInput)
|
|
{
|
|
InputVector = NewInput.GetClampedToMaxSize(1.0f);
|
|
InputVector.Z = 0.0f;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TICK
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::TickComponent(
|
|
const float DeltaTime,
|
|
const ELevelTick TickType,
|
|
FActorComponentTickFunction* ThisTickFunction)
|
|
{
|
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
|
|
|
if (!MovementConfig || !OwnerCapsule)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Phase 1: Input -> Velocity
|
|
ApplyAccelerationAndFriction(DeltaTime);
|
|
|
|
// Phase 2: Rotation
|
|
ApplyRotation(DeltaTime);
|
|
|
|
// Phase 3: Gravity
|
|
ApplyGravity(DeltaTime);
|
|
|
|
// Phase 4: Collision Resolution
|
|
FVector NewLocation = ResolveMovementWithCollision(DeltaTime);
|
|
GetOwner()->SetActorLocation(NewLocation);
|
|
|
|
// Phase 5: Ground Snapping
|
|
FHitResult SnapHit;
|
|
const bool bJustSnapped = PerformGroundSnapping(NewLocation, SnapHit);
|
|
|
|
if (bJustSnapped)
|
|
{
|
|
GetOwner()->SetActorLocation(NewLocation);
|
|
|
|
// Preserve momentum on slopes
|
|
if (!InputVector.IsNearlyZero())
|
|
{
|
|
Velocity = UTengriCollisionResolver::ProjectVelocity(Velocity, SnapHit.ImpactNormal);
|
|
}
|
|
}
|
|
|
|
// Phase 6: State Update
|
|
// Note: MoveResult info is stored during Phase 4
|
|
UpdateGroundedState(false, 0.f, bJustSnapped);
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 1: ACCELERATION
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::ApplyAccelerationAndFriction(const float DeltaTime)
|
|
{
|
|
const float CurrentZ = Velocity.Z;
|
|
FVector HorizontalVelocity(Velocity.X, Velocity.Y, 0.f);
|
|
|
|
if (!InputVector.IsNearlyZero())
|
|
{
|
|
const FVector TargetVelocity = InputVector * MovementConfig->MaxSpeed;
|
|
HorizontalVelocity = FMath::VInterpTo(
|
|
HorizontalVelocity,
|
|
TargetVelocity,
|
|
DeltaTime,
|
|
MovementConfig->Acceleration
|
|
);
|
|
}
|
|
else
|
|
{
|
|
HorizontalVelocity = FMath::VInterpTo(
|
|
HorizontalVelocity,
|
|
FVector::ZeroVector,
|
|
DeltaTime,
|
|
MovementConfig->Friction
|
|
);
|
|
}
|
|
|
|
Velocity = HorizontalVelocity;
|
|
Velocity.Z = CurrentZ;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 2: ROTATION
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::ApplyRotation(const float DeltaTime) const
|
|
{
|
|
if (const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation); Velocity.SizeSquared2D() > MinSpeedSq)
|
|
{
|
|
const FRotator CurrentRot = GetOwner()->GetActorRotation();
|
|
|
|
FRotator TargetRot = Velocity.ToOrientationRotator();
|
|
TargetRot.Pitch = 0.0f;
|
|
TargetRot.Roll = 0.0f;
|
|
|
|
const FRotator NewRot = FMath::RInterpConstantTo(
|
|
CurrentRot,
|
|
TargetRot,
|
|
DeltaTime,
|
|
MovementConfig->RotationSpeed
|
|
);
|
|
|
|
GetOwner()->SetActorRotation(NewRot);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 3: GRAVITY
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::ApplyGravity(float DeltaTime)
|
|
{
|
|
if (!bIsGrounded)
|
|
{
|
|
Velocity.Z -= MovementConfig->Gravity * DeltaTime;
|
|
}
|
|
else
|
|
{
|
|
Velocity.Z = 0.0f;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 4: COLLISION RESOLUTION
|
|
// ============================================================================
|
|
|
|
FVector UTengriMovementComponent::ResolveMovementWithCollision(float DeltaTime)
|
|
{
|
|
const FVector DesiredDelta = Velocity * DeltaTime;
|
|
|
|
const FTengriSweepResult MoveResult = UTengriCollisionResolver::ResolveMovement(
|
|
this,
|
|
GetOwner()->GetActorLocation(),
|
|
DesiredDelta,
|
|
OwnerCapsule,
|
|
CachedThresholds,
|
|
MovementConfig->MaxStepHeight,
|
|
MovementConfig->MaxSlideIterations,
|
|
false
|
|
);
|
|
|
|
// Store for state update
|
|
if (MoveResult.bBlocked)
|
|
{
|
|
UpdateGroundedState(MoveResult.bBlocked, MoveResult.Hit.ImpactNormal.Z, false);
|
|
}
|
|
|
|
return MoveResult.Location;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 5: GROUND SNAPPING
|
|
// ============================================================================
|
|
|
|
bool UTengriMovementComponent::PerformGroundSnapping(
|
|
FVector& InOutLocation,
|
|
FHitResult& OutSnapHit) const
|
|
{
|
|
// Only snap if we were grounded or just landed
|
|
if (!bIsGrounded)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FVector SnapLocation;
|
|
const bool bSnapped = UTengriCollisionResolver::SnapToGround(
|
|
this,
|
|
OwnerCapsule,
|
|
MovementConfig->GroundSnapDistance,
|
|
CachedThresholds,
|
|
SnapLocation,
|
|
OutSnapHit
|
|
);
|
|
|
|
if (bSnapped)
|
|
{
|
|
// Add micro-offset to prevent floor penetration
|
|
InOutLocation = SnapLocation + FVector(0.f, 0.f, MovementConfig->GroundSnapOffset);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHASE 6: STATE UPDATE
|
|
// ============================================================================
|
|
|
|
void UTengriMovementComponent::UpdateGroundedState(
|
|
const bool bMoveBlocked,
|
|
const float HitNormalZ,
|
|
const bool bJustSnapped)
|
|
{
|
|
const bool bHitWalkable = bMoveBlocked && HitNormalZ >= CachedThresholds.WalkableZ;
|
|
bIsGrounded = bJustSnapped || bHitWalkable;
|
|
|
|
// Prevent Z velocity accumulation when grounded
|
|
if (bIsGrounded && Velocity.Z < 0.f)
|
|
{
|
|
Velocity.Z = 0.f;
|
|
}
|
|
} |