2 Commits
Author SHA1 Message Date
Dolobarsch befed4c2fa Merge pull request 'Neues Menüsystem implementiert' (#5) from Basic-HUD into main
Reviewed-on: #5
2026-09-05 00:35:26 +02:00
Dolobarsch b55eda5e51 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.
2026-09-05 00:33:04 +02:00
31 changed files with 794 additions and 2 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
[/Script/EngineSettings.GameMapsSettings]
GameDefaultMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
EditorStartupMap=/Game/ThirdPerson/Lvl_ThirdPerson.Lvl_ThirdPerson
GameDefaultMap=/Game/Levels/MainMenu.MainMenu
EditorStartupMap=/Game/Levels/MainMenu.MainMenu
GlobalDefaultGameMode=/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode_C
bOffsetPlayerGamepadIds=False
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
};