feat(character): integrate camera system with aiming mode

Character (C++):
- Add camera components to character (SpringArm, Camera, CameraManager)
  * Initialize camera manager in BeginPlay with component references
  * SpringArm attached to capsule with 60cm vertical offset
  * Camera attached to SpringArm socket
- Add AimingCameraConfig for over-the-shoulder aiming view
  * Switch to aiming config when RMB/L2 pressed
  * Return to default config when aim button released
- Improve OnThrowInput code clarity
  * Simplify controller validation flow
  * Clean up trajectory calculation comments
- Add forward declarations for camera classes
- Update class documentation to mention camera integration

Blueprint:
- Remove legacy camera component (AC_Camera)
  * Camera rotation now handled by C++ TengriCameraComponent
  * SpringArm interpolation managed by camera config system
- Update look input to use native controller input
  * AddControllerYawInput/PitchInput for FreeLook mode
  * Skip input in side-scroller mode (camera is fixed)
- Remove camera-related variables (moved to C++ config)
- Simplify EventTick (camera logic now in C++ component)
- Pass DA_CameraAiming config to character constructor

Camera now seamlessly transitions between default and aiming modes,
working in tandem with strafe movement for precise targeting.
main
Nikolay Petrov 2026-01-07 00:38:43 +05:00
parent 74996e5e4b
commit d89e3fb3b3
9 changed files with 180 additions and 525 deletions

View File

