Files
Unreal_EmberWild/Source/Survival_projet/Private/FpsPlayer.cpp
T
Mathew 41d6e6a7f3 (Feat) Add Storage Chests + Item Details Panel
Coffres : AStorageContainer, un UInventoryComponent configure en conteneur,
affiche comme panneau de WBP_InventoryScreen plutot que dans un ecran a lui.
Deplacement unifie par UInventoryComponent::TransferSlot / TransferAllTo.

Panneau de detail : UItemDetailsWidget, pose DANS WBP_Inventory a droite de la
grille. Icone, nom, separation, description, barre d'usure. Il recoit un couple
(inventaire, index) et s'abonne a OnInventoryChanged, donc une charge consommee
sous le curseur se voit.

Une case ne connait plus sa grille : elle diffuse OnHoverChanged. La barre
rapide s'y abonne aussi et relaie par le PlayerController, seul a posseder a la
fois le HUD et l'ecran. Le panneau efface la case qu'on QUITTE et non lui-meme,
sinon passer de la barre a la grille viderait ce qui vient d'etre affiche.

L'ecran allume et eteint le panneau (SetItemDetailsEnabled) : WBP_Inventory
etant instancie deux fois en mode coffre, une case a cocher par instance serait
un bug qui attend qu'on oublie de la decocher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:51:24 +02:00

739 lines
25 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "FpsPlayer.h"
#include "Camera/CameraComponent.h"
#include "Camera/CameraTypes.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 "CraftingComponent.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "PickupItem.h"
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
float FCameraSpring::Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset)
{
// L'impulsion n'est pas versee d'un coup : la vitesse passerait de 0 a
// plusieurs centaines d'unites par seconde en une frame, et cette
// discontinuite se lit comme un a-coup. On l'etale sur RampTime.
if (!FMath::IsNearlyZero(PendingImpulse))
{
const float ReleaseRatio = (RampTime > KINDA_SMALL_NUMBER)
? FMath::Clamp(Step / RampTime, 0.f, 1.f)
: 1.f;
const float Released = PendingImpulse * ReleaseRatio;
Velocity += Released;
PendingImpulse -= 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 * Frequency;
const float Acceleration = -(Omega * Omega * Offset) - (2.f * Damping * Omega * Velocity);
Velocity += Acceleration * Step;
Offset = FMath::Clamp(Offset + Velocity * Step, -MaxOffset, MaxOffset);
return Offset;
}
// 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"));
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
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);
// Sans cet abonnement, un objet fabrique alors que l'inventaire est plein
// serait purement et simplement perdu.
CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow);
// 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)
{
// On ACTIVE le contexte ici, mais on ne le DECLARE pas comme
// remappable : c'est USurvivalGameInstance qui s'en charge, parce
// que le menu principal n'a pas de pawn et doit quand meme
// afficher les touches.
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. Ressorts : impact (saut, atterrissage) et bascule de tete
// ==================================================================
PunchSpring.Update(Step, PunchFrequency, PunchDamping, PunchImpulseRampTime, PunchMaxOffset);
// Le garde-fou est en unites normalisees, ou 1 = le pic d'une bascule
// complete. A 1.5 on laisse passer un leger cumul quand deux impulsions se
// suivent de tres pres, sans jamais laisser la tete partir.
CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);
// ==================================================================
// 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));
// L'application est laissee a ApplyCameraTransform : CalcCamera la rappelle
// juste avant que la vue soit lue, avec l'etat le plus frais de la capsule.
ApplyCameraTransform();
}
void AFpsPlayer::ApplyCameraTransform()
{
// ==================================================================
// 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 --
// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset));
// ==================================================================
// Rotation
// ==================================================================
// La bascule ne depend plus de l'avancement de la transition : c'est l'etat
// d'un ressort, donc elle reste continue meme si le joueur change d'avis en
// plein mouvement. L'ancienne forme sin(alpha*PI) multipliee par un signe
// sautait de +CrouchTiltPitch a -CrouchTiltPitch en une frame des qu'on
// s'accroupissait et se relevait coup sur coup.
const float CrouchTilt = CrouchTiltSpring.Offset;
FRotator ViewOffset(-CrouchTiltPitch * CrouchTilt, 0.f, CrouchTiltRoll * CrouchTilt);
// 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 += PunchSpring.Offset * PunchPitchPerCm;
if (const AController* OwningController = GetController())
{
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
}
}
void AFpsPlayer::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
ApplyCameraTransform();
Super::CalcCamera(DeltaTime, OutResult);
}
void AFpsPlayer::AddCrouchTilt(float Direction)
{
// Impulsion normalisee pour qu'un ressort critique culmine exactement a 1 :
// x(t) = v0 * t * e^(-w*t) atteint son maximum v0/(e*w) en t = 1/w, donc
// v0 = e*w. CrouchTiltPitch et CrouchTiltRoll restent ainsi des degres au
// pic, et non un reglage a l'aveugle.
const float Omega = 2.f * PI * CrouchTiltFrequency;
CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega);
}
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.
PunchSpring.AddImpulse(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)
{
return DropFromInventory(InventoryComponent, SlotIndex, Quantity);
}
bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
if (!Inventory)
{
return false;
}
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet."));
return false;
}
FInventorySlot Taken;
if (!Inventory->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 d'ou il vient, usure comprise.
Inventory->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
return false;
}
return true;
}
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
if (!Item || Quantity <= 0)
{
return;
}
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
Quantity, *GetNameSafe(Item));
return;
}
FInventorySlot Overflow;
Overflow.Item = Item;
Overflow.Quantity = Quantity;
Overflow.RemainingUses = RemainingUses;
// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
// une cloison qu'on ne regarde meme pas.
const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;
if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."),
Quantity, *GetNameSafe(Item));
}
}
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. On teste l'INTENTION
// (bWantsToCrouch) et non l'etat applique (bIsCrouched) : le CharacterMovement
// ne redimensionne la capsule qu'a son propre tick, donc deux appuis dans la
// meme frame verraient tous les deux bIsCrouched a false et le personnage
// resterait accroupi. Symptome typique quand on martele la touche.
if (bCrouchToggleMode && GetCharacterMovement()->bWantsToCrouch)
{
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;
AddCrouchTilt(1.f);
}
void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
CrouchTargetAlpha = 0.f;
AddCrouchTilt(-1.f);
}