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