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