Game Dev Artisan Logo
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.
Tutorials

User Input Prompt in Godot 4 - Await User Input

Author Avatar
2024-06-28
8 min read

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.

modal

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: scene-tree

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!

Want to support our work?Support us via Ko-fi