Files
Unreal_EmberWild/Source/Survival_projet/Private/FpsPlayer.cpp
T
Mathew 5ba5fbfebf (Feat) Customisation de personnage -- donnees et montage
Lot 2 du chantier : le corps du lot precedent devient assemblable. Tete,
coiffure, sourcils, barbe et moustache sont montes en leader pose sur le
mesh du Character, et les couleurs sont posees sur des MID.

FCharacterAppearance tient en 11 octets : des INDEX et jamais des chemins
d'assets. Un chemin couterait une chaine a chaque replication, alors qu'un
index tient dans un octet et se VALIDE contre le catalogue -- un client ne
peut donc pas demander un asset arbitraire.

Le corps se monte depuis une LISTE de meshes meme si elle n'a qu'une case.
Le pack livre exprès SKM_BodyA_torso, _arms, _legs_01/02/03 pour qu'une
armure puisse eteindre les segments qu'elle recouvre : le jour venu la case
0 devient SKM_BodyA_empty et les huit segments suivent, sans que ce code
change. Un pointeur unique imposerait de tout reprendre au premier plastron.

Trois pieges rencontres, aucun visible depuis le code :

- Les UFUNCTION(Exec) d'un UActorComponent ne sont JAMAIS appelees. La
  chaine de routage (UPlayer::Exec) interroge le monde, le PlayerInput, le
  controller, le pawn, le HUD, le GameMode, le CheatManager, le GameState et
  le camera manager -- jamais les composants, AActor ne surchargeant pas
  ProcessConsoleExec. Et rien ne proteste. D'ou EmberPart / EmberColor /
  EmberAppearanceDump posees sur AFpsPlayer, qui delegue.

- L'ordre des slots de materiau varie d'un mesh a l'autre : sur
  SKM_HeadA_01 l'Element 0 est celui des YEUX et le 1 celui de la peau. Les
  six tetes venant de deux packs, on cherche le slot par son NOM et on ne
  pose rien si aucun ne correspond -- ecraser le slot de la peau repeint le
  visage entier de la couleur des yeux.

- Un nom de parametre materiau se lit dans le Material INSTANCE, pas dans
  les chaines du master : M_Head_Base contient « Skin Base color » mais le
  parametre expose s'appelle « Skin ». D'ou des UPROPERTY de catalogue
  plutot que des constantes, pour corriger sans recompiler.

Body B reste a remplir dans DA_CharacterParts, avec son ABP_PlayerBody_B et
sans barbes ni moustaches -- le pack n'en livre pas, et une liste vide se
comporte deja comme NoPart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:48:37 +02:00

