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:
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:
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!