Table of Contents
Introduction
Let's create a Confirmation Modal that awaits User input. We can use a confirmation modal to prompt a user to continue or cancel and execute follow up code accordingly.
Prompt Usage Preview
Once we've built out or scene and script, we'll be able to customize and await a prompt to determine the behavior we want to execute.
func prompt_quit_game() -> void:
conf.customize(
"Are you sure?",
"Any unsaved progress will be lost.",
"Confirm",
"Cancel"
)
var is_confirmed = await conf.prompt(true)
if is_confirmed:
get_tree().quit()
With this, our function will run, prompt the User to confirm or cancel, and returns the execution to the caller until the prompt has been confirmed. Once confirmed, we continue execution and quit our game.
Video
You can also follow along with the video on this topic.
Await
The await
keyword is documented on Godot's Documentation page and has a simplified example of this use case that we can review.
From these docs we can see how using the await
keyword will turn a simple function, into a coroutine that waits for a signal to be emitted, which resumes execution from the point at which it stops.
Confirmation Modal Scene
To lay out of ConfirmationModal
scene, we'll start with a Root Control
Node and call it ConfirmationModal
. This will have a child ColorRect
(named BG) and PanelContainer
(named Modal).
We can set the ConfirmationModal
and BG
Nodes to be Full Rect for anchor presets. For our Modal
we can set it's custom minimum size to 128x64.
Or Modal
will have a child MarginContainer
with margins of 16, 12, 16, 12. Our MarginContainer
will have a child VBoxContainer
with a vertical separation of 8.
This VBoxContainer will have the children of Label
(named HeaderLabel), a Label
(named MessageLabel), and an HBoxContainer
to contain our 2 Button Nodes (named ConfirmButton and CancelButton). Our Label
and Button
Nodes should all have Unique Name Flags set, so that we can reference them from within our script.
Set the default text for our Label
and Button
Nodes, and adjust the alignment to our liking; for me I set the HBox to center the buttons, and set them to not fill the full container.
Be sure to set the process mode of our ConfirmationModal
root Control
node to always process, to prevent it from pausing as well when we pause upon prompt.
Our Scene Tree should look like this:
Confirmation Modal Script
We can attach a script to our scene and give it a class_name of ConfirmationModal
that extends Control
.
class_name ConfirmationModal extends Control
...
Next we'll define our signal confirmed
that passes a value of is_confirmed
as a Boolean.
signal confirmed(is_confirmed: bool)
CTRL/CMD + DRAG and DROP our Label
and Button
Nodes to create an onready reference.
@onready var header_label: Label = %HeaderLabel
@onready var message_label: Label = %MessageLabel
@onready var confirm_button: Button = %ConfirmButton
@onready var cancel_button: Button = %CancelButton
Define our variables to track if our Modal is open, and if it should unpause when dismissed.
var is_open: bool = false
var _should_unpause: bool = false
Next we'll setup our ready
function to disable input handling and connect our button's pressed
signal to some handler methods.
func _ready() -> void:
set_process_unhandled_key_input(false)
if confirm_button:
confirm_button.pressed.connect(_on_confirm_button_pressed)
if cancel_button:
cancel_button.pressed.connect(_on_cancel_button_pressed)
hide()
We'll stub out those handler methods.
func _on_confirm_button_pressed() -> void:
pass
func _on_cancel_button_pressed() -> void:
pass
We'll add an input handler for when we cancel out using the ui_cancel
action. We'll define our cancel
method in a moment.
func _unhandled_key_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
cancel()
Next we'll define our internal handler for closing out a modal and confirming it with a boolean. Notice we use the set_deferred
method to update our is_open
variable in a deferred way, preventing timing issues. This will emit our confirmed
signal, which completes our coroutine and resumes execution from our prompt
function seen later.
func _close_modal(is_confirmed: bool) -> void:
set_process_unhandled_key_input(false)
confirmed.emit(is_confirmed)
set_deferred("is_open", false)
hide()
if _should_unpause:
get_tree().paused = false
With our handler for confirmation, we can add convenience methods for closing, confirming, and canceling the modal.
func close(is_confirmed: bool = false) -> void:
if is_confirmed:
confirm()
else:
cancel()
func confirm() -> void:
_close_modal(true)
func cancel() -> void:
_close_modal(false)
We can update our button pressed handlers to call the respective methods.
func _on_confirm_button_pressed() -> void:
confirm()
func _on_cancel_button_pressed() -> void:
cancel()
Now we can create the method that allows us to customize our Modal. We'll simply update the text of our Label
and Button
nodes.
func customize(header: String, message: String, confirm_text: String = "Yes", cancel_text: String = "No") -> ConfirmationModal:
header_label.text = header
message_label.text = message
confirm_button.text = confirm_text
cancel_button.text = cancel_text
return self
Lastly, our main function to prompt for our User's input, the prompt
function. Calling this will await for our confirmed
signal to be emitted before resuming it's execution and returning the boolean of our confirmation.
Also our _should_unpause
is a check if we are not already paused and we want to pause the game when using this prompt. We also enable our handler for key input to listen for our ui_cancel
action to be pressed.
func prompt(pause: bool = false) -> bool:
_should_unpause = get_tree().paused == false and pause
if pause:
get_tree().paused = true
show()
is_open = true
set_process_unhandled_key_input(true)
var is_confirmed = await confirmed
return is_confirmed
Our full script should look like this:
class_name ConfirmationModal extends Control
signal confirmed(is_confirmed: bool)
@onready var header_label: Label = %HeaderLabel
@onready var message_label: Label = %MessageLabel
@onready var confirm_button: Button = %ConfirmButton
@onready var cancel_button: Button = %CancelButton
var is_open: bool = false
var _should_unpause: bool = false
func _ready() -> void:
set_process_unhandled_key_input(false)
if confirm_button:
confirm_button.pressed.connect(_on_confirm_button_pressed)
if cancel_button:
cancel_button.pressed.connect(_on_cancel_button_pressed)
hide()
func _unhandled_key_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
cancel()
func prompt(pause: bool = false) -> bool:
_should_unpause = get_tree().paused == false and pause
if pause:
get_tree().paused = true
show()
is_open = true
set_process_unhandled_key_input(true)
var is_confirmed = await confirmed
return is_confirmed
func customize(header: String, message: String, confirm_text: String = "Yes", cancel_text: String = "No") -> ConfirmationModal:
header_label.text = header
message_label.text = message
confirm_button.text = confirm_text
cancel_button.text = cancel_text
return self
func close(is_confirmed: bool = false) -> void:
if is_confirmed:
confirm()
else:
cancel()
func confirm() -> void:
_close_modal(true)
func cancel() -> void:
_close_modal(false)
func _close_modal(is_confirmed: bool) -> void:
set_process_unhandled_key_input(false)
confirmed.emit(is_confirmed)
set_deferred("is_open", false)
hide()
if _should_unpause:
get_tree().paused = false
func _on_confirm_button_pressed() -> void:
confirm()
func _on_cancel_button_pressed() -> void:
cancel()
You can also find the source code on Github .
How to use
To use our newly created ConfirmationModal
we can add it to our Scene Tree and create a reference in our script and call a few methods.
@onready var conf: ConfirmationModal = $ConfirmationModal
func prompt_quit_game() -> void:
conf.customize(
"Are you sure?",
"Any unsaved progress will be lost.",
"Confirm",
"Cancel"
)
var is_confirmed = await conf.prompt(true)
if is_confirmed:
get_tree().quit()
A call to customize will let you modify the Label
and Button
texts.
To prompt a User for input to wait for, you simply call the prompt
method on the ConfirmationModal
and await the result.
Recap
With this all put together, you have a reusable ConfirmationModal
that you can use to prompt a User for their input and tie that into the desired behavior based on a Player's choice.
Check out the source code on Github and feel free to report any bugs you may find.
If you want to chat more, join us over on Discord where we have channels for support, as well as showing of what we're all working on.
If you want to support my work, consider donating or becoming a member over on my Ko-Fi page
Thanks for reading, Happy Coding!