1198 lines
33 KiB
C++
1198 lines
33 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 "SurvivalStatsComponent.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
if (bFadeInOnStart)
|
|
{
|
|
ScreenFade->FadeIn(StartupFadeDuration);
|
|
}
|
|
}
|
|
|
|
void AFpsPlayerController::OnPossess(APawn* InPawn)
|
|
{
|
|
Super::OnPossess(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 (Crosshair) { /* pas d'abonnement */ }
|
|
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())
|
|
{
|
|
Stats->OnDied.AddDynamic(this, &AFpsPlayerController::HandlePawnDied);
|
|
}
|
|
}
|
|
}
|
|
|
|
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::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;
|
|
bQuittingToMainMenu = true;
|
|
|
|
if (QuitToMenuDialog)
|
|
{
|
|
QuitToMenuDialog->Close();
|
|
}
|
|
|
|
if (PauseMenu)
|
|
{
|
|
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
|
|
}
|
|
|
|
// On reste en pause pendant le fondu : relancer le monde une seconde pour
|
|
// l'abandonner ensuite laisserait le temps a la faim ou a une chute de tuer
|
|
// un joueur qui a deja quitte. C'est UScreenFadeComponent qui continue de
|
|
// ticker en pause, pas le monde.
|
|
ScreenFade->FadeOut(QuitToMenuFadeDuration);
|
|
}
|
|
|
|
void AFpsPlayerController::HandleQuitToMenuCancelled()
|
|
{
|
|
bQuitToMenuDialogOpen = false;
|
|
|
|
if (QuitToMenuDialog)
|
|
{
|
|
QuitToMenuDialog->Close();
|
|
}
|
|
|
|
if (PauseMenu)
|
|
{
|
|
PauseMenu->SetVisibility(ESlateVisibility::Visible);
|
|
}
|
|
}
|
|
|
|
void AFpsPlayerController::HandleFadeFinished()
|
|
{
|
|
if (!bQuittingToMainMenu)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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
|