343 lines
10 KiB
C++
343 lines
10 KiB
C++
// Request Games © All rights reserved
|
|
|
|
// Source/TengriPlatformer/Character/TengriCharacter.cpp
|
|
|
|
#include "TengriCharacter.h"
|
|
#include "Components/CapsuleComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "Components/ArrowComponent.h"
|
|
#include "TengriPlatformer/Movement/TengriMovementComponent.h"
|
|
#include "TengriPlatformer/World/TengriPickupActor.h"
|
|
#include "Kismet/KismetSystemLibrary.h"
|
|
|
|
// Enhanced Input
|
|
#include "EnhancedInputComponent.h"
|
|
#include "EnhancedInputSubsystems.h"
|
|
#include "InputMappingContext.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogTengriCharacter, Log, All);
|
|
|
|
// ============================================================================
|
|
// CONSTANTS
|
|
// ============================================================================
|
|
|
|
namespace TengriCharacter
|
|
{
|
|
// Camera rotation speed multiplier in strafe mode
|
|
constexpr float StrafeRotationSpeedMultiplier = 2.0f;
|
|
}
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION
|
|
// ============================================================================
|
|
|
|
ATengriCharacter::ATengriCharacter()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
|
|
// Setup collision capsule
|
|
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
|
|
CapsuleComponent->InitCapsuleSize(34.0f, 88.0f);
|
|
CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
|
|
RootComponent = CapsuleComponent;
|
|
|
|
// Setup debug arrow
|
|
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
|
|
ArrowComponent->SetupAttachment(CapsuleComponent);
|
|
ArrowComponent->SetRelativeLocation(FVector(0.f, 0.f, 50.f));
|
|
|
|
// Setup character mesh
|
|
Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
|
|
Mesh->SetupAttachment(CapsuleComponent);
|
|
Mesh->SetRelativeLocation(FVector(0.0f, 0.0f, -88.0f));
|
|
Mesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
|
|
|
// Setup custom movement component
|
|
MovementComponent = CreateDefaultSubobject<UTengriMovementComponent>(TEXT("TengriMovement"));
|
|
}
|
|
|
|
void ATengriCharacter::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
}
|
|
|
|
// ============================================================================
|
|
// INPUT SETUP
|
|
// ============================================================================
|
|
|
|
void ATengriCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
|
{
|
|
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
|
|
|
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
|
|
if (!EnhancedInput)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Error,
|
|
TEXT("SetupPlayerInputComponent: Enhanced Input not available"));
|
|
return;
|
|
}
|
|
|
|
// Bind Interact action (always active via IMC_Default)
|
|
if (InteractAction)
|
|
{
|
|
EnhancedInput->BindAction(InteractAction, ETriggerEvent::Started,
|
|
this, &ATengriCharacter::Interact);
|
|
}
|
|
|
|
// Bind Throw action (enabled via IMC_ItemHeld when item equipped)
|
|
if (ThrowAction)
|
|
{
|
|
EnhancedInput->BindAction(ThrowAction, ETriggerEvent::Started,
|
|
this, &ATengriCharacter::OnThrowInput);
|
|
}
|
|
|
|
// Bind Aim action (enabled via IMC_ItemHeld when item equipped)
|
|
if (AimAction)
|
|
{
|
|
EnhancedInput->BindAction(AimAction, ETriggerEvent::Started,
|
|
this, &ATengriCharacter::OnAimInput);
|
|
EnhancedInput->BindAction(AimAction, ETriggerEvent::Completed,
|
|
this, &ATengriCharacter::OnAimInput);
|
|
}
|
|
}
|
|
|
|
void ATengriCharacter::ToggleItemHeldContext(const bool bEnable) const
|
|
{
|
|
const APlayerController* PC = Cast<APlayerController>(GetController());
|
|
if (!PC)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Warning,
|
|
TEXT("ToggleItemHeldContext: No player controller"));
|
|
return;
|
|
}
|
|
|
|
UEnhancedInputLocalPlayerSubsystem* Subsystem =
|
|
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
|
|
|
|
if (!Subsystem)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Error,
|
|
TEXT("ToggleItemHeldContext: Enhanced Input subsystem not available"));
|
|
return;
|
|
}
|
|
|
|
if (!ItemHeldMappingContext)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Warning,
|
|
TEXT("ToggleItemHeldContext: ItemHeldMappingContext not assigned"));
|
|
return;
|
|
}
|
|
|
|
if (bEnable)
|
|
{
|
|
// Add item-equipped context (Priority 1 overrides default controls)
|
|
Subsystem->AddMappingContext(ItemHeldMappingContext, 1);
|
|
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context enabled"));
|
|
}
|
|
else
|
|
{
|
|
// Remove item context (revert to default movement controls)
|
|
Subsystem->RemoveMappingContext(ItemHeldMappingContext);
|
|
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context disabled"));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// INTERACTION LOGIC
|
|
// ============================================================================
|
|
|
|
void ATengriCharacter::Interact()
|
|
{
|
|
// SCENARIO 1: Holding item -> Drop gently
|
|
if (HeldItem)
|
|
{
|
|
const FVector GentleImpulse = GetActorForwardVector() * GentleDropSpeed;
|
|
|
|
// Apply gentle impulse (bVelChange = true ignores mass)
|
|
HeldItem->OnDropped(GentleImpulse, true);
|
|
HeldItem = nullptr;
|
|
|
|
// Disable combat mode controls
|
|
ToggleItemHeldContext(false);
|
|
return;
|
|
}
|
|
|
|
// SCENARIO 2: Empty hands -> Pick up nearest item
|
|
if (ATengriPickupActor* FoundItem = FindNearestPickup())
|
|
{
|
|
HeldItem = FoundItem;
|
|
HeldItem->OnPickedUp(Mesh, HandSocketName);
|
|
|
|
// Enable combat mode controls (Throw/Aim now active)
|
|
ToggleItemHeldContext(true);
|
|
|
|
UE_LOG(LogTengriCharacter, Verbose,
|
|
TEXT("Picked up item: %s"), *FoundItem->GetName());
|
|
}
|
|
}
|
|
|
|
void ATengriCharacter::OnThrowInput()
|
|
{
|
|
if (!HeldItem)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Warning,
|
|
TEXT("OnThrowInput: No item held"));
|
|
return;
|
|
}
|
|
|
|
// Default to forward direction if raycasting fails
|
|
FVector ThrowDirection = GetActorForwardVector();
|
|
|
|
// ИСПРАВЛЕНИЕ: Переименовали Controller -> PC, чтобы избежать конфликта имен
|
|
AController* PC = GetController();
|
|
|
|
if (UWorld* World = GetWorld(); !PC || !World)
|
|
{
|
|
UE_LOG(LogTengriCharacter, Warning,
|
|
TEXT("OnThrowInput: Invalid controller or world context"));
|
|
// Continue with fallback direction
|
|
}
|
|
else
|
|
{
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// PRECISE THROW TRAJECTORY CALCULATION
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
// Get camera position and rotation
|
|
FVector CameraLoc;
|
|
FRotator CameraRot;
|
|
PC->GetPlayerViewPoint(CameraLoc, CameraRot); // Используем PC вместо Controller
|
|
|
|
// Raycast from camera forward
|
|
const FVector TraceStart = CameraLoc;
|
|
const FVector TraceEnd = CameraLoc + (CameraRot.Vector() * ThrowTraceDistance);
|
|
|
|
FHitResult Hit;
|
|
FCollisionQueryParams QueryParams;
|
|
QueryParams.AddIgnoredActor(this);
|
|
QueryParams.AddIgnoredActor(HeldItem);
|
|
|
|
// Find world target point (wall/enemy/floor where camera is looking)
|
|
const bool bHit = World->LineTraceSingleByChannel(
|
|
Hit,
|
|
TraceStart,
|
|
TraceEnd,
|
|
ECC_Visibility,
|
|
QueryParams
|
|
);
|
|
|
|
// Use hit point if found, otherwise use far endpoint
|
|
const FVector TargetPoint = bHit ? Hit.ImpactPoint : TraceEnd;
|
|
|
|
// Calculate throw direction FROM item TO target
|
|
const FVector HandLocation = HeldItem->GetActorLocation();
|
|
ThrowDirection = (TargetPoint - HandLocation).GetSafeNormal();
|
|
}
|
|
|
|
// Add arc elevation for natural parabolic trajectory
|
|
ThrowDirection += FVector(0.0f, 0.0f, ThrowArcElevation);
|
|
ThrowDirection.Normalize();
|
|
|
|
// Rotate character instantly to face throw direction
|
|
FRotator CharacterFaceRot = ThrowDirection.Rotation();
|
|
CharacterFaceRot.Pitch = 0.0f;
|
|
CharacterFaceRot.Roll = 0.0f;
|
|
|
|
if (MovementComponent)
|
|
{
|
|
MovementComponent->ForceRotation(CharacterFaceRot);
|
|
}
|
|
|
|
// Execute throw with calculated trajectory
|
|
const FVector Impulse = ThrowDirection * ThrowForce;
|
|
HeldItem->OnDropped(Impulse, true);
|
|
HeldItem = nullptr;
|
|
|
|
// Disable combat controls
|
|
ToggleItemHeldContext(false);
|
|
|
|
UE_LOG(LogTengriCharacter, Verbose,
|
|
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)
|
|
{
|
|
// Extract boolean state (true = button pressed, false = released)
|
|
const bool bIsPressed = Value.Get<bool>();
|
|
|
|
// Skip if state hasn't changed (optimization)
|
|
if (bIsPressed == bIsAiming)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bIsAiming = bIsPressed;
|
|
|
|
// Sync strafe mode with movement component
|
|
if (MovementComponent)
|
|
{
|
|
MovementComponent->SetStrafing(bIsAiming);
|
|
UE_LOG(LogTengriCharacter, Verbose,
|
|
TEXT("Aim mode: %s"), bIsAiming ? TEXT("ON") : TEXT("OFF"));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PICKUP DETECTION
|
|
// ============================================================================
|
|
|
|
ATengriPickupActor* ATengriCharacter::FindNearestPickup() const
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
const FVector SpherePos = GetActorLocation();
|
|
|
|
// Define object types to detect (physics objects and dynamic actors)
|
|
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
|
|
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_PhysicsBody));
|
|
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic));
|
|
|
|
// Ignore self in overlap query
|
|
TArray<AActor*> IgnoreActors;
|
|
IgnoreActors.Add(const_cast<ATengriCharacter*>(this));
|
|
|
|
// Find all overlapping pickups
|
|
TArray<AActor*> OverlappingActors;
|
|
UKismetSystemLibrary::SphereOverlapActors(
|
|
World,
|
|
SpherePos,
|
|
PickupRadius,
|
|
ObjectTypes,
|
|
ATengriPickupActor::StaticClass(),
|
|
IgnoreActors,
|
|
OverlappingActors
|
|
);
|
|
|
|
// Find nearest un-held pickup
|
|
ATengriPickupActor* NearestItem = nullptr;
|
|
float MinDistSq = FMath::Square(PickupRadius);
|
|
|
|
for (AActor* Actor : OverlappingActors)
|
|
{
|
|
ATengriPickupActor* Item = Cast<ATengriPickupActor>(Actor);
|
|
if (!Item || Item->IsHeld())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation());
|
|
if (DistSq < MinDistSq)
|
|
{
|
|
MinDistSq = DistSq;
|
|
NearestItem = Item;
|
|
}
|
|
}
|
|
|
|
return NearestItem;
|
|
} |