954 lines
33 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "FpsPlayer.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.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 "CharacterAppearanceComponent.h"
#include "CraftingComponent.h"
#include "HeadBobComponent.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Net/UnrealNetwork.h"
#include "PickupItem.h"
#include "SurvivalGameInstance.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;
// Bras vue premiere personne, accroches a la camera pour suivre le regard.
FirstPersonArms = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonArms"));
FirstPersonArms->SetupAttachment(FirstPersonCamera);
// Symetrique exact du mesh 3e personne ci-dessous : chacun ne voit que ses
// propres bras. Sans ca, les bras d'un ami flotteraient devant notre objectif.
FirstPersonArms->SetOnlyOwnerSee(true);
// Un mesh colle a l'objectif projetterait une ombre enorme en travers du
// decor. L'ombre du joueur, elle, vient du mesh 3e personne.
FirstPersonArms->SetCastShadow(false);
FirstPersonArms->bCastDynamicShadow = false;
// Les bras ne sont pas dans le monde, ils sont devant l'objectif : une
// collision les ferait pousser des objets et surtout bloquer le trace
// d'interaction, qui part exactement d'entre eux.
FirstPersonArms->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Chez l'hote, le pawn de chaque ami porte AUSSI ce composant, invisible pour
// tout le monde. Sans cette option le serveur evaluerait quatre AnimBlueprints
// pour quatre meshes que personne ne rend.
FirstPersonArms->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;
// Point de depart a affiner dans BP_FpsPlayer : la bonne valeur depend de
// l'origine du FBX (racine aux pieds ? au bassin ?) et de l'orientation
// d'export. Le yaw de -90 est le decalage habituel d'un FBX Blender/Maya.
FirstPersonArms->SetRelativeLocationAndRotation(FVector(0.f, 0.f, -160.f), FRotator(0.f, -90.f, 0.f));
HeadBob = CreateDefaultSubobject<UHeadBobComponent>(TEXT("HeadBob"));
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
// 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)
{
// La DECLARATION des contextes remappables appartient au
// GameInstance -- le menu principal n'a pas de pawn et doit
// pourtant afficher les touches. On la demande quand meme ici, et
// AVANT d'activer le contexte : c'est elle qui traduit les touches
// vers la disposition clavier du joueur, et un AZERTY doit trouver
// ses touches des la premiere image, pas au premier passage dans
// l'ecran de reglages. L'appel est idempotent.
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
{
GameInstance->RegisterMappableContexts();
}
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();
// Le curseur "secousses de camera" vaut aussi pour le balancement : un joueur
// qui le met a zero demande une camera fixe, pas une camera fixe qui respire.
HeadBob->SetIntensityScale(CameraShakeMultiplier);
// 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. Balancement de tete
// ==================================================================
// Integre ici et applique dans ApplyCameraTransform, exactement comme les
// ressorts ci-dessus : c'est ce qui permet a CalcCamera de recomposer la
// transform sans faire avancer le temps deux fois.
HeadBob->UpdateBob(Step);
// ==================================================================
// 4. 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();
// Le balancement s'AJOUTE au reste. Son Y est un decalage lateral dans le
// repere de la capsule, dont le yaw suit deja le regard (bUseControllerRotationYaw) :
// la tete se porte donc bien a droite de ce qu'on regarde, sans conversion.
// Le tangage, lui, n'est pas dans la capsule -- c'est voulu, le balancement
// reste horizontal meme quand on regarde ses pieds.
const FVector BobOffset = HeadBob->GetLocationOffset();
FirstPersonCamera->SetRelativeLocation(FVector(
BobOffset.X,
BobOffset.Y,
EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset + BobOffset.Z));
// ==================================================================
// 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;
// Roulis et tangage du balancement, dans le meme sac que le reste.
ViewOffset += HeadBob->GetRotationOffset();
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);
}
// ----------------------------------------------------------------------
// Commandes console d'apparence -- de simples delegations. Voir le commentaire
// de FpsPlayer.h : les UFUNCTION(Exec) d'un composant ne sont jamais routees,
// le pawn est le maillon le plus proche qui le soit.
// ----------------------------------------------------------------------
void AFpsPlayer::EmberPart(const FString& Part, int32 Index)
{
if (AppearanceComponent)
{
AppearanceComponent->ApplyPartByName(Part, Index);
}
}
void AFpsPlayer::EmberColor(const FString& Part, int32 ColorIndex)
{
if (AppearanceComponent)
{
AppearanceComponent->ApplyColorByName(Part, ColorIndex);
}
}
void AFpsPlayer::EmberAppearanceDump()
{
if (AppearanceComponent)
{
AppearanceComponent->LogAppearance();
}
}
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;
}
SetSprinting(true);
}
void AFpsPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// COND_SkipOwner : celui qui sprinte le sait deja, il vient de l'appliquer
// en local. Le renvoyer ecraserait son etat courant par une valeur qui a un
// aller-retour de retard -- exactement ce qui fait "sauter" un sprint qu'on
// relance vite.
DOREPLIFETIME_CONDITION(AFpsPlayer, bIsSprinting, COND_SkipOwner);
}
void AFpsPlayer::StopSprint()
{
SetSprinting(false);
}
void AFpsPlayer::SetSprinting(bool bNewSprinting)
{
if (bIsSprinting == bNewSprinting)
{
return;
}
bIsSprinting = bNewSprinting;
ApplySprintSpeed();
// Applique en local d'abord pour que la vitesse soit instantanee sous les
// doigts, puis previent le serveur. Le sens inverse -- attendre la reponse --
// donnerait un sprint qui demarre avec un temps de retard.
if (!HasAuthority())
{
Server_SetSprinting(bNewSprinting);
}
}
void AFpsPlayer::Server_SetSprinting_Implementation(bool bNewSprinting)
{
bIsSprinting = bNewSprinting;
ApplySprintSpeed();
}
void AFpsPlayer::OnRep_IsSprinting()
{
ApplySprintSpeed();
}
void AFpsPlayer::ApplySprintSpeed()
{
GetCharacterMovement()->MaxWalkSpeed = bIsSprinting ? SprintSpeed : WalkSpeed;
}
void AFpsPlayer::HandleSprintReleased()
{
// En bascule, relacher la touche ne doit rien faire.
if (bSprintToggleMode)
{
return;
}
StopSprint();
}
void AFpsPlayer::UseSelectedItem()
{
const int32 SelectedIndex = InventoryComponent->GetSelectedHotbarIndex();
// L'hote agit tout de suite, le client demande. On envoie meme si le slot
// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
// peut avoir une frame d'avance sur le notre.
if (!HasAuthority())
{
Server_UseItem(SelectedIndex);
return;
}
PerformUseItem(SelectedIndex);
}
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
{
PerformUseItem(SlotIndex);
}
void AFpsPlayer::PerformUseItem(int32 SlotIndex)
{
if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex))
{
return;
}
const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex];
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(SlotIndex);
}
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);
}
void AFpsPlayer::Server_DropFromInventory_Implementation(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
DropFromInventory(Inventory, SlotIndex, Quantity);
}
bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
if (!Inventory)
{
return false;
}
// Jeter engendre un acteur dans le monde : c'est une action d'autorite.
// La position de depot est calculee cote SERVEUR a partir de la camera
// repliquee du pawn -- a quelques centimetres pres du point vise par le
// client, ce qui n'a aucune importance pour un objet pose au sol.
if (!HasAuthority())
{
Server_DropFromInventory(Inventory, SlotIndex, Quantity);
return true;
}
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()
{
// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
// pour le jour ou un client l'appellerait : il engendrerait des pickups
// visibles de lui seul, qui disparaitraient au premier nettoyage reseau.
if (!HasAuthority())
{
return;
}
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()
{
// Le geste part AVANT la RPC, pas apres sa reponse : un montage declenche a
// l'aller-retour serveur donnerait des bras qui repondent 60 ms apres le clic,
// et le joueur lirait ca comme un jeu qui rame. Le prix a payer est un geste
// dans le vide si le serveur refuse l'interaction -- infiniment moins genant.
//
// Le test de type est ici et pas dans le composant d'interaction : celui-ci ne
// connait que l'interface IInteractable et n'a pas a apprendre ce qu'est un
// pickup. Un coffre ou un etabli n'ont pas a declencher un geste de ramassage.
if (Cast<APickupItem>(InteractionComponent->GetFocusedActor()))
{
PlayArmsMontage(GrabMontage);
}
InteractionComponent->TryInteract();
}
float AFpsPlayer::PlayArmsMontage(UAnimMontage* Montage, float PlayRate)
{
// IsLocallyControlled : chez l'hote, le pawn de chaque ami passerait aussi par
// ici a chaque ramassage et animerait un mesh que personne ne rend.
if (!Montage || !IsLocallyControlled())
{
return 0.f;
}
UAnimInstance* ArmsAnim = FirstPersonArms->GetAnimInstance();
return ArmsAnim ? ArmsAnim->Montage_Play(Montage, PlayRate) : 0.f;
}
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);
}