05.adding-buttons-to-dialogue-box

Adding buttons to our dialogue box

In this lesson, we’ll add buttons to our dialogue box based on each dialogue line’s dictionary.

By the end, you’ll be able to jump to various lines of dialogue.

Creating buttons

As we saw in a previous lesson, we use a Dictionary to represent buttons associated with a line of dialogue.

It maps the text to display on buttons to the ID of the line to jump to next.

# ...
{
    "Let me sleep a little longer": 2,
    "Let's do it!": 1,
}
# ...

To create buttons based on this data, we write a new function named create_buttons(). It takes a Dictionary like the one above as a parameter.

onready var buttons_column := $MarginContainer/VBoxContainer/ButtonsColumn

func create_buttons(buttons_data: Dictionary) -> void:
    for text in buttons_data:
        var button := Button.new()
        button.text = text
        buttons_column.add_child(button)

For each key in the buttons_data dictionary, we create a Button node and add it as a child of the ButtonsColumn node.

That’ll allow us to remove existing buttons easily when moving to the next line.

We want to jump to the button’s target line ID when pressing the button. We can do that by connecting to the button’s pressed signal and binding the ID.

Binding is the process of storing a value and “attaching” it to our signal’s callback.

We first extract the target line’s ID from the buttons_data. We then pass the value in the connect() function’s fourth argument.

func create_buttons(buttons_data: Dictionary) -> void:
    for text in buttons_data:
        # ...
        var target_line_id: int = buttons_data[text]
        # The last argument means that pressing the button will call the
        # show_line() function with the value of `target_line_id`.
        #
        # We must store this value in an array as this is what the connect()
        # function expects.
        button.connect("pressed", self, "show_line", [target_line_id])

When this button gets pressed, the show_line() function will be called with the target_line_id as its argument.

The complete function should have the following code:

func create_buttons(buttons_data: Dictionary) -> void:
    for text in buttons_data:
        var button := Button.new()
        button.text = text
        buttons_column.add_child(button)
        var target_line_id: int = buttons_data[text]
        # The last argument means that pressing the button will call the
        # show_line() function with the value of `target_line_id`.
        #
        # We must store this value in an array as this is what the connect()
        # function expects.
        button.connect("pressed", self, "show_line", [target_line_id])

We can now call the create_buttons() function from show_line().

func show_line(id: int) -> void:
    # ...
    if line_data.buttons:
        create_buttons(line_data.buttons)

Destroying buttons

If you run the scene now, the buttons appear, and clicking them takes you to a new line of dialogue.

However, we have a problem: each button click causes more buttons to appear.

When displaying a new line of dialogue, we need to delete the buttons from the previous line. We can do that with the queue_free() member function.

The queue_free() function

Every node in Godot comes with a member function named queue_free(). Calling it asks the engine to mark the node for deletion and delete it as soon as possible.

Godot keeps a queue of these nodes in an array and deletes as many as it can at the end of each frame.

We must delete existing buttons before creating new ones. Otherwise, no button will ever appear. Add the new code before calling create_buttons().

func show_line(id: int) -> void:
    # ...
    for button in buttons_column.get_children():
        # Use this function to safely destroy a node when you don't need it
        # anymore.
        button.queue_free()
    # ...

The function get_children() returns an array with every Button that exists as a child of the ButtonsColumn node.

We can use it in a for loop to free every Button in the ButtonsColumn.

Quit button

When we reach the dialogue’s end, we don’t have any new buttons to create.

In this case, we can create a button that quits the game, as we did in the slideshow series.

We write a new function that creates a button, sets its text, and adds it as a child of the ButtonsColumn node.

func add_quit_button() -> void:
    var button := Button.new()
    button.text = "Quit"
    buttons_column.add_child(button)
    # We can directly connect a signal to a function or another object. Clicking
    # the button will call the scene tree's quit() function.
    button.connect("pressed", get_tree(), "quit")

We then call the new function from the show_line() function.

func show_line(id: int) -> void:
    # ...
    if line_data.buttons:
        create_buttons(line_data.buttons)
    # If the line has no buttons, we reached the dialogue's end, so we create a
    # single quit button.
    else:
        add_quit_button()

In the next lesson, we’ll make the dialogue box more appealing with text animation and audio.

Challenge: wiring the main menu screen

Open the practice Main menu screen.

Once you complete the practice, you should be able to navigate between the main menu screens by clicking the buttons.

The code

Here’s the complete code for DialogueTree.gd so far.

extends PanelContainer

var dialogue = {
    0: {
        "text": "Hey, [i]wake up![/i] It's time to make video games.",
        "expression": "neutral",
        "buttons":
        {
            "Let me sleep a little longer": 2,
            "Let's do it!": 1,
        }
    },
    1: {
        "text": "Great! Your first task will be to write a [b]dialogue tree[/b].",
        "expression": "neutral",
        "buttons":
        {
            "I'm not sure if i'm ready, but i will do my best": 3,
            "No, let me go back to sleep": 2,
        }
    },
    2: {
        "text": "Oh, come on! It'll be fun.",
        "expression": "sad",
        "buttons":
        {
            "No, really, let me go back to sleep": 0,
            "Alright, I'll try": 3,
        }
    },
    3: {
        "text": "That's the spirit! You can do it!\n[wave][b]YOU WIN[/b][/wave]",
        "expression": "happy",
        "buttons": {}
    }
}

onready var texture_rect := $MarginContainer/VBoxContainer/HBoxContainer/TextureRect
onready var rich_text_label := $MarginContainer/VBoxContainer/HBoxContainer/RichTextLabel
onready var buttons_column := $MarginContainer/VBoxContainer/ButtonsColumn


func _ready() -> void:
    show_line(0)


func show_line(id: int) -> void:
    # We extract the line's data from the `dialogue` variable.
    var line_data: Dictionary = dialogue[id]
    # We then pass the text and expression values of the line's data to
    # functions that update the displayed text and character portrait.
    set_text(line_data.text)
    set_expression(line_data.expression)

    for button in buttons_column.get_children():
        # Use this function to safely destroy a node when you don't need it
        # anymore.
        button.queue_free()

    if line_data.buttons:
        create_buttons(line_data.buttons)
    # If the line has no buttons, we reached the dialogue's end, so we create a
    # single quit button.
    else:
        add_quit_button()


func set_text(new_text: String) -> void:
    rich_text_label.bbcode_text = new_text


func set_expression(expression: String) -> void:
    if expression == "sad":
        texture_rect.texture = preload("portraits/sophia_sad.png")
    # Elif is the contraction of "else if." This block will run if the previous
    # condition didn't pass.
    elif expression == "happy":
        texture_rect.texture = preload("portraits/sophia_happy.png")
    # Any `expression` that we don't list above will display the neutral
    # character portrait.
    else:
        texture_rect.texture = preload("portraits/sophia_neutral.png")


func add_quit_button() -> void:
    var button := Button.new()
    button.text = "Quit"
    buttons_column.add_child(button)
    # We can directly connect a signal to a function or another object. Clicking
    # the button will call the scene tree's quit() function.
    button.connect("pressed", get_tree(), "quit")


func create_buttons(buttons_data: Dictionary) -> void:
    for text in buttons_data:
        var button := Button.new()
        button.text = text
        buttons_column.add_child(button)
        var target_line_id: int = buttons_data[text]
        # The last argument means that pressing the button will call the
        # show_line() function with the value of `target_line_id`.
        #
        # We must store this value in an array as this is what the connect()
        # function expects.
        button.connect("pressed", self, "show_line", [target_line_id])