tengri/Source/TengriPlatformer/Movement/TengriMovementComponent.cpp

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;
}
}