Add Resume/Exit buttons to pause menu, handle logic for returning to main menu and starting a new game

This commit is contained in:
2025-12-05 20:05:28 -08:00
parent 678916be89
commit 5b9de11e5a
17 changed files with 354 additions and 239 deletions

View File

@@ -24,6 +24,8 @@ public partial class AppLogic
public readonly record struct ShowMainMenu; public readonly record struct ShowMainMenu;
public readonly record struct CloseGame;
public readonly record struct ExitGame; public readonly record struct ExitGame;
public readonly record struct GameOver; public readonly record struct GameOver;

View File

@@ -9,7 +9,7 @@ public partial class AppLogic
public partial record State public partial record State
{ {
[Meta] [Meta]
public partial record GameStarted : State public partial record GameStarted : State, IGet<Input.QuitGame>
{ {
public GameStarted() public GameStarted()
{ {
@@ -26,6 +26,11 @@ public partial class AppLogic
OnDetach(() => Get<IAppRepo>().GameExited -= OnGameExited); OnDetach(() => Get<IAppRepo>().GameExited -= OnGameExited);
} }
public Transition On(in Input.QuitGame input)
{
Output(new Output.CloseGame());
return To<MainMenu>();
}
public void OnGameExited() => Input(new Input.QuitGame()); public void OnGameExited() => Input(new Input.QuitGame());
} }
} }

View File

@@ -8,10 +8,14 @@ public partial class GameState
public readonly record struct LoadGame; public readonly record struct LoadGame;
public readonly record struct ExitGame;
public readonly record struct LoadNextFloor; public readonly record struct LoadNextFloor;
public readonly record struct InventoryButtonPressed; public readonly record struct InventoryButtonPressed;
public readonly record struct InteractButtonPressed;
public readonly record struct PauseButtonPressed; public readonly record struct PauseButtonPressed;
public readonly record struct DebugButtonPressed; public readonly record struct DebugButtonPressed;

View File

