diff --git a/Content/Camera/DA_CameraAiming.ts b/Content/Camera/DA_CameraAiming.ts new file mode 100644 index 0000000..a5ec59e --- /dev/null +++ b/Content/Camera/DA_CameraAiming.ts @@ -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; +} diff --git a/Content/Camera/DA_CameraAiming.uasset b/Content/Camera/DA_CameraAiming.uasset new file mode 100644 index 0000000..26f856c --- /dev/null +++ b/Content/Camera/DA_CameraAiming.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe737079a9dd451242b2b3af0ffe5f7012f0ec2f2f59af87a7665d6c045edfd4 +size 1451 diff --git a/Content/Camera/DA_CameraDefault.ts b/Content/Camera/DA_CameraDefault.ts new file mode 100644 index 0000000..7e723c0 --- /dev/null +++ b/Content/Camera/DA_CameraDefault.ts @@ -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; +} diff --git a/Content/Camera/DA_CameraDefault.uasset b/Content/Camera/DA_CameraDefault.uasset new file mode 100644 index 0000000..e606096 --- /dev/null +++ b/Content/Camera/DA_CameraDefault.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:194bbfec8cba7c8c1faea40e5b83c831524e5f2a452c02e15da23160b1c91d91 +size 1456 diff --git a/Content/Camera/DA_CameraScroller.ts b/Content/Camera/DA_CameraScroller.ts new file mode 100644 index 0000000..a389ca4 --- /dev/null +++ b/Content/Camera/DA_CameraScroller.ts @@ -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; +} diff --git a/Content/Camera/DA_CameraSideScroller.uasset b/Content/Camera/DA_CameraSideScroller.uasset new file mode 100644 index 0000000..6abb639 --- /dev/null +++ b/Content/Camera/DA_CameraSideScroller.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da53aee10682271458da492438bae9d24cafd74fcc82bc9c5897935c8f050bff +size 1788 diff --git a/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.cpp b/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.cpp new file mode 100644 index 0000000..87a3854 --- /dev/null +++ b/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.cpp @@ -0,0 +1,5 @@ +// Request Games © All rights reserved + +// Source/TengriPlatformer/Camera/Core/TengriCameraConfig.cpp + +#include "TengriCameraConfig.h" \ No newline at end of file diff --git a/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.h b/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.h new file mode 100644 index 0000000..0705fa9 --- /dev/null +++ b/Source/TengriPlatformer/Camera/Core/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; +}; \ No newline at end of file diff --git a/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts b/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts new file mode 100644 index 0000000..9d3ccc1 --- /dev/null +++ b/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts @@ -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; +} diff --git a/Source/TengriPlatformer/Camera/TengriCameraComponent.cpp b/Source/TengriPlatformer/Camera/TengriCameraComponent.cpp new file mode 100644 index 0000000..cee15f9 --- /dev/null +++ b/Source/TengriPlatformer/Camera/TengriCameraComponent.cpp @@ -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()) + { + 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 + ); +} \ No newline at end of file diff --git a/Source/TengriPlatformer/Camera/TengriCameraComponent.h b/Source/TengriPlatformer/Camera/TengriCameraComponent.h new file mode 100644 index 0000000..7dcda95 --- /dev/null +++ b/Source/TengriPlatformer/Camera/TengriCameraComponent.h @@ -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 DefaultConfig; + + /** Currently active camera configuration */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "Config") + TObjectPtr CurrentConfig; + +private: + // ════════════════════════════════════════════════════════════════════ + // COMPONENT REFERENCES + // ════════════════════════════════════════════════════════════════════ + + UPROPERTY() + TObjectPtr SpringArm; + + UPROPERTY() + TObjectPtr 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; +}; \ No newline at end of file diff --git a/Source/TengriPlatformer/Camera/TengriCameraComponent.ts b/Source/TengriPlatformer/Camera/TengriCameraComponent.ts new file mode 100644 index 0000000..96eddfb --- /dev/null +++ b/Source/TengriPlatformer/Camera/TengriCameraComponent.ts @@ -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; +}