// 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(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() : 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()) { 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(); 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(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(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(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()); Inventory->SelectHotbarSlot(Number - 1); } void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value) { const float Axis = Value.Get(); 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(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(this, CrosshairClass); if (Crosshair) { Crosshair->AddToViewport(ZOrderCrosshair); } } if (!InteractionPrompt) { if (InteractionPromptClass) { InteractionPrompt = CreateWidget(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(this, SurvivalStatsClass); if (SurvivalStatsWidget) { SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats); } } if (!Hotbar && HotbarClass) { Hotbar = CreateWidget(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(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(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(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(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(); 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()) { State->SetCharacterAppearance(InAppearance); } } void AFpsPlayerController::ShowInviteFriends() { if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr) { Sessions->ShowInviteUI(); } } bool AFpsPlayerController::CanInviteFriends() const { const USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : 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(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(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() : 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(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()) { 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() : 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