Game Dev Artisan Logo
In this post we’re going to continue our series on Godot Fundamentals. We’ll be creating a new main menu in which our game will start at and allow us to start and quit our game, as well as introduce some scene transitions when leaving and entering our menu.

Table of Contents

Introduction 

In this post we’re going to continue our series on Godot Fundamentals. We’ll be creating a new main menu in which our game will start at and allow us to start and quit our game, as well as introduce some scene transitions when leaving and entering our menu.

Video 

Create Our Main Menu Scene 

We'll begin by creating our new Main Menu scene based on a Control node, we'll save it a new folder called menus in our scenes/ui/ folder. We'll name this scene scenes/ui/menus/main_menu.tscn and give our Root node a name MainMenu.

To compose our scene we'll start with a ColorRect for our BG, we'll give this a color of #11161f and set it's anchor preset to Full Rect.

We'll add another child of our Root node as a sibling to our BG; this will be a MarginContainer and will have an anchor preset of Full Rect as well. We'll set the margin to 32px on all sides through the theme_override_constants values.

Next we'll want to align our title text as well as button based options in a vertical alignment using the VBoxContainer node. This will use the container sizing for fill in both vertical and horizontal alignment.

We'll add inside our VBoxContainer a Label and set it's Horizontal and Vertical Alignment to Center and the container sizing to also be fill in both settings, with expand set for the vertical. We'll also want to bump our font size to 32px by setting it in the theme_override_font_sizes/font_size. I'm setting the text for this label to Godot Fundamentals as that's our main project here, name it whatever you'd like, or maybe you've got a fun logo to use in it's place!

Next we'll add a VBoxContainer as a child of our current VBoxContainer to contain our Buttons. We'll name this node ButtonsVBox. This new container should have it's container sizing set to Fill on Horizontal and Shrink End on Vertical with Expand enabled.

Lastly let's add our Button children to this new VBoxContainer. We'll create one for our Start Game called StartGameButton and Exit called QuitButton. Each Button should have it's text set respectively. For the container sizing of our Buttons we'll set them to Shink Center on both Horizontal and Vertical.

Focus Our Buttons 

To improve on our menu navigation, we can enable the Focus elements by defining the layout and tabbing expectation for each of our buttons.

We'll be setting the Neighbor direction that is next in logical order, and works for wrapping around.

We can also use this approach for Tab behavior by using the Next and Previous fields. Define the correct order for Neighbors as Start goes to Exit both Top and Bottom, as well as Next and Previous.

Our Exit should goto Start via the Top and Bottom Neighbors and The Next and Previous.

We'll introduce more options when we update our in game menu later.

Theme Our Menus 

Currently our font and buttons are a bit basic out of the box. To improve this we can setup a new Theme by selecting the theme proprty and creating a new Theme. Let's save this in a new folder called themes in our assets folder -> assets/themes/default_theme.tres.

This will open up the Theme editor on the bottom panel and in the inspector we'll see the types of overrides we've introduce as part of our theme. We can start by setting our default font and font size.

For this example I'm using the Early GameBoy font that I got from dafont.com . You can use any .ttf type font. I'll be storing mine in the assets/fonts folder and setting our default font to this .ttf once imported. I'll also set the default font size to 16px.

Next I'll add an override for our Buttons for styling and color. Add an override by click the + icon in the Theme editor to add the Button Item Type. We can then choose the color and style overrides for the different types/states of our button.

I'll be setting a new focus, hover, and pressed color (first tab). Also inside our styles (second to last tab) i'll be overriding all to use a StyleBoxEmpty.

Save these changes, and now you can use this same theme on all your Control nodes that you want to use the same Theme. We'll be covering Themes in more detail in a later post, so stay tuned for that in the future.

You should have a main menu that looks like this:

main-menu-screenshot

Wiring It Up 

Now that we have the visuals, let's add some signals and wire up how we want to interact with this scene.

A practice that I like doing with menu's is that I'll setup the scene to handle button presses as normal and the root node will delegate the intended behavior to the outside world. For example we have 2 outcomes in this scene:

  • Start Game
  • Exit Game

We can have the MainMenu scene emit a signal when we want to start_game and for our exit_game we don't need to notify of the intent, just tell the scene tree to quit.

    get_tree().quit()

So let's start our script for the MainMenu -> scenes/ui/menus/main_menu.gd:

# main_menu.gd
extends Control

signal start_game()

func _on_start_game_button_pressed() -> void:
    start_game.emit()
    hide()

func _on_quit_button_pressed() -> void:
    get_tree().quit()

Another thing we can do here is set our expected default focus button. We can add a new function that handles focusing a button for us and call it when our node is ready as well as whenever the menu comes back into visibility (more on this later).

Let's give our ButtonsVBox a UniqueAccess name and bind that in our script, then we'll create ouf new function for focus_button. We also want to connect our visibility_changed signal to a function as well to check if we are visible we want to also call the focus_button function.

