215 lines
7.6 KiB
C++
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
|
|
);
|
|
} |