Skip to content

Menu and Levels

Adding in a title screen and new levels

Section titled Adding in a title screen and new levels
  1. Create a new scene and name it main_menu.tscn.
    Section titled Create a new scene and name it main_menu.tscn.

    Add these nodes to the scene tree:

    • A Control node and name it “Main Menu”. Then add these nodes as children:

      • A Label which will be your games’ name.

      • A TextureRect (for photos) or ColourRect (for just a colour) which will be the background for your games main menu.

      • A “Start Game” Button. You can also change the font/colour themes for the buttons too.

      • A “Levels” Button.

      • An “Exit” Button.

      It could look something like this: title screen photo with start game, level menu, and quit game buttons

  2. Change this scene to your main scene. Do this by clicking Project > Project Settings > Application > Run and changing the “Main Scene” to main_menu.tscn.

  3. Add a script to your “Main Menu” Control node. Leave this for now.

  4. Connect your three buttons when each “BaseButton” is pressed.

  5. Create a new scene and name it levels_menu.tscn.
    Section titled Create a new scene and name it levels_menu.tscn.

    Copy CMD/CTRL C and paste CMD/CTRL V all the main_menu.tscn nodes into this scene. Change these nodes in the scene tree:

    • Name the Control node “Levels Menu”.

      1. Keep the Label the same.

      2. Keep the TextureRect or ColourRect the same.

      3. Remove the Signal connection from the “Start Game” Button and rename it to “Level 1”.

        Removing signal connection: Select the Node > pressed() function and CMD/CTRL BACKSPACE it.

      4. Remove the “Levels” Button.

      5. Remove the “Exit” Button.

      It could look something like this: levels menu photo with start game and level 1 to 3 buttons

  6. In the “Level 1” Button, check the Inspector > BaseButton > Toggle Mode to “On”.

  7. In the same section, click the down arrow in the ButtonGroup and click New ButtonGroup.

    Click the down arrow again and click “Save As…” and name it levels.tres.

  8. Copy and paste this Button according to the number of levels you want to have. I have done 3. Make sure each Level Button is in the levels.tres ButtonGroup!!

  9. Add a new script onto the root node of levels_menu.tscn. You might need to remove the previous script if you copy/pasted it from main_menu.tscn.

  10. Add a “Start Game” Button and connect a pressed() Signal to it. You should know how to do this.

  11. Create a new scene and name it track_2.tscn. In the code later on, you’ll see why these tracks need the track_[num].tscn naming convention!

  12. Click track_1.tscn and click the top Node3D. Copy and paste this into your new track scene.

    • You’ll notice it pastes everything in there. In order to just change your new track without also changing the first track, you’ll need to delete everything apart from the top node.
    • After this, go into your track_1.tscn and copy everything underneath the top node. Paste this into your track_2.tscn. While it’s still all selected, right-click these nodes and click Access as Unique Name.
    • This should make each track unique, so that you can change the Path3D points without worrying about messing up your original track!
  13. Now change/add/remove your Path3D points and Area3D Checkpoint/s to create a harder or longer track.

    • If you have spare time, feel free to change the textures for a different look on each track.

    This is what my new track looks like! new track photo

Adding functionality to the levels menu and main menu

Section titled Adding functionality to the levels menu and main menu

So far, when you play these two scenes, nothing will happen.

For the levels menu, players should be able to choose a level, and the script will load the appropriate track. If they pick the first level, the default world scene is used. For other levels, the script dynamically modifies the world to include the chosen track.

Add this code to the levels_menu.tscn script. Go through each line and read the comments to understand why it’s there.

GDScript Code for the levels menu
levels_menu.gd
extends Control
# The 'levels.tres' button group, used for which level the player has selected.
@onready var button_group = $"Level 1".button_group
# Preloads the base world scene. This is the template for all levels.
@onready var world = preload("res://world.tscn") as PackedScene
func _ready() -> void:
pass
func _process(delta: float) -> void:
pass
# When the "Start Game" button is pressed.
func _on_start_game_pressed() -> void:
# Get the level number from the selected button's name (e.g., "Level 1").
var num = button_group.get_pressed_button().name.split(" ")[1]
# If the selected level is "1", just load the default world scene directly.
if (num == "1"): # Level 1 is already part of the base world scene.
get_tree().change_scene_to_packed(world) # Switch to the base world scene.
else:
# For levels other than 1, modify the world scene with the chosen track.
change_world_level(num)
# This function creates and loads a custom world for the selected level.
func change_world_level(num):
# Create an instance (a copy) of the base world scene to modify it.
var world_instance = world.instantiate()
# Check if there's already a track in the base world and remove it.
var track_1 = world_instance.get_node_or_null("Track") # Look for an existing track node.
if track_1:
world_instance.remove_child(track_1) # Detach the track node from the world.
track_1.queue_free() # Delete the old track from memory.
# Load the new track scene based on the chosen level number.
var new_track = load("res://track_" + str(num) + ".tscn") as PackedScene
var new_track_instance = new_track.instantiate() # Create an instance of the new track.
# Add the new track to the world instance.
world_instance.add_child(new_track_instance)
new_track_instance.name = "Track" # Name the new track so it can be identified later.
new_track_instance.owner = world_instance # Set ownership so the track is saved properly.
# Create a new PackedScene to store the modified world with the new track.
var new_world = PackedScene.new()
if new_world.pack(world_instance) == OK: # Try to "pack" the modified world into a new scene.
# Define a temporary file path to save the new world scene.
var temp_scene_path = "user://temp_world.tscn"
# Save the packed scene to the temporary file path.
ResourceSaver.save(new_world, temp_scene_path)
# Load the saved scene into the game.
get_tree().change_scene_to_file(temp_scene_path)
else:
# If packing the scene fails, print an error message for debugging.
print("Error: Could not pack the modified world scene!")