# main_menu.gd
extends Control
...
@onready var buttons_v_box: VboxContainer = %ButtonsVBox

func _ready() -> void:
    focus_button()
...
func _on_visbility_changed() -> void:
    if visible:
        focus_button()
...
func focus_button() -> void:
    if buttons_v_box:
        var button: Button = buttons_v_box.get_child(0)
        if button is Button:
            button.grab_focus()

Integrate Main Menu and UI 

Next let's update our UI scene to integrate the new Main Menu scene. We'll leverage our UI's script to wrap our expected contract of behavior for when our MainMenu's signals are emitted and the behavior we expect at a UI layer.

For example, when we select Start Game from the main menu, we want our UI layer to play a transition, meanwhile informing our Game scene that we want to populate our World scene and start the game from there. More on this later.

If we open our UI scene, we can add our MainMenu as a child of our Control node. Let's open our UI script and create a reference to our MainMenu using the Access Unique Name and bind it in our code.

# ui.gd
@onready var main_menu = %MainMenu

We'll also want to create a start_game signal

# ui.gd
signal start_game()

Next let's wire up our MainMenu's signal for start_game to our _on_main_menu_start_game function.

# ui.gd
func _on_main_menu_start_game():
    start_game.emit()

For now this just emits it's own signal for the UI layer, but we'll be adding additional behavior here as well, such as ensuring transitions complete their animations and other UI elements are properly visible.

Refactor Game Scene 

Now that our UI contains a MainMenu scene, we want our Game's default scene to no longer jump right into the World with our Tank. Instead we'll refactor our Game's scene to instantiate the scenes once we receive the start_game signal from our UI.

To get start started let's open our Game scene and remove the World and Tank children. This will break our connection for our Game's exported variable for tank as well as our Camera's target exported variable.

To fix our Game's exported variables let's create a TankScene and WorldScene exported variables of type PackedScene and refactor our tank variable to no longer be exported, it'll be a standard class variable now. We'll also create a world variable:

# game.gd
@export var TankScene: PackedScene
@export var WorldScene: PackedScene
...
var tank: Tank
var world: World

Now that our Tank and World scenes are exported, we can instantiate them once we call to the start_game function, this will also handle connecting our signals previously done in the _ready function, which can be removed entirely:

# game.gd
func start_game():
    world = WorldScene.instantiate()
    add_child(world)
    move_child(world, 0)

    tank = TankScene.instantiate()
    world.add_child(tank)
    tank.position = Vector2(922, 623)
    tank.collected.connect(ui._on_collected)
    tank.reload_progress.connect(ui._on_reload_progress)
    tank.reloaded.connect(ui._on_reloaded)
    tank.has_control = false
    
    camera.change_target(tank)
# camera.gd
@export var target: Node = null:
    set(tar):
        fallback_target = tar if fallback_target == null else fallback_target
        target = tar

When this function is called, it create's an instance of World and adds it to our Game's scene and move's it to the first index. We'll then create our Tank and add it World's scene. Our Tank position will be set and we'll wire up the signals, then change our camera target to our Tank.

We update our Camera to update our fallback_target when our target when it's initially set.

Since we deleted our _ready function, we'll be calling our start_game function by connecting our UI's start_game signal to our _on_ui_layer_start_game and calling the start_game function.

func _on_ui_layer_start_game():
    start_game()

In Game Menu Improvements 

Next we'll add some improvements to our in game menu to allow us to navigate back to our main menu and quit the game, as well as properly pausing our game when our menu is open.

Let's start by extracting our UI's Menu control node into it's own scene, we'll save it as scenes/ui/menus/game_menu.tscn.

Let's rename the VBoxContainer that contains our GridContainer to ButtonsVBox and we'll add our children buttons to this VBoxContainer.

First we'll create our ReturnToGameButton and move it to before our GridContainer, then add our MainMenuButton and QuitButton after the GridContainer. Set all of these Buttons Text Behavior to Left Alignment.

We'll also setup the Focus Neighbors for each button so that our Navigation is fluid. We can tie our Button's neighbors to also focus the Sliders for SFX and BG.

Since we've refactored our GameMenu out of our UI scene, we must also migrate over the Audio based connections and functions into our GameMenu script:

# game_menu.gd
extends Control
...
@onready var SFX_BUS_ID = AudioServer.get_bus_index("SFX")
@onready var MUSIC_BUS_ID = AudioServer.get_bus_index("Music")
@onready var buttons_v_box = %ButtonsVBox


func _on_sfx_slider_value_changed(value):
    AudioServer.set_bus_volume_db(SFX_BUS_ID, linear_to_db(value))
    AudioServer.set_bus_mute(SFX_BUS_ID, value < .05)


func _on_bg_slider_value_changed(value):
    AudioServer.set_bus_volume_db(MUSIC_BUS_ID, linear_to_db(value))
    AudioServer.set_bus_mute(MUSIC_BUS_ID, value < .05)

