tengri/Source/TengriPlatformer/Movement/TengriMovementComponent.h

219 lines
8.2 KiB
C++

// Request Games © All rights reserved
// Source/TengriPlatformer/Movement/TengriMovementComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "TengriPlatformer/Movement/Core/TengriMovementConfig.h"
#include "TengriMovementComponent.generated.h"
class UCapsuleComponent;
struct FPendingLandingEvent; // Forward declaration
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTengriLandingSignature, bool, bIsHeavy);
/**
* Custom movement component for deterministic 3D platformer physics.
* Uses fixed timestep physics with interpolated rendering.
*
* Architecture:
* - Physics State: Updated at fixed rate (default 120Hz) for determinism
* - Render State: Interpolated between physics states for smooth visuals
* - Accumulator: Manages variable frame delta accumulation
* - Event Accumulation: Events during physics loop are broadcast after completion
*/
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class TENGRIPLATFORMER_API UTengriMovementComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTengriMovementComponent();
/** Get interpolated render location */
UFUNCTION(BlueprintPure, Category = "Tengri Movement")
FVector GetRenderLocation() const { return RenderLocation; }
/** Event triggered when character lands (broadcast after physics loop completes) */
UPROPERTY(BlueprintAssignable, Category = "Tengri Movement|Events")
FOnTengriLandingSignature OnLanded;
/** Instantly snaps character rotation to a new value (bypassing interpolation) */
void ForceRotation(const FRotator& NewRotation);
/** Enable strafe mode (character rotates toward camera instead of movement direction) */
void SetStrafing(const bool bEnabled) { bStrafing = bEnabled; }
protected:
virtual void BeginPlay() override;
/** Strafe mode flag */
bool bStrafing = false;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
// ========================================================================
// BLUEPRINT API
// ========================================================================
/**
* Sets the movement input vector. Call every frame from PlayerController.
* @param NewInput - Raw input (will be clamped to 1.0 and Z zeroed)
*/
UFUNCTION(BlueprintCallable, Category = "Tengri Movement")
void SetInputVector(FVector NewInput);
/**
* Updates jump input state. Call from PlayerController.
* @param bPressed - True if button just pressed or currently held
*/
UFUNCTION(BlueprintCallable, Category = "Tengri Movement")
void SetJumpInput(bool bPressed);
// ========================================================================
// CONFIGURATION
// ========================================================================
/** Movement parameters data asset */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Tengri Movement")
TObjectPtr<UTengriMovementConfig> MovementConfig;
// ========================================================================
// RUNTIME STATE
// ========================================================================
/** Current velocity in cm/s (synced from PhysicsVelocity each frame) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Tengri Movement|State")
FVector Velocity;
/** True when character is on walkable ground */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Tengri Movement|State")
bool bIsGrounded = false;
/** Cached surface thresholds (cosines) for performance */
UPROPERTY(VisibleAnywhere, Category = "Tengri Movement|Debug")
FSurfaceThresholds CachedThresholds;
private:
// ========================================================================
// PHYSICS STATE (Deterministic)
// ========================================================================
/** Physics location updated at fixed timestep */
FVector PhysicsLocation = FVector::ZeroVector;
/** Physics rotation updated at fixed timestep */
FRotator PhysicsRotation = FRotator::ZeroRotator;
/** Physics velocity in cm/s */
FVector PhysicsVelocity = FVector::ZeroVector;
// ========================================================================
// RENDER STATE (Interpolated)
// ========================================================================
/** Interpolated location for smooth rendering */
FVector RenderLocation = FVector::ZeroVector;
/** Interpolated rotation for smooth rendering */
FRotator RenderRotation = FRotator::ZeroRotator;
// ========================================================================
// FIXED TIMESTEP
// ========================================================================
/** Fixed timestep duration in seconds (calculated from PhysicsTickRate) */
float FixedTimeStep = 1.0f / 120.0f;
/** Accumulated variable frame time */
float TimeAccumulator = 0.0f;
/** Maximum accumulator value to prevent spiral of death */
float MaxAccumulatorTime = 0.1f;
// ========================================================================
// INTERPOLATION HISTORY
// ========================================================================
/** Previous physics location for interpolation */
FVector PreviousPhysicsLocation = FVector::ZeroVector;
/** Previous physics rotation for interpolation */
FRotator PreviousPhysicsRotation = FRotator::ZeroRotator;
// ========================================================================
// INTERNAL STATE
// ========================================================================
UPROPERTY()
TObjectPtr<UCapsuleComponent> OwnerCapsule;
/** Normalized input vector (Z always 0) */
FVector InputVector = FVector::ZeroVector;
// ========================================================================
// JUMP STATE (Internal)
// ========================================================================
/** Timer for Jump Buffering. > 0 means jump was recently pressed. */
float JumpBufferTimer = 0.0f;
/** Timer for Coyote Time. > 0 means we can still jump even if in air. */
float CoyoteTimer = 0.0f;
/** True if jump button is currently held down (for variable jump height) */
bool bIsJumpHeld = false;
/** Flag to prevent Coyote Time reactivation immediately after jumping */
bool bHasJumpedThisFrame = false;
// ========================================================================
// INITIALIZATION
// ========================================================================
void InitializeSystem();
// ========================================================================
// PHYSICS TICK (UPDATED)
// ========================================================================
/**
* Deterministic physics update at fixed timestep.
* All movement logic runs here with constant delta time.
* @param FixedDeltaTime - Fixed timestep duration (e.g., 1/120 sec)
* @param OutPendingLandings - Accumulator for landing events (broadcast later)
*/
void TickPhysics(float FixedDeltaTime, TArray<FPendingLandingEvent>& OutPendingLandings);
/** Save current physics state before next physics step */
void SavePreviousPhysicsState();
// ========================================================================
// INTERPOLATION
// ========================================================================
/**
* Interpolate render state between previous and current physics states.
* @param Alpha - Interpolation factor [0..1]
*/
void InterpolateRenderState(float Alpha);
/** Apply interpolated render state to actor transform */
void ApplyRenderState() const;
// ========================================================================
// PHYSICS HELPERS
// ========================================================================
/**
* Snap to ground to prevent slope jitter.
* @param InOutLocation - Physics location (modified if snap succeeds)
* @param OutSnapHit - Hit result if ground found
* @return True if snapped to ground
*/
bool PerformGroundSnapping(FVector& InOutLocation, FHitResult& OutSnapHit) const;
};