Neues Menüsystem implementiert

Ein flexibles Menüsystem wurde hinzugefügt, das ein Hauptmenü und ein In-Game-Menü umfasst. Das Hauptmenü bietet Optionen wie "Neues Spiel starten", "Optionen" und "Credits". Das In-Game-Menü ermöglicht Pausenfunktionen wie "Fortsetzen", "Optionen" und "Zum Desktop beenden".

Neue Klassen wurden eingeführt, darunter `MenuManagerSubsystem` zur Verwaltung von Menübildschirmen, `MenuScreenWidget` als Basisklasse für Menüs sowie spezifische Widgets wie `MainMenuWidget`, `InGameMenuWidget` und `OptionsMenuWidget`.

Zusätzlich wurden stilisierte Schaltflächen (`MenuButtonWidget`) und ein neuer `MenuPlayerController` für menübasierte Levels implementiert. Änderungen an `KingshearthLegacyPlayerController` ermöglichen die Steuerung des In-Game-Menüs. Binäre Assets für Menüs und UI wurden hinzugefügt.
This commit is contained in:
2026-09-05 00:33:04 +02:00
parent 64f4a59b0b
commit b55eda5e51
31 changed files with 794 additions and 2 deletions
@@ -30,6 +30,7 @@ public class KingshearthLegacy : ModuleRules
"KingshearthLegacy",
"KingshearthLegacy/AttributeSets",
"KingshearthLegacy/GameplayEffects",
"KingshearthLegacy/UI/Menu",
"KingshearthLegacy/Variant_Platforming",
"KingshearthLegacy/Variant_Platforming/Animation",
"KingshearthLegacy/Variant_Combat",
@@ -2,13 +2,17 @@
#include "KingshearthLegacyPlayerController.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
#include "InputMappingContext.h"
#include "Blueprint/UserWidget.h"
#include "KingshearthLegacy.h"
#include "Widgets/Input/SVirtualJoystick.h"
#include "UI/PlayerHUDWidget.h"
#include "UI/Menu/InGameMenuWidget.h"
#include "UI/Menu/MenuManagerSubsystem.h"
#include "KingshearthLegacyCharacter.h"
void AKingshearthLegacyPlayerController::BeginPlay()
@@ -59,6 +63,14 @@ void AKingshearthLegacyPlayerController::SetupInputComponent()
}
}
}
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
{
if (MenuAction)
{
EnhancedInputComponent->BindAction(MenuAction, ETriggerEvent::Started, this, &AKingshearthLegacyPlayerController::ToggleInGameMenu);
}
}
}
}
@@ -100,3 +112,27 @@ bool AKingshearthLegacyPlayerController::ShouldUseTouchControls() const
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}
void AKingshearthLegacyPlayerController::ToggleInGameMenu()
{
if (!IsLocalPlayerController() || !InGameMenuClass)
{
return;
}
UGameInstance* GI = GetGameInstance();
UMenuManagerSubsystem* MenuManager = GI ? GI->GetSubsystem<UMenuManagerSubsystem>() : nullptr;
if (!MenuManager)
{
return;
}
if (MenuManager->IsAnyScreenOpen())
{
MenuManager->PopScreen();
}
else
{
MenuManager->PushScreen(InGameMenuClass);
}
}
@@ -7,8 +7,10 @@
#include "KingshearthLegacyPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
class UUserWidget;
class UPlayerHUDWidget;
class UInGameMenuWidget;
/**
* Basic PlayerController class for a third person game
@@ -49,6 +51,14 @@ protected:
UPROPERTY(BlueprintReadOnly, Category = "UI")
TObjectPtr<UPlayerHUDWidget> PlayerHUD;
/** In-game pause menu widget, pushed/popped through the menu manager by ToggleInGameMenu */
UPROPERTY(EditAnywhere, Category = "UI")
TSubclassOf<UInGameMenuWidget> InGameMenuClass;
/** Input action that opens the in-game menu, or closes the top menu screen if one is already open */
UPROPERTY(EditAnywhere, Category = "Input|Input Mappings")
TObjectPtr<UInputAction> MenuAction;
/** Gameplay initialization */
virtual void BeginPlay() override;
@@ -61,4 +71,8 @@ protected:
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
/** Opens InGameMenuClass, or closes the top menu screen if one is already open */
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleInGameMenu();
};
@@ -0,0 +1,15 @@
#pragma once
#include "CoreMinimal.h"
#include "UI/Menu/MenuScreenWidget.h"
#include "CreditsMenuWidget.generated.h"
/**
* Placeholder credits screen - the inherited BackButton is all it needs today.
* The actual credits text/scroll is a Blueprint (WBP_CreditsMenu) content concern.
*/
UCLASS(abstract)
class UCreditsMenuWidget : public UMenuScreenWidget
{
GENERATED_BODY()
};
@@ -0,0 +1,64 @@
#include "InGameMenuWidget.h"
#include "MenuButtonWidget.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
void UInGameMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (ContinueButton)
{
ContinueButton->OnClicked.AddUniqueDynamic(this, &UInGameMenuWidget::Continue);
}
if (OptionsButton)
{
OptionsButton->OnClicked.AddUniqueDynamic(this, &UInGameMenuWidget::OpenOptions);
}
if (QuitToMenuButton)
{
QuitToMenuButton->OnClicked.AddUniqueDynamic(this, &UInGameMenuWidget::QuitToMenu);
}
if (QuitToDesktopButton)
{
QuitToDesktopButton->OnClicked.AddUniqueDynamic(this, &UInGameMenuWidget::QuitToDesktop);
}
UGameplayStatics::SetGamePaused(this, true);
}
void UInGameMenuWidget::NotifyScreenClosing()
{
UGameplayStatics::SetGamePaused(this, false);
}
void UInGameMenuWidget::Continue()
{
Close();
}
void UInGameMenuWidget::OpenOptions()
{
OpenScreen(OptionsScreenClass);
}
void UInGameMenuWidget::QuitToMenu()
{
if (!MainMenuLevel.IsNull())
{
UGameplayStatics::SetGamePaused(this, false);
UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}
}
void UInGameMenuWidget::QuitToDesktop()
{
if (APlayerController* PC = GetOwningPlayer())
{
UKismetSystemLibrary::QuitGame(this, PC, EQuitPreference::Quit, false);
}
}
@@ -0,0 +1,57 @@
#pragma once
#include "CoreMinimal.h"
#include "UI/Menu/MenuScreenWidget.h"
#include "InGameMenuWidget.generated.h"
class UMenuButtonWidget;
class UWorld;
/**
* Pause screen opened from gameplay (see AKingshearthLegacyPlayerController::ToggleInGameMenu).
* Pauses the game while open; NotifyScreenClosing unpauses however the screen ends up closed
* (Continue, an options sub-screen going back to it, or the toggle input closing it directly).
*/
UCLASS(abstract)
class UInGameMenuWidget : public UMenuScreenWidget
{
GENERATED_BODY()
public:
/** Screen pushed by the Options button. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|Screens")
TSubclassOf<UMenuScreenWidget> OptionsScreenClass;
/** Level opened by Quit to Menu. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|Quit")
TSoftObjectPtr<UWorld> MainMenuLevel;
virtual void NotifyScreenClosing() override;
protected:
virtual void NativeConstruct() override;
UFUNCTION(BlueprintCallable, Category = "Menu")
void Continue();
UFUNCTION(BlueprintCallable, Category = "Menu")
void OpenOptions();
UFUNCTION(BlueprintCallable, Category = "Menu")
void QuitToMenu();
UFUNCTION(BlueprintCallable, Category = "Menu")
void QuitToDesktop();
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> ContinueButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> OptionsButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> QuitToMenuButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> QuitToDesktopButton;
};
@@ -0,0 +1,68 @@
#include "MainMenuWidget.h"
#include "MenuButtonWidget.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (StartNewGameButton)
{
StartNewGameButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::StartNewGame);
}
if (LoadGameButton)
{
LoadGameButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::LoadGame);
LoadGameButton->SetIsEnabled(bLoadGameAvailable);
}
if (OptionsButton)
{
OptionsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OpenOptions);
}
if (CreditsButton)
{
CreditsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OpenCredits);
}
if (ExitButton)
{
ExitButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::ExitGame);
}
}
void UMainMenuWidget::StartNewGame()
{
if (!NewGameLevel.IsNull())
{
UGameplayStatics::OpenLevelBySoftObjectPtr(this, NewGameLevel);
}
}
void UMainMenuWidget::LoadGame()
{
// No save system yet - intentionally left unimplemented. The button is disabled by
// default via bLoadGameAvailable until one exists.
}
void UMainMenuWidget::OpenOptions()
{
OpenScreen(OptionsScreenClass);
}
void UMainMenuWidget::OpenCredits()
{
OpenScreen(CreditsScreenClass);
}
void UMainMenuWidget::ExitGame()
{
if (APlayerController* PC = GetOwningPlayer())
{
UKismetSystemLibrary::QuitGame(this, PC, EQuitPreference::Quit, false);
}
}
@@ -0,0 +1,69 @@
#pragma once
#include "CoreMinimal.h"
#include "UI/Menu/MenuScreenWidget.h"
#include "MainMenuWidget.generated.h"
class UMenuButtonWidget;
class UWorld;
/**
* Top-level main menu screen. Add a new entry the same way as the existing ones: a
* BindWidgetOptional UMenuButtonWidget pointer + an OnClicked binding in NativeConstruct.
*/
UCLASS(abstract)
class UMainMenuWidget : public UMenuScreenWidget
{
GENERATED_BODY()
public:
/** Level opened by Start New Game. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|New Game")
TSoftObjectPtr<UWorld> NewGameLevel;
/** Screen pushed by the Options button. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|Screens")
TSubclassOf<UMenuScreenWidget> OptionsScreenClass;
/** Screen pushed by the Credits button. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|Screens")
TSubclassOf<UMenuScreenWidget> CreditsScreenClass;
/** No save system exists yet - keeps the Load Game button present but disabled until one does. */
UPROPERTY(EditDefaultsOnly, Category = "Menu|New Game")
bool bLoadGameAvailable = false;
protected:
virtual void NativeConstruct() override;
UFUNCTION(BlueprintCallable, Category = "Menu")
void StartNewGame();
/** Intentionally unimplemented - no save system exists yet. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void LoadGame();
UFUNCTION(BlueprintCallable, Category = "Menu")
void OpenOptions();
UFUNCTION(BlueprintCallable, Category = "Menu")
void OpenCredits();
UFUNCTION(BlueprintCallable, Category = "Menu")
void ExitGame();
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> StartNewGameButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> LoadGameButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> OptionsButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> CreditsButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> ExitButton;
};
@@ -0,0 +1,68 @@
#include "MenuButtonWidget.h"
#include "Components/Button.h"
#include "Components/Border.h"
#include "Components/TextBlock.h"
void UMenuButtonWidget::NativePreConstruct()
{
Super::NativePreConstruct();
RefreshStyle();
}
void UMenuButtonWidget::NativeConstruct()
{
Super::NativeConstruct();
if (DefaultButton)
{
DefaultButton->OnClicked.AddUniqueDynamic(this, &UMenuButtonWidget::HandleClicked);
DefaultButton->OnHovered.AddUniqueDynamic(this, &UMenuButtonWidget::HandleHovered);
DefaultButton->OnUnhovered.AddUniqueDynamic(this, &UMenuButtonWidget::HandleUnhovered);
}
if (Highlight)
{
Highlight->SetVisibility(ESlateVisibility::Collapsed);
}
RefreshStyle();
}
void UMenuButtonWidget::RefreshStyle()
{
if (ButtonText)
{
ButtonText->SetText(Label);
}
if (ButtonBackground)
{
ButtonBackground->SetBrushColor(BackgroundColor);
}
if (Highlight)
{
Highlight->SetBrushColor(HighlightColor);
}
}
void UMenuButtonWidget::HandleClicked()
{
OnClicked.Broadcast();
}
void UMenuButtonWidget::HandleHovered()
{
if (Highlight)
{
Highlight->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void UMenuButtonWidget::HandleUnhovered()
{
if (Highlight)
{
Highlight->SetVisibility(ESlateVisibility::Collapsed);
}
}
@@ -0,0 +1,70 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MenuButtonWidget.generated.h"
class UButton;
class UBorder;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMenuButtonClicked);
/**
* Styled menu button (WBP_Button): DefaultButton wraps ButtonBackground/ButtonText/Highlight.
* Bind to OnClicked instead of reaching into DefaultButton - callers shouldn't need to know
* this is built out of a Button + two Borders + a TextBlock. Label/BackgroundColor/HighlightColor
* are instance-editable, so a widget that places a WBP_Button just configures it in its Details
* panel (no per-instance Blueprint graph needed).
*/
UCLASS(abstract)
class UMenuButtonWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Fired when the button is clicked. */
UPROPERTY(BlueprintAssignable, Category = "Button")
FOnMenuButtonClicked OnClicked;
/** Text shown on the button. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
FText Label;
/** ButtonBackground tint while idle. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|Style")
FLinearColor BackgroundColor = FLinearColor(0.03f, 0.03f, 0.03f, 0.75f);
/** Highlight tint shown while hovered/focused. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|Style")
FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
/** Pushes Label/BackgroundColor/HighlightColor onto the bound widgets. Runs in the Designer too, so edits preview live. */
UFUNCTION(BlueprintCallable, Category = "Button")
void RefreshStyle();
protected:
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
UFUNCTION()
void HandleClicked();
UFUNCTION()
void HandleHovered();
UFUNCTION()
void HandleUnhovered();
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UButton> DefaultButton;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UBorder> ButtonBackground;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UTextBlock> ButtonText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UBorder> Highlight;
};
@@ -0,0 +1,118 @@
#include "MenuManagerSubsystem.h"
#include "MenuScreenWidget.h"
#include "Engine/GameInstance.h"
#include "GameFramework/PlayerController.h"
#include "UObject/UObjectGlobals.h"
void UMenuManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UMenuManagerSubsystem::HandlePreLoadMap);
}
void UMenuManagerSubsystem::Deinitialize()
{
FCoreUObjectDelegates::PreLoadMap.RemoveAll(this);
Super::Deinitialize();
}
void UMenuManagerSubsystem::HandlePreLoadMap(const FString& MapName)
{
ClearStack();
}
UMenuScreenWidget* UMenuManagerSubsystem::PushScreen(TSubclassOf<UMenuScreenWidget> ScreenClass)
{
if (!ScreenClass)
{
return nullptr;
}
APlayerController* OwningController = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr;
if (!OwningController)
{
return nullptr;
}
UMenuScreenWidget* NewScreen = CreateWidget<UMenuScreenWidget>(OwningController, ScreenClass);
if (!NewScreen)
{
return nullptr;
}
// Hide (not just cover) whatever screen was on top - it stops rendering and stops taking
// input, rather than sitting underneath the new screen doing both pointlessly.
if (UMenuScreenWidget* PreviousTop = GetActiveScreen())
{
PreviousTop->SetVisibility(ESlateVisibility::Collapsed);
}
NewScreen->AddToPlayerScreen(static_cast<int32>(BaseZOrder + ScreenStack.Num()));
ScreenStack.Add(NewScreen);
RefreshInputMode();
return NewScreen;
}
void UMenuManagerSubsystem::PopScreen()
{
if (ScreenStack.Num() == 0)
{
return;
}
UMenuScreenWidget* TopScreen = ScreenStack.Pop();
if (TopScreen)
{
TopScreen->NotifyScreenClosing();
TopScreen->RemoveFromParent();
}
if (UMenuScreenWidget* NewTop = GetActiveScreen())
{
NewTop->SetVisibility(ESlateVisibility::Visible);
}
RefreshInputMode();
}
void UMenuManagerSubsystem::ClearStack()
{
for (UMenuScreenWidget* Screen : ScreenStack)
{
if (Screen)
{
Screen->NotifyScreenClosing();
Screen->RemoveFromParent();
}
}
ScreenStack.Reset();
RefreshInputMode();
}
UMenuScreenWidget* UMenuManagerSubsystem::GetActiveScreen() const
{
return ScreenStack.Num() > 0 ? ScreenStack.Last() : nullptr;
}
void UMenuManagerSubsystem::RefreshInputMode()
{
APlayerController* OwningController = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr;
if (!OwningController)
{
return;
}
if (UMenuScreenWidget* Top = GetActiveScreen())
{
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(Top->TakeWidget());
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
OwningController->SetInputMode(InputMode);
OwningController->SetShowMouseCursor(true);
}
else
{
OwningController->SetInputMode(FInputModeGameOnly());
OwningController->SetShowMouseCursor(false);
}
}
@@ -0,0 +1,59 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MenuManagerSubsystem.generated.h"
class UMenuScreenWidget;
/**
* Owns the stack of currently open menu screens (Main Menu, In-Game Menu, Options, Credits, ...).
* Any UMenuScreenWidget subclass can be pushed without this subsystem knowing about it in
* advance - add a new menu by writing the widget and pushing its class, no changes needed here.
*/
UCLASS()
class UMenuManagerSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
/** Creates ScreenClass, adds it above any currently open screens, and pushes it onto the stack. */
UFUNCTION(BlueprintCallable, Category = "Menu")
UMenuScreenWidget* PushScreen(TSubclassOf<UMenuScreenWidget> ScreenClass);
/** Notifies, then removes, the top screen. Restores game input once the stack is empty. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void PopScreen();
/** Notifies, then removes, every screen on the stack. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void ClearStack();
UFUNCTION(BlueprintPure, Category = "Menu")
UMenuScreenWidget* GetActiveScreen() const;
UFUNCTION(BlueprintPure, Category = "Menu")
bool IsAnyScreenOpen() const { return ScreenStack.Num() > 0; }
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
protected:
/** Applies UI-only input + shows the cursor while a screen is open, restores game input otherwise. */
void RefreshInputMode();
/**
* Bound to FCoreUObjectDelegates::PreLoadMap so ANY level travel clears the stack first,
* regardless of which screen (or future screen) triggered it. Without this, a screen that
* opens a level without popping itself first leaves the game viewport stuck in the UI-only
* input mode it set (SetInputMode configures the GameInstance's GameViewportClient, which
* survives a level load, unlike the World/PlayerController) - the new level's pawn would be
* possessed but unable to receive movement input.
*/
void HandlePreLoadMap(const FString& MapName);
UPROPERTY()
TArray<TObjectPtr<UMenuScreenWidget>> ScreenStack;
static constexpr int32 BaseZOrder = 100;
};
@@ -0,0 +1,22 @@
#include "MenuPlayerController.h"
#include "MainMenuWidget.h"
#include "MenuManagerSubsystem.h"
#include "Engine/GameInstance.h"
void AMenuPlayerController::BeginPlay()
{
Super::BeginPlay();
if (!IsLocalPlayerController() || !MainMenuClass)
{
return;
}
if (UGameInstance* GI = GetGameInstance())
{
if (UMenuManagerSubsystem* MenuManager = GI->GetSubsystem<UMenuManagerSubsystem>())
{
MenuManager->PushScreen(MainMenuClass);
}
}
}
@@ -0,0 +1,24 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MenuPlayerController.generated.h"
class UMainMenuWidget;
/**
* PlayerController for menu-only levels (no pawn, e.g. a dedicated main menu level).
* Pushes MainMenuClass through the menu manager on BeginPlay.
*/
UCLASS(abstract)
class AMenuPlayerController : public APlayerController
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "UI")
TSubclassOf<UMainMenuWidget> MainMenuClass;
protected:
virtual void BeginPlay() override;
};
@@ -0,0 +1,41 @@
#include "MenuScreenWidget.h"
#include "MenuManagerSubsystem.h"
#include "MenuButtonWidget.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
void UMenuScreenWidget::NativeConstruct()
{
Super::NativeConstruct();
if (BackButton)
{
BackButton->OnClicked.AddUniqueDynamic(this, &UMenuScreenWidget::HandleBackClicked);
}
}
UMenuManagerSubsystem* UMenuScreenWidget::GetMenuManager() const
{
const UWorld* World = GetWorld();
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
return GameInstance ? GameInstance->GetSubsystem<UMenuManagerSubsystem>() : nullptr;
}
void UMenuScreenWidget::Close()
{
if (UMenuManagerSubsystem* MenuManager = GetMenuManager())
{
MenuManager->PopScreen();
}
}
UMenuScreenWidget* UMenuScreenWidget::OpenScreen(TSubclassOf<UMenuScreenWidget> ScreenClass)
{
UMenuManagerSubsystem* MenuManager = GetMenuManager();
return MenuManager ? MenuManager->PushScreen(ScreenClass) : nullptr;
}
void UMenuScreenWidget::HandleBackClicked()
{
Close();
}
@@ -0,0 +1,50 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MenuScreenWidget.generated.h"
class UMenuButtonWidget;
class UMenuManagerSubsystem;
/**
* Base class for a full-screen menu (Main Menu, In-Game Menu, Options, Credits, ...).
* Subclass this for each new screen: add whatever BindWidgetOptional buttons/controls it
* needs, bind them to BlueprintCallable functions in NativeConstruct (see UMainMenuWidget),
* and push the screen via GetMenuManager()->PushScreen() or the OpenScreen() helper below.
* A BackButton is wired up automatically if the widget layout provides one.
*/
UCLASS(abstract)
class UMenuScreenWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Pops this screen off the menu stack. Bound automatically to BackButton if present. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void Close();
/** Pushes another screen on top of this one (e.g. Options, Credits). */
UFUNCTION(BlueprintCallable, Category = "Menu")
UMenuScreenWidget* OpenScreen(TSubclassOf<UMenuScreenWidget> ScreenClass);
/**
* Called by the menu manager right before this screen is removed from the stack,
* regardless of what triggered the pop (Close(), an input toggle, ClearStack(), ...).
* Override to clean up screen-specific state (e.g. UInGameMenuWidget unpausing).
*/
virtual void NotifyScreenClosing() {}
protected:
virtual void NativeConstruct() override;
UFUNCTION(BlueprintPure, Category = "Menu")
UMenuManagerSubsystem* GetMenuManager() const;
UFUNCTION()
void HandleBackClicked();
/** Optional - present on any screen that just needs a simple "go back" button (Options, Credits, ...). */
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UMenuButtonWidget> BackButton;
};
@@ -0,0 +1,16 @@
#pragma once
#include "CoreMinimal.h"
#include "UI/Menu/MenuScreenWidget.h"
#include "OptionsMenuWidget.generated.h"
/**
* Placeholder options screen - the inherited BackButton is all it needs today.
* Extend with real settings (volume sliders, graphics quality, key rebinding, ...) as they're
* built: add BindWidgetOptional controls and handlers here the same way UMainMenuWidget does.
*/
UCLASS(abstract)
class UOptionsMenuWidget : public UMenuScreenWidget
{
GENERATED_BODY()
};