func focus_button() -> void:
    if buttons_v_box:
        var button: Button = buttons_v_box.get_child(0)
        if button is Button:
            button.grab_focus()

Notice that we also create a reference to our ButtonsVBox and bring in our focus_button function to grab the first button's focus by default. We'll also tie that function in when our GameMenus visibility changes and connect our signals for when our buttons are pressed, emitting the proper signals for when our GameMenu catches our button presses.

# game_menu.gd
signal return_to_game()
signal main_menu()

func _on_return_to_game_button_pressed():
    return_to_game.emit()


func _on_main_menu_button_pressed():
    main_menu.emit()


func _on_quit_button_pressed():
    get_tree().quit()


func _on_visibility_changed():
    if visible:
        focus_button()

Wire Game Menu to UI 

Back in our UI we will wire up our GameMenu's new signals to handle returning to the main menu and opening/closing the game menu.

We'll create new signals for our menu opening/closing, as well as when our UI quits to the menu. We'll update our _input function to only open our game menu if our main menu is not visible, and emit signals when opening/closing our game menu. We can also wire up our signals to the proper functions:

# ui.gd

signal menu_opened()
signal menu_closed()
signal quit_to_menu()

func _input(event):
    if !main_menu.visible and event.is_action_pressed("ui_cancel"):
        menu.visible = !menu.visible
        if menu.visible:
            menu_opened.emit()
        else:
            menu_closed.emit()

func _on_menu_main_menu():
    menu.hide()
    quit_to_menu.emit()
    main_menu.show()


func _on_menu_return_to_game():
    menu.hide()
    menu_closed.emit()

Next we'll wire our UI's new quit_to_menu signal to our Game's _on_ui_layer_quit_to_menu function to free our World and reset the camera.

# game.gd
func _on_ui_layer_quit_to_menu():
    if world:
        world.queue_free()
        world = null
    tank = null
    camera.reset()
# camera.gd
func reset() -> void:
    target = null
    fallback_target = null

Pause Game on Menus 

Now that we are opening/closing our menu in game, we want to pause our game to prevent movement and camera panning around. To pause our game, we must first define which of our nodes should respect our pause state, and what nodes we allow continue processing as usual. Read more about Pausing games and process mode from the Godot Documentations .

We can implement pausing using Godot's built in process_mode capability by simply modifying the property inside the inspector. For example we want our World scene to have it's default process_mode set to Pausable in the inspector.

This mode will tell this node to not run it's process functions while our Scene Tree's paused variable is true. We can set our UI's process_mode to Always which means the UI will always be processed, regardless of the Scene Tree's paused state.

If we want our BG music to always play, we'll set the AudioStreamPlayer's process_mode to Always.

Next we'll toggle the pause state when we handle our UI's signals within our Game script and update our start_game function to also unpause our tree when we start the game, after we set our Camera's target:

# game.gd

func start_game():
    ...
    camera.change_target(tank)
    get_tree().paused = false

func _on_ui_layer_menu_closed():
    get_tree().paused = false


func _on_ui_layer_menu_opened():
    get_tree().paused = true

Scene Transitions 

Alright, now let's setup a transition animation between our scene changes. Inside our UI scene add a new child in our Control node of type ColorRect, we'll call this Transition and grant it Access Unique Name. We'll set the color to a solid black.

Inside the AnimationPlayer we'll create a new Animation called screen_transition. This will track the Transition's modulate property to go from the default solid white keyframed on frame 0, allowing the RESET track to be created. Then on our final frame (1s) we'll transition the property to a fully transparent color and keyframe it.

We'll also modify the Easing to 2.0 on our first keyframe, so that we ramp more towards the end of the animation.

Now we'll add the transition call inside our UI's script when we start_game and when we return the the main_menu:

# ui.gd
func _on_main_menu_start_game():
    start_game.emit()
    transition.show()
    animation_player.play("screen_transition")
    await animation_player.animation_finished
    transition.hide()


func _on_menu_main_menu():
    if animation_player.is_playing():
        await animation_player.animation_finished
    menu.hide()
    transition.show()
    animation_player.play_backwards("screen_transition")
    await animation_player.animation_finished
    transition.hide()
    quit_to_menu.emit()
    main_menu.show()


func _on_menu_return_to_game():
    if animation_player.is_playing():
        await animation_player.animation_finished
    menu.hide()
    menu_closed.emit()

Note that we are awaiting the animation_player's animation_finished signal to be emitted before hiding or finishing out the transition.

Recap 

We've successfully created a Themed menu with transitions between our Main Menu and our Game World. As always, this is just an introduction to the concept of scene transition and the methods in which we've wrapped our logic. Be sure to check out the project's GitHub project  for the source code! If you have questions or ideas on how to leverage these concepts, be sure to jump into our growing Discord community  and start a chat!

Happy Coding.

Want to support our work?Support us via Ko-fi