@@ -6,6 +6,8 @@ public partial class GameState
{ {
public readonly record struct InitializeGame; public readonly record struct InitializeGame;
public readonly record struct ExitGame;
public readonly record struct LoadGameFromFile; public readonly record struct LoadGameFromFile;
public readonly record struct OpenInventoryMenu; public readonly record struct OpenInventoryMenu;

View File

@@ -8,9 +8,9 @@ public partial class GameState
public partial record State public partial record State
{ {
[Meta, LogicBlock(typeof(State), Diagram = true)] [Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record InventoryScreen : State, IGet<Input.InventoryButtonPressed> public partial record InventoryScreen : State, IGet<Input.InteractButtonPressed>
{ {
public Transition On(in Input.InventoryButtonPressed input) public Transition On(in Input.InteractButtonPressed input)
{ {
Output(new Output.CloseInventoryMenu()); Output(new Output.CloseInventoryMenu());
return To<InGame>(); return To<InGame>();

View File

@@ -1,5 +1,6 @@
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Chickensoft.LogicBlocks; using Chickensoft.LogicBlocks;
using static Zennysoft.Ma.Adapter.GameState.Output;
namespace Zennysoft.Ma.Adapter; namespace Zennysoft.Ma.Adapter;
@@ -8,13 +9,20 @@ public partial class GameState
public partial record State public partial record State
{ {
[Meta, LogicBlock(typeof(State), Diagram = true)] [Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record PauseScreen : State, IGet<Input.PauseButtonPressed> public partial record PauseScreen : State, IGet<Input.PauseButtonPressed>, IGet<Input.ExitGame>
{ {
public Transition On(in Input.PauseButtonPressed input) public Transition On(in Input.PauseButtonPressed input)
{ {
Output(new Output.ClosePauseScreen()); Output(new Output.ClosePauseScreen());
return To<InGame>(); return To<InGame>();
} }
public Transition On(in Input.ExitGame input)
{
Output(new Output.ClosePauseScreen());
Output(new Output.ExitGame());
return To<State>();
}
} }
} }
} }

View File

@@ -1,20 +1,14 @@
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.Collections;
using Chickensoft.GodotNodeInterfaces; using Chickensoft.GodotNodeInterfaces;
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Godot; using Godot;
using Godot.Collections;
using NathanHoad; using NathanHoad;
using SimpleInjector.Lifestyles; using SimpleInjector.Lifestyles;
using System;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Zennysoft.Game.Abstractions; using Zennysoft.Game.Abstractions;
using Zennysoft.Game.Implementation; using Zennysoft.Game.Implementation;
using Zennysoft.Ma.Adapter; using Zennysoft.Ma.Adapter;
using static Zennysoft.Game.Ma.SceneLoader;
using static Zennysoft.Ma.Adapter.AppLogic.State;
namespace Zennysoft.Game.Ma; namespace Zennysoft.Game.Ma;
@@ -37,8 +31,6 @@ public partial class App : Node, IApp
[Node] private GalleryMenu GalleryMenu { get; set; } [Node] private GalleryMenu GalleryMenu { get; set; }
public IInstantiator Instantiator { get; set; } = default!;
IAppRepo IProvide<IAppRepo>.Value() => AppRepo; IAppRepo IProvide<IAppRepo>.Value() => AppRepo;
public IAppRepo AppRepo { get; set; } = default!; public IAppRepo AppRepo { get; set; } = default!;
@@ -57,9 +49,6 @@ public partial class App : Node, IApp
private IGame _game; private IGame _game;
private IDataViewer _enemyViewer; private IDataViewer _enemyViewer;
private event Action OnGameLoaded;
private event Action OnEnemyViewerLoaded;
private double _reportedProgress = 0; private double _reportedProgress = 0;
public void Initialize() public void Initialize()
@@ -110,6 +99,11 @@ public partial class App : Node, IApp
this.Provide(); this.Provide();
} }
private void GameExitRequested()
{
AppLogic.Input(new AppLogic.Input.QuitGame());
}
private void DeleteSaveData() private void DeleteSaveData()
{ {
var saveFileManager = _container.GetInstance<ISaveFileManager>(); var saveFileManager = _container.GetInstance<ISaveFileManager>();
@@ -150,27 +144,31 @@ public partial class App : Node, IApp
}) })
.Handle((in AppLogic.Output.SetupGameScene _) => .Handle((in AppLogic.Output.SetupGameScene _) =>
{ {
LoadingScreen.Show();
LoadGame(GAME_SCENE_PATH); LoadGame(GAME_SCENE_PATH);
MainMenu.ReleaseFocus();
MainMenu.Hide();
}) })
.Handle((in AppLogic.Output.ShowMainMenu _) => .Handle((in AppLogic.Output.ShowMainMenu _) =>
{ {
}) })
.Handle((in AppLogic.Output.ShowGame _) => .Handle((in AppLogic.Output.CloseGame _) =>
{ {
LoadingScreen.Hide();
_game.GameExitRequested -= GameExitRequested;
MainMenu.StartGameButton.GrabFocus();
_game.CallDeferred(MethodName.QueueFree, []);
GetTree().Paused = false;
}) })
.Handle((in AppLogic.Output.StartLoadingSaveFile _) => .Handle((in AppLogic.Output.StartLoadingSaveFile _) =>
{ {
}) })
.Handle((in AppLogic.Output.EnemyViewerOpened _) => .Handle((in AppLogic.Output.EnemyViewerOpened _) =>
{ {
LoadingScreen.Show();
LoadEnemyViewer(ENEMY_VIEWER_PATH); LoadEnemyViewer(ENEMY_VIEWER_PATH);
MainMenu.ReleaseFocus();
MainMenu.Hide();
}) })
.Handle((in AppLogic.Output.EnemyViewerExited _) => .Handle((in AppLogic.Output.EnemyViewerExited _) =>
{ {
LoadingScreen.Hide();
if (_enemyViewer != null && _enemyViewer is DataViewer enemyViewer) if (_enemyViewer != null && _enemyViewer is DataViewer enemyViewer)
enemyViewer.CallDeferred(MethodName.QueueFree); enemyViewer.CallDeferred(MethodName.QueueFree);
MainMenu.Show(); MainMenu.Show();
@@ -202,6 +200,7 @@ public partial class App : Node, IApp
{ {
var scene = await LoadSceneInternal(sceneName); var scene = await LoadSceneInternal(sceneName);
_game = scene as IGame; _game = scene as IGame;
_game.GameExitRequested += GameExitRequested;
await ToSignal(GetTree().CreateTimer(0.8f), "timeout"); await ToSignal(GetTree().CreateTimer(0.8f), "timeout");
CallDeferred(MethodName.AddChild, scene); CallDeferred(MethodName.AddChild, scene);
} }
@@ -216,6 +215,7 @@ public partial class App : Node, IApp
private async Task<Node> LoadSceneInternal(string sceneName) private async Task<Node> LoadSceneInternal(string sceneName)
{ {
LoadingScreen.Show();
LoadingScreen.ProgressBar.Value = 0; LoadingScreen.ProgressBar.Value = 0;
var sceneLoader = new SceneLoader(); var sceneLoader = new SceneLoader();
CallDeferred(MethodName.AddChild, sceneLoader); CallDeferred(MethodName.AddChild, sceneLoader);

View File

@@ -10,12 +10,13 @@
process_mode = 3 process_mode = 3
script = ExtResource("1_rt73h") script = ExtResource("1_rt73h")
[node name="LoadingScreen" parent="." instance=ExtResource("3_3st5l")]
unique_name_in_owner = true
[node name="MainMenu" parent="." instance=ExtResource("2_1uiag")] [node name="MainMenu" parent="." instance=ExtResource("2_1uiag")]
unique_name_in_owner = true unique_name_in_owner = true
[node name="LoadingScreen" parent="." instance=ExtResource("3_3st5l")]
unique_name_in_owner = true
visible = false
[node name="OptionsMenu" parent="." instance=ExtResource("2_v0mgf")] [node name="OptionsMenu" parent="." instance=ExtResource("2_v0mgf")]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false

View File

@@ -56,6 +56,8 @@ public partial class Game : Node3D, IGame
[Signal] [Signal]
public delegate void SaveFileLoadedEventHandler(); public delegate void SaveFileLoadedEventHandler();
public event Action GameExitRequested;
#endregion #endregion
public RescuedItemDatabase RescuedItems { get; set; } = default!; public RescuedItemDatabase RescuedItems { get; set; } = default!;
@@ -174,6 +176,8 @@ public partial class Game : Node3D, IGame
GameOverMenu.NewGame += OnNewGame; GameOverMenu.NewGame += OnNewGame;
GameOverMenu.QuitGame += OnQuit; GameOverMenu.QuitGame += OnQuit;
PauseMenu.ExitGamePressed += OnQuit;
GameRepo.IsPaused.Sync += IsPaused_Sync; GameRepo.IsPaused.Sync += IsPaused_Sync;
InGameUI.PlayerInfoUI.Activate(); InGameUI.PlayerInfoUI.Activate();
} }
@@ -252,6 +256,12 @@ public partial class Game : Node3D, IGame
{ {
if (@event.IsActionPressed(GameInputs.Debug)) if (@event.IsActionPressed(GameInputs.Debug))
GameState.Input(new GameState.Input.DebugButtonPressed()); GameState.Input(new GameState.Input.DebugButtonPressed());
if (@event.IsActionPressed(GameInputs.Pause))
GameState.Input(new GameState.Input.PauseButtonPressed());
if (Input.IsActionJustPressed(GameInputs.Inventory))
GameState.Input(new GameState.Input.InventoryButtonPressed());
if (Input.IsActionJustPressed(GameInputs.Interact))
GameState.Input(new GameState.Input.InteractButtonPressed());
} }
private void HandleGameLogic() private void HandleGameLogic()
@@ -280,12 +290,13 @@ public partial class Game : Node3D, IGame
}) })
.Handle((in GameState.Output.OpenInventoryMenu _) => .Handle((in GameState.Output.OpenInventoryMenu _) =>
{ {
//InGameUI.InventoryMenu.RefreshInventoryScreen(); GameRepo.Pause();
InGameUI.InventoryMenu.Show(); InGameUI.InventoryMenu.Show();
InGameUI.InventoryMenu.SetProcessInput(true); InGameUI.InventoryMenu.SetProcessInput(true);
}) })
.Handle((in GameState.Output.CloseInventoryMenu _) => .Handle((in GameState.Output.CloseInventoryMenu _) =>
{ {
GameRepo.Resume();
InGameUI.InventoryMenu.Hide(); InGameUI.InventoryMenu.Hide();
InGameUI.InventoryMenu.SetProcessInput(false); InGameUI.InventoryMenu.SetProcessInput(false);
}) })
@@ -337,6 +348,10 @@ public partial class Game : Node3D, IGame
} }
LoadNextLevel.FadeOut(); LoadNextLevel.FadeOut();
}) })
.Handle((in GameState.Output.ExitGame _) =>
{
OnQuit();
})
.Handle((in GameState.Output.GameOver _) => .Handle((in GameState.Output.GameOver _) =>
{ {
//GameRepo.Pause(); //GameRepo.Pause();
@@ -507,7 +522,7 @@ public partial class Game : Node3D, IGame
LoadNextLevel.Hide(); LoadNextLevel.Hide();
} }
private void OnQuit() => GetTree().Root.QueueFree(); private void OnQuit() => GameExitRequested?.Invoke();
public void OnExitTree() public void OnExitTree()
{ {

View File

@@ -4,6 +4,7 @@ namespace Zennysoft.Game.Ma;
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.GodotNodeInterfaces; using Chickensoft.GodotNodeInterfaces;
using Chickensoft.SaveFileBuilder; using Chickensoft.SaveFileBuilder;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Zennysoft.Ma.Adapter; using Zennysoft.Ma.Adapter;
@@ -30,4 +31,6 @@ public interface IGame : IProvide<IGame>, IProvide<IGameRepo>, IProvide<IPlayer>
public Task Save(); public Task Save();
public QuestData QuestData { get; } public QuestData QuestData { get; }
public event Action GameExitRequested;
} }

View File

@@ -7,6 +7,7 @@
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ctjd"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ctjd"]
[node name="Control" type="Control"] [node name="Control" type="Control"]
process_mode = 3
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0

View File

@@ -60,5 +60,14 @@ public partial class GalleryMenu : Control
BackButton.Pressed += BackButton_Pressed; BackButton.Pressed += BackButton_Pressed;
} }
public override void _Input(InputEvent @event)
{
if (!Visible)
return;
if (@event.IsActionPressed(GameInputs.Interact))
BackButton.GrabFocus();
}
private void BackButton_Pressed() => EmitSignal(SignalName.GalleryExited); private void BackButton_Pressed() => EmitSignal(SignalName.GalleryExited);
} }

