Files
Unreal_EmberWild/Source/Survival_projet/Private/FpsPlayerController.cpp
T
Mathew 5970db0512 (Feat) Customisation -- replication et persistance
Lot 3 : l'apparence survit desormais au respawn, au changement de map et au
relancement du jeu, et les trois autres joueurs la voient.

Elle vit sur ASurvivalPlayerState et pas sur le pawn : c'est le JOUEUR
qu'elle decrit, pas le corps. Le pawn meurt et reapparait, le PlayerState
non -- reapparaitre avec le visage de quelqu'un d'autre n'aurait aucun sens,
alors que perdre son inventaire au sol est la regle du jeu.

SubmitCharacterAppearance() sur AFpsPlayerController est le point d'entree
UNIQUE : il persiste ET annonce au serveur, jamais l'un sans l'autre.
Persister sans annoncer laisserait le joueur seul a se voir change, annoncer
sans persister le ferait repartir au visage par defaut au lancement suivant.
C'est par la que passent les commandes console, et par la que passera
l'ecran du lot 4.

La course pawn / PlayerState est resolue des DEUX cotes. Chez un client ils
arrivent par deux chemins de replication independants et rien ne dit lequel
gagne : OnRep_CharacterAppearance monte le personnage si le pawn est deja la,
AFpsPlayer::OnRep_PlayerState va chercher l'apparence si c'est l'inverse. Les
deux appellent la meme fonction idempotente. Chez l'hote aucun OnRep ne se
declenche, d'ou l'appel local dans SetCharacterAppearance -- sans lui, l'hote
serait le seul a ne pas voir son propre personnage.

Rien n'est valide cote PlayerState, deliberement : borner un index demande le
catalogue, que seul UCharacterAppearanceComponent connait, et celui-ci passe
deja tout ce qu'il monte par Catalog->Sanitize() sur chaque machine.

Piege de test note dans CLAUDE.md : en PIE a deux joueurs, les deux fenetres
lisent le meme .ini, donc les deux personnages demarrent identiques.

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

