What is the best way to handle menu NAVIGATION in gdscript / scene layout

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Connor

I am trying to create a main menu for my game, which I technically already have, but it currently has no way to transition between multiple levels of menu (“into Game select screen, back up to the main menu, into options, back again, etc”) and every single resource on designing a menu ignores this part. I am fairly new to gdscript and it is loose enough that it would be extremely easy to make a system that is extremely janky.

So my question essentially is does anyone know of the “best practices” or common approaches / design patterns used for menu navigation in gdscript / godot? If there are any resources on this matter that you know of that would be nice to know about too. Loading separate scenes one after another doesn’t seem like a good option, stacking the entire menu on one scene in layers and showing / hiding them doesn’t seem particularly clean. Is there something I am failing to think of here or are those really the only options?

:bust_in_silhouette: Reply From: Connor

I took a page out of my old unreal engine games book and made a menu handler in my GameState singleton (autoload) that handles menus in a similar way to the same method used for handling scene switching in a singleton.

For anyone looking to do this it looks as follows:

you preload all of your menus into a dictionary:

#MENU_LEVEL.MAIN is index 1 not zero so keep that in mind if you change to an array
enum MENU_LEVEL {
    	NONE,
    	MAIN,
    	START,
    	JOIN,
    	OPTIONS
    }

var menus = {
	MENU_LEVEL.MAIN : preload("res://gui/MainMenuScreen.tscn").instance(), 
	MENU_LEVEL.START : preload("res://gui/StartGameScreen.tscn").instance(),
	MENU_LEVEL.JOIN : preload("res://gui/JoinGameScreen.tscn").instance(),
	MENU_LEVEL.OPTIONS : preload("res://gui/OptionsScreen.tscn").instance()
}

the enum is optional but it makes it nicer to call the switch menu function later. You could technically store all of the menus in an array or individually but that is lame. :stuck_out_tongue:

after the menus dictionary you have this var: var current_menu : Node = null
which stores a reference to the currently open menu

in _ready() call this: load_menu(MENU_LEVEL.MAIN)

and here is the magic sauce:

func load_menu(menulevel):
	call_deferred("_deferred_load_menu", menulevel)
	

func _deferred_load_menu(menulevel):
	#replace the current menus instance with the new ones
	current_menu = menus[menulevel]

	var container = current_scene.find_node("menu", false, false)
	if not container:
		var menunode = Node.new()
		menunode.set_name("menu")
		current_scene.add_child(menunode)
		container = menunode
	#clear the current menu item/s
	for location in container.get_children():
		container.remove_child(location)
	#add our selected menu
	container.add_child(current_menu)

that deferred calls the load menu function which replaces current_menu with a reference to our already initialized menu of chosen index, and then gets our “menu” node in our scene (or adds it if it doesn’t exist yet), empties the menu node of all children, and adds our current_menu back again to it.

The benefits of this approach are:

  • you only have to initialize your menu once and keep a copy of it
    without deleting and reinitializing menus every time you switch
    scenes
  • switching menus is as simple as calling load_menu(MENU_LEVEL.OPTIONS) and it will handle removing the current
    ui, replacing with the new one, keeping it organized in its own
    container node, and all of that other stuff
  • adding new menu items is as simple as providing the path to its scene and adding a new enum for it
  • you can make a re-usable main menu back button that always calls load_menu(MENU_LEVEL.MAIN) ← i did :wink:
  • it is very reliable and you can modify it to work in tandum with in game menus if you want as well.

Best of luck with this singleton menu controller pattern :slight_smile: Let me know if anyone reads this, uses this, likes it, or notices some sort of error. Feedback is most welcome!

Enjoy!

2 Likes

I did it and it seems to work ! Here’s my menu manager code :

extends Node

@onready var canvas_layer = get_tree().root.get_node("World/CanvasLayer")

enum MENU_LEVEL {
	NONE,
	MAIN 
        # ...
}

var menus = {
	MENU_LEVEL.MAIN: preload("res://scenes/main_menu.tscn").instantiate(),
}
var current_menu

func load_menu(menu_level):
	var container = canvas_layer.find_child("menu", false, false)
	if not container:
		var menu_node = Node.new()
		menu_node.set_name("menu")
		canvas_layer.add_child(menu_node)
		container = menu_node
	
	for menu_child in container.get_children():
		container.remove_child(menu_child)
	
	if menu_level != MENU_LEVEL.NONE:
		current_menu = menus[menu_level]
		container.add_child(current_menu)

added the if for 'MENU_LEVEL.NONE to use it to return to game (maybe a new function could be used for that)
I also referenced my canvas layer instead of the current scene to show it more easily