Godot Progress Button

In many games there are user interface (UI) buttons that can only be pressed when a certain amount of progress occurrs. The progress can be as simple as the passage of time or part of a sophisticated point accumulation system. These types of buttons are common in real time strategy (RTS) and role playing games (RPG). This type of UI element may be thought of as a progress button (ProgressButton). This article examines how to construct a ProgressButton for Godot using GDScript.

Scenario

Our game calls for a button that can only be pressed after a milestone has been reached. The button controls a tooltip that selects what the player can place in the game. The milestone is an epoch. The button can be pressed as soon as a certain number of epochs have elapsed. The player needs to know roughly how much progress has been made towards the epoch milestone in visual form. Once the milestone is reached the button is enabled for the player to press. When the button is pressed the tooltip is loaded. After the player clicks the left mouse button the ProgressButton is reset and the tooltip is disabled until the next epoch milestone.

Construction

Progress button layout in Godot editorThe ProgressButton is made with the control nodes VBoxContainer, Button, and ProgressBar. The Button and ProgressBar nodes are children of the VBoxContainer to make the nodes stack vertically.

To Build:

  1. Create a new scene.The progress button
  2. Add a VBoxContainer.
  3. Rename the VboxContainer to ProgressButton.
  4. Add a Button as a child of ProgressButton.
  5. Add a ProgressBar as a child of ProgressBar.
  6. Attach a script to ProgressButton called ProgressButton.gd.
  7. With Button selected connect toggled signal to the ProgressButton node.
    • This step adds the _on_Button_toggled function to ProgressButton.gd
  8. With ProgressBar selected connect progress_changed signal to the ProgressButton node.
    • This step adds the _on_ProgressBar_value_changed function to ProgressButton.gd

 

ProgressButton.gd

ProgressButton.gd provides the interface other nodes consume to instance copies of ProgressButton. The interface consists of max_val to represent the maximum value of the ProgressBar, tooltip_mode to represent the mode the ProgressButton sets the tooltip to, tooltip which supplies ProgressButton an interface to control the tooltip, and clicks to track the number of clicks allowed by the tooltip after the epoch milestone is reached.

extends VBoxContainer

export var max_val = 100
export var tooltip_mode = 0
var tooltip_enabled = false
var tooltip

ProgressButton defines a signal to tell the parent node when it should reset ProgressButton to its next state.

signal reset

The reset signal is used to tell the parent node when to reset the state of the progress button.

func _ready():
    $ProgressBar.max_value = max_val

The max_value of the progress bar is set in the _ready function.

func _on_ProgressBar_value_changed(value):
    if value >= $ProgressBar.max_value:
        $Button.disabled = false
    else:
        $Button.disabled = true

When the value of the progress bar changes a signal is emitted. Whenever that signal is emitted if the value of the progress bar is greater than or equal to the max value of the progress bar the button is enabled.

func _on_Button_toggled(button_pressed):
    if button_pressed and not tooltip_enabled:
        tooltip_enabled = true

When the button is pressed the tooltip is enabled. This allows the player to spend their tooltip “click”.

func _input(_event):
    # If a tooltip is registered check the tooltip mode against the button mode.
    if tooltip and tooltip.mode == tooltip_mode:
        handle_click()
    else:
        handle_click()

If there is a tooltip registered to this button and the tooltip mode matches the mode set for the button then _input calls handle_click. This prevents handle_click from being called when other ProgressButtons control the tooltip. If no tooltip is registered handle_click is called to allow testing the ProgressButton.

func handle_click():
    if (Input.is_action_just_pressed("mouse_left")
    and tooltip_enabled
    and $Button.pressed
    and not $Button.is_hovered()):
        $ProgressBar.value = 0
        tooltip_enabled = false
        emit_signal("reset")

handle_click handles the player’s input after the ProgressButton has been pressed.

Testing the ProgressButton

Testing in this case means building an interactive example that uses the ProgressButton.

To build the example:

  1. Create a new scene.
  2. Add a Control node.
  3. Rename the control node to ProgressButtonTest.
  4. Instance the ProgressButton as a child of ProgressButtonTest.
  5. Attach a script to ProgressButtonTest called ProgressButtonTest.gd.
  6. With ProgressButton selected connect reset to the ProgressButtonTest node.
    • This step adds _on_ProgressButton_reset to ProgressButtonTest.gd.

ProgressButtonTest.gd

extends Control

var intervals = 0
var interval = 0

intervals represents how many intervals are shown in the ProgressButton progress bar. interval represents the current interval based on the elapsed epochs.

signal epoch

The epoch signal is emitted every time an epoch has elapsed. In this example an epoch is one second.

func create_timer(wait_time):
    var timer = Timer.new()
    timer.set_wait_time(wait_time)
    timer.set_one_shot(true)
    timer.connect("timeout", timer, "queue_free")
    add_child(timer)
    timer.start()
    return timer

A timer node is used to for emitting the epoch signal. The create_timer function is a helper for creating new timer nodes.

func _ready():
    $ProgressButton/ProgressBar.value = interval
    intervals = $ProgressButton.max_val
    connect("epoch", self, "_on_epoch")
    while true:
        emit_signal("epoch")
        yield(create_timer(1), "timeout")

On ready, the value of the ProgessButton progress bar is set to the current interval and intervals is set to the maximum value of the ProgressButton. Next the epoch signal is connected to the ProgressButtonTest node. _on_epoch advances the state of the ProgressButton each time the epoch signal is emitted. Finally, an infinite loop emits the epoch signal and yields execution until the next timer ends. This allows other processing to occur while waiting for the next epoch.

func _on_epoch():
    if interval < intervals:
        interval += 1
        $ProgressButton/ProgressBar.value = interval

For each epoch, if the current interval is less than the maximum intervals interval is incremented and the ProgressButton‘s progress bar value is set to the current interval.

func _on_ProgressButton_reset():
    interval = 0
    $ProgressButton/ProgressBar.value = interval

When it is time to reset the progress button interval and the progress bar are both reset to zero.

Conclusion

The ProgresssButton is useful controlling actions that have “cooldown” periods after the action has been used. The construction of this UI element is straight forward. Although this example focuses on the passage of time, other mechanisms such as progress towards a certain goal may be used instead.