tengri/Source/TengriPlatformer/Camera/TengriCameraComponent.cpp

215 lines
7.6 KiB
C++

// Request Games © All rights reserved
// Source/TengriPlatformer/Camera/TengriCameraComponent.cpp
#include "TengriCameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "DrawDebugHelpers.h"
#include "TengriPlatformer/Movement/TengriMovementComponent.h"
UTengriCameraComponent::UTengriCameraComponent()
{
PrimaryComponentTick.bCanEverTick = true;
// Update after physics to prevent jitter with interpolated character movement
PrimaryComponentTick.TickGroup = TG_PostPhysics;
}
void UTengriCameraComponent::BeginPlay()
{
Super::BeginPlay();
// Initialize focus to owner location to prevent initial snap
if (const AActor* Owner = GetOwner())
{
CurrentFocusLocation = Owner->GetActorLocation();
}
// Apply default config if assigned
if (DefaultConfig)
{
SetCameraConfig(DefaultConfig.Get());
}
}
void UTengriCameraComponent::InitializeCamera(USpringArmComponent* InSpringArm, UCameraComponent* InCamera)
{
SpringArm = InSpringArm;
Camera = InCamera;
if (SpringArm)
{
// Take full control of SpringArm transform (no automatic parenting)
SpringArm->SetUsingAbsoluteLocation(true);
SpringArm->SetUsingAbsoluteRotation(true);
// Disable built-in lag systems (we implement custom lag)
SpringArm->bUsePawnControlRotation = false;
SpringArm->bEnableCameraLag = false;
SpringArm->bEnableCameraRotationLag = false;
}
}
void UTengriCameraComponent::SetCameraConfig(UTengriCameraConfig* NewConfig)
{
CurrentConfig = NewConfig ? NewConfig : DefaultConfig.Get();
}
void UTengriCameraComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!SpringArm || !CurrentConfig || !GetOwner()) return;
// ════════════════════════════════════════════════════════════════════
// Phase 1: Determine Target Position
// ════════════════════════════════════════════════════════════════════
FVector TargetPos;
// Use interpolated render position to avoid jitter
if (auto* MoveComp = GetOwner()->FindComponentByClass<UTengriMovementComponent>())
{
TargetPos = MoveComp->GetRenderLocation();
}
else
{
TargetPos = GetOwner()->GetActorLocation(); // Fallback (will jitter)
}
// Apply dead zone logic in SideScroller mode
if (CurrentConfig->BehaviorType == ETengriCameraBehavior::SideScroller)
{
TargetPos = CalculateDeadZoneTarget(TargetPos);
}
// ════════════════════════════════════════════════════════════════════
// Phase 2: Smooth Camera Movement (Lag)
// ════════════════════════════════════════════════════════════════════
if (CurrentConfig->bEnableLag)
{
CurrentFocusLocation = FMath::VInterpTo(
CurrentFocusLocation,
TargetPos,
DeltaTime,
CurrentConfig->LagSpeed
);
}
else
{
CurrentFocusLocation = TargetPos;
}
// ════════════════════════════════════════════════════════════════════
// Phase 3: Apply Position to SpringArm
// ════════════════════════════════════════════════════════════════════
SpringArm->SetWorldLocation(CurrentFocusLocation);
// ════════════════════════════════════════════════════════════════════
// Phase 4: Interpolate SpringArm Parameters
// ════════════════════════════════════════════════════════════════════
const float TransSpeed = CurrentConfig->TransitionSpeed;
SpringArm->TargetArmLength = FMath::FInterpTo(
SpringArm->TargetArmLength,
CurrentConfig->TargetArmLength,
DeltaTime,
TransSpeed
);
SpringArm->SocketOffset = FMath::VInterpTo(
SpringArm->SocketOffset,
CurrentConfig->SocketOffset,
DeltaTime,
TransSpeed
);
// ════════════════════════════════════════════════════════════════════
// Phase 5: Handle Rotation
// ════════════════════════════════════════════════════════════════════
if (CurrentConfig->BehaviorType == ETengriCameraBehavior::FreeLook)
{
// Free look: use controller rotation (mouse/gamepad)
if (const APawn* PawnOwner = Cast<APawn>(GetOwner()))
{
SpringArm->SetWorldRotation(PawnOwner->GetControlRotation());
}
}
else
{
// Side scroller: interpolate to fixed rotation
const FRotator CurrentRot = SpringArm->GetComponentRotation();
const FRotator NewRot = FMath::RInterpTo(
CurrentRot,
CurrentConfig->FixedRotation,
DeltaTime,
TransSpeed
);
SpringArm->SetWorldRotation(NewRot);
// Update control rotation to match camera (for correct input interpretation)
if (APawn* PawnOwner = Cast<APawn>(GetOwner()))
{
if (AController* C = PawnOwner->GetController())
{
C->SetControlRotation(NewRot);
}
}
}
// ════════════════════════════════════════════════════════════════════
// Phase 6: Debug Visualization
// ════════════════════════════════════════════════════════════════════
#if WITH_EDITOR
if (CurrentConfig->bDrawDebugBox)
{
DrawDebugDeadZone(CurrentFocusLocation, CurrentConfig->DeadZoneExtent);
}
#endif
}
FVector UTengriCameraComponent::CalculateDeadZoneTarget(const FVector& PlayerLocation) const
{
// Camera doesn't move while player stays inside dead zone box.
// When player exits bounds, camera shifts just enough to keep player at box edge.
FVector DesiredFocus = CurrentFocusLocation;
const FVector Diff = PlayerLocation - DesiredFocus;
const FVector Extent = CurrentConfig->DeadZoneExtent;
// X-axis (Forward/Backward or Left/Right)
if (FMath::Abs(Diff.X) > Extent.X)
{
DesiredFocus.X = PlayerLocation.X - (FMath::Sign(Diff.X) * Extent.X);
}
// Z-axis (Up/Down)
if (FMath::Abs(Diff.Z) > Extent.Z)
{
DesiredFocus.Z = PlayerLocation.Z - (FMath::Sign(Diff.Z) * Extent.Z);
}
// Y-axis (Depth) - usually locked to player in 2.5D to prevent going off-screen
DesiredFocus.Y = PlayerLocation.Y;
return DesiredFocus;
}
void UTengriCameraComponent::DrawDebugDeadZone(const FVector& Center, const FVector& Extent) const
{
DrawDebugBox(
GetWorld(),
Center,
Extent,
FColor::Green,
false, // bPersistentLines
-1.0f, // LifeTime
0, // DepthPriority
2.0f // Thickness
);
}