1468 lines
42 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "FpsPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
#include "SurvivalGameInstance.h"
#include "SurvivalPlayerState.h"
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
#include "HotbarWidget.h"
#include "InputActionValue.h"
#include "InputCoreTypes.h"
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "CraftingComponent.h"
#include "InventoryScreenWidget.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerState.h"
#include "Interfaces/OnlineIdentityInterface.h"
#include "Kismet/GameplayStatics.h"
#include "OnlineSubsystem.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SessionSubsystem.h"
#include "SettingsMenuWidget.h"
#include "StorageContainer.h"
#include "SurvivalStatsWidget.h"
#define LOCTEXT_NAMESPACE "PauseMenu"
namespace
{
// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
//
// La barre rapide passe AU-DESSUS de l'inventaire, et pas l'inverse : le
// Background Blur de l'inventaire floute tout ce qui est rendu en dessous
// de lui. Avec un ZOrder inferieur, la barre se retrouverait floutee et
// inutilisable des l'ouverture de l'inventaire.
//
// Bonus : etant devant, elle reste une cible de depot valide -- on peut
// glisser un objet de la grille directement dans la barre.
//
// Pas de 10 pour laisser de la place aux widgets a venir.
constexpr int32 ZOrderCrosshair = 0;
constexpr int32 ZOrderSurvivalStats = 5;
constexpr int32 ZOrderInteractionPrompt = 10;
constexpr int32 ZOrderInventory = 20;
constexpr int32 ZOrderHotbar = 30;
/**
* Periode de surveillance de l'eloignement du coffre. Quatre fois par
* seconde suffit largement : c'est une comparaison de distance, pas une
* requete physique, et un quart de seconde de tolerance ne se voit pas.
*/
constexpr float StorageDistanceCheckPeriod = 0.25f;
// Le menu de pause passe devant tout le HUD, et la boite de confirmation
// devant lui. Meme echelle que dans le menu principal.
//
// Les noms doivent rester uniques dans TOUT le module : le build unity
// concatene les .cpp, et un namespace anonyme n'isole plus rien. D'ou le
// prefixe InGame, qui evite la collision avec le menu principal.
constexpr int32 ZOrderPauseMenu = 100;
constexpr int32 ZOrderInGameSettingsMenu = 150;
constexpr int32 ZOrderQuitToMenuDialog = 200;
}
AFpsPlayerController::AFpsPlayerController()
{
// Le fondu est un systeme a part entiere, donc un composant -- et le menu
// principal reutilise exactement le meme.
//
// Rappel Unreal : ce constructeur tourne aussi dans l'editeur, sur le CDO.
// Il ne doit contenir que de la construction, jamais de logique de jeu.
ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));
QuitToMenuMessage = LOCTEXT("QuitToMenuConfirm", "Retourner au menu principal ?\nTa progression sera perdue.");
}
void AFpsPlayerController::BeginPlay()
{
Super::BeginPlay();
// Le mode d'input vit sur le VIEWPORT, pas sur le controller. OpenLevel
// detruit le controller du menu principal mais pas la fenetre : son
// GameAndUI et son curseur visible restent en place, et la camera ne
// repondrait qu'au clic droit maintenu. On ne suppose donc jamais l'etat
// laisse par la map precedente, on le repose.
if (IsLocalController())
{
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
// Annonce notre pseudo Steam. Seul le client connait le sien : l'API
// d'identite ne renseigne que l'utilisateur local de chaque machine.
const FString Nickname = ResolveLocalPlayerNickname();
if (!Nickname.IsEmpty())
{
// L'hote est deja l'autorite : passer par la RPC marcherait, mais
// l'appel direct evite un aller-retour pour rien.
if (HasAuthority())
{
Server_SetPlayerNickname_Implementation(Nickname);
}
else
{
Server_SetPlayerNickname(Nickname);
}
}
// Dans la foulee du pseudo : les deux repondent au meme constat, le
// serveur ne peut connaitre ni l'un ni l'autre. Ils partent donc
// ensemble, au seul moment ou l'on est certain que le PlayerState local
// existe.
AnnounceCharacterAppearance();
}
// Le delegue se declenche a la fin de CHAQUE fondu : c'est
// bQuittingToMainMenu qui distingue le fondu de sortie du reste.
ScreenFade->OnFadeFinished.AddDynamic(this, &AFpsPlayerController::HandleFadeFinished);
// Une invitation peut tomber en pleine partie : sans ce fondu, l'ecran
// sauterait directement dans le monde de l'autre hote.
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
{
Sessions->OnTravelPending.AddDynamic(this, &AFpsPlayerController::HandleSessionTravelPending);
}
// IsLocalController est INDISPENSABLE ici : sur la machine de l'hote, chaque
// joueur distant a AUSSI un controller cote serveur. Sans cette garde, le
// controller d'un ami qui se connecte irait poser un fondu et rallumer le
// rendu du viewport de l'HOTE, a un moment totalement arbitraire.
if (bFadeInOnStart && IsLocalController())
{
// On NOIRCIT et on attend que la camera soit sur notre corps, au lieu
// d'eclaircir tout de suite. Un client demarre sur un SpectatorPawn ne
// au centre du monde -- sous le terrain -- et y reste encore un instant
// apres l'acquittement de la possession. C'est TryStartupFadeIn qui
// decidera du bon moment.
ScreenFade->SnapToBlack();
bStartupFadePending = true;
// Filet : si la possession n'arrivait jamais, l'ecran resterait noir a
// vie et le joueur croirait le jeu plante.
// Le filet appelle CompleteStartupFadeIn et non TryStartupFadeIn : sans
// ca il retomberait dans la boucle de reessai et l'ecran resterait noir
// pour toujours.
GetWorldTimerManager().SetTimer(
StartupFadeTimeoutHandle, this, &AFpsPlayerController::CompleteStartupFadeIn,
FMath::Max(StartupFadeTimeout, 1.f), /*bLoop=*/false);
// L'hote possede deja son pawn ET sa camera est dessus : il n'attendra
// rien. Le test reel est dans TryStartupFadeIn.
TryStartupFadeIn();
}
else if (IsLocalController())
{
// Mode sans fondu : personne ne rallumera le rendu, il faut le faire ici
// sous peine d'ecran noir jusqu'au filet de securite.
if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
{
SurvivalGameInstance->ResumeWorldRendering();
}
}
}
void AFpsPlayerController::TryStartupFadeIn()
{
if (!bStartupFadePending)
{
return;
}
// Un controller distant vu depuis le serveur n'a pas d'ecran a eclaircir, et
// surtout pas celui de l'hote.
if (!IsLocalController())
{
return;
}
// La camera doit deja regarder par NOS yeux. Un client demarre sur un
// SpectatorPawn ne au centre du monde -- sous le terrain -- et y reste
// encore un instant apres l'acquittement de la possession. Eclaircir a cet
// instant montre exactement ca.
const APawn* OwnPawn = GetPawn();
if (!OwnPawn || GetViewTarget() != OwnPawn)
{
GetWorldTimerManager().SetTimer(
StartupFadeRetryHandle, this, &AFpsPlayerController::TryStartupFadeIn,
StartupFadeRetry, /*bLoop=*/false);
return;
}
CompleteStartupFadeIn();
}
void AFpsPlayerController::CompleteStartupFadeIn()
{
if (!bStartupFadePending)
{
return;
}
bStartupFadePending = false;
GetWorldTimerManager().ClearTimer(StartupFadeTimeoutHandle);
GetWorldTimerManager().ClearTimer(StartupFadeRetryHandle);
// Le rendu du monde est suspendu depuis le chargement de la map : on le
// rend AVANT le fondu, alors que le voile est encore parfaitement noir.
// L'ordre inverse laisserait passer les images qu'on cherche a cacher.
USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
if (!SurvivalGameInstance)
{
StartStartupFade();
return;
}
SurvivalGameInstance->ResumeWorldRendering();
// On laisse le monde rendre quelques images DERRIERE le voile noir avant de
// l'eclaircir : c'est ce qui donne a Nanite, aux ombres virtuelles et a
// Lumen le temps de converger sans que personne le voie.
SurvivalGameInstance->BeginSceneWarmup(
FSimpleDelegate::CreateUObject(this, &AFpsPlayerController::StartStartupFade));
}
void AFpsPlayerController::StartStartupFade()
{
ScreenFade->FadeIn(StartupFadeDuration);
}
void AFpsPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
SetupForPossessedPawn(InPawn);
}
void AFpsPlayerController::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
// Le chemin CLIENT. Sans lui, un joueur qui rejoint n'a aucun HUD : le
// serveur a bien appele OnPossess, mais chez lui, pas chez le client.
SetupForPossessedPawn(P);
}
void AFpsPlayerController::SetupForPossessedPawn(APawn* InPawn)
{
// On cree les widgets ici et pas dans BeginPlay : a ce stade le pawn est
// garanti possede, et le prompt comme l'inventaire ont besoin de lui pour
// trouver leurs composants des leur NativeConstruct.
CreateHudWidgets();
// Apres un respawn les widgets existent deja, mais pointent vers les
// composants du cadavre. On les rebranche sur le nouveau pawn.
if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
if (Hotbar) { Hotbar->BindToOwningPawn(); }
if (InventoryScreen) { InventoryScreen->BindToOwningPawn(); }
if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); }
if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(InPawn))
{
if (USurvivalStatsComponent* Stats = PlayerPawn->GetSurvivalStats())
{
// AddUnique et non Add : chez l'hote, OnPossess ET
// AcknowledgePossession passent tous les deux ici. Un double
// abonnement declencherait deux fondus de mort et deux respawns.
Stats->OnDied.AddUniqueDynamic(this, &AFpsPlayerController::HandlePawnDied);
}
}
// Le corps est la : on peut enfin montrer le monde. Sans effet si le fondu
// d'arrivee a deja eu lieu, ce qui est le cas a chaque respawn.
TryStartupFadeIn();
}
void AFpsPlayerController::HandlePawnDied()
{
// L'ecran ouvert au moment de la mort resterait affiche par-dessus l'ecran
// noir, curseur compris. Sa fermeture rend le coffre au passage, couvercle
// compris.
if (bInventoryOpen)
{
ToggleInventory();
}
ScreenFade->FadeOut(DeathFadeDuration);
GetWorldTimerManager().SetTimer(
RespawnTimerHandle, this, &AFpsPlayerController::RespawnPlayer,
FMath::Max(RespawnDelay, DeathFadeDuration), /*bLoop=*/false);
}
void AFpsPlayerController::RespawnPlayer()
{
// Le fondu de retour est purement local : il appartient a l'ecran de ce
// joueur, et il doit remonter meme si la reponse du serveur tarde.
ScreenFade->FadeIn(StartupFadeDuration);
if (!HasAuthority())
{
Server_RequestRespawn();
return;
}
Server_RequestRespawn_Implementation();
}
void AFpsPlayerController::Server_RequestRespawn_Implementation()
{
// On depossede AVANT de detruire : detruire un pawn encore possede laisse
// le controller dans un etat incoherent le temps d'une frame.
APawn* PreviousPawn = GetPawn();
UnPossess();
if (IsValid(PreviousPawn))
{
PreviousPawn->Destroy();
}
// RestartPlayer fait apparaitre un nouveau pawn sur un PlayerStart et le
// possede. Le nouveau USurvivalStatsComponent repart plein tout seul dans
// son BeginPlay : rien a reinitialiser a la main.
if (AGameModeBase* GameMode = GetWorld()->GetAuthGameMode())
{
GameMode->RestartPlayer(this);
}
else
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : aucun GameMode, impossible de reapparaitre."));
}
}
void AFpsPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(InputComponent);
if (!Input)
{
return;
}
if (ToggleInventoryAction)
{
Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory);
}
if (ToggleCraftingAction)
{
Input->BindAction(ToggleCraftingAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleCrafting);
}
if (DropItemAction)
{
Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem);
}
// La barre rapide est bindee sur le controller et non sur le pawn : ainsi
// changer d'objet actif reste possible meme quand l'inventaire est ouvert
// et que les inputs du pawn sont coupes.
if (HotbarSelectAction)
{
Input->BindAction(HotbarSelectAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleHotbarSelect);
}
if (HotbarScrollAction)
{
Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll);
}
if (PauseAction)
{
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandlePauseInput);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseAction n'est pas assignee, le menu de pause sera inaccessible."));
}
}
UInventoryComponent* AFpsPlayerController::GetPawnInventory() const
{
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
return PlayerPawn ? PlayerPawn->GetInventoryComponent() : nullptr;
}
void AFpsPlayerController::HandleHotbarSelect(const FInputActionValue& Value)
{
UInventoryComponent* Inventory = GetPawnInventory();
if (!Inventory)
{
return;
}
// La valeur vient du modificateur Scalar de la touche : 1 pour la touche 1,
// 2 pour la touche 2, etc. On repasse en index a partir de zero.
const int32 Number = FMath::RoundToInt(Value.Get<float>());
Inventory->SelectHotbarSlot(Number - 1);
}
void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value)
{
const float Axis = Value.Get<float>();
if (FMath::IsNearlyZero(Axis))
{
return;
}
if (UInventoryComponent* Inventory = GetPawnInventory())
{
// Molette vers le haut = slot precedent, comme dans la plupart des FPS.
Inventory->CycleHotbarSelection(Axis > 0.f ? -1 : 1);
}
}
void AFpsPlayerController::HandleInventoryTabChanged(bool bInventoryTabActive)
{
bCraftingTabActive = !bInventoryTabActive;
UpdateHotbarVisibility();
}
void AFpsPlayerController::UpdateHotbarVisibility()
{
if (!Hotbar)
{
return;
}
// La barre rapide reste affichee sur l'onglet Inventaire : elle en fait
// partie, on y glisse des objets. Elle n'a en revanche aucun sens sous la
// page de craft, ou elle ne ferait qu'encombrer.
// Surtout pas "bHidden" : AActor en a deja un, le masquer donne une erreur
// de compilation (C4458) plutot qu'un simple avertissement.
const bool bHideHotbar = bInventoryOpen && bCraftingTabActive;
Hotbar->SetVisibility(bHideHotbar ? ESlateVisibility::Collapsed : HotbarDefaultVisibility);
}
void AFpsPlayerController::HandleHotbarSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered)
{
if (InventoryScreen && bInventoryOpen)
{
InventoryScreen->NotifyExternalSlotHovered(Inventory, SlotIndex, bHovered);
}
}
void AFpsPlayerController::DropHoveredItem()
{
UInventoryComponent* HoveredInventory = nullptr;
int32 SlotIndex = INDEX_NONE;
// L'ecran renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la
// touche ne doit pas agir sur une case cachee derriere la grille de craft.
// Il consulte le panneau de coffre en premier quand il est ouvert.
if (!bInventoryOpen || !InventoryScreen || !InventoryScreen->GetHoveredSlot(HoveredInventory, SlotIndex))
{
return;
}
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
if (!PlayerPawn)
{
return;
}
// Ctrl enfonce : un seul exemplaire, comme pour le glisser. On lit l'etat
// de la touche plutot que d'en faire une action separee : le geste est
// modal, pas une commande distincte.
const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);
// MAX_int32 pour "tout" : la fonction borne d'elle-meme a ce que contient
// le slot. On passe l'inventaire survole et non celui du pawn : viser une
// case du coffre doit sortir l'objet du coffre.
PlayerPawn->DropFromInventory(HoveredInventory, SlotIndex, bSingle ? 1 : MAX_int32);
}
void AFpsPlayerController::CreateHudWidgets()
{
// Un widget n'existe que sur la machine du joueur local : inutile et
// incorrect d'en creer un sur un serveur dedie.
if (!IsLocalController())
{
return;
}
if (!Crosshair && CrosshairClass)
{
Crosshair = CreateWidget<UUserWidget>(this, CrosshairClass);
if (Crosshair)
{
Crosshair->AddToViewport(ZOrderCrosshair);
}
}
if (!InteractionPrompt)
{
if (InteractionPromptClass)
{
InteractionPrompt = CreateWidget<UInteractionPromptWidget>(this, InteractionPromptClass);
if (InteractionPrompt)
{
InteractionPrompt->AddToViewport(ZOrderInteractionPrompt);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InteractionPromptClass n'est pas assignee, aucun prompt ne s'affichera."));
}
}
// Le voile de fondu n'est plus cree ici : UScreenFadeComponent s'en charge
// dans son propre BeginPlay, et son ZOrder de 1000 le garde devant tout le
// HUD quel que soit l'ordre d'ajout.
if (!SurvivalStatsWidget && SurvivalStatsClass)
{
SurvivalStatsWidget = CreateWidget<USurvivalStatsWidget>(this, SurvivalStatsClass);
if (SurvivalStatsWidget)
{
SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats);
}
}
if (!Hotbar && HotbarClass)
{
Hotbar = CreateWidget<UHotbarWidget>(this, HotbarClass);
if (Hotbar)
{
// Relevee avant tout masquage : c'est la valeur a laquelle on
// reviendra en quittant l'onglet Fabrication.
HotbarDefaultVisibility = Hotbar->GetVisibility();
Hotbar->AddToViewport(ZOrderHotbar);
// La barre est un widget de HUD, l'ecran d'inventaire un autre : ni
// l'un ni l'autre ne se connait. Le controller les possede tous les
// deux, c'est donc lui qui fait le pont -- exactement comme pour la
// visibilite de la barre sur l'onglet Fabrication.
Hotbar->OnSlotHovered.AddDynamic(this, &AFpsPlayerController::HandleHotbarSlotHovered);
}
}
if (!InventoryScreen)
{
if (InventoryScreenClass)
{
InventoryScreen = CreateWidget<UInventoryScreenWidget>(this, InventoryScreenClass);
if (InventoryScreen)
{
// Abonnement AVANT l'ajout au viewport : c'est lui qui declenche
// NativeConstruct, donc la premiere diffusion d'onglet. S'abonner
// apres, on la manquerait.
InventoryScreen->OnTabChanged.AddDynamic(this, &AFpsPlayerController::HandleInventoryTabChanged);
InventoryScreen->AddToViewport(ZOrderInventory);
// Cree des le depart pour que les abonnements de ses deux pages
// soient actifs, mais cache : ainsi l'ecran reste a jour meme
// ferme, et l'ouverture est instantanee.
InventoryScreen->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryScreenClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
}
}
if (!PauseMenu)
{
if (PauseMenuClass)
{
PauseMenu = CreateWidget<UPauseMenuWidget>(this, PauseMenuClass);
if (PauseMenu)
{
PauseMenu->AddToViewport(ZOrderPauseMenu);
// Cree une fois pour toutes et simplement cache, comme
// l'inventaire : le menu de pause survit ainsi aux respawns.
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseMenuClass n'est pas assignee, le menu de pause ne s'ouvrira pas."));
}
}
}
void AFpsPlayerController::ToggleInventory()
{
// Tab veut dire "ouvre mon sac". Rouvrir sur l'onglet Fabrication parce
// qu'on l'avait laisse la serait une surprise a chaque fois.
SetInventoryScreenOpen(!bInventoryOpen, /*bShowCraftingTab=*/false);
}
void AFpsPlayerController::ToggleCrafting()
{
// Deja sur l'onglet Fabrication : la meme touche referme, comme Tab le fait
// pour l'inventaire. Depuis l'onglet Inventaire, elle bascule sans fermer --
// c'est le geste attendu quand on cherche quoi construire avec ce qu'on vient
// de ramasser.
if (bInventoryOpen && bCraftingTabActive)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
return;
}
// Le panneau de coffre est un FRERE du TabSwitcher : il resterait affiche a
// cote de la grille de recettes. On rend le coffre plutot que d'ignorer la
// touche, qui exprime une intention claire.
ReleaseActiveStorage();
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}
void AFpsPlayerController::OpenCraftingAtStation(ECraftingStation Station)
{
if (Station == ECraftingStation::Count)
{
return;
}
// Appelee depuis Interact_Implementation, donc sur le SERVEUR. Pour un
// client distant on accorde le poste ici -- c'est ce controller-la que
// Server_Craft interrogera -- et on demande a l'ecran de s'ouvrir la-bas.
// Chez un hote, IsLocalController() est vrai et on passe tout droit.
if (!IsLocalController())
{
GrantStation(Station);
Client_OpenCraftingAtStation(Station);
return;
}
GrantStation(Station);
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}
void AFpsPlayerController::Client_OpenCraftingAtStation_Implementation(ECraftingStation Station)
{
// On rejoue le chemin local : accorder le poste sur NOTRE copie du
// composant, sans quoi la grille de recettes filtrerait sur {Hands} et
// l'onglet paraitrait vide devant un etabli.
GrantStation(Station);
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}
void AFpsPlayerController::GrantStation(ECraftingStation Station)
{
// Deja ouvert sur un autre poste : on rend le precedent avant d'accorder le
// nouveau, sinon le compteur du composant ne redescendrait jamais.
ReleaseActiveStation();
if (UCraftingComponent* Crafting = GetPawnCrafting())
{
Crafting->AddAvailableStation(Station);
ActiveStation = Station;
}
}
void AFpsPlayerController::Server_ReleaseStation_Implementation(ECraftingStation Station)
{
if (UCraftingComponent* Crafting = GetPawnCrafting())
{
Crafting->RemoveAvailableStation(Station);
}
}
void AFpsPlayerController::Server_ReleaseStorage_Implementation(AStorageContainer* Container)
{
if (Container)
{
Container->SetOpen(false);
}
}
void AFpsPlayerController::ReleaseActiveStation()
{
if (ActiveStation == ECraftingStation::Count)
{
return;
}
if (UCraftingComponent* Crafting = GetPawnCrafting())
{
Crafting->RemoveAvailableStation(ActiveStation);
}
// Le serveur tient sa propre liste de postes accessibles, celle qui sert a
// valider Server_Craft. La laisser ouverte rendrait les recettes d'etabli
// fabricables n'importe ou pour le reste de la partie.
if (!HasAuthority())
{
Server_ReleaseStation(ActiveStation);
}
ActiveStation = ECraftingStation::Count;
}
UCraftingComponent* AFpsPlayerController::GetPawnCrafting() const
{
const AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
return PlayerPawn ? PlayerPawn->GetCraftingComponent() : nullptr;
}
void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab)
{
if (!InventoryScreen)
{
return;
}
// Ecran deja ouvert : on ne fait que changer d'onglet. Repasser par le mode
// d'input serait au mieux inutile, au pire nuisible -- SetIgnoreLookInput
// gere un compteur qu'un appel en trop desequilibrerait durablement.
if (bOpen && bInventoryOpen)
{
if (bShowCraftingTab)
{
InventoryScreen->ShowCraftingTab();
}
else
{
InventoryScreen->ShowInventoryTab();
}
return;
}
if (bOpen == bInventoryOpen)
{
return;
}
bInventoryOpen = bOpen;
// Une fermeture, quelle qu'en soit la cause -- Tab, Echap, la mort --
// rend le poste ET le coffre. Le faire ici et nulle part ailleurs garantit
// qu'aucun chemin de sortie ne laisse l'etabli accessible depuis l'autre
// bout de la carte, ni un couvercle leve derriere soi.
if (!bInventoryOpen)
{
ReleaseActiveStation();
ReleaseActiveStorage();
}
if (bInventoryOpen)
{
if (bShowCraftingTab)
{
InventoryScreen->ShowCraftingTab();
}
else
{
InventoryScreen->ShowInventoryTab();
}
}
InventoryScreen->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
// Fermer l'ecran doit toujours rendre la barre, meme si on l'avait laissee
// sur l'onglet Fabrication.
UpdateHotbarVisibility();
SetUiInputMode(bInventoryOpen);
}
void AFpsPlayerController::SetUiInputMode(bool bUiOpen)
{
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
if (bUiOpen)
{
// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
// Input ne passerait et on ne pourrait plus refermer avec Tab.
FInputModeGameAndUI Mode;
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
Mode.SetHideCursorDuringCapture(false);
SetInputMode(Mode);
SetShowMouseCursor(true);
// Le mode GameAndUI laisse passer le jeu : sans ces deux appels, la
// souris ferait tourner la camera pendant qu'on navigue dans la grille.
SetIgnoreLookInput(true);
SetIgnoreMoveInput(true);
// Les deux appels ci-dessus ne couvrent que le deplacement et le regard.
// Accroupi, sprint, saut et interaction resteraient actifs -- appuyer
// sur Ctrl pour glisser un objet ferait s'accroupir le personnage.
// DisableInput retire l'input component du PAWN de la pile ; celui du
// controller reste, donc Tab continue de repondre.
if (PlayerPawn)
{
PlayerPawn->ClearTransientInputStates();
PlayerPawn->DisableInput(this);
}
}
else
{
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
// Reset et non SetIgnoreXxx(false) : ces fonctions gerent un compteur,
// et un desequilibre finirait par bloquer definitivement les inputs.
ResetIgnoreLookInput();
ResetIgnoreMoveInput();
if (PlayerPawn)
{
PlayerPawn->EnableInput(this);
}
}
}
void AFpsPlayerController::OpenStorage(AStorageContainer* Container)
{
if (!Container)
{
return;
}
// Appelee depuis Interact_Implementation, donc sur le SERVEUR. Le couvercle
// est de l'etat de monde : il se leve ICI, et se replique aux quatre
// joueurs. Seul l'ecran repart chez le client.
if (!IsLocalController())
{
Container->SetOpen(true);
Client_OpenStorage(Container);
return;
}
if (!InventoryScreen)
{
return;
}
// Deja devant un autre coffre : on rend le precedent avant d'ouvrir le
// nouveau, sinon son couvercle resterait leve pour le reste de la partie.
ReleaseActiveStorage();
ActiveStorage = Container;
bStorageOpen = true;
InventoryScreen->ShowStorage(Container);
// SetOpen ne fait rien sans autorite : chez un client c'est la replication
// de bOpen qui animera le couvercle. L'appel reste la pour l'hote, qui est
// bien l'autorite et doit lever le sien tout de suite.
Container->SetOpen(true);
// La barre rapide reste visible et cliquable par-dessus l'ecran : on lui
// donne le coffre pour que le clic droit y marche aussi.
if (Hotbar)
{
Hotbar->SetQuickTransferTarget(Container->GetStorage());
}
// Le coffre n'est pas un ecran a part : c'est un panneau de plus sur
// l'ecran d'inventaire, sur l'onglet Inventaire. Consequence directe -- le
// sac du joueur ne bouge pas d'un pixel entre les deux situations.
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/false);
// La surveillance ne tourne QUE pendant l'ecran : un timer permanent pour
// une condition vraie deux minutes par partie serait du gaspillage.
GetWorldTimerManager().SetTimer(
StorageDistanceTimerHandle, this, &AFpsPlayerController::CheckStorageDistance,
StorageDistanceCheckPeriod, /*bLoop=*/true);
}
void AFpsPlayerController::Client_OpenStorage_Implementation(AStorageContainer* Container)
{
// Chez le client IsLocalController() est vrai : OpenStorage prend donc sa
// branche locale et ouvre reellement l'ecran. Le couvercle, lui, a deja ete
// leve par le serveur et arrive par replication de bOpen.
OpenStorage(Container);
}
void AFpsPlayerController::CloseStorage()
{
if (!bStorageOpen)
{
return;
}
// Le panneau vit dans l'ecran d'inventaire : le refermer ferme l'ecran
// entier, qui rend le coffre au passage par ReleaseActiveStorage().
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
void AFpsPlayerController::ReleaseActiveStorage()
{
if (!bStorageOpen)
{
return;
}
bStorageOpen = false;
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
if (AStorageContainer* Container = ActiveStorage.Get())
{
// Chez un client SetOpen ne fait rien : le couvercle appartient au
// serveur. On le lui demande, sinon le coffre resterait ouvert pour les
// trois autres joueurs alors que l'ecran est referme ici.
if (HasAuthority())
{
Container->SetOpen(false);
}
else
{
Server_ReleaseStorage(Container);
}
}
ActiveStorage.Reset();
if (InventoryScreen)
{
InventoryScreen->HideStorage();
}
if (Hotbar)
{
Hotbar->SetQuickTransferTarget(nullptr);
}
}
void AFpsPlayerController::CheckStorageDistance()
{
if (!bStorageOpen)
{
return;
}
const AStorageContainer* Container = ActiveStorage.Get();
const APawn* PlayerPawn = GetPawn();
// Coffre detruit ou joueur sans corps : dans les deux cas l'ecran pilote
// quelque chose qui n'existe plus.
if (!Container || !PlayerPawn)
{
CloseStorage();
return;
}
// DistSquared : on evite une racine carree quatre fois par seconde pour une
// comparaison qui n'a pas besoin de la distance reelle.
const float MaxDistSquared = FMath::Square(MaxStorageDistance);
if (FVector::DistSquared(Container->GetActorLocation(), PlayerPawn->GetActorLocation()) > MaxDistSquared)
{
CloseStorage();
}
}
void AFpsPlayerController::HandlePauseInput()
{
// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
// on l'ignore quand meme : repondre ici refermerait le menu en laissant la
// question posee.
if (bQuitToMenuDialogOpen)
{
return;
}
// L'ecran de reglages consomme Echap lui-meme tant qu'il a le focus. Comme
// pour la boite, on ne repond ici que s'il l'a perdu.
if (bSettingsOpen)
{
HandleSettingsCloseRequested();
return;
}
// Echap sert d'abord a annuler ce qui est ouvert. Le coffre passe avant
// l'inventaire : les deux ne sont jamais ouverts ensemble, mais l'ordre
// resterait juste le jour ou ca changerait.
if (bStorageOpen)
{
CloseStorage();
return;
}
if (bInventoryOpen)
{
ToggleInventory();
return;
}
if (bPauseMenuOpen)
{
ResumeGame();
return;
}
OpenPauseMenu();
}
void AFpsPlayerController::OpenPauseMenu()
{
if (!PauseMenu || bPauseMenuOpen)
{
return;
}
// Mettre en pause pendant l'ecran de mort figerait le timer de respawn : le
// joueur resterait devant un ecran noir dont plus rien ne le sortirait.
if (GetWorldTimerManager().IsTimerActive(RespawnTimerHandle))
{
return;
}
bPauseMenuOpen = true;
PauseMenu->SetVisibility(ESlateVisibility::Visible);
// AUCUN SetPause : en coop la pause est impossible par nature. Cote client
// l'appel est ignore -- la pause appartient a l'autorite -- et cote hote il
// figerait le monde des quatre joueurs parce qu'un seul a ouvert son menu.
// Le monde continue donc de tourner, comme dans Valheim ou Raft : ouvrir son
// menu ne met pas a l'abri, et c'est coherent avec un jeu de survie.
//
// Consequence directe : il faut desormais couper l'input du PAWN, ce que la
// pause faisait gratuitement. C'est exactement le travail de SetUiInputMode,
// deja utilise par l'inventaire et le coffre -- un second chemin finirait
// par diverger, et le symptome (camera qui tourne derriere le menu, sprint
// bloque en sortant) se cherche tres longtemps.
SetUiInputMode(true);
}
void AFpsPlayerController::ResumeGame()
{
if (!bPauseMenuOpen)
{
return;
}
// Reprendre alors qu'une boite ou les reglages sont encore ouverts les
// laisserait affiches par-dessus la partie relancee.
if (bQuitToMenuDialogOpen)
{
HandleQuitToMenuCancelled();
}
if (bSettingsOpen)
{
HandleSettingsCloseRequested();
}
bPauseMenuOpen = false;
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
SetUiInputMode(false);
}
FString AFpsPlayerController::ResolveLocalPlayerNickname() const
{
// On passe par l'interface generique, jamais par du Steam explicite : le
// meme code rend le pseudo Steam en ligne et le nom local en LAN.
if (const IOnlineSubsystem* Online = IOnlineSubsystem::Get())
{
const IOnlineIdentityPtr Identity = Online->GetIdentityInterface();
if (Identity.IsValid())
{
const FString Nickname = Identity->GetPlayerNickname(0);
if (!Nickname.IsEmpty())
{
return Nickname;
}
}
}
// Repli sans service en ligne : le nom du LocalPlayer, qui vaut le plus
// souvent "Player". Mieux qu'une etiquette vide.
if (const ULocalPlayer* LocalPlayerRef = GetLocalPlayer())
{
return LocalPlayerRef->GetNickname();
}
return FString();
}
void AFpsPlayerController::Server_SetPlayerNickname_Implementation(const FString& Nickname)
{
APlayerState* State = GetPlayerState<APlayerState>();
if (!State || Nickname.IsEmpty())
{
return;
}
// Borne de longueur : un pseudo Steam monte a 32 caracteres, mais rien
// n'empeche un client modifie d'en envoyer un de dix mille. On tronque
// plutot que de refuser -- refuser laisserait une etiquette vide, tronquer
// donne un nom lisible.
State->SetPlayerName(Nickname.Left(32));
UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : pseudo [%s]."), *State->GetPlayerName());
}
void AFpsPlayerController::SubmitCharacterAppearance(const FCharacterAppearance& InAppearance)
{
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetCharacterAppearance(InAppearance);
// Ecrit tout de suite, contrairement aux reglages de l'ecran d'options
// qui attendent sa fermeture : un changement d'apparence n'a pas
// d'ecran a fermer quand il vient de la console, et le perdre a la
// sortie du jeu serait le plus sur moyen de croire que rien ne marche.
Settings->SaveSettings();
}
// Meme raccourci que pour le pseudo : l'hote est deja l'autorite, passer
// par la RPC marcherait mais ferait un aller-retour pour rien.
if (HasAuthority())
{
Server_SetCharacterAppearance_Implementation(InAppearance);
}
else
{
Server_SetCharacterAppearance(InAppearance);
}
}
void AFpsPlayerController::AnnounceCharacterAppearance()
{
if (const USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
SubmitCharacterAppearance(Settings->GetCharacterAppearance());
}
}
void AFpsPlayerController::Server_SetCharacterAppearance_Implementation(const FCharacterAppearance& InAppearance)
{
if (ASurvivalPlayerState* State = GetPlayerState<ASurvivalPlayerState>())
{
State->SetCharacterAppearance(InAppearance);
}
}
void AFpsPlayerController::ShowInviteFriends()
{
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
{
Sessions->ShowInviteUI();
}
}
bool AFpsPlayerController::CanInviteFriends() const
{
const USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
return Sessions && Sessions->CanInvite();
}
void AFpsPlayerController::OpenSettings()
{
if (bSettingsOpen)
{
return;
}
if (!SettingsMenuClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas."));
return;
}
// Creee a la premiere ouverture seulement : un ecran de reglages qu'on ne
// visite peut-etre jamais n'a pas a peser sur chaque demarrage de partie.
if (!SettingsMenu)
{
SettingsMenu = CreateWidget<USettingsMenuWidget>(this, SettingsMenuClass);
if (!SettingsMenu)
{
return;
}
SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
SettingsMenu->AddToViewport(ZOrderInGameSettingsMenu);
// L'ecran de reglages est partage avec le menu principal, dont
// l'empilement n'a rien a voir avec celui du jeu : c'est a nous, qui
// possedons la pile, de lui dire ou poser sa boite. +10 la place devant
// lui sans empieter sur la boite d'abandon de partie a 200.
SettingsMenu->SetDialogZOrder(ZOrderInGameSettingsMenu + 10);
}
bSettingsOpen = true;
SettingsMenu->Open();
// Le menu de pause reste lisible derriere mais cesse de repondre : sans ca
// on pourrait cliquer Reprendre a travers l'ecran de reglages.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void AFpsPlayerController::HandleSettingsCloseRequested()
{
bSettingsOpen = false;
if (SettingsMenu)
{
SettingsMenu->Close();
}
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
}
}
void AFpsPlayerController::RequestQuitToMainMenu()
{
// Le fondu de sortie est deja lance : on ignore le second clic plutot que
// d'empiler deux chargements.
if (bQuittingToMainMenu)
{
return;
}
if (MainMenuLevel.IsNull())
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : MainMenuLevel n'est pas assignee, impossible de retourner au menu."));
return;
}
// Sans boite assignee on ne bloque pas le joueur dans sa partie : on part.
if (!QuitToMenuDialogClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : QuitToMenuDialogClass n'est pas assignee, retour au menu sans confirmation."));
HandleQuitToMenuConfirmed();
return;
}
// Creee au premier clic seulement, puis reutilisee.
if (!QuitToMenuDialog)
{
QuitToMenuDialog = CreateWidget<UConfirmDialogWidget>(this, QuitToMenuDialogClass);
if (!QuitToMenuDialog)
{
return;
}
QuitToMenuDialog->OnConfirmed.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuConfirmed);
QuitToMenuDialog->OnCancelled.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuCancelled);
QuitToMenuDialog->AddToViewport(ZOrderQuitToMenuDialog);
}
bQuitToMenuDialogOpen = true;
QuitToMenuDialog->Open(QuitToMenuMessage);
// Le menu reste lisible derriere mais cesse de repondre : sans ca, on
// pourrait cliquer Reprendre alors que la question est encore posee.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void AFpsPlayerController::HandleQuitToMenuConfirmed()
{
bQuitToMenuDialogOpen = false;
// L'hote emmene tout le monde avec lui : sa partie EST la partie. On
// previent avant de fondre, pour que les quatre fondus tournent ensemble et
// que personne ne voie l'ecran se figer.
if (HasAuthority())
{
KickRemoteClientsToMainMenu();
}
BeginQuitToMainMenu();
}
void AFpsPlayerController::HandleSessionTravelPending()
{
USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
if (!Sessions || bQuittingToMainMenu)
{
return;
}
// Les ecrans ouverts resteraient affiches par-dessus le noir, curseur
// compris, et arriveraient tels quels dans la partie de l'ami.
if (bInventoryOpen)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
if (bPauseMenuOpen)
{
ResumeGame();
}
ScreenFade->FadeOut(Sessions->GetTravelFadeDuration());
}
void AFpsPlayerController::Client_ReturnToMainMenu_Implementation()
{
UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : l'hote a quitte la partie, retour au menu."));
BeginQuitToMainMenu();
}
void AFpsPlayerController::KickRemoteClientsToMainMenu()
{
UWorld* World = GetWorld();
if (!World || !HasAuthority())
{
return;
}
for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It)
{
AFpsPlayerController* Other = Cast<AFpsPlayerController>(It->Get());
// Soi-meme exclu : on a notre propre chemin, et l'hote n'est de toute
// facon pas joignable par une RPC Client_ depuis lui-meme.
if (Other && Other != this)
{
Other->Client_ReturnToMainMenu();
}
}
}
void AFpsPlayerController::BeginQuitToMainMenu()
{
// Un client peut avoir clique Quitter juste avant de recevoir l'ordre de
// l'hote : deux fondus simultanes se marcheraient dessus.
if (bQuittingToMainMenu)
{
return;
}
bQuittingToMainMenu = true;
// Le GameInstance doit savoir que ce depart est VOULU : sans ca, la
// connexion qui tombe pendant le fondu declencherait son filet de secours,
// qui chargerait le menu immediatement et couperait le fondu en deux.
if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
{
SurvivalGameInstance->BeginReturnToMainMenu();
}
if (QuitToMenuDialog)
{
QuitToMenuDialog->Close();
}
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
// L'ecran d'inventaire ou de coffre resterait affiche par-dessus le noir,
// curseur compris. Sa fermeture rend le coffre au passage.
if (bInventoryOpen)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
ScreenFade->FadeOut(QuitToMenuFadeDuration);
}
void AFpsPlayerController::HandleQuitToMenuCancelled()
{
bQuitToMenuDialogOpen = false;
if (QuitToMenuDialog)
{
QuitToMenuDialog->Close();
}
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
}
}
void AFpsPlayerController::HandleFadeFinished()
{
if (!bQuittingToMainMenu)
{
return;
}
// Fermer la session AVANT de voyager. Chez l'hote c'est ce qui la retire de
// la liste de tes amis a l'instant ou tu quittes, plutot qu'a l'arrivee sur
// le menu ; chez un client c'est ce qui libere sa place.
//
// Les autres joueurs, eux, perdront la connexion : c'est
// USurvivalGameInstance::HandleNetworkFailure qui les ramene a leur menu.
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
{
Sessions->LeaveSession();
}
UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}
void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (SettingsMenu)
{
SettingsMenu->RemoveFromParent();
SettingsMenu = nullptr;
}
if (QuitToMenuDialog)
{
QuitToMenuDialog->RemoveFromParent();
QuitToMenuDialog = nullptr;
}
if (PauseMenu)
{
PauseMenu->RemoveFromParent();
PauseMenu = nullptr;
}
if (InventoryScreen)
{
InventoryScreen->RemoveFromParent();
InventoryScreen = nullptr;
}
// Le timer d'eloignement rappellerait une methode d'un controller en train
// de disparaitre. ClearTimer est sans effet s'il ne tourne pas.
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
if (InteractionPrompt)
{
InteractionPrompt->RemoveFromParent();
InteractionPrompt = nullptr;
}
if (Hotbar)
{
Hotbar->RemoveFromParent();
Hotbar = nullptr;
}
if (SurvivalStatsWidget)
{
SurvivalStatsWidget->RemoveFromParent();
SurvivalStatsWidget = nullptr;
}
if (Crosshair)
{
Crosshair->RemoveFromParent();
Crosshair = nullptr;
}
// Le voile de fondu est nettoye par UScreenFadeComponent::EndPlay.
Super::EndPlay(EndPlayReason);
}
#undef LOCTEXT_NAMESPACE