added main and pause menus

This commit is contained in:
2026-06-05 20:00:33 +03:00
parent 0792f7cdfd
commit 61d5a57d8d
53 changed files with 1402 additions and 49 deletions
@@ -0,0 +1,32 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "AudioSettingsConfig.generated.h"
class USoundMix;
class USoundClass;
// Data-driven audio routing for the settings sliders (§17.4). The UAudioSettingsSubsystem
// pushes per-class volume overrides through SettingsMix when the user changes a slider.
// Author one SoundMix + three SoundClasses and assign on UNakedDesireGameInstance::AudioConfig.
UCLASS()
class NAKEDDESIRE_API UAudioSettingsConfig : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundMix> SettingsMix;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> MasterClass;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> MusicClass;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> SfxClass;
};
@@ -0,0 +1,76 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "AudioSettingsSubsystem.h"
#include "AudioSettingsConfig.h"
#include "NakedDesireGameInstance.h"
#include "NakedDesireUserSettings.h"
#include "Kismet/GameplayStatics.h"
#include "Sound/SoundClass.h"
#include "Sound/SoundMix.h"
#include "UObject/UObjectGlobals.h"
void UAudioSettingsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->OnSettingsChanged.AddUniqueDynamic(this, &UAudioSettingsSubsystem::HandleSettingsChanged);
// SoundMix overrides are world-scoped, so re-push them every time a level finishes loading.
MapLoadHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UAudioSettingsSubsystem::HandleMapLoaded);
}
void UAudioSettingsSubsystem::Deinitialize()
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->OnSettingsChanged.RemoveDynamic(this, &UAudioSettingsSubsystem::HandleSettingsChanged);
FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(MapLoadHandle);
Super::Deinitialize();
}
void UAudioSettingsSubsystem::HandleSettingsChanged(UNakedDesireUserSettings* Settings)
{
ApplyVolumes();
}
void UAudioSettingsSubsystem::HandleMapLoaded(UWorld* LoadedWorld)
{
ApplyVolumes();
}
void UAudioSettingsSubsystem::ApplyVolumes()
{
const UNakedDesireGameInstance* GameInstance = Cast<UNakedDesireGameInstance>(GetGameInstance());
if (!GameInstance || !GameInstance->AudioConfig)
return;
const UAudioSettingsConfig* Config = GameInstance->AudioConfig;
if (!Config->SettingsMix)
return;
UWorld* World = GetWorld();
if (!World)
return;
const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
auto ApplyClass = [&](USoundClass* SoundClass, float Volume)
{
if (!SoundClass)
return;
// Pitch 1.0, no fade — instantaneous so the slider feels responsive.
UGameplayStatics::SetSoundMixClassOverride(World, Config->SettingsMix, SoundClass, Volume, 1.0f, 0.0f, true);
};
ApplyClass(Config->MasterClass, Settings->GetGlobalVolume());
ApplyClass(Config->MusicClass, Settings->GetMusicVolume());
ApplyClass(Config->SfxClass, Settings->GetSfxVolume());
UGameplayStatics::PushSoundMixModifier(World, Config->SettingsMix);
}
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "AudioSettingsSubsystem.generated.h"
class UNakedDesireUserSettings;
// Applies the master / music / SFX volume settings to SoundClasses via the configured
// SoundMix (UNakedDesireGameInstance::AudioConfig). Re-applies whenever settings change
// and on each world start so volumes survive level travel.
UCLASS()
class NAKEDDESIRE_API UAudioSettingsSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Pushes the current settings volumes onto the configured SoundMix. Safe to call any time.
void ApplyVolumes();
private:
UFUNCTION()
void HandleSettingsChanged(UNakedDesireUserSettings* Settings);
void HandleMapLoaded(UWorld* LoadedWorld);
FDelegateHandle MapLoadHandle;
};
+33
View File
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MainMenuHUD.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/PlayerController.h"
#include "NakedDesire/UI/Menu/MainMenuWidget.h"
void AMainMenuHUD::BeginPlay()
{
Super::BeginPlay();
if (!MainMenuWidgetClass)
return;
MainMenuWidget = CreateWidget<UMainMenuWidget>(GetWorld(), MainMenuWidgetClass);
if (!MainMenuWidget)
return;
MainMenuWidget->AddToViewport();
MainMenuWidget->ActivateWidget();
if (APlayerController* PC = GetOwningPlayerController())
{
PC->SetShowMouseCursor(true);
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(MainMenuWidget->TakeWidget());
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
PC->SetInputMode(InputMode);
}
}
+27
View File
@@ -0,0 +1,27 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "MainMenuHUD.generated.h"
class UMainMenuWidget;
// HUD for the main-menu map. Creates the front-end widget and routes input to it.
// Assign this (or a BP child) as the HUD class on the MainMenu map's GameMode.
UCLASS()
class NAKEDDESIRE_API AMainMenuHUD : public AHUD
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UMainMenuWidget> MainMenuWidgetClass;
private:
UPROPERTY()
TObjectPtr<UMainMenuWidget> MainMenuWidget;
};
@@ -9,6 +9,7 @@ class UStartingSaveData;
class UCommissionBoardConfig;
class UNPCDirectorConfig;
class ULossPresentationConfig;
class UAudioSettingsConfig;
UCLASS()
class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
@@ -30,4 +31,8 @@ public:
// Cutscene-per-loss-cause map the USessionLossResolver plays before teleporting home (§4.4).
UPROPERTY(EditDefaultsOnly, Category = "Session")
TObjectPtr<ULossPresentationConfig> LossPresentation;
// SoundMix / SoundClass routing the UAudioSettingsSubsystem drives from the audio settings sliders.
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<UAudioSettingsConfig> AudioConfig;
};
@@ -5,7 +5,7 @@
void UNakedDesireUserSettings::SetGlobalVolume(float Value)
{
GlobalVolume = Value;
GlobalVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
float UNakedDesireUserSettings::GetGlobalVolume() const
@@ -13,6 +13,26 @@ float UNakedDesireUserSettings::GetGlobalVolume() const
return GlobalVolume;
}
void UNakedDesireUserSettings::SetMusicVolume(float Value)
{
MusicVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
float UNakedDesireUserSettings::GetMusicVolume() const
{
return MusicVolume;
}
void UNakedDesireUserSettings::SetSfxVolume(float Value)
{
SfxVolume = FMath::Clamp(Value, 0.0f, 1.0f);
}
float UNakedDesireUserSettings::GetSfxVolume() const
{
return SfxVolume;
}
void UNakedDesireUserSettings::SetIsCensorshipEnabled(bool Value)
{
IsCensorshipEnabled = Value;
@@ -23,6 +23,18 @@ public:
UFUNCTION(BlueprintPure)
float GetGlobalVolume() const;
UFUNCTION(BlueprintCallable)
void SetMusicVolume(float Value);
UFUNCTION(BlueprintPure)
float GetMusicVolume() const;
UFUNCTION(BlueprintCallable)
void SetSfxVolume(float Value);
UFUNCTION(BlueprintPure)
float GetSfxVolume() const;
UFUNCTION(BlueprintCallable)
void SetIsCensorshipEnabled(bool Value);
@@ -46,6 +58,12 @@ protected:
UPROPERTY(Config)
float GlobalVolume = 0.5f;
UPROPERTY(Config)
float MusicVolume = 0.5f;
UPROPERTY(Config)
float SfxVolume = 0.5f;
UPROPERTY(Config)
bool IsCensorshipEnabled = false;
@@ -89,6 +89,7 @@ void ANakedDesireCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInp
EnhancedInputComponent->BindAction(EquipmentAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnEquipmentPress);
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnInteractPress);
EnhancedInputComponent->BindAction(PhoneAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnPhonePress);
EnhancedInputComponent->BindAction(PauseAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnPausePress);
}
}
@@ -270,6 +271,11 @@ void ANakedDesireCharacter::OnPhonePress(const FInputActionValue& Value)
HUD->GetGameLayoutWidget()->OpenPhone();
}
void ANakedDesireCharacter::OnPausePress(const FInputActionValue& Value)
{
HUD->GetGameLayoutWidget()->OpenPauseMenu();
}
void ANakedDesireCharacter::NotifyNoticed(ANPCAIController* NPCController)
{
OnNoticed.Broadcast(NPCController);
@@ -61,6 +61,9 @@ public:
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputAction* PhoneAction;
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputAction* PauseAction;
// Clothing slot meshes are spawned per equipped slot at runtime by ClothingVisualsComponent.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Clothing")
UClothingVisualsComponent* ClothingVisualsComponent;
@@ -145,6 +148,7 @@ private:
void OnEquipmentPress(const FInputActionValue& Value);
void OnInteractPress(const FInputActionValue& Value);
void OnPhonePress(const FInputActionValue& Value);
void OnPausePress(const FInputActionValue& Value);
bool CheckSight(const FVector& StartLocation, const FVector& EndLocation, FHitResult& HitResult, const AActor* IgnoreActor);
@@ -9,6 +9,7 @@
#include "NakedDesire/Items/ItemDefinition.h"
#include "NakedDesire/Items/ItemInstance.h"
#include "NakedDesire/Global/NakedDesireGameInstance.h"
#include "Kismet/GameplayStatics.h"
void USaveSubsystem::LoadGame(const FString& SlotName)
{
@@ -27,6 +28,20 @@ void USaveSubsystem::Initialize(FSubsystemCollectionBase& Collection)
LoadGame();
}
bool USaveSubsystem::DoesSaveExist() const
{
return UGameplayStatics::DoesSaveGameExist(ActiveSlotName, SLOT_PLAYER);
}
void USaveSubsystem::StartNewGame()
{
UGameplayStatics::DeleteGameInSlot(ActiveSlotName, SLOT_PLAYER);
CurrentSave = UGlobalSaveGameData::CreateNewSaveGame();
PopulateStartingData(CurrentSave);
UGlobalSaveGameData::SaveGame(CurrentSave, ActiveSlotName);
}
UGlobalSaveGameData* USaveSubsystem::GetCurrentSave()
{
if (!CurrentSave)
+13 -2
View File
@@ -22,11 +22,22 @@ public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
UGlobalSaveGameData* GetCurrentSave();
// True when a persisted save exists in the active slot — drives the main-menu
// Continue button's enabled state.
UFUNCTION(BlueprintPure, Category = "Save")
bool DoesSaveExist() const;
// Wipes the active slot and reseeds a fresh save from StartingSaveData, replacing
// the cached CurrentSave so the next GetCurrentSave returns the new game. Call before
// opening the gameplay level from the main-menu "New Game" flow.
UFUNCTION(BlueprintCallable, Category = "Save")
void StartNewGame();
protected:
UFUNCTION(BlueprintCallable)
void BP_LoadGame();
UFUNCTION(BlueprintCallable)
void BP_SaveGame();
@@ -3,6 +3,7 @@
#include "Inventory/InventoryScreenWidget.h"
#include "Inventory/Wardrobe/WardrobeScreenWidget.h"
#include "Phone/PhoneScreenWidget.h"
#include "Menu/PauseMenuWidget.h"
void UGameLayoutWidget::OpenInventory()
{
@@ -18,3 +19,8 @@ void UGameLayoutWidget::OpenPhone()
{
PhoneScreenWidget = WidgetStack->AddWidget<UPhoneScreenWidget>(PhoneScreenWidgetClass);
}
void UGameLayoutWidget::OpenPauseMenu()
{
PauseMenuWidget = WidgetStack->AddWidget<UPauseMenuWidget>(PauseMenuWidgetClass);
}
+8
View File
@@ -7,6 +7,7 @@
class UWardrobeScreenWidget;
class UInventoryScreenWidget;
class UPhoneScreenWidget;
class UPauseMenuWidget;
class UHUDWidget;
class UCommonActivatableWidgetStack;
@@ -39,8 +40,15 @@ class NAKEDDESIRE_API UGameLayoutWidget : public UCommonUserWidget
UPROPERTY()
TObjectPtr<UPhoneScreenWidget> PhoneScreenWidget;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UPauseMenuWidget> PauseMenuWidgetClass;
UPROPERTY()
TObjectPtr<UPauseMenuWidget> PauseMenuWidget;
public:
void OpenInventory();
void OpenWardrobe();
void OpenPhone();
void OpenPauseMenu();
};
@@ -0,0 +1,43 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ConfirmModalWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
void UConfirmModalWidget::Setup(const FText& InTitle, const FText& InMessage)
{
PendingTitle = InTitle;
PendingMessage = InMessage;
if (TitleText)
TitleText->SetText(PendingTitle);
if (MessageText)
MessageText->SetText(PendingMessage);
}
void UConfirmModalWidget::NativeOnActivated()
{
Super::NativeOnActivated();
if (TitleText)
TitleText->SetText(PendingTitle);
if (MessageText)
MessageText->SetText(PendingMessage);
ConfirmButton->OnClicked.AddUniqueDynamic(this, &UConfirmModalWidget::HandleConfirmClicked);
CancelButton->OnClicked.AddUniqueDynamic(this, &UConfirmModalWidget::HandleCancelClicked);
}
void UConfirmModalWidget::HandleConfirmClicked()
{
OnConfirmed.Broadcast();
DeactivateWidget();
}
void UConfirmModalWidget::HandleCancelClicked()
{
OnCancelled.Broadcast();
DeactivateWidget();
}
@@ -0,0 +1,52 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "ConfirmModalWidget.generated.h"
class UButton;
class UTextBlock;
// Reusable yes/no confirmation popup. Pushed onto a stack by the menu that owns it;
// the opener calls Setup() for the copy and binds OnConfirmed / OnCancelled to react.
UCLASS(Abstract)
class NAKEDDESIRE_API UConfirmModalWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
// Sets the popup copy. Safe to call before the widget tree exists (cached and applied on activate).
void Setup(const FText& InTitle, const FText& InMessage);
// C++-only result events — the opener binds whichever it cares about.
DECLARE_MULTICAST_DELEGATE(FOnConfirmModalResult);
FOnConfirmModalResult OnConfirmed;
FOnConfirmModalResult OnCancelled;
protected:
virtual void NativeOnActivated() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ConfirmButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CancelButton;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> TitleText;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> MessageText;
FText PendingTitle;
FText PendingMessage;
UFUNCTION()
void HandleConfirmClicked();
UFUNCTION()
void HandleCancelClicked();
};
@@ -0,0 +1,18 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "CreditsWidget.h"
#include "Components/Button.h"
void UCreditsWidget::NativeOnActivated()
{
Super::NativeOnActivated();
BackButton->OnClicked.AddUniqueDynamic(this, &UCreditsWidget::HandleBackClicked);
}
void UCreditsWidget::HandleBackClicked()
{
DeactivateWidget();
}
@@ -0,0 +1,26 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "CreditsWidget.generated.h"
class UButton;
// Credits screen. Pushed onto a stack by the menu; the Back button closes it.
UCLASS(Abstract)
class NAKEDDESIRE_API UCreditsWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
protected:
virtual void NativeOnActivated() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> BackButton;
UFUNCTION()
void HandleBackClicked();
};
@@ -0,0 +1,126 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MainMenuWidget.h"
#include "ConfirmModalWidget.h"
#include "Components/Button.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
#include "Widgets/CommonActivatableWidgetContainer.h"
#define LOCTEXT_NAMESPACE "MainMenu"
UMainMenuWidget::UMainMenuWidget()
{
// Sensible default for the slice; override per-asset on WBP_MainMenu.
GameplayMap = TSoftObjectPtr<UWorld>(FSoftObjectPath(TEXT("/Game/Test/Maps/L_TestCity.L_TestCity")));
}
void UMainMenuWidget::NativeOnActivated()
{
Super::NativeOnActivated();
// Continue is only available when a save exists in the active slot.
bool bHasSave = false;
if (const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this))
{
if (const USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
bHasSave = SaveSubsystem->DoesSaveExist();
}
ContinueButton->SetIsEnabled(bHasSave);
ContinueButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnContinueClicked);
NewGameButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnNewGameClicked);
SettingsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnSettingsClicked);
CreditsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnCreditsClicked);
QuitButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnQuitClicked);
}
void UMainMenuWidget::OnContinueClicked()
{
OpenGameplayLevel();
}
void UMainMenuWidget::OnNewGameClicked()
{
if (!ConfirmModalClass || !ModalStack)
{
HandleNewGameConfirmed();
return;
}
UConfirmModalWidget* Modal = ModalStack->AddWidget<UConfirmModalWidget>(ConfirmModalClass);
if (!Modal)
return;
Modal->Setup(
LOCTEXT("NewGameTitle", "New Game"),
LOCTEXT("NewGameBody", "Start a new game? Any existing progress will be lost."));
Modal->OnConfirmed.AddUObject(this, &UMainMenuWidget::HandleNewGameConfirmed);
}
void UMainMenuWidget::OnSettingsClicked()
{
if (SettingsWidgetClass && ModalStack)
ModalStack->AddWidget(SettingsWidgetClass);
}
void UMainMenuWidget::OnCreditsClicked()
{
if (CreditsWidgetClass && ModalStack)
ModalStack->AddWidget(CreditsWidgetClass);
}
void UMainMenuWidget::OnQuitClicked()
{
if (!ConfirmModalClass || !ModalStack)
{
HandleQuitConfirmed();
return;
}
UConfirmModalWidget* Modal = ModalStack->AddWidget<UConfirmModalWidget>(ConfirmModalClass);
if (!Modal)
return;
Modal->Setup(
LOCTEXT("QuitTitle", "Quit Game"),
LOCTEXT("QuitBody", "Quit to desktop?"));
Modal->OnConfirmed.AddUObject(this, &UMainMenuWidget::HandleQuitConfirmed);
}
void UMainMenuWidget::HandleNewGameConfirmed()
{
if (UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this))
{
if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
SaveSubsystem->StartNewGame();
}
OpenGameplayLevel();
}
void UMainMenuWidget::HandleQuitConfirmed()
{
UKismetSystemLibrary::QuitGame(this, GetOwningPlayer(), EQuitPreference::Quit, false);
}
void UMainMenuWidget::OpenGameplayLevel()
{
if (GameplayMap.IsNull())
return;
if (APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0))
{
PC->SetShowMouseCursor(false);
const FInputModeGameOnly InputMode;
PC->SetInputMode(InputMode);
}
UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameplayMap);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,80 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "MainMenuWidget.generated.h"
class UButton;
class UCommonActivatableWidgetStack;
class UConfirmModalWidget;
// Front-end main menu. Owns the Continue / New Game / Settings / Credits / Quit flow.
// Confirmation popups, credits and settings are pushed onto ModalStack (a stack in the
// widget tree). The gameplay / menu maps are data-driven via the soft map references.
UCLASS(Abstract)
class NAKEDDESIRE_API UMainMenuWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UMainMenuWidget();
protected:
virtual void NativeOnActivated() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ContinueButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> NewGameButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> SettingsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CreditsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> QuitButton;
// Stack the confirm popup / credits / settings screens are pushed onto.
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCommonActivatableWidgetStack> ModalStack;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UConfirmModalWidget> ConfirmModalClass;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> CreditsWidgetClass;
// Optional — wired once the settings screen exists. Settings button no-ops until then.
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> SettingsWidgetClass;
// Level "New Game" / "Continue" load into (the §0.2 slice district).
UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World"))
TSoftObjectPtr<UWorld> GameplayMap;
UFUNCTION()
void OnContinueClicked();
UFUNCTION()
void OnNewGameClicked();
UFUNCTION()
void OnSettingsClicked();
UFUNCTION()
void OnCreditsClicked();
UFUNCTION()
void OnQuitClicked();
void HandleNewGameConfirmed();
void HandleQuitConfirmed();
void OpenGameplayLevel();
};
@@ -0,0 +1,84 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "PauseMenuWidget.h"
#include "ConfirmModalWidget.h"
#include "Components/Button.h"
#include "Kismet/GameplayStatics.h"
#include "Widgets/CommonActivatableWidgetContainer.h"
#define LOCTEXT_NAMESPACE "PauseMenu"
UPauseMenuWidget::UPauseMenuWidget()
{
// Default to the front-end map; override per-asset on WBP_PauseMenu.
MainMenuMap = TSoftObjectPtr<UWorld>(FSoftObjectPath(TEXT("/Game/Maps/MainMenu.MainMenu")));
}
void UPauseMenuWidget::NativeOnActivated()
{
Super::NativeOnActivated();
GetWorld()->GetTimerManager().SetTimer(PauseTimerHandle, this, &UPauseMenuWidget::PauseGame, 0.1f);
ResumeButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnResumeClicked);
SettingsButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnSettingsClicked);
MainMenuButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnMainMenuClicked);
}
void UPauseMenuWidget::NativeOnDeactivated()
{
Super::NativeOnDeactivated();
// Unpause whenever the menu closes — Resume, back action, or being popped.
if (UGameplayStatics::IsGamePaused(this))
UGameplayStatics::SetGamePaused(this, false);
}
void UPauseMenuWidget::OnResumeClicked()
{
DeactivateWidget();
}
void UPauseMenuWidget::OnSettingsClicked()
{
if (SettingsWidgetClass && ModalStack)
ModalStack->AddWidget(SettingsWidgetClass);
}
void UPauseMenuWidget::OnMainMenuClicked()
{
if (!ConfirmModalClass || !ModalStack)
{
HandleMainMenuConfirmed();
return;
}
UConfirmModalWidget* Modal = ModalStack->AddWidget<UConfirmModalWidget>(ConfirmModalClass);
if (!Modal)
return;
Modal->Setup(
LOCTEXT("MainMenuTitle", "Main Menu"),
LOCTEXT("MainMenuBody", "Return to the main menu? Unsaved progress may be lost."));
Modal->OnConfirmed.AddUObject(this, &UPauseMenuWidget::HandleMainMenuConfirmed);
}
void UPauseMenuWidget::HandleMainMenuConfirmed()
{
// Unpause before travel — level transitions while paused are unreliable.
UGameplayStatics::SetGamePaused(this, false);
if (MainMenuMap.IsNull())
return;
UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuMap);
}
void UPauseMenuWidget::PauseGame()
{
UGameplayStatics::SetGamePaused(this, true);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,64 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "PauseMenuWidget.generated.h"
class UButton;
class UCommonActivatableWidgetStack;
class UConfirmModalWidget;
// In-game pause menu. Pauses the game while active and offers Resume / Settings / Main Menu.
// The Main Menu action confirms first. Confirm popup / settings push onto ModalStack.
UCLASS(Abstract)
class NAKEDDESIRE_API UPauseMenuWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
UPauseMenuWidget();
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ResumeButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> SettingsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> MainMenuButton;
// Stack the confirm popup / settings screen are pushed onto (nested so this menu stays active).
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCommonActivatableWidgetStack> ModalStack;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UConfirmModalWidget> ConfirmModalClass;
// Optional — wired once the settings screen exists. Settings button no-ops until then.
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> SettingsWidgetClass;
UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World"))
TSoftObjectPtr<UWorld> MainMenuMap;
FTimerHandle PauseTimerHandle;
UFUNCTION()
void OnResumeClicked();
UFUNCTION()
void OnSettingsClicked();
UFUNCTION()
void OnMainMenuClicked();
void HandleMainMenuConfirmed();
void PauseGame();
};
@@ -0,0 +1,61 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "AudioSettingsTab.h"
#include "Components/Slider.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Global/AudioSettingsSubsystem.h"
#include "NakedDesire/Global/NakedDesireUserSettings.h"
void UAudioSettingsTab::NativeConstruct()
{
Super::NativeConstruct();
MasterSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnMasterChanged);
MusicSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnMusicChanged);
SfxSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnSfxChanged);
RefreshFromSettings();
}
void UAudioSettingsTab::RefreshFromSettings()
{
const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
MasterSlider->SetValue(Settings->GetGlobalVolume());
MusicSlider->SetValue(Settings->GetMusicVolume());
SfxSlider->SetValue(Settings->GetSfxVolume());
}
void UAudioSettingsTab::OnMasterChanged(float Value)
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->SetGlobalVolume(Value);
ApplyLive();
}
void UAudioSettingsTab::OnMusicChanged(float Value)
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->SetMusicVolume(Value);
ApplyLive();
}
void UAudioSettingsTab::OnSfxChanged(float Value)
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->SetSfxVolume(Value);
ApplyLive();
}
void UAudioSettingsTab::ApplyLive()
{
if (const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this))
{
if (UAudioSettingsSubsystem* Audio = GameInstance->GetSubsystem<UAudioSettingsSubsystem>())
Audio->ApplyVolumes();
}
}
@@ -0,0 +1,44 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "AudioSettingsTab.generated.h"
class USlider;
// Audio settings tab. Master / Music / SFX volume sliders. Changes apply live through
// UAudioSettingsSubsystem (routed via the configured SoundMix) and persist on close.
UCLASS(Abstract)
class NAKEDDESIRE_API UAudioSettingsTab : public UCommonUserWidget
{
GENERATED_BODY()
public:
void RefreshFromSettings();
protected:
virtual void NativeConstruct() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<USlider> MasterSlider;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USlider> MusicSlider;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USlider> SfxSlider;
UFUNCTION()
void OnMasterChanged(float Value);
UFUNCTION()
void OnMusicChanged(float Value);
UFUNCTION()
void OnSfxChanged(float Value);
void ApplyLive();
};
@@ -0,0 +1,82 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "GameplaySettingsTab.h"
#include "Components/CheckBox.h"
#include "Components/ComboBoxString.h"
#include "Kismet/KismetInternationalizationLibrary.h"
#include "NakedDesire/Global/NakedDesireUserSettings.h"
namespace
{
// Index-aligned: combo display name → UE culture code. Add a row here when a new
// translation ships, and make sure the culture is in DefaultGame.ini CulturesToStage.
struct FLanguageOption
{
const TCHAR* DisplayName;
const TCHAR* CultureCode;
};
constexpr FLanguageOption LanguageOptions[] = {
{ TEXT("English"), TEXT("en") },
{ TEXT("Українська"), TEXT("uk") },
{ TEXT("日本語"), TEXT("ja") }
};
}
void UGameplaySettingsTab::NativeConstruct()
{
Super::NativeConstruct();
CensorshipCheckBox->OnCheckStateChanged.AddUniqueDynamic(this, &UGameplaySettingsTab::OnCensorshipChanged);
LanguageCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGameplaySettingsTab::OnLanguageChanged);
RefreshFromSettings();
}
void UGameplaySettingsTab::RefreshFromSettings()
{
const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
CensorshipCheckBox->SetIsChecked(Settings->GetIsCensorshipEnabled());
// Language: match the active culture (e.g. "uk-UA") against our supported codes by prefix.
LanguageCombo->ClearOptions();
const FString CurrentCulture = UKismetInternationalizationLibrary::GetCurrentLanguage();
int32 SelectedIndex = 0;
for (int32 i = 0; i < static_cast<int32>(UE_ARRAY_COUNT(LanguageOptions)); ++i)
{
LanguageCombo->AddOption(LanguageOptions[i].DisplayName);
if (CurrentCulture.StartsWith(LanguageOptions[i].CultureCode))
SelectedIndex = i;
}
LanguageCombo->SetSelectedIndex(SelectedIndex);
}
void UGameplaySettingsTab::OnCensorshipChanged(bool bIsChecked)
{
UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
Settings->SetIsCensorshipEnabled(bIsChecked);
// Broadcasts OnSettingsChanged so the censorship meshes update immediately.
Settings->ApplyNonResolutionSettings();
}
void UGameplaySettingsTab::OnLanguageChanged(FString SelectedItem, ESelectInfo::Type SelectionType)
{
// Skip the programmatic SetSelectedIndex during RefreshFromSettings.
if (SelectionType == ESelectInfo::Direct)
return;
const int32 Index = LanguageCombo->GetSelectedIndex();
if (Index < 0 || Index >= static_cast<int32>(UE_ARRAY_COUNT(LanguageOptions)))
return;
// Applies immediately (all LOCTEXT re-evaluates) and persists to the game config.
UKismetInternationalizationLibrary::SetCurrentLanguageAndLocale(LanguageOptions[Index].CultureCode, /*SaveToConfig*/ true);
}
@@ -0,0 +1,39 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "GameplaySettingsTab.generated.h"
class UCheckBox;
class UComboBoxString;
// Gameplay settings tab. Censorship toggle (§ compliance) — applies live through
// UNakedDesireUserSettings::OnSettingsChanged, which UCensorshipComponent listens to —
// plus the UI/text language selector (English / Ukrainian / Japanese).
UCLASS(Abstract)
class NAKEDDESIRE_API UGameplaySettingsTab : public UCommonUserWidget
{
GENERATED_BODY()
public:
// Re-reads the current values into the controls. Called by the settings screen on show.
void RefreshFromSettings();
protected:
virtual void NativeConstruct() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCheckBox> CensorshipCheckBox;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> LanguageCombo;
UFUNCTION()
void OnCensorshipChanged(bool bIsChecked);
UFUNCTION()
void OnLanguageChanged(FString SelectedItem, ESelectInfo::Type SelectionType);
};
@@ -0,0 +1,137 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "GraphicsSettingsTab.h"
#include "Components/CheckBox.h"
#include "Components/ComboBoxString.h"
#include "Kismet/KismetSystemLibrary.h"
#include "NakedDesire/Global/NakedDesireUserSettings.h"
namespace
{
// Combo order → EWindowMode. Index-aligned with the WindowModeCombo entries below.
constexpr EWindowMode::Type WindowModes[] = {
EWindowMode::Fullscreen,
EWindowMode::WindowedFullscreen,
EWindowMode::Windowed
};
FString ResolutionToText(const FIntPoint& Resolution)
{
return FString::Printf(TEXT("%d x %d"), Resolution.X, Resolution.Y);
}
}
void UGraphicsSettingsTab::NativeConstruct()
{
Super::NativeConstruct();
QualityCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnQualityChanged);
WindowModeCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnWindowModeChanged);
ResolutionCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnResolutionChanged);
VSyncCheckBox->OnCheckStateChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnVSyncChanged);
RefreshFromSettings();
}
void UGraphicsSettingsTab::RefreshFromSettings()
{
const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
// Quality presets (Scalability levels 0-3).
QualityCombo->ClearOptions();
QualityCombo->AddOption(TEXT("Low"));
QualityCombo->AddOption(TEXT("Medium"));
QualityCombo->AddOption(TEXT("High"));
QualityCombo->AddOption(TEXT("Epic"));
const int32 Quality = FMath::Clamp(Settings->GetOverallScalabilityLevel(), 0, 3);
QualityCombo->SetSelectedIndex(Quality);
// Window mode.
WindowModeCombo->ClearOptions();
WindowModeCombo->AddOption(TEXT("Fullscreen"));
WindowModeCombo->AddOption(TEXT("Borderless Windowed"));
WindowModeCombo->AddOption(TEXT("Windowed"));
const EWindowMode::Type CurrentMode = Settings->GetFullscreenMode();
int32 ModeIndex = 0;
for (int32 i = 0; i < UE_ARRAY_COUNT(WindowModes); ++i)
{
if (WindowModes[i] == CurrentMode)
{
ModeIndex = i;
break;
}
}
WindowModeCombo->SetSelectedIndex(ModeIndex);
// Resolutions.
ResolutionCombo->ClearOptions();
AvailableResolutions.Reset();
UKismetSystemLibrary::GetSupportedFullscreenResolutions(AvailableResolutions);
const FIntPoint CurrentResolution = Settings->GetScreenResolution();
int32 ResolutionIndex = INDEX_NONE;
for (int32 i = 0; i < AvailableResolutions.Num(); ++i)
{
ResolutionCombo->AddOption(ResolutionToText(AvailableResolutions[i]));
if (AvailableResolutions[i] == CurrentResolution)
ResolutionIndex = i;
}
// Current resolution may not be in the supported list (e.g. windowed) — surface it anyway.
if (ResolutionIndex == INDEX_NONE)
{
ResolutionCombo->AddOption(ResolutionToText(CurrentResolution));
AvailableResolutions.Add(CurrentResolution);
ResolutionIndex = AvailableResolutions.Num() - 1;
}
ResolutionCombo->SetSelectedIndex(ResolutionIndex);
VSyncCheckBox->SetIsChecked(Settings->IsVSyncEnabled());
}
void UGraphicsSettingsTab::OnQualityChanged(FString SelectedItem, ESelectInfo::Type SelectionType)
{
if (SelectionType == ESelectInfo::Direct)
return;
UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
Settings->SetOverallScalabilityLevel(QualityCombo->GetSelectedIndex());
}
void UGraphicsSettingsTab::OnWindowModeChanged(FString SelectedItem, ESelectInfo::Type SelectionType)
{
if (SelectionType == ESelectInfo::Direct)
return;
UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
const int32 Index = FMath::Clamp(WindowModeCombo->GetSelectedIndex(), 0, static_cast<int32>(UE_ARRAY_COUNT(WindowModes)) - 1);
Settings->SetFullscreenMode(WindowModes[Index]);
}
void UGraphicsSettingsTab::OnResolutionChanged(FString SelectedItem, ESelectInfo::Type SelectionType)
{
if (SelectionType == ESelectInfo::Direct)
return;
UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings();
if (!Settings)
return;
const int32 Index = ResolutionCombo->GetSelectedIndex();
if (AvailableResolutions.IsValidIndex(Index))
Settings->SetScreenResolution(AvailableResolutions[Index]);
}
void UGraphicsSettingsTab::OnVSyncChanged(bool bIsChecked)
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->SetVSyncEnabled(bIsChecked);
}
@@ -0,0 +1,53 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "GraphicsSettingsTab.generated.h"
class UComboBoxString;
class UCheckBox;
// Graphics settings tab. Overall quality, window mode, resolution and VSync, backed by the
// inherited UGameUserSettings. Changes stage into the settings object; the settings screen's
// Apply button calls ApplySettings to commit resolution/quality changes.
UCLASS(Abstract)
class NAKEDDESIRE_API UGraphicsSettingsTab : public UCommonUserWidget
{
GENERATED_BODY()
public:
void RefreshFromSettings();
protected:
virtual void NativeConstruct() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> QualityCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> WindowModeCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> ResolutionCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCheckBox> VSyncCheckBox;
// Index-aligned with ResolutionCombo entries.
TArray<FIntPoint> AvailableResolutions;
UFUNCTION()
void OnQualityChanged(FString SelectedItem, ESelectInfo::Type SelectionType);
UFUNCTION()
void OnWindowModeChanged(FString SelectedItem, ESelectInfo::Type SelectionType);
UFUNCTION()
void OnResolutionChanged(FString SelectedItem, ESelectInfo::Type SelectionType);
UFUNCTION()
void OnVSyncChanged(bool bIsChecked);
};
@@ -0,0 +1,72 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "SettingsScreenWidget.h"
#include "AudioSettingsTab.h"
#include "GameplaySettingsTab.h"
#include "GraphicsSettingsTab.h"
#include "Components/Button.h"
#include "Components/WidgetSwitcher.h"
#include "NakedDesire/Global/NakedDesireUserSettings.h"
void USettingsScreenWidget::NativeOnActivated()
{
Super::NativeOnActivated();
GameplayTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowGameplayTab);
AudioTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowAudioTab);
GraphicsTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowGraphicsTab);
ApplyButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::OnApplyClicked);
BackButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::OnBackClicked);
// Pull live values into every tab so the controls reflect the current state.
GameplayTab->RefreshFromSettings();
AudioTab->RefreshFromSettings();
GraphicsTab->RefreshFromSettings();
ShowGameplayTab();
}
void USettingsScreenWidget::NativeOnDeactivated()
{
Super::NativeOnDeactivated();
// Persist whatever the player changed, even if they didn't hit Apply.
PersistSettings();
}
void USettingsScreenWidget::ShowGameplayTab()
{
TabSwitcher->SetActiveWidget(GameplayTab);
}
void USettingsScreenWidget::ShowAudioTab()
{
TabSwitcher->SetActiveWidget(AudioTab);
}
void USettingsScreenWidget::ShowGraphicsTab()
{
TabSwitcher->SetActiveWidget(GraphicsTab);
}
void USettingsScreenWidget::OnApplyClicked()
{
// Commits resolution + scalability changes staged by the graphics tab.
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->ApplySettings(false);
PersistSettings();
}
void USettingsScreenWidget::OnBackClicked()
{
DeactivateWidget();
}
void USettingsScreenWidget::PersistSettings()
{
if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings())
Settings->SaveSettings();
}
@@ -0,0 +1,71 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "SettingsScreenWidget.generated.h"
class UButton;
class UWidgetSwitcher;
class UGameplaySettingsTab;
class UAudioSettingsTab;
class UGraphicsSettingsTab;
// Settings screen with Gameplay / Audio / Graphics tabs. Pushed onto a menu's ModalStack
// (assign as SettingsWidgetClass on the main / pause menus). Apply commits + persists;
// closing the screen also persists pending changes.
UCLASS(Abstract)
class NAKEDDESIRE_API USettingsScreenWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidgetSwitcher> TabSwitcher;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GameplayTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> AudioTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GraphicsTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ApplyButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> BackButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UGameplaySettingsTab> GameplayTab;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UAudioSettingsTab> AudioTab;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UGraphicsSettingsTab> GraphicsTab;
UFUNCTION()
void ShowGameplayTab();
UFUNCTION()
void ShowAudioTab();
UFUNCTION()
void ShowGraphicsTab();
UFUNCTION()
void OnApplyClicked();
UFUNCTION()
void OnBackClicked();
void PersistSettings();
};