diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 254dd40..631e962 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -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 diff --git a/Content/FireSystem/FX/NS_CampFire.uasset b/Content/FireSystem/FX/NS_CampFire.uasset index d5a7bca..6bbb3fe 100644 Binary files a/Content/FireSystem/FX/NS_CampFire.uasset and b/Content/FireSystem/FX/NS_CampFire.uasset differ diff --git a/Content/FireSystem/Materials/M_Smoke.uasset b/Content/FireSystem/Materials/M_Smoke.uasset index 202c839..2e0ba9b 100644 Binary files a/Content/FireSystem/Materials/M_Smoke.uasset and b/Content/FireSystem/Materials/M_Smoke.uasset differ diff --git a/Content/Input/Actions/IA_Menu.uasset b/Content/Input/Actions/IA_Menu.uasset new file mode 100644 index 0000000..7c6b2e4 Binary files /dev/null and b/Content/Input/Actions/IA_Menu.uasset differ diff --git a/Content/Input/IMC_Default.uasset b/Content/Input/IMC_Default.uasset index 57c1ef7..7935a05 100644 Binary files a/Content/Input/IMC_Default.uasset and b/Content/Input/IMC_Default.uasset differ diff --git a/Content/Levels/MainMenu.umap b/Content/Levels/MainMenu.umap new file mode 100644 index 0000000..c66a436 Binary files /dev/null and b/Content/Levels/MainMenu.umap differ diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonPlayerController.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonPlayerController.uasset index c0d4fc0..bb6f4e5 100644 Binary files a/Content/ThirdPerson/Blueprints/BP_ThirdPersonPlayerController.uasset and b/Content/ThirdPerson/Blueprints/BP_ThirdPersonPlayerController.uasset differ diff --git a/Content/UI/Menu/BP_MainMenuGameMode.uasset b/Content/UI/Menu/BP_MainMenuGameMode.uasset new file mode 100644 index 0000000..633930a Binary files /dev/null and b/Content/UI/Menu/BP_MainMenuGameMode.uasset differ diff --git a/Content/UI/Menu/BP_MenuPlayerController.uasset b/Content/UI/Menu/BP_MenuPlayerController.uasset new file mode 100644 index 0000000..322a3b5 Binary files /dev/null and b/Content/UI/Menu/BP_MenuPlayerController.uasset differ diff --git a/Content/UI/Menu/WBP_Button.uasset b/Content/UI/Menu/WBP_Button.uasset new file mode 100644 index 0000000..bd7158e Binary files /dev/null and b/Content/UI/Menu/WBP_Button.uasset differ diff --git a/Content/UI/Menu/WBP_CreditsMenu.uasset b/Content/UI/Menu/WBP_CreditsMenu.uasset new file mode 100644 index 0000000..54267a7 Binary files /dev/null and b/Content/UI/Menu/WBP_CreditsMenu.uasset differ diff --git a/Content/UI/Menu/WBP_InGameMenu.uasset b/Content/UI/Menu/WBP_InGameMenu.uasset new file mode 100644 index 0000000..93954f8 Binary files /dev/null and b/Content/UI/Menu/WBP_InGameMenu.uasset differ diff --git a/Content/UI/Menu/WBP_MainMenu.uasset b/Content/UI/Menu/WBP_MainMenu.uasset new file mode 100644 index 0000000..7831e8d Binary files /dev/null and b/Content/UI/Menu/WBP_MainMenu.uasset differ diff --git a/Content/UI/Menu/WBP_OptionsMenu.uasset b/Content/UI/Menu/WBP_OptionsMenu.uasset new file mode 100644 index 0000000..7c7c6a9 Binary files /dev/null and b/Content/UI/Menu/WBP_OptionsMenu.uasset differ diff --git a/Source/KingshearthLegacy/KingshearthLegacy.Build.cs b/Source/KingshearthLegacy/KingshearthLegacy.Build.cs index c82cf22..d5407f7 100644 --- a/Source/KingshearthLegacy/KingshearthLegacy.Build.cs +++ b/Source/KingshearthLegacy/KingshearthLegacy.Build.cs @@ -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", diff --git a/Source/KingshearthLegacy/KingshearthLegacyPlayerController.cpp b/Source/KingshearthLegacy/KingshearthLegacyPlayerController.cpp index 7ff2101..f72a280 100644 --- a/Source/KingshearthLegacy/KingshearthLegacyPlayerController.cpp +++ b/Source/KingshearthLegacy/KingshearthLegacyPlayerController.cpp @@ -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(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() : nullptr; + if (!MenuManager) + { + return; + } + + if (MenuManager->IsAnyScreenOpen()) + { + MenuManager->PopScreen(); + } + else + { + MenuManager->PushScreen(InGameMenuClass); + } +} diff --git a/Source/KingshearthLegacy/KingshearthLegacyPlayerController.h b/Source/KingshearthLegacy/KingshearthLegacyPlayerController.h index cedc5b6..e35931c 100644 --- a/Source/KingshearthLegacy/KingshearthLegacyPlayerController.h +++ b/Source/KingshearthLegacy/KingshearthLegacyPlayerController.h @@ -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 PlayerHUD; + /** In-game pause menu widget, pushed/popped through the menu manager by ToggleInGameMenu */ + UPROPERTY(EditAnywhere, Category = "UI") + TSubclassOf 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 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(); + }; diff --git a/Source/KingshearthLegacy/UI/Menu/CreditsMenuWidget.h b/Source/KingshearthLegacy/UI/Menu/CreditsMenuWidget.h new file mode 100644 index 0000000..0bcffef --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/CreditsMenuWidget.h @@ -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() +}; diff --git a/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.cpp b/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.cpp new file mode 100644 index 0000000..93e58df --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.cpp @@ -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); + } +} diff --git a/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.h b/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.h new file mode 100644 index 0000000..77f7e2d --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/InGameMenuWidget.h @@ -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 OptionsScreenClass; + + /** Level opened by Quit to Menu. */ + UPROPERTY(EditDefaultsOnly, Category = "Menu|Quit") + TSoftObjectPtr 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 ContinueButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr OptionsButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr QuitToMenuButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr QuitToDesktopButton; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.cpp b/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.cpp new file mode 100644 index 0000000..7f1522e --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.cpp @@ -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); + } +} diff --git a/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.h b/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.h new file mode 100644 index 0000000..99476c2 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MainMenuWidget.h @@ -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 NewGameLevel; + + /** Screen pushed by the Options button. */ + UPROPERTY(EditDefaultsOnly, Category = "Menu|Screens") + TSubclassOf OptionsScreenClass; + + /** Screen pushed by the Credits button. */ + UPROPERTY(EditDefaultsOnly, Category = "Menu|Screens") + TSubclassOf 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 StartNewGameButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr LoadGameButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr OptionsButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr CreditsButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr ExitButton; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.cpp b/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.cpp new file mode 100644 index 0000000..9eb4d92 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.cpp @@ -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); + } +} diff --git a/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.h b/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.h new file mode 100644 index 0000000..1644bfc --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuButtonWidget.h @@ -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 DefaultButton; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr ButtonBackground; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr ButtonText; + + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr Highlight; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.cpp b/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.cpp new file mode 100644 index 0000000..96a8a6e --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.cpp @@ -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 ScreenClass) +{ + if (!ScreenClass) + { + return nullptr; + } + + APlayerController* OwningController = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr; + if (!OwningController) + { + return nullptr; + } + + UMenuScreenWidget* NewScreen = CreateWidget(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(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); + } +} diff --git a/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.h b/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.h new file mode 100644 index 0000000..4b549b2 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuManagerSubsystem.h @@ -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 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> ScreenStack; + + static constexpr int32 BaseZOrder = 100; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.cpp b/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.cpp new file mode 100644 index 0000000..d7f9943 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.cpp @@ -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()) + { + MenuManager->PushScreen(MainMenuClass); + } + } +} diff --git a/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.h b/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.h new file mode 100644 index 0000000..c990b53 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuPlayerController.h @@ -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 MainMenuClass; + +protected: + virtual void BeginPlay() override; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.cpp b/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.cpp new file mode 100644 index 0000000..57a4f46 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.cpp @@ -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() : nullptr; +} + +void UMenuScreenWidget::Close() +{ + if (UMenuManagerSubsystem* MenuManager = GetMenuManager()) + { + MenuManager->PopScreen(); + } +} + +UMenuScreenWidget* UMenuScreenWidget::OpenScreen(TSubclassOf ScreenClass) +{ + UMenuManagerSubsystem* MenuManager = GetMenuManager(); + return MenuManager ? MenuManager->PushScreen(ScreenClass) : nullptr; +} + +void UMenuScreenWidget::HandleBackClicked() +{ + Close(); +} diff --git a/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.h b/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.h new file mode 100644 index 0000000..ed00af6 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/MenuScreenWidget.h @@ -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 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 BackButton; +}; diff --git a/Source/KingshearthLegacy/UI/Menu/OptionsMenuWidget.h b/Source/KingshearthLegacy/UI/Menu/OptionsMenuWidget.h new file mode 100644 index 0000000..f3ee267 --- /dev/null +++ b/Source/KingshearthLegacy/UI/Menu/OptionsMenuWidget.h @@ -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() +};