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