After you’ve read the comments and understand most of the code, feel free to delete it to reduce clutter.

Now add this code to the main_menu.tscn script. This script is for the main menu of your racing game. It handles actions when players click buttons like Start Game, Choose Level, or Exit Game. Depending on what the player selects, the game either starts, shows the level selection screen, or exits.

Go through each line and read the comments to understand why it’s there.

GDScript Code for the main menu
main_menu.gd
extends Control
# Preloads the default starting level scene. This is the first race track.
@onready var start_level = preload("res://world.tscn") as PackedScene
# Preloads the level selection menu scene. This screen lets the player pick a specific level.
@onready var choose_level = preload("res://levels_menu.tscn") as PackedScene
func _ready() -> void:
pass
func _process(delta: float) -> void:
pass
# This function is called when the "Start Game" button is clicked.
func _on_start_game_pressed() -> void:
# Switches the game to the default starting level.
get_tree().change_scene_to_packed(start_level)
# This function is called when the "Exit Game" button is clicked.
func _on_exit_game_pressed() -> void:
# Quits the game application.
get_tree().quit()
# This function is called when the "Choose Levels" button is clicked.
func _on_levels_pressed() -> void:
# Switches the game to the level selection menu.
get_tree().change_scene_to_packed(choose_level)

Adding in a pause menu in the HUD

Section titled Adding in a pause menu in the HUD

If you want to pause the game and switch levels or restart your current game if you’ve crashed, we’ll need a popup menu!

Here’s a preview of the end result: new track photo

The red lines indicate the new nodes that you will be adding into your HUD scene.

  1. Inside the hud.tscn scene, add a script to the root Control node. This will be for connecting signals for the various buttons - like restarting the game - and the functions to do this.

  2. Under your Panel which contains the “Laps”, “Reversing”, and “Time” labels, add a “Pause Race” Button.

    Connect a “pressed()” signal via the Node tab on the sidebar.

  3. Turn the Inspector > Visibility > Top Level to “On”, so you can still interact with the Button when the crashed and finished screens come up. I also changed the font size to 25px.

  4. Add a PanelContainer to the root node of this scene. Center this on your HUD scene.

  5. In the inspector, you can change the alignment to “Center”.

  6. Add 4 Button’s to your VBoxContainer. Name them “Home”, “Quit Game”, “Restart”, and “Resume”.

    Each Button should have:

    • Font size 30px.
    • A “pressed()” signal connected to the hud.gd script. Each function name needs to be different.
  7. Go to your hud.gd script.

    Add this functionality for each of the buttons to work properly.

    hud.gd
    extends Control
    # Reference to the pause menu UI (a PanelContainer node in your scene).
    @onready var pause_menu = $PanelContainer
    # Preloads the main menu scene so it’s ready to load when the player chooses to go home.
    @onready var home_scene = preload("res://main_menu.tscn") as PackedScene
    # This function runs when the node enters the scene tree for the first time.
    func _ready() -> void:
    # Hide the pause menu when the game starts.
    pause_menu.hide()
    func _process(delta: float) -> void:
    pass
    func _on_pause_pressed() -> void:
    # Show the pause menu and pause the game.
    pause_menu.show()
    get_tree().paused = true # This stops the game’s logic and animations.
    func _on_home_pressed() -> void:
    # Unpause the game and hide the pause menu before going to the main menu.
    get_tree().paused = false
    pause_menu.hide()
    get_tree().change_scene_to_file("res://main_menu.tscn") # Switch to the main menu scene.
    func _on_quit_pressed() -> void:
    # Quit the game application.
    get_tree().quit()
    func _on_restart_pressed() -> void:
    # Unpause the game and reload the current level.
    get_tree().paused = false
    get_tree().reload_current_scene() # Reloads the currently active scene.
    func _on_resume_pressed() -> void:
    # Hide the pause menu and unpause the game to continue playing.
    pause_menu.hide()
    get_tree().paused = false
Contribute Donate