//rp.wtf

Godot enum drop-down

Wed Oct 04 2023

Enums are great if you want to represent a finite choice in your data, like your character's race in an RPG, a graphics preset, a category of an inventory item, etc. Lots of programming languages have enums, and GDScript isn't an exception. It's a neat way to express a type of thing.

So, if you didn't already know, you can define and @export an enum for a Node in Godot like this:

enum Direction {
    LEFT,
    RIGHT,
}

@export var direction: Direction

and in the Godot inspector, it generates this neat drop-down menu:

Godot enum export drop down menu

Then we can use that value to implement some sort of handling:

func _process(_delta: float) -> void:
    match direction:
        Direction.LEFT:
            # do this

        Direction.RIGHT:
            # do that

(Thanks to @njamster for noticing a silly syntax error the code originally had.)

But what if we want to offer The Player a choice in the same manner?

The OptionButton

Godot offers the OptionButton which, where I come from, is simply called a drop down menu. Just like most Control nodes, you can theme it and style it however you like, but we are not gonna go into that (I also don't have a sense of style).

So, one way we would use that to create a menu for the player would be to manually add every item of the enum to the menu, and while that gets the job done, when we start needing multiple of these menus or start changing the enum itself, it, as they say, does not scale very well. That's why we are going to create a scene that will take an enum and populate the menu using its keys!

Create a new scene by pressing Scene → New Scene. Then select "Other Node" in the "Create Root Node" box and look for "OptionButton" in the Create New Node screen. Straight away, you can save it as, say "enum_option_button.tscn", then right-click on the freshly created root node and click "Attach Script". Select "Object: Empty" as the "Template" so we start with a clean slate and click "Create".

We're going to expose two variables - option_enum for the actual choices and default in case we want to preselect a value, and we want to define setters, because we need stuff to happen when those values are assigned!

@export var default: int:
    set = set_default

# we won't export "option_enum", we only do that programmatically
var option_enum: Dictionary:
    set = set_option_enum

You might notice that option_enum has a type of Dictionary. That's because named enums (Direction is a named enum since it has, well, a name) are essentially a Dictionary and that's the fact we want to exploit to build our menu. We want to add two methods that will make implementing our setters easier:

func select_value(value: int) -> void:
    select(get_item_index(value))


func select_default() -> void:
    select_value(default)

Firstly, select_value() is a convenience method that uses two OptionButton methods - select() which takes an index to select and get_item_index() which returns the item index of a value within the menu. And then we have select_default() which utilizes that method to just select the current default. Now let's implement the setters!

func set_default(new_default: int) -> void:
    default = new_default
    select_default()


func set_option_enum(new_option_enum: Dictionary) -> void:
    option_enum = new_option_enum

    clear()

    for key in option_enum:
        var value: int = option_enum.get(key)
        add_item(key.capitalize(), value)

    select_default()

The setter for set_default isn't very interesting. It just assigns the value and tells the OptionButton to select it using the methods we defined previously. Now, the option_enum setter clears all the current items, then iterates through all the keys in the dictionary and adds them to the menu by using the enum key as the label for the menu item and the enum value as the id of the menu item. And finally, it selects the default.

As the last thing for our enum option button scene, the OptionButton has a signal item_selected which emits the item index, which is fine, but for this scene to become really convenient, we will define a custom one, which emits the selected value instead:

signal value_selected(value: int)


func _ready() -> void:
	item_selected.connect(
		func (index: int): value_selected.emit(
			option_enum.values()[index]
		)
	)

So, what we're doing here is defining our own value_selected signal, which connects to the item_selected signal of the OptionButton and then gets the enum values as an Array from which it gets the selected value by using the index sent by the item_selected signal.

That's it. To recap, here's the complete script:

# enum_option_button.gd
extends OptionButton

signal value_selected(value: int)

@export var default: int:
	set = set_default

# we won't export "option_enum", we only do that programmatically
var option_enum: Dictionary:
	set = set_option_enum


func _ready() -> void:
	item_selected.connect(
		func (index: int): value_selected.emit(
			option_enum.values()[index]
		)
	)


func set_default(new_default: int) -> void:
	default = new_default
	select_default()


func set_option_enum(new_option_enum: Dictionary) -> void:
	option_enum = new_option_enum

	clear()

	for key in option_enum:
		var value: int = option_enum.get(key)
		add_item(key.capitalize(), value)

	select_default()


func select_value(value: int) -> void:
	select(get_item_index(value))


func select_default() -> void:
	select_value(default)

Now we can instantiate this scene as a child scene anywhere and assign an enum to it:

# somewhere in your game
enum Direction {
	Left,
	Right,
}

@onready var enum_button: OptionButton = get_node("%EnumOptionButton")

func _ready() -> void:
	enum_button.option_enum = Direction

And it should result in something like this:

Godot enum export drop down menu

To get the actual value, you can use either, get_selected_id() or the value_selected signal we defined:

var some_direction: Direction = enum_button.get_selected_id() as Direction

# of course, you can also connect the signal via the inspector
enum_button.value_selected.connect(
        func (value: Direction): some_direction = value
)

enum_button.default = Direction.RIGHT

Keep in mind that if you have assigned custom values to the enum keys, e.g., LEFT = 10, RIGHT = 20, you have to assign a default otherwise, the first option won't be selected!

That's it. I hope this helped you learn something new. If you have any questions, suggestions, or corrections, feel free to reach out!