(Feat) Add MainMenu

This commit is contained in:
2026-07-28 14:10:17 +02:00
parent ed7c7fd991
commit 175bef228c
33 changed files with 2331 additions and 12 deletions
@@ -4,6 +4,7 @@
#include "FpsPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
@@ -14,9 +15,14 @@
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "InventoryWidget.h"
#include "Kismet/GameplayStatics.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SettingsMenuWidget.h"
#include "SurvivalStatsWidget.h"
#define LOCTEXT_NAMESPACE "PauseMenu"
namespace
{
// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
@@ -35,6 +41,12 @@ namespace
constexpr int32 ZOrderInteractionPrompt = 10;
constexpr int32 ZOrderInventory = 20;
constexpr int32 ZOrderHotbar = 30;
// Le menu de pause passe devant tout le HUD, et la boite de confirmation
// devant lui. Meme echelle que dans le menu principal.
constexpr int32 ZOrderPauseMenu = 100;
constexpr int32 ZOrderSettingsMenu = 150;
constexpr int32 ZOrderQuitToMenuDialog = 200;
}
AFpsPlayerController::AFpsPlayerController()
@@ -45,12 +57,29 @@ AFpsPlayerController::AFpsPlayerController()
// 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);
}
// 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);
@@ -157,6 +186,15 @@ void AFpsPlayerController::SetupInputComponent()
{
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
@@ -298,6 +336,24 @@ void AFpsPlayerController::CreateHudWidgets()
}
}
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()
@@ -356,8 +412,277 @@ void AFpsPlayerController::ToggleInventory()
}
}
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. Fermer l'inventaire est
// ce que le joueur attend en premier.
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);
// GameAndUI et non UIOnly, meme raison que l'inventaire : en UIOnly plus
// aucune action Enhanced Input ne passe, et Echap ne refermerait rien.
FInputModeGameAndUI Mode;
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
Mode.SetHideCursorDuringCapture(false);
SetInputMode(Mode);
SetShowMouseCursor(true);
// Pas de DisableInput sur le pawn ici, contrairement a l'inventaire : le
// monde est reellement fige, et les actions du pawn n'ont pas
// "Trigger When Paused" -- elles ne se declenchent donc plus d'elles-memes.
SetPause(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);
}
SetPause(false);
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
}
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(ZOrderSettingsMenu);
}
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;
}
// La pause vit sur les WorldSettings, que OpenLevel detruit avec le monde.
// On la leve quand meme : un monde qu'on quitte en etat de pause est un
// piege si le chargement echoue.
SetPause(false);
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 (InventoryWidget)
{
InventoryWidget->RemoveFromParent();
@@ -392,3 +717,5 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
Super::EndPlay(EndPlayReason);
}
#undef LOCTEXT_NAMESPACE