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:
@@ -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()
|
||||
};
|
||||
Reference in New Issue
Block a user