View File

@@ -57,13 +57,4 @@ public partial class InGameUI : Control, IInGameUI
InventoryMenu.Hide(); InventoryMenu.Hide();
InventoryMenu.SetProcessInput(false); InventoryMenu.SetProcessInput(false);
} }
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed(GameInputs.Inventory) && !InventoryMenu.Visible)
{
GD.Print("Inventory button pressed");
InGameUILogic.Input(new InGameUILogic.Input.ShowInventory());
}
}
} }

View File

@@ -100,7 +100,7 @@ public partial class InventoryMenu : Control, IInventoryMenu
SfxDatabase.Instance.Play(SoundEffect.MoveUI); SfxDatabase.Instance.Play(SoundEffect.MoveUI);
} }
public override void _Input(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
{ {
if (!Visible) if (!Visible)
return; return;
@@ -108,12 +108,6 @@ public partial class InventoryMenu : Control, IInventoryMenu
if ((!Input.IsActionJustPressed(GameInputs.UiUp) && Input.IsActionPressed(GameInputs.UiUp)) || (!Input.IsActionJustPressed(GameInputs.UiDown) && Input.IsActionPressed(GameInputs.UiDown))) if ((!Input.IsActionJustPressed(GameInputs.UiUp) && Input.IsActionPressed(GameInputs.UiUp)) || (!Input.IsActionJustPressed(GameInputs.UiDown) && Input.IsActionPressed(GameInputs.UiDown)))
AcceptEvent(); AcceptEvent();
if (Input.IsActionJustPressed(GameInputs.Inventory) && !(UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()))
{
SfxDatabase.Instance.Play(SoundEffect.CancelUI);
AcceptEvent();
_gameRepo.CloseInventory();
}
if (Input.IsActionJustPressed(GameInputs.UiCancel) && (UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus())) if (Input.IsActionJustPressed(GameInputs.UiCancel) && (UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()))
{ {
SfxDatabase.Instance.Play(SoundEffect.CancelUI); SfxDatabase.Instance.Play(SoundEffect.CancelUI);
@@ -153,6 +147,7 @@ public partial class InventoryMenu : Control, IInventoryMenu
} }
else else
{ {
SfxDatabase.Instance.Play(SoundEffect.CancelUI);
_enableMenuSound = false; _enableMenuSound = false;
} }
} }

View File

@@ -2,6 +2,7 @@ using Chickensoft.AutoInject;
using Chickensoft.GodotNodeInterfaces; using Chickensoft.GodotNodeInterfaces;
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Godot; using Godot;
using System;
namespace Zennysoft.Game.Ma; namespace Zennysoft.Game.Ma;
public interface IPauseMenu : IControl public interface IPauseMenu : IControl
@@ -10,6 +11,8 @@ public interface IPauseMenu : IControl
void FadeOut(); void FadeOut();
event PauseMenu.UnpauseButtonPressedEventHandler UnpauseButtonPressed; event PauseMenu.UnpauseButtonPressedEventHandler UnpauseButtonPressed;
event PauseMenu.TransitionCompletedEventHandler TransitionCompleted; event PauseMenu.TransitionCompletedEventHandler TransitionCompleted;
public event Action ExitGamePressed;
} }
[Meta(typeof(IAutoNode))] [Meta(typeof(IAutoNode))]
@@ -24,14 +27,32 @@ public partial class PauseMenu : Control, IPauseMenu
[Node] public IAnimationPlayer AnimationPlayer { get; set; } = default!; [Node] public IAnimationPlayer AnimationPlayer { get; set; } = default!;
[Node] public Button ResumeButton { get; set; } = default!;
[Node] public Button ExitButton { get; set; } = default!;
public event Action ExitGamePressed;
public void OnResolved() public void OnResolved()
{ {
AnimationPlayer.AnimationFinished += OnAnimationFinished; AnimationPlayer.AnimationFinished += OnAnimationFinished;
ResumeButton.Pressed += ResumeButton_Pressed;
ExitButton.Pressed += ExitButton_Pressed;
} }
public void FadeIn() => AnimationPlayer.Play("fade_in"); private void ExitButton_Pressed() => ExitGamePressed?.Invoke();
private void ResumeButton_Pressed() => FadeOut();
public void FadeIn()
{
ResumeButton.GrabFocus();
AnimationPlayer.Play("fade_in");
}
public void FadeOut() => AnimationPlayer.Play("fade_out"); public void FadeOut()
{
ResumeButton.ReleaseFocus();
ExitButton.ReleaseFocus();
AnimationPlayer.Play("fade_out");
}
public override void _UnhandledInput(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
{ {
@@ -41,5 +62,7 @@ public partial class PauseMenu : Control, IPauseMenu
public void OnAnimationFinished(StringName name) public void OnAnimationFinished(StringName name)
{ {
if (name == "fade_out")
Hide();
} }
} }

View File

@@ -1,4 +1,9 @@
[gd_scene load_steps=5 format=3 uid="uid://blbqgw3wosc1w"] [gd_scene load_steps=9 format=3 uid="uid://blbqgw3wosc1w"]
[ext_resource type="Script" uid="uid://cbal5oeaha4nx" path="res://src/ui/pause_menu/PauseMenu.cs" id="1_1mfd6"]
[ext_resource type="FontFile" uid="uid://beh6d5lo5ihq0" path="res://src/ui/fonts/georgiai.ttf" id="2_o1qdr"]
[ext_resource type="StyleBox" uid="uid://bxuy4tnftibfq" path="res://src/options/SelectedOptionsBox.tres" id="2_xhp56"]
[ext_resource type="StyleBox" uid="uid://bl15q835s4ene" path="res://src/options/UnselectedOptionsBox.tres" id="3_o1qdr"]
[sub_resource type="Animation" id="Animation_f1eqn"] [sub_resource type="Animation" id="Animation_f1eqn"]
length = 0.001 length = 0.001
@@ -62,6 +67,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_1mfd6")
[node name="AnimationPlayer" type="AnimationPlayer" parent="."] [node name="AnimationPlayer" type="AnimationPlayer" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
@@ -77,3 +83,53 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
color = Color(0, 0, 0, 1) color = Color(0, 0, 0, 1)
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="ResumeButton" type="Button" parent="CenterContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_fonts/font = ExtResource("2_o1qdr")
theme_override_font_sizes/font_size = 38
theme_override_styles/focus = ExtResource("2_xhp56")
theme_override_styles/disabled_mirrored = ExtResource("3_o1qdr")
theme_override_styles/disabled = ExtResource("3_o1qdr")
theme_override_styles/hover_pressed_mirrored = ExtResource("3_o1qdr")
theme_override_styles/hover_pressed = ExtResource("3_o1qdr")
theme_override_styles/hover_mirrored = ExtResource("3_o1qdr")
theme_override_styles/hover = ExtResource("3_o1qdr")
theme_override_styles/pressed_mirrored = ExtResource("3_o1qdr")
theme_override_styles/pressed = ExtResource("3_o1qdr")
theme_override_styles/normal_mirrored = ExtResource("3_o1qdr")
theme_override_styles/normal = ExtResource("3_o1qdr")
text = "Resume"
flat = true
[node name="ExitButton" type="Button" parent="CenterContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_fonts/font = ExtResource("2_o1qdr")
theme_override_font_sizes/font_size = 38
theme_override_styles/focus = ExtResource("2_xhp56")
theme_override_styles/disabled_mirrored = ExtResource("3_o1qdr")
theme_override_styles/disabled = ExtResource("3_o1qdr")
theme_override_styles/hover_pressed_mirrored = ExtResource("3_o1qdr")
theme_override_styles/hover_pressed = ExtResource("3_o1qdr")
theme_override_styles/hover_mirrored = ExtResource("3_o1qdr")
theme_override_styles/hover = ExtResource("3_o1qdr")
theme_override_styles/pressed_mirrored = ExtResource("3_o1qdr")
theme_override_styles/pressed = ExtResource("3_o1qdr")
theme_override_styles/normal_mirrored = ExtResource("3_o1qdr")
theme_override_styles/normal = ExtResource("3_o1qdr")
text = "Exit Game"
flat = true