@ -1,6 +1,5 @@
// Content/Blueprints/BP_MainCharacter.ts // Content/Blueprints/BP_MainCharacter.ts
import { AC_Camera } from '/Content/Camera/AC_Camera.ts';
import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts'; import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts'; import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { IMC_Default } from '/Content/Input/IMC_Default.ts'; import { IMC_Default } from '/Content/Input/IMC_Default.ts';
@ -11,11 +10,8 @@ import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLoca
import type { Float } from '/Content/UE/Float.ts'; import type { Float } from '/Content/UE/Float.ts';
import { MathLibrary } from '/Content/UE/MathLibrary.ts'; import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import type { PlayerController } from '/Content/UE/PlayerController.ts'; import type { PlayerController } from '/Content/UE/PlayerController.ts';
import { Rotator } from '/Content/UE/Rotator.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { Vector } from '/Content/UE/Vector.ts'; import { Vector } from '/Content/UE/Vector.ts';
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts';
import { TengriCharacter } from '/Source/TengriPlatformer/Character/TengriCharacter.ts'; import { TengriCharacter } from '/Source/TengriPlatformer/Character/TengriCharacter.ts';
import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts'; import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts';
import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts'; import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts';
@ -23,9 +19,9 @@ import { IA_Aim } from '/Content/Input/Actions/IA_Aim.ts';
import { IMC_ItemHeld } from '/Content/Input/IMC_ItemHeld.ts'; import { IMC_ItemHeld } from '/Content/Input/IMC_ItemHeld.ts';
import { CreateWidget } from '/Content/UE/CteateWidget.ts'; import { CreateWidget } from '/Content/UE/CteateWidget.ts';
import { WBP_HUD } from '/Content/UI/WBP_HUD.ts'; import { WBP_HUD } from '/Content/UI/WBP_HUD.ts';
import { SpringArmComponent } from '/Content/UE/SpringArmComponent.ts';
import { LinearColor } from '/Content/UE/LinearColor.ts';
import { CustomDefaultSkeletalMesh } from '/Content/BasicShapes/CustomDefaultSkeletalMesh.ts'; import { CustomDefaultSkeletalMesh } from '/Content/BasicShapes/CustomDefaultSkeletalMesh.ts';
import { ETengriCameraBehavior } from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
import { DA_CameraAiming } from '/Content/Camera/DA_CameraAiming.ts';
/** /**
* Main Character Blueprint * Main Character Blueprint
@ -90,17 +86,13 @@ export class BP_MainCharacter extends TengriCharacter {
actionValueX: Float, actionValueX: Float,
actionValueY: Float actionValueY: Float
): void { ): void {
this.CameraComponent.ProcessLookInput( if (
new Vector(actionValueX, actionValueY, 0), this.CameraManager.CurrentConfig.BehaviorType ===
this.DeltaTime ETengriCameraBehavior.FreeLook
); ) {
this.AddControllerYawInput(actionValueX);
this.AddControllerPitchInput(actionValueY);
} }
/**
* Reset look input when look action is completed
*/
EnhancedInputActionLookCompleted(): void {
this.CameraComponent.ProcessLookInput(new Vector(0, 0, 0), this.DeltaTime);
} }
/** /**
@ -132,7 +124,7 @@ export class BP_MainCharacter extends TengriCharacter {
return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y, 0); return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y, 0);
}; };
this.TengriMovement.SetInputVector( this.MovementComponent.SetInputVector(
CalculateResultMovementInputVector( CalculateResultMovementInputVector(
MathLibrary.GetRightVector( MathLibrary.GetRightVector(
this.GetControlRotation().roll, this.GetControlRotation().roll,
@ -150,7 +142,7 @@ export class BP_MainCharacter extends TengriCharacter {
* Reset movement input when move action is completed * Reset movement input when move action is completed
*/ */
EnhancedInputActionMoveCompleted(): void { EnhancedInputActionMoveCompleted(): void {
this.TengriMovement.SetInputVector(new Vector(0, 0, 0)); this.MovementComponent.SetInputVector(new Vector(0, 0, 0));
} }
EnhancedInputActionJumpTriggered(): void { EnhancedInputActionJumpTriggered(): void {
@ -161,24 +153,6 @@ export class BP_MainCharacter extends TengriCharacter {
this.MovementComponent.SetJumpInput(false); this.MovementComponent.SetJumpInput(false);
} }
MovementComponentOnLanded(IsHeavy: boolean): void {
if (IsHeavy) {
SystemLibrary.PrintString(
'Boom! (Heavy)',
true,
true,
new LinearColor(1, 0, 0, 1)
);
} else {
SystemLibrary.PrintString(
'Tap (Light)',
true,
true,
new LinearColor(0, 1, 0, 1)
);
}
}
/** /**
* Initialize all systems when character spawns * Initialize all systems when character spawns
* Order: Toast Debug Movement (movement last as it may generate debug output) * Order: Toast Debug Movement (movement last as it may generate debug output)
@ -200,11 +174,6 @@ export class BP_MainCharacter extends TengriCharacter {
); );
} }
this.CameraComponent.InitializeCameraSystem(
this.InputDeviceComponent,
this.DebugHUDComponent
);
CreateWidget(WBP_HUD).AddToViewport(); CreateWidget(WBP_HUD).AddToViewport();
} }
@ -212,60 +181,21 @@ export class BP_MainCharacter extends TengriCharacter {
* Update all systems each frame * Update all systems each frame
* Called by Unreal Engine game loop * Called by Unreal Engine game loop
*/ */
EventTick(DeltaTime: Float): void { EventTick(): void {
this.DeltaTime = DeltaTime;
if (this.ShowDebugInfo) { if (this.ShowDebugInfo) {
this.DebugHUDComponent.UpdateHUD(SystemLibrary.GetGameTimeInSeconds()); this.DebugHUDComponent.UpdateHUD(SystemLibrary.GetGameTimeInSeconds());
this.ToastSystemComponent.UpdateToastSystem(); this.ToastSystemComponent.UpdateToastSystem();
} }
this.CameraComponent.UpdateCameraRotation(DeltaTime);
this.GetController().SetControlRotation(
new Rotator(
0,
this.CameraComponent.GetCameraRotation().Pitch,
this.CameraComponent.GetCameraRotation().Yaw
)
);
if (this.ShowDebugInfo) { if (this.ShowDebugInfo) {
this.InputDeviceComponent.UpdateDebugPage(); this.InputDeviceComponent.UpdateDebugPage();
this.CameraComponent.UpdateDebugPage();
} }
this.SpringArm.TargetArmLength = MathLibrary.FInterpTo(
this.SpringArm.TargetArmLength,
this.bIsAiming ? this.AimArmLength : this.DefaultArmLength,
DeltaTime,
10.0
);
this.SpringArm.TargetOffset = MathLibrary.VInterpTo(
this.SpringArm.TargetOffset,
this.bIsAiming ? this.AimSocketOffset : this.DefaultSocketOffset,
DeltaTime,
10.0
);
} }
// ════════════════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES // VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════════════════
/**
* Camera system component - handles camera rotation and sensitivity
* @category Components
*/
SpringArm = new SpringArmComponent();
/**
* Camera system component - handles camera rotation and sensitivity
* @category Components
*/
CameraComponent = new AC_Camera();
/** /**
* Input device detection component - manages input device state and detection * Input device detection component - manages input device state and detection
* @category Components * @category Components
@ -278,10 +208,6 @@ export class BP_MainCharacter extends TengriCharacter {
*/ */
ToastSystemComponent = new AC_ToastSystem(); ToastSystemComponent = new AC_ToastSystem();
TengriMovement = new TengriMovementComponent({
MovementConfig: DA_TengriMovementConfig,
});
/** /**
* Debug HUD system - displays movement parameters and performance metrics * Debug HUD system - displays movement parameters and performance metrics
* @category Components * @category Components
@ -295,37 +221,8 @@ export class BP_MainCharacter extends TengriCharacter {
*/ */
private ShowDebugInfo: boolean = true; private ShowDebugInfo: boolean = true;
/**
* @category Camera
* @instanceEditable true
*/
private readonly DefaultArmLength: Float = 400.0;
/**
* @category Camera
* @instanceEditable true
*/
private readonly AimArmLength: Float = 250.0;
/**
* @category Camera
* @instanceEditable true
*/
private readonly DefaultSocketOffset: Vector = new Vector(0.0, 0.0, 0.0);
/**
* @category Camera
* @instanceEditable true
*/
private readonly AimSocketOffset: Vector = new Vector(0.0, 100.0, 60.0);
/**
* Cached delta time from last tick - used for time-based calculations
*/
private DeltaTime: Float = 0.0;
constructor() { constructor() {
super(); super(new DA_CameraAiming());
this.InteractAction = IA_Interact; this.InteractAction = IA_Interact;
this.ThrowAction = IA_Throw; this.ThrowAction = IA_Throw;

BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,323 +0,0 @@
// Content/Camera/Components/AC_Camera.ts
import type { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts';
import type { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts';
import { ActorComponent } from '/Content/UE/ActorComponent.ts';
import type { Float } from '/Content/UE/Float.ts';
import { MathLibrary } from '/Content/UE/MathLibrary.ts';
import { SystemLibrary } from '/Content/UE/SystemLibrary.ts';
import { Vector } from '/Content/UE/Vector.ts';
/**
* Camera System Component
* Deterministic camera control with smooth rotation and device-aware sensitivity
* Provides precise control over camera behavior for consistent experience
*/
export class AC_Camera extends ActorComponent {
// ════════════════════════════════════════════════════════════════════════════════════════
// FUNCTIONS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Process look input and update camera rotation
* @param InputDelta - Input delta (X = Yaw, Y = Pitch)
* @param DeltaTime - Time since last frame
* @category Input Processing
*/
public ProcessLookInput(InputDelta: Vector, DeltaTime: Float): void {
if (this.IsInitialized) {
const invertMultiplier = this.InvertYAxis ? -1.0 : 1.0;
let sensitivity: Float = 0;
if (SystemLibrary.IsValid(this.InputDeviceComponent)) {
sensitivity = this.InputDeviceComponent.IsGamepad()
? this.GamepadSensitivity
: this.MouseSensitivity;
} else {
sensitivity = this.MouseSensitivity;
}
const CalculateTargetPitch = (
targetPitch: Float,
inputDeltaY: Float,
deltaTime: Float
): Float =>
targetPitch - inputDeltaY * sensitivity * invertMultiplier * deltaTime;
const CalculateTargetYaw = (
targetYaw: Float,
inputDeltaX: Float,
deltaTime: Float
): Float => targetYaw + inputDeltaX * sensitivity * deltaTime;
this.TargetPitch = MathLibrary.ClampFloat(
CalculateTargetPitch(this.TargetPitch, InputDelta.Y, DeltaTime),
this.PitchMin,
this.PitchMax
);
this.TargetYaw = CalculateTargetYaw(
this.TargetYaw,
InputDelta.X,
DeltaTime
);
this.InputMagnitude = MathLibrary.VectorLength(InputDelta);
}
}
/**
* Update camera rotation with smooth interpolation
* @param DeltaTime - Time since last frame
* @category Camera Updates
*/
public UpdateCameraRotation(DeltaTime: Float): void {
if (this.IsInitialized) {
if (this.SmoothingSpeed > 0) {
// Smooth interpolation to target rotation
this.CurrentPitch = MathLibrary.FInterpTo(
this.CurrentPitch,
this.TargetPitch,
DeltaTime,
this.SmoothingSpeed
);
this.CurrentYaw = MathLibrary.FInterpTo(
this.CurrentYaw,
this.TargetYaw,
DeltaTime,
this.SmoothingSpeed
);
} else {
// Instant rotation (no smoothing)
this.CurrentPitch = this.TargetPitch;
this.CurrentYaw = this.TargetYaw;
}
}
}
/**
* Get current camera rotation for applying to SpringArm
* @returns Current camera rotation values
* @category Public Interface
* @pure true
*/
public GetCameraRotation(): { Pitch: Float; Yaw: Float } {
return {
Pitch: this.CurrentPitch,
Yaw: this.CurrentYaw,
};
}
/**
* Check if camera is currently rotating
* @returns True if there's active rotation input
* @category State Queries
* @pure true
*/
public IsCameraRotating(): boolean {
return this.InputMagnitude > 0.01;
}
/**
* Initialize camera system with default settings
* @category System Setup
*/
public InitializeCameraSystem(
InputDeviceRef: AC_InputDevice,
DebugComponentRef: AC_DebugHUD
): void {
this.InputDeviceComponent = InputDeviceRef;
this.DebugHUDComponent = DebugComponentRef;
this.IsInitialized = true;
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
this.DebugHUDComponent.AddDebugPage(
this.DebugPageID,
'Camera System',
60
);
}
}
/**
* Update debug HUD with current camera info
* @category Debug
*/
public UpdateDebugPage(): void {
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
if (
this.DebugHUDComponent.ShouldUpdatePage(
this.DebugPageID,
SystemLibrary.GetGameTimeInSeconds()
)
) {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
`Current Device: ${SystemLibrary.IsValid(this.InputDeviceComponent) ? this.InputDeviceComponent.GetCurrentInputDevice() : 'Input Device Component Not Found'}\n` +
`Sensitivity: ${SystemLibrary.IsValid(this.InputDeviceComponent) && this.InputDeviceComponent.IsGamepad() ? this.GamepadSensitivity : this.MouseSensitivity}\n` +
`Pitch: ${this.GetCameraRotation().Pitch}°\n` +
`Yaw: ${this.GetCameraRotation().Yaw}°\n` +
`Is Rotating: ${this.IsCameraRotating() ? 'Yes' : 'No'}\n` +
`Smoothing: ${this.SmoothingSpeed}\n` +
`Invert Y: ${this.InvertYAxis ? 'Yes' : 'No'}`
);
}
}
}
/**
* Get camera configuration and state data for testing purposes
* Provides read-only access to private variables for automated tests
* Only includes essential data needed for test validation
* @category Debug
* @returns Object containing camera settings (sensitivity, pitch limits) for test assertions
*/
public GetTestData(): {
MouseSensitivity: Float;
GamepadSensitivity: Float;
PitchMin: Float;
PitchMax: Float;
} {
return {
MouseSensitivity: this.MouseSensitivity,
GamepadSensitivity: this.GamepadSensitivity,
PitchMin: this.PitchMin,
PitchMax: this.PitchMax,
};
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Mouse sensitivity multiplier for camera rotation
* Higher values result in faster camera movement with mouse input
* Typical range: 50.0 (slow) - 200.0 (fast)
* @category Camera Config
* @instanceEditable true
* @default 100.0
*/
private readonly MouseSensitivity: Float = 100.0;
/**
* Gamepad sensitivity multiplier for camera rotation
* Higher than mouse sensitivity to compensate for analog stick precision
* Typical range: 100.0 (slow) - 300.0 (fast)
* @category Camera Config
* @instanceEditable true
* @default 150.0
*/
private readonly GamepadSensitivity: Float = 150.0;
/**
* Invert vertical axis for camera rotation
* When true, pushing up on input rotates camera down and vice versa
* Common preference for flight-sim style controls
* @category Camera Config
* @instanceEditable true
* @default false
*/
private readonly InvertYAxis: boolean = false;
/**
* Minimum pitch angle in degrees (looking down)
* Prevents camera from rotating beyond this angle
* Set to -89° to avoid gimbal lock at -90°
* @category Camera Config
* @instanceEditable true
* @default -89.0
*/
private readonly PitchMin: Float = -89.0;
/**
* Maximum pitch angle in degrees (looking up)
* Prevents camera from rotating beyond this angle
* Set to +89° to avoid gimbal lock at +90°
* @category Camera Config
* @instanceEditable true
* @default 89.0
*/
private readonly PitchMax: Float = 89.0;
/**
* Speed of smooth interpolation to target rotation
* Higher values make camera more responsive but less smooth
* Set to 0 for instant rotation without interpolation
* Typical range: 10.0 (smooth) - 30.0 (responsive)
* @category Camera Config
* @instanceEditable true
* @default 20.0
*/
private readonly SmoothingSpeed: Float = 20.0;
/**
* Current pitch angle for rendering
* Smoothly interpolates towards TargetPitch based on SmoothingSpeed
* Updated every frame by UpdateCameraRotation()
* @category Camera State
*/
private CurrentPitch: Float = 0;
/**
* Current yaw angle for rendering
* Smoothly interpolates towards TargetYaw based on SmoothingSpeed
* Updated every frame by UpdateCameraRotation()
* @category Camera State
*/
private CurrentYaw: Float = 0;
/**
* Target pitch angle from player input
* Updated by ProcessLookInput() based on input delta
* Clamped to PitchMin/PitchMax range
* @category Camera State
*/
private TargetPitch: Float = 0;
/**
* Target yaw angle from player input
* Updated by ProcessLookInput() based on input delta
* No clamping - can rotate freely beyond 360°
* @category Camera State
*/
private TargetYaw: Float = 0;
/**
* Magnitude of current input vector
* Used by IsCameraRotating() to detect active camera input
* Cached to avoid recalculating VectorLength every frame
* @category Camera State
* @default 0.0
*/
private InputMagnitude: Float = 0;
/**
* System initialization state flag
* @category Camera State
*/
private IsInitialized: boolean = false;
/**
* Reference to input device component for device detection
* Set externally, used for sensitivity adjustments
* @category Components
*/
public InputDeviceComponent: AC_InputDevice | null = null;
/**
* Reference to debug HUD component for displaying camera info
* Optional, used for debugging purposes
* @category Components
*/
public DebugHUDComponent: AC_DebugHUD | null = null;
/**
* Debug page identifier for organizing debug output
* Used by debug HUD to categorize information
* @category Debug
*/
public readonly DebugPageID: string = 'CameraInfo';
}

BIN
Content/Camera/AC_Camera.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -0,0 +1,10 @@
// Content/UE/CameraComponent.ts
import { SceneComponent } from '/Content/UE/SceneComponent.ts';
import type { UEObject } from '/Content/UE/UEObject.ts';
export class CameraComponent extends SceneComponent {
constructor(outer: UEObject | null = null, name: string = 'None') {
super(outer, name);
}
}

View File

@ -5,6 +5,7 @@ import { Controller } from '/Content/UE/Controller.ts';
import { Name } from '/Content/UE/Name.ts'; import { Name } from '/Content/UE/Name.ts';
import { Rotator } from '/Content/UE/Rotator.ts'; import { Rotator } from '/Content/UE/Rotator.ts';
import { UEObject } from '/Content/UE/UEObject.ts'; import { UEObject } from '/Content/UE/UEObject.ts';
import type { Float } from '/Content/UE/Float.ts';
export class Pawn extends Actor { export class Pawn extends Actor {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) { constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
@ -18,4 +19,12 @@ export class Pawn extends Actor {
public GetControlRotation(): Rotator { public GetControlRotation(): Rotator {
return new Rotator(); return new Rotator();
} }
public AddControllerYawInput(Val: Float): void {
console.log(Val);
}
public AddControllerPitchInput(Val: Float): void {
console.log(Val);
}
} }

View File

@ -6,7 +6,10 @@
#include "Components/CapsuleComponent.h" #include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h" #include "Components/SkeletalMeshComponent.h"
#include "Components/ArrowComponent.h" #include "Components/ArrowComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "TengriPlatformer/Movement/TengriMovementComponent.h" #include "TengriPlatformer/Movement/TengriMovementComponent.h"
#include "TengriPlatformer/Camera/TengriCameraComponent.h"
#include "TengriPlatformer/World/TengriPickupActor.h" #include "TengriPlatformer/World/TengriPickupActor.h"
#include "Kismet/KismetSystemLibrary.h" #include "Kismet/KismetSystemLibrary.h"
@ -54,11 +57,29 @@ ATengriCharacter::ATengriCharacter()
// Setup custom movement component // Setup custom movement component
MovementComponent = CreateDefaultSubobject<UTengriMovementComponent>(TEXT("TengriMovement")); MovementComponent = CreateDefaultSubobject<UTengriMovementComponent>(TEXT("TengriMovement"));
// Setup camera system
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(CapsuleComponent);
SpringArmComp->bUsePawnControlRotation = true;
SpringArmComp->SetRelativeLocation(FVector(0.f, 0.f, 60.f));
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
CameraComp->bUsePawnControlRotation = false;
CameraManager = CreateDefaultSubobject<UTengriCameraComponent>(TEXT("CameraManager"));
} }
void ATengriCharacter::BeginPlay() void ATengriCharacter::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
// Initialize camera manager with component references
if (CameraManager)
{
CameraManager->InitializeCamera(SpringArmComp, CameraComp);
}
} }
// ============================================================================ // ============================================================================
@ -188,27 +209,20 @@ void ATengriCharacter::OnThrowInput()
// Default to forward direction if raycasting fails // Default to forward direction if raycasting fails
FVector ThrowDirection = GetActorForwardVector(); FVector ThrowDirection = GetActorForwardVector();
// ИСПРАВЛЕНИЕ: Переименовали Controller -> PC, чтобы избежать конфликта имен
AController* PC = GetController(); AController* PC = GetController();
if (UWorld* World = GetWorld(); !PC || !World) if (UWorld* World = GetWorld(); !PC || !World)
{ {
UE_LOG(LogTengriCharacter, Warning, UE_LOG(LogTengriCharacter, Warning,
TEXT("OnThrowInput: Invalid controller or world context")); TEXT("OnThrowInput: Invalid controller or world context"));
// Continue with fallback direction
} }
else else
{ {
// ───────────────────────────────────────────────────────────────── // Calculate precise throw trajectory using camera raycast
// PRECISE THROW TRAJECTORY CALCULATION
// ─────────────────────────────────────────────────────────────────
// Get camera position and rotation
FVector CameraLoc; FVector CameraLoc;
FRotator CameraRot; FRotator CameraRot;
PC->GetPlayerViewPoint(CameraLoc, CameraRot); // Используем PC вместо Controller PC->GetPlayerViewPoint(CameraLoc, CameraRot);
// Raycast from camera forward
const FVector TraceStart = CameraLoc; const FVector TraceStart = CameraLoc;
const FVector TraceEnd = CameraLoc + (CameraRot.Vector() * ThrowTraceDistance); const FVector TraceEnd = CameraLoc + (CameraRot.Vector() * ThrowTraceDistance);
@ -217,7 +231,7 @@ void ATengriCharacter::OnThrowInput()
QueryParams.AddIgnoredActor(this); QueryParams.AddIgnoredActor(this);
QueryParams.AddIgnoredActor(HeldItem); QueryParams.AddIgnoredActor(HeldItem);
// Find world target point (wall/enemy/floor where camera is looking) // Find world target point where camera is looking
const bool bHit = World->LineTraceSingleByChannel( const bool bHit = World->LineTraceSingleByChannel(
Hit, Hit,
TraceStart, TraceStart,
@ -226,10 +240,9 @@ void ATengriCharacter::OnThrowInput()
QueryParams QueryParams
); );
// Use hit point if found, otherwise use far endpoint
const FVector TargetPoint = bHit ? Hit.ImpactPoint : TraceEnd; const FVector TargetPoint = bHit ? Hit.ImpactPoint : TraceEnd;
// Calculate throw direction FROM item TO target // Calculate throw direction from item to target
const FVector HandLocation = HeldItem->GetActorLocation(); const FVector HandLocation = HeldItem->GetActorLocation();
ThrowDirection = (TargetPoint - HandLocation).GetSafeNormal(); ThrowDirection = (TargetPoint - HandLocation).GetSafeNormal();
} }
@ -258,16 +271,13 @@ void ATengriCharacter::OnThrowInput()
UE_LOG(LogTengriCharacter, Verbose, UE_LOG(LogTengriCharacter, Verbose,
TEXT("Threw item with force: %.1f cm/s"), ThrowForce); TEXT("Threw item with force: %.1f cm/s"), ThrowForce);
// Note: bIsAiming state will be cleared by OnAimInput when player releases RMB/L2
} }
void ATengriCharacter::OnAimInput(const FInputActionValue& Value) void ATengriCharacter::OnAimInput(const FInputActionValue& Value)
{ {
// Extract boolean state (true = button pressed, false = released)
const bool bIsPressed = Value.Get<bool>(); const bool bIsPressed = Value.Get<bool>();
// Skip if state hasn't changed (optimization) // Skip if state hasn't changed
if (bIsPressed == bIsAiming) if (bIsPressed == bIsAiming)
{ {
return; return;
@ -279,9 +289,24 @@ void ATengriCharacter::OnAimInput(const FInputActionValue& Value)
if (MovementComponent) if (MovementComponent)
{ {
MovementComponent->SetStrafing(bIsAiming); MovementComponent->SetStrafing(bIsAiming);
}
// Switch camera configuration for aiming
if (CameraManager)
{
if (bIsAiming && AimingCameraConfig)
{
CameraManager->SetCameraConfig(AimingCameraConfig);
}
else
{
// Return to default config (nullptr triggers fallback)
CameraManager->SetCameraConfig(nullptr);
}
}
UE_LOG(LogTengriCharacter, Verbose, UE_LOG(LogTengriCharacter, Verbose,
TEXT("Aim mode: %s"), bIsAiming ? TEXT("ON") : TEXT("OFF")); TEXT("Aim mode: %s"), bIsAiming ? TEXT("ON") : TEXT("OFF"));
}
} }
// ============================================================================ // ============================================================================
@ -290,7 +315,7 @@ void ATengriCharacter::OnAimInput(const FInputActionValue& Value)
ATengriPickupActor* ATengriCharacter::FindNearestPickup() const ATengriPickupActor* ATengriCharacter::FindNearestPickup() const
{ {
UWorld* World = GetWorld(); const UWorld* World = GetWorld();
if (!World) if (!World)
{ {
return nullptr; return nullptr;
@ -331,8 +356,8 @@ ATengriPickupActor* ATengriCharacter::FindNearestPickup() const
continue; continue;
} }
const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation()); if (const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation());
if (DistSq < MinDistSq) DistSq < MinDistSq)
{ {
MinDistSq = DistSq; MinDistSq = DistSq;
NearestItem = Item; NearestItem = Item;

View File

@ -12,15 +12,20 @@
// Forward declarations // Forward declarations
class UCapsuleComponent; class UCapsuleComponent;
class USkeletalMeshComponent; class USkeletalMeshComponent;
class UTengriMovementComponent;
class ATengriPickupActor;
class UArrowComponent; class UArrowComponent;
class USpringArmComponent;
class UCameraComponent;
class UTengriMovementComponent;
class UTengriCameraComponent;
class UTengriCameraConfig;
class ATengriPickupActor;
class UInputMappingContext; class UInputMappingContext;
class UInputAction; class UInputAction;
/** /**
* Main player character class with item interaction and throwing mechanics. * Main player character class with item interaction and throwing mechanics.
* Supports dynamic input context switching for item-based actions. * Supports dynamic input context switching for item-based actions.
* Features integrated camera system with multiple behavior modes.
*/ */
UCLASS() UCLASS()
class TENGRIPLATFORMER_API ATengriCharacter : public APawn class TENGRIPLATFORMER_API ATengriCharacter : public APawn
@ -51,6 +56,23 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UTengriMovementComponent> MovementComponent; TObjectPtr<UTengriMovementComponent> MovementComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<USpringArmComponent> SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UCameraComponent> CameraComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UTengriCameraComponent> CameraManager;
// ========================================================================
// CAMERA CONFIGS
// ========================================================================
/** Camera configuration for aiming mode (over-the-shoulder view) */
UPROPERTY(EditDefaultsOnly, Category = "Camera|Configs")
TObjectPtr<UTengriCameraConfig> AimingCameraConfig;
// ======================================================================== // ========================================================================
// INPUT CONFIG // INPUT CONFIG
// ======================================================================== // ========================================================================
@ -94,7 +116,7 @@ public:
/** /**
* Handle aim input state changes (RMB/L2). * Handle aim input state changes (RMB/L2).
* Toggles strafe mode on MovementComponent for camera-aligned rotation. * Toggles strafe mode and switches camera configuration.
* @param Value - Input action value (true = pressed, false = released) * @param Value - Input action value (true = pressed, false = released)
*/ */
void OnAimInput(const FInputActionValue& Value); void OnAimInput(const FInputActionValue& Value);

View File

@ -7,15 +7,24 @@ import { ArrowComponent } from '/Content/UE/ArrowComponent.ts';
import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts'; import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts';
import { InputAction } from '/Content/UE/InputAction.ts'; import { InputAction } from '/Content/UE/InputAction.ts';
import { InputMappingContext } from '/Content/UE/InputMappingContext.ts'; import { InputMappingContext } from '/Content/UE/InputMappingContext.ts';
import { SpringArmComponent } from '/Content/UE/SpringArmComponent.ts';
import { CameraComponent } from '/Content/UE/CameraComponent.ts';
import { TengriCameraComponent } from '/Source/TengriPlatformer/Camera/TengriCameraComponent.ts';
import { TengriCameraConfig } from '/Source/TengriPlatformer/Camera/Core/TengriCameraConfig.ts';
export class TengriCharacter extends Pawn { export class TengriCharacter extends Pawn {
constructor() { constructor(AimingCameraConfig: TengriCameraConfig) {
super(); super();
this.CapsuleComponent = new CapsuleComponent(); this.CapsuleComponent = new CapsuleComponent();
this.Mesh = new SkeletalMesh(); this.Mesh = new SkeletalMesh();
this.ArrowComponent = new ArrowComponent(); this.ArrowComponent = new ArrowComponent();
this.MovementComponent = new TengriMovementComponent(); this.MovementComponent = new TengriMovementComponent();
this.SpringArmComponent = new SpringArmComponent();
this.CameraComponent = new CameraComponent();
this.CameraManager = new TengriCameraComponent();
this.AimingCameraConfig = AimingCameraConfig;
this.InteractAction = new InputAction(); this.InteractAction = new InputAction();
this.ThrowAction = new InputAction(); this.ThrowAction = new InputAction();
@ -31,6 +40,15 @@ export class TengriCharacter extends Pawn {
public Mesh: SkeletalMesh; public Mesh: SkeletalMesh;
public ArrowComponent: ArrowComponent; public ArrowComponent: ArrowComponent;
public MovementComponent: TengriMovementComponent; public MovementComponent: TengriMovementComponent;
public SpringArmComponent: SpringArmComponent;
public CameraComponent: CameraComponent;
public CameraManager: TengriCameraComponent;
// ========================================================================
// CAMERA CONFIGS
// ========================================================================
public AimingCameraConfig: TengriCameraConfig;
// ======================================================================== // ========================================================================
// INPUT CONFIG // INPUT CONFIG