feat(camera): add dynamic camera system with multiple behavior modes
- Add TengriCameraConfig data asset for camera parameter configuration * Support FreeLook (3D) and SideScroller (2.5D) behavior modes * Configurable dead zone system for 2.5D gameplay * Smooth transition parameters between configs * Camera lag settings for cinematic feel - Add TengriCameraComponent for runtime camera management * Post-physics tick to prevent visual jitter with interpolation * Dead zone logic: camera only moves when player exits bounds * Smooth config transitions using interpolation * Debug visualization for dead zone boundaries - Use interpolated render position from movement component - Support dynamic config switching via Blueprint (e.g. camera volumes) Camera system designed for seamless transitions between 3D exploration and 2.5D platforming sections with configurable dead zones.main
parent
14d3696805
commit
74996e5e4b
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
ETengriCameraBehavior,
|
||||||
|
TengriCameraConfig,
|
||||||
|
} from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
|
||||||
|
import { Vector } from '/Content/UE/Vector.ts';
|
||||||
|
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||||
|
|
||||||
|
export class DA_CameraAiming extends TengriCameraConfig {
|
||||||
|
override BehaviorType = ETengriCameraBehavior.FreeLook;
|
||||||
|
|
||||||
|
override FixedRotation = new Rotator(0, -15, -90);
|
||||||
|
override TargetArmLength = 250;
|
||||||
|
override SocketOffset = new Vector(0, 100, 60);
|
||||||
|
|
||||||
|
override TransitionSpeed = 2;
|
||||||
|
|
||||||
|
override bEnableLag = false;
|
||||||
|
|
||||||
|
override DeadZoneExtent = new Vector(200, 0, 150);
|
||||||
|
override DeadZoneOffset = new Vector(0, 0, 50);
|
||||||
|
|
||||||
|
override bDrawDebugBox = false;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
ETengriCameraBehavior,
|
||||||
|
TengriCameraConfig,
|
||||||
|
} from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
|
||||||
|
import { Vector } from '/Content/UE/Vector.ts';
|
||||||
|
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||||
|
|
||||||
|
export class DA_CameraDefault extends TengriCameraConfig {
|
||||||
|
override BehaviorType = ETengriCameraBehavior.FreeLook;
|
||||||
|
|
||||||
|
override FixedRotation = new Rotator(0, -15, -90);
|
||||||
|
override TargetArmLength = 400;
|
||||||
|
override SocketOffset = new Vector(0, 0, 0);
|
||||||
|
|
||||||
|
override TransitionSpeed = 2;
|
||||||
|
|
||||||
|
override bEnableLag = false;
|
||||||
|
|
||||||
|
override DeadZoneExtent = new Vector(200, 0, 150);
|
||||||
|
override DeadZoneOffset = new Vector(0, 0, 50);
|
||||||
|
|
||||||
|
override bDrawDebugBox = false;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {
|
||||||
|
ETengriCameraBehavior,
|
||||||
|
TengriCameraConfig,
|
||||||
|
} from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
|
||||||
|
import { Vector } from '/Content/UE/Vector.ts';
|
||||||
|
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||||
|
|
||||||
|
export class DA_CameraScroller extends TengriCameraConfig {
|
||||||
|
override BehaviorType = ETengriCameraBehavior.FreeLook;
|
||||||
|
|
||||||
|
override FixedRotation = new Rotator(0, 0, 0);
|
||||||
|
override TargetArmLength = 1200;
|
||||||
|
override SocketOffset = new Vector(0, 0, 100);
|
||||||
|
|
||||||
|
override TransitionSpeed = 1.5;
|
||||||
|
|
||||||
|
override bEnableLag = true;
|
||||||
|
override LagSpeed = 5;
|
||||||
|
|
||||||
|
override DeadZoneExtent = new Vector(100, 300, 100);
|
||||||
|
override DeadZoneOffset = new Vector(0, 0, 50);
|
||||||
|
|
||||||
|
override bDrawDebugBox = false;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Request Games © All rights reserved
|
||||||
|
|
||||||
|
// Source/TengriPlatformer/Camera/Core/TengriCameraConfig.cpp
|
||||||
|
|
||||||
|
#include "TengriCameraConfig.h"
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Request Games © All rights reserved
|
||||||
|
|
||||||
|
// Source/TengriPlatformer/Camera/Core/TengriCameraConfig.h
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "TengriCameraConfig.generated.h"
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ETengriCameraBehavior : uint8
|
||||||
|
{
|
||||||
|
FreeLook UMETA(DisplayName = "Free Look (3D)"),
|
||||||
|
SideScroller UMETA(DisplayName = "Side Scroller (2.5D with DeadZone)")
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera configuration data asset.
|
||||||
|
* Defines camera behavior, positioning, smoothing, and dead zone parameters.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class TENGRIPLATFORMER_API UTengriCameraConfig : public UDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// BEHAVIOR
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Behavior")
|
||||||
|
ETengriCameraBehavior BehaviorType = ETengriCameraBehavior::FreeLook;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// TRANSFORM
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Fixed camera rotation (SideScroller mode only) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Transform")
|
||||||
|
FRotator FixedRotation = FRotator(-15.0f, -90.0f, 0.0f);
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Transform")
|
||||||
|
float TargetArmLength = 1000.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Transform")
|
||||||
|
FVector SocketOffset = FVector(0.f, 0.f, 100.f);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// SMOOTHING
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Interpolation speed for config transitions (arm length, rotation) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Smoothing")
|
||||||
|
float TransitionSpeed = 2.0f;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// LAG
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Enable camera lag for cinematic feel */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lag")
|
||||||
|
bool bEnableLag = true;
|
||||||
|
|
||||||
|
/** Lag interpolation speed (lower = slower/cinematic, higher = tighter) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lag", meta = (EditCondition = "bEnableLag", ClampMin = "0.1", ClampMax = "100.0"))
|
||||||
|
float LagSpeed = 5.0f;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// DEAD ZONE
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dead zone extents (cm) - camera only moves when player exits this box.
|
||||||
|
* X = Forward/Backward, Y = Depth (usually 0 in 2.5D), Z = Up/Down
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dead Zone")
|
||||||
|
FVector DeadZoneExtent = FVector(200.0f, 0.0f, 150.0f);
|
||||||
|
|
||||||
|
/** Offset dead zone center relative to player (e.g. see more ahead than behind) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dead Zone")
|
||||||
|
FVector DeadZoneOffset = FVector(0.0f, 0.0f, 50.0f);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// DEBUG
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Draw debug box showing dead zone boundaries */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug")
|
||||||
|
bool bDrawDebugBox = false;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts
|
||||||
|
|
||||||
|
import { Rotator } from '/Content/UE/Rotator.ts';
|
||||||
|
import type { Float } from '/Content/UE/Float.ts';
|
||||||
|
import { Vector } from '/Content/UE/Vector.ts';
|
||||||
|
|
||||||
|
export enum ETengriCameraBehavior {
|
||||||
|
FreeLook = 'Free Look (3D)',
|
||||||
|
SideScroller = 'Side Scroller (2.5D with DeadZone)',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TengriCameraConfig {
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// BEHAVIOR
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly BehaviorType: ETengriCameraBehavior =
|
||||||
|
ETengriCameraBehavior.FreeLook;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// TRANSFORM
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly FixedRotation: Rotator = new Rotator(-15.0, -90.0, 0.0);
|
||||||
|
public readonly TargetArmLength: Float = 1000.0;
|
||||||
|
public readonly SocketOffset: Vector = new Vector(0.0, 0.0, 100.0);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// SMOOTHING
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly TransitionSpeed: Float = 2.0;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// LAG
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly bEnableLag: boolean = true;
|
||||||
|
public readonly LagSpeed: Float = 5.0;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// DEAD ZONE
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly DeadZoneExtent: Vector = new Vector(200.0, 0.0, 150.0);
|
||||||
|
public readonly DeadZoneOffset: Vector = new Vector(0.0, 0.0, 50.0);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// DEBUG
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public readonly bDrawDebugBox: boolean = false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Request Games © All rights reserved
|
||||||
|
|
||||||
|
// Source/TengriPlatformer/Camera/TengriCameraComponent.h
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Core/TengriCameraConfig.h"
|
||||||
|
#include "TengriCameraComponent.generated.h"
|
||||||
|
|
||||||
|
class USpringArmComponent;
|
||||||
|
class UCameraComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic camera system with multiple behavior modes and smooth transitions.
|
||||||
|
* Supports free look (3D) and side-scroller (2.5D) with configurable dead zones.
|
||||||
|
* Updates after physics to prevent visual jitter during interpolation.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
||||||
|
class TENGRIPLATFORMER_API UTengriCameraComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UTengriCameraComponent();
|
||||||
|
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||||
|
FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize camera system with spring arm and camera references.
|
||||||
|
* Must be called before TickComponent runs.
|
||||||
|
* @param InSpringArm - Spring arm component for camera positioning
|
||||||
|
* @param InCamera - Camera component for rendering
|
||||||
|
*/
|
||||||
|
void InitializeCamera(USpringArmComponent* InSpringArm, UCameraComponent* InCamera);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a new camera configuration (e.g. from camera volume trigger).
|
||||||
|
* Smoothly transitions between configs using TransitionSpeed.
|
||||||
|
* @param NewConfig - New camera configuration to apply
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Tengri Camera")
|
||||||
|
void SetCameraConfig(UTengriCameraConfig* NewConfig);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// CONFIGURATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Default camera configuration applied at BeginPlay */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Config")
|
||||||
|
TObjectPtr<UTengriCameraConfig> DefaultConfig;
|
||||||
|
|
||||||
|
/** Currently active camera configuration */
|
||||||
|
UPROPERTY(Transient, BlueprintReadOnly, Category = "Config")
|
||||||
|
TObjectPtr<UTengriCameraConfig> CurrentConfig;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// COMPONENT REFERENCES
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<USpringArmComponent> SpringArm;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UCameraComponent> Camera;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// STATE
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Current smoothed camera focus point in world space */
|
||||||
|
FVector CurrentFocusLocation = FVector::ZeroVector;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// INTERNAL HELPERS
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate target focus position with dead zone clamping.
|
||||||
|
* Returns input position if player is inside dead zone,
|
||||||
|
* otherwise returns edge of dead zone closest to player.
|
||||||
|
* @param PlayerLocation - Current player position
|
||||||
|
* @return Target camera focus position
|
||||||
|
*/
|
||||||
|
FVector CalculateDeadZoneTarget(const FVector& PlayerLocation) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw debug visualization of dead zone box.
|
||||||
|
* Only enabled in editor builds when bDrawDebugBox is true.
|
||||||
|
* @param Center - Dead zone center point
|
||||||
|
* @param Extent - Dead zone half-extents
|
||||||
|
*/
|
||||||
|
void DrawDebugDeadZone(const FVector& Center, const FVector& Extent) const;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Source/TengriPlatformer/Camera/TengriCameraComponent.ts
|
||||||
|
|
||||||
|
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
|
||||||
|
import { TengriCameraConfig } from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
|
||||||
|
|
||||||
|
export class TengriCameraComponent extends ActorComponent {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.CurrentConfig = new TengriCameraConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetCameraConfig(NewConfig: TengriCameraConfig): void {
|
||||||
|
console.log(NewConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrentConfig: TengriCameraConfig;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue