644 lines
22 KiB
C++
644 lines
22 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "FpsPlayer.h"
|
|
|
|
#include "Camera/CameraComponent.h"
|
|
#include "Components/CapsuleComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "EnhancedInputComponent.h"
|
|
#include "EnhancedInputSubsystems.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "GameFramework/CharacterMovementComponent.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "InputActionValue.h"
|
|
#include "InteractionComponent.h"
|
|
#include "InventoryComponent.h"
|
|
#include "ItemDataAsset.h"
|
|
#include "PickupItem.h"
|
|
#include "SurvivalStatsComponent.h"
|
|
#include "SurvivalUserSettings.h"
|
|
|
|
// Sets default values
|
|
AFpsPlayer::AFpsPlayer()
|
|
{
|
|
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
|
|
// Gabarit du joueur : ~176 cm de haut, 34 cm de rayon
|
|
UCapsuleComponent* Capsule = GetCapsuleComponent();
|
|
Capsule->InitCapsuleSize(34.f, 88.f);
|
|
|
|
// La souris fait tourner le personnage horizontalement (yaw) ;
|
|
// le pitch reste sur le controller et n'est applique qu'a la camera.
|
|
bUseControllerRotationYaw = true;
|
|
bUseControllerRotationPitch = false;
|
|
bUseControllerRotationRoll = false;
|
|
|
|
// Camera a hauteur des yeux. La position est exprimee par rapport au centre
|
|
// de la capsule, d'ou la soustraction de la demi-hauteur.
|
|
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
|
|
FirstPersonCamera->SetupAttachment(Capsule);
|
|
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightStanding - Capsule->GetUnscaledCapsuleHalfHeight()));
|
|
FirstPersonCamera->SetFieldOfView(BaseFOV);
|
|
// On pilote la rotation nous-memes dans UpdateCameraEffects() : bUsePawnControlRotation
|
|
// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
|
|
FirstPersonCamera->bUsePawnControlRotation = false;
|
|
|
|
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
|
|
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
|
|
SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
|
|
|
|
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
|
|
// mais on garde son ombre.
|
|
GetMesh()->SetOwnerNoSee(true);
|
|
GetMesh()->SetCastHiddenShadow(true);
|
|
|
|
UCharacterMovementComponent* Movement = GetCharacterMovement();
|
|
Movement->MaxWalkSpeed = WalkSpeed;
|
|
Movement->MaxWalkSpeedCrouched = CrouchSpeed;
|
|
Movement->JumpZVelocity = 450.f;
|
|
Movement->AirControl = 0.3f;
|
|
Movement->BrakingDecelerationWalking = 2000.f;
|
|
// 120 cm de haut accroupi (une vraie position accroupie, pas un rampant)
|
|
Movement->SetCrouchedHalfHeight(60.f);
|
|
// Indispensable, sinon Crouch() est ignore
|
|
Movement->GetNavAgentPropertiesRef().bCanCrouch = true;
|
|
}
|
|
|
|
// Called when the game starts or when spawned
|
|
void AFpsPlayer::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);
|
|
|
|
// Les reglages d'abord : le pawn est recree a chaque respawn et repart des
|
|
// valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre
|
|
// ensuite les changements faits en cours de partie depuis le menu de pause.
|
|
ApplyUserSettings();
|
|
|
|
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
|
{
|
|
Settings->OnSurvivalSettingsApplied.AddDynamic(this, &AFpsPlayer::ApplyUserSettings);
|
|
}
|
|
|
|
// Le FOV est interpole chaque frame vers BaseFOV : sans ce calage initial on
|
|
// verrait la camera s'ouvrir lentement jusqu'a la valeur reglee a chaque
|
|
// apparition.
|
|
FirstPersonCamera->SetFieldOfView(BaseFOV);
|
|
|
|
// On enregistre le mapping context aupres du sous-systeme Enhanced Input
|
|
// du joueur local. Sans ca, aucune touche n'est lue.
|
|
if (const APlayerController* PlayerController = Cast<APlayerController>(GetController()))
|
|
{
|
|
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
|
|
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
|
|
{
|
|
if (DefaultMappingContext)
|
|
{
|
|
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DefaultMappingContext n'est pas assigne, aucun input ne fonctionnera."));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AFpsPlayer::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
// Le singleton de reglages survit au monde : sans ce desabonnement, chaque
|
|
// cadavre laisserait derriere lui un abonne mort de plus.
|
|
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
|
{
|
|
Settings->OnSurvivalSettingsApplied.RemoveDynamic(this, &AFpsPlayer::ApplyUserSettings);
|
|
}
|
|
|
|
Super::EndPlay(EndPlayReason);
|
|
}
|
|
|
|
void AFpsPlayer::ApplyUserSettings()
|
|
{
|
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
|
if (!Settings)
|
|
{
|
|
// Pas de reglages : on garde les valeurs du Blueprint. Le jeu reste
|
|
// jouable, il n'est simplement pas configurable.
|
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : USurvivalUserSettings introuvable, les valeurs du Blueprint sont conservees."));
|
|
return;
|
|
}
|
|
|
|
LookSensitivityX = Settings->GetLookMultiplierX();
|
|
LookSensitivityY = Settings->GetLookMultiplierY();
|
|
bInvertLookY = Settings->GetInvertLookY();
|
|
BaseFOV = Settings->GetFieldOfView();
|
|
CameraShakeMultiplier = Settings->GetCameraShakeMultiplier();
|
|
bSprintToggleMode = Settings->GetSprintToggle();
|
|
bCrouchToggleMode = Settings->GetCrouchToggle();
|
|
|
|
// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
|
|
// personnage bloque en sprint : on repart d'un etat propre.
|
|
if (!bSprintToggleMode && bIsSprinting)
|
|
{
|
|
StopSprint();
|
|
}
|
|
}
|
|
|
|
// Called every frame
|
|
void AFpsPlayer::Tick(float DeltaTime)
|
|
{
|
|
Super::Tick(DeltaTime);
|
|
|
|
// On memorise la vitesse de chute a chaque frame : au moment ou Landed()
|
|
// est appele, la vitesse a deja pu etre remise a zero par le moteur.
|
|
if (GetCharacterMovement()->IsFalling())
|
|
{
|
|
LastFallSpeed = GetCharacterMovement()->Velocity.Z;
|
|
}
|
|
|
|
UpdateCameraEffects(DeltaTime);
|
|
}
|
|
|
|
void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
|
|
{
|
|
// Un gros hoquet de framerate (chargement, breakpoint) ferait diverger
|
|
// l'integration du ressort ci-dessous. On plafonne le pas de temps.
|
|
const float Step = FMath::Min(DeltaTime, 1.f / 30.f);
|
|
|
|
// ==================================================================
|
|
// 1. Animation d'accroupissement
|
|
// ==================================================================
|
|
// CrouchAlpha va de 0 (debout) a 1 (accroupi) en CrouchTransitionDuration
|
|
// secondes. Duree fixe plutot qu'un FInterpTo : le mouvement est identique
|
|
// a chaque fois et se termine vraiment, au lieu de tendre asymptotiquement.
|
|
if (!FMath::IsNearlyEqual(CrouchAlpha, CrouchTargetAlpha))
|
|
{
|
|
const float CrouchStep = Step / FMath::Max(CrouchTransitionDuration, KINDA_SMALL_NUMBER);
|
|
CrouchAlpha = FMath::Clamp(CrouchAlpha + CrouchStep * FMath::Sign(CrouchTargetAlpha - CrouchAlpha), 0.f, 1.f);
|
|
}
|
|
|
|
// ==================================================================
|
|
// 2. Ressort d'impact (saut, atterrissage, et tout AddCameraPunch)
|
|
// ==================================================================
|
|
// L'impulsion n'est pas versee d'un coup : la vitesse de la camera passerait
|
|
// de 0 a plusieurs centaines de cm/s en une frame, et cette discontinuite se
|
|
// lit comme un a-coup. On l'etale sur PunchImpulseRampTime.
|
|
if (!FMath::IsNearlyZero(PendingPunch))
|
|
{
|
|
const float ReleaseRatio = (PunchImpulseRampTime > KINDA_SMALL_NUMBER)
|
|
? FMath::Clamp(Step / PunchImpulseRampTime, 0.f, 1.f)
|
|
: 1.f;
|
|
const float Released = PendingPunch * ReleaseRatio;
|
|
PunchVelocity += Released;
|
|
PendingPunch -= Released;
|
|
}
|
|
|
|
// Ressort amorti classique : acceleration = -k*x - c*v.
|
|
// On exprime k et c a partir d'une frequence et d'un taux d'amortissement,
|
|
// beaucoup plus parlants a regler qu'une raideur brute.
|
|
const float Omega = 2.f * PI * PunchFrequency;
|
|
const float Acceleration = -(Omega * Omega * PunchOffset) - (2.f * PunchDamping * Omega * PunchVelocity);
|
|
PunchVelocity += Acceleration * Step;
|
|
PunchOffset = FMath::Clamp(PunchOffset + PunchVelocity * Step, -PunchMaxOffset, PunchMaxOffset);
|
|
|
|
// ==================================================================
|
|
// 3. FOV de sprint
|
|
// ==================================================================
|
|
// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
|
|
// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
|
|
const bool bMovingFastEnough = GetCharacterMovement()->Velocity.SizeSquared2D() > FMath::Square(WalkSpeed * 0.9f);
|
|
const float TargetFOV = BaseFOV + ((bIsSprinting && bMovingFastEnough) ? SprintFOVOffset : 0.f);
|
|
FirstPersonCamera->SetFieldOfView(FMath::FInterpTo(FirstPersonCamera->FieldOfView, TargetFOV, Step, FOVInterpSpeed));
|
|
|
|
// ==================================================================
|
|
// 4. Application : position
|
|
// ==================================================================
|
|
// SmoothStep donne une courbe en S : demarrage doux, acceleration, arrivee douce.
|
|
const float Smoothed = FMath::SmoothStep(0.f, 1.f, CrouchAlpha);
|
|
const float EyeHeightAboveFeet = FMath::Lerp(EyeHeightStanding, EyeHeightCrouched, Smoothed);
|
|
|
|
// Point cle : quand la capsule retrecit, le CharacterMovement redescend l'acteur
|
|
// d'un coup pour garder les pieds au sol. La camera, attachee a la capsule,
|
|
// suivait ce saut instantane -- c'est ce qui rendait le crouch "snap".
|
|
// En raisonnant en hauteur d'oeil AU-DESSUS DES PIEDS et en soustrayant la
|
|
// demi-hauteur courante de la capsule, ce saut est annule exactement.
|
|
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
|
|
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchOffset));
|
|
|
|
// ==================================================================
|
|
// 5. Application : rotation
|
|
// ==================================================================
|
|
// sin(alpha * PI) vaut 0 aux deux extremites et 1 au milieu : le tilt monte
|
|
// puis redescend tout seul pendant la transition, et est nul au repos.
|
|
// CrouchTiltDirection inverse le sens selon qu'on descend ou qu'on remonte.
|
|
const float CrouchTiltShape = FMath::Sin(CrouchAlpha * PI) * CrouchTiltDirection;
|
|
|
|
FRotator ViewOffset(-CrouchTiltPitch * CrouchTiltShape, 0.f, CrouchTiltRoll * CrouchTiltShape);
|
|
// Le regard suit le deplacement vertical de la camera : elle descend, on
|
|
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
|
|
ViewOffset.Pitch += PunchOffset * PunchPitchPerCm;
|
|
|
|
if (const AController* OwningController = GetController())
|
|
{
|
|
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
|
|
}
|
|
}
|
|
|
|
void AFpsPlayer::AddCameraPunch(float Strength)
|
|
{
|
|
// Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui
|
|
// secoue la camera passe par cette porte, y compris les explosions et les
|
|
// reculs d'arme a venir. A 0, la camera ne bouge plus du tout.
|
|
// Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects
|
|
// qui l'injecte progressivement dans le ressort.
|
|
PendingPunch += Strength * CameraShakeMultiplier;
|
|
}
|
|
|
|
void AFpsPlayer::Landed(const FHitResult& Hit)
|
|
{
|
|
Super::Landed(Hit);
|
|
|
|
// L'amplitude suit la vitesse de chute : une marche d'escalier ne doit pas
|
|
// secouer autant qu'une chute de dix metres.
|
|
const float FallSpeed = FMath::Abs(LastFallSpeed);
|
|
if (FallSpeed > LandPunchMinFallSpeed)
|
|
{
|
|
const float Range = FMath::Max(LandPunchMaxFallSpeed - LandPunchMinFallSpeed, 1.f);
|
|
const float Ratio = FMath::Clamp((FallSpeed - LandPunchMinFallSpeed) / Range, 0.f, 1.f);
|
|
AddCameraPunch(-LandPunchStrength * Ratio);
|
|
|
|
// Aide au reglage : montre la vitesse d'impact reelle et l'amplitude
|
|
// obtenue. Supprime ce log quand le ressenti te convient.
|
|
UE_LOG(LogTemp, Log, TEXT("Atterrissage : chute %.0f cm/s, ratio %.2f, impulsion %.0f cm/s (~%.1f cm de camera)"),
|
|
FallSpeed, Ratio, LandPunchStrength * Ratio, LandPunchStrength * Ratio * 0.018f);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTemp, Log, TEXT("Atterrissage ignore : chute %.0f cm/s, sous le seuil de %.0f cm/s"),
|
|
FallSpeed, LandPunchMinFallSpeed);
|
|
}
|
|
|
|
LastFallSpeed = 0.f;
|
|
}
|
|
|
|
void AFpsPlayer::OnJumped_Implementation()
|
|
{
|
|
Super::OnJumped_Implementation();
|
|
|
|
AddCameraPunch(-JumpPunchStrength);
|
|
}
|
|
|
|
// Called to bind functionality to input
|
|
void AFpsPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
|
{
|
|
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
|
|
|
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
|
|
if (!Input)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : l'input component n'est pas un UEnhancedInputComponent."));
|
|
return;
|
|
}
|
|
|
|
if (MoveAction)
|
|
{
|
|
Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Move);
|
|
}
|
|
|
|
if (LookAction)
|
|
{
|
|
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Look);
|
|
}
|
|
|
|
if (JumpAction)
|
|
{
|
|
// Jump() et StopJumping() viennent directement de ACharacter
|
|
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
|
|
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
|
|
}
|
|
|
|
if (SprintAction)
|
|
{
|
|
// Le mode maintien / bascule se decide dans les handlers et non ici :
|
|
// rebrancher les bindings a chaud demanderait de reconstruire l'input
|
|
// component a chaque changement de reglage.
|
|
Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint);
|
|
Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleSprintReleased);
|
|
}
|
|
|
|
if (CrouchAction)
|
|
{
|
|
Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch);
|
|
Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleCrouchReleased);
|
|
}
|
|
|
|
if (InteractAction)
|
|
{
|
|
Input->BindAction(InteractAction, ETriggerEvent::Started, this, &AFpsPlayer::Interact);
|
|
}
|
|
|
|
if (UseItemAction)
|
|
{
|
|
Input->BindAction(UseItemAction, ETriggerEvent::Started, this, &AFpsPlayer::UseSelectedItem);
|
|
}
|
|
}
|
|
|
|
void AFpsPlayer::Move(const FInputActionValue& Value)
|
|
{
|
|
const FVector2D Axis = Value.Get<FVector2D>();
|
|
if (Axis.IsNearlyZero() || !Controller)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// On se deplace par rapport a la direction du regard, mais a plat :
|
|
// on ne garde que le yaw pour ne pas "voler" en regardant vers le haut.
|
|
const FRotator YawRotation(0.f, Controller->GetControlRotation().Yaw, 0.f);
|
|
const FVector Forward = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
|
|
const FVector Right = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
|
|
|
|
AddMovementInput(Forward, Axis.Y);
|
|
AddMovementInput(Right, Axis.X);
|
|
}
|
|
|
|
void AFpsPlayer::Look(const FInputActionValue& Value)
|
|
{
|
|
// Si IA_Look n'est pas en Axis2D, Get<FVector2D>() renvoie toujours Y = 0
|
|
// et le regard reste bloque sur l'axe horizontal. On le signale une fois.
|
|
if (Value.GetValueType() != EInputActionValueType::Axis2D && !bLoggedLookTypeWarning)
|
|
{
|
|
bLoggedLookTypeWarning = true;
|
|
UE_LOG(LogTemp, Error,
|
|
TEXT("AFpsPlayer : IA_Look doit avoir Value Type = Axis2D (Vector2D). ")
|
|
TEXT("Avec un autre type, l'axe vertical est ignore."));
|
|
}
|
|
|
|
const FVector2D Axis = Value.Get<FVector2D>();
|
|
|
|
// Pas de DeltaTime ici : la souris fournit un delta, pas une vitesse.
|
|
// Le multiplier par DeltaTime rendrait la visee dependante du framerate.
|
|
AddControllerYawInput(Axis.X * LookSensitivityX);
|
|
AddControllerPitchInput(Axis.Y * LookSensitivityY * (bInvertLookY ? -1.f : 1.f));
|
|
}
|
|
|
|
void AFpsPlayer::StartSprint()
|
|
{
|
|
// Pas de sprint accroupi : on se releve d'abord
|
|
if (bIsCrouched)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// En mode bascule, la meme touche coupe le sprint.
|
|
if (bSprintToggleMode && bIsSprinting)
|
|
{
|
|
StopSprint();
|
|
return;
|
|
}
|
|
|
|
bIsSprinting = true;
|
|
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
|
|
}
|
|
|
|
void AFpsPlayer::StopSprint()
|
|
{
|
|
bIsSprinting = false;
|
|
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
|
|
}
|
|
|
|
void AFpsPlayer::HandleSprintReleased()
|
|
{
|
|
// En bascule, relacher la touche ne doit rien faire.
|
|
if (bSprintToggleMode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StopSprint();
|
|
}
|
|
|
|
void AFpsPlayer::UseSelectedItem()
|
|
{
|
|
const FInventorySlot Selected = InventoryComponent->GetSelectedSlot();
|
|
if (Selected.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const UItemDataAsset* Item = Selected.Item;
|
|
|
|
// Objet non consommable : on ne fait rien pour l'instant. C'est ici que
|
|
// viendront les outils (frapper), les armes (tirer), les placeables.
|
|
if (!Item->bIsConsumable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
|
|
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
|
|
|
|
// ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et
|
|
// usages multiples (une charge en moins, l'objet reste tant qu'il en a).
|
|
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
|
|
// n'importe quelle pile du meme objet, y compris dans le sac.
|
|
InventoryComponent->ConsumeUse(InventoryComponent->GetSelectedHotbarIndex());
|
|
}
|
|
|
|
FVector AFpsPlayer::PlaceOnGround(const FVector& Start) const
|
|
{
|
|
// APickupItem ne simule pas la physique : sans ce trace vers le bas,
|
|
// l'objet resterait suspendu en l'air la ou on l'a lache.
|
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(DropGroundTrace), /*bTraceComplex=*/false, this);
|
|
|
|
FHitResult GroundHit;
|
|
if (GetWorld()->LineTraceSingleByChannel(GroundHit, Start, Start - FVector(0.f, 0.f, 500.f), ECC_Visibility, Params))
|
|
{
|
|
return GroundHit.ImpactPoint + FVector(0.f, 0.f, DropGroundOffset);
|
|
}
|
|
|
|
return Start;
|
|
}
|
|
|
|
APickupItem* AFpsPlayer::SpawnPickup(const FInventorySlot& Slot, const FVector& Location)
|
|
{
|
|
if (!DroppedPickupClass || Slot.IsEmpty())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// SpawnActorDeferred et non SpawnActor : il faut poser ItemData AVANT que
|
|
// OnConstruction et BeginPlay tournent, sinon le pickup apparait sans mesh
|
|
// et son CanInteract le rendrait inerte.
|
|
const FTransform SpawnTransform(FRotator(0.f, GetActorRotation().Yaw, 0.f), Location);
|
|
|
|
APickupItem* Dropped = GetWorld()->SpawnActorDeferred<APickupItem>(
|
|
DroppedPickupClass, SpawnTransform, this, nullptr,
|
|
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
|
|
|
|
if (!Dropped)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
Dropped->InitPickup(Slot.Item, Slot.Quantity, Slot.RemainingUses);
|
|
Dropped->FinishSpawning(SpawnTransform);
|
|
|
|
return Dropped;
|
|
}
|
|
|
|
bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
|
|
{
|
|
if (!DroppedPickupClass)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet."));
|
|
return false;
|
|
}
|
|
|
|
FInventorySlot Taken;
|
|
if (!InventoryComponent->TakeFromSlot(SlotIndex, Quantity, Taken) || Taken.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FVector EyeLocation = FirstPersonCamera->GetComponentLocation();
|
|
const FVector Forward = FirstPersonCamera->GetForwardVector();
|
|
FVector DropLocation = EyeLocation + Forward * DropDistance;
|
|
|
|
// Un mur devant nous : on depose juste avant, sinon l'objet finirait
|
|
// de l'autre cote de la cloison et deviendrait inatteignable.
|
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(DropItemTrace), /*bTraceComplex=*/false, this);
|
|
FHitResult WallHit;
|
|
if (GetWorld()->LineTraceSingleByChannel(WallHit, EyeLocation, DropLocation, ECC_Visibility, Params))
|
|
{
|
|
DropLocation = WallHit.ImpactPoint - Forward * 20.f;
|
|
}
|
|
|
|
if (!SpawnPickup(Taken, PlaceOnGround(DropLocation)))
|
|
{
|
|
// On ne perd pas l'objet : il retourne dans l'inventaire, usure comprise.
|
|
InventoryComponent->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AFpsPlayer::DropAllItems()
|
|
{
|
|
if (!DroppedPickupClass)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, le butin est perdu."));
|
|
return;
|
|
}
|
|
|
|
const int32 SlotCount = InventoryComponent->GetSlots().Num();
|
|
const FVector Center = GetActorLocation();
|
|
|
|
int32 Scattered = 0;
|
|
for (int32 Index = 0; Index < SlotCount; ++Index)
|
|
{
|
|
FInventorySlot Taken;
|
|
// TakeFromSlot renvoie false sur un slot vide : pas besoin de tester.
|
|
if (!InventoryComponent->TakeFromSlot(Index, MAX_int32, Taken) || Taken.IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Angle d'or : repartit les objets en spirale reguliere autour du
|
|
// corps, sans qu'ils se superposent ni forment de motif visible.
|
|
const float Angle = FMath::DegreesToRadians(Scattered * 137.5f);
|
|
const float Radius = 40.f + Scattered * 4.f;
|
|
const FVector Offset(FMath::Cos(Angle) * Radius, FMath::Sin(Angle) * Radius, 0.f);
|
|
|
|
SpawnPickup(Taken, PlaceOnGround(Center + Offset));
|
|
++Scattered;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("AFpsPlayer : %d piles laissees au sol a la mort."), Scattered);
|
|
}
|
|
|
|
void AFpsPlayer::HandleDeath()
|
|
{
|
|
if (bIsDead)
|
|
{
|
|
return;
|
|
}
|
|
bIsDead = true;
|
|
|
|
// Tout tombe sur place. On le fait AVANT de couper les inputs : l'ordre
|
|
// n'a pas d'importance fonctionnelle, mais perdre le butin en silence
|
|
// serait le pire bug possible dans un jeu de survie.
|
|
DropAllItems();
|
|
|
|
ClearTransientInputStates();
|
|
GetCharacterMovement()->DisableMovement();
|
|
|
|
if (APlayerController* OwningController = Cast<APlayerController>(GetController()))
|
|
{
|
|
DisableInput(OwningController);
|
|
}
|
|
}
|
|
|
|
void AFpsPlayer::ClearTransientInputStates()
|
|
{
|
|
StopSprint();
|
|
StopJumping();
|
|
|
|
// L'accroupissement n'est volontairement pas annule : c'est un etat
|
|
// visible que le joueur maitrise, et il se retablit tout seul au
|
|
// prochain appui sur la touche.
|
|
}
|
|
|
|
void AFpsPlayer::StartCrouch()
|
|
{
|
|
// En mode bascule, la meme touche releve le personnage.
|
|
if (bCrouchToggleMode && bIsCrouched)
|
|
{
|
|
StopCrouch();
|
|
return;
|
|
}
|
|
|
|
StopSprint();
|
|
Crouch();
|
|
}
|
|
|
|
void AFpsPlayer::HandleCrouchReleased()
|
|
{
|
|
if (bCrouchToggleMode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StopCrouch();
|
|
}
|
|
|
|
void AFpsPlayer::StopCrouch()
|
|
{
|
|
// UnCrouch() echoue silencieusement s'il y a un plafond : le perso
|
|
// reste accroupi jusqu'a ce que la place se libere.
|
|
UnCrouch();
|
|
}
|
|
|
|
void AFpsPlayer::Interact()
|
|
{
|
|
InteractionComponent->TryInteract();
|
|
}
|
|
|
|
void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
|
|
{
|
|
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
|
|
|
|
CrouchTargetAlpha = 1.f;
|
|
CrouchTiltDirection = 1.f;
|
|
}
|
|
|
|
void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
|
|
{
|
|
Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
|
|
|
|
CrouchTargetAlpha = 0.f;
|
|
CrouchTiltDirection = -1.f;
|
|
}
|