The Skip button

In this lesson, we will add a skip button, a common feature in visual novels.

When pressing the button, dialogues advance really fast, up to a choice or any event that requires player action.

Players use the button to either skip dialogues they are not interested in or to replay your game, test different choices, and try to unlock different endings, perhaps.

Once again, we can build upon our existing code and implement it with minimal changes.

We’ll start by creating the button itself, which will emit a signal, and we will connect that signal to our text box to move conversations forward.

Creating the skip button

Create a new scene with a Button named SkipButton at the root and add a Timer named DelayTimer as a child of it.

When pressing the button, we will start the timer and emit a signal every time it times out.

In the Inspector, set the button’s text to Skip and its Visibility -> Self Modulate to a transparent white so it doesn’t call the player’s attention too much.

We anchor the SkipButton in the bottom-right using the Layout -> Bottom Right option in the toolbar. This way, it’ll appear in the text box’s bottom-right corner. Move the button a bit to offset it from the corner.

We then want to ensure that clicking on the button and dragging the mouse away from it does not cancel the skipping state.

By default, when you click a button and move the mouse outside of it, it is not considered as pressed upon release. But there is a property to change that behavior.

Click the checkbox next to Keep Pressed Outside in the Inspector to turn it on.

That’s all we need from the button. Next is the DelayTimer.

The timer’s Wait Time controls the rate at which the skip button causes our game to advance.

If it is set to 1.0, the skip button triggers once every second. You can set it to 0.1 to make game dialogues advance faster. Ensure it is not set to One Shot so the timer loops and keeps emitting its timeout signal. We’ll rely on this in the skipping process.

Save the SkipButton scene inside the TextBox/ directory and add a script to the SkipButton node.

The script mostly listens to the timer, the button_down and button_up signals, and emits a timer_ticked signal when the game should move forward.

extends Button

## Emitted when the DelayTimer times out.
signal timer_ticked

onready var _timer := $DelayTimer


func _ready() -> void:
    connect("button_down", self, "_on_button_down")
    connect("button_up", self, "_on_button_up")
    _timer.connect("timeout", self, "_on_DelayTimer_timeout")


# As soon we press the skip button, we start the timer.
func _on_button_down() -> void:
    _timer.start()


# Whenever the timer emits the `timeout` signal, we emit our `time_ticked`
# signal, telling the text box to advance dialogues. We will implement that
# next.
func _on_DelayTimer_timeout() -> void:
    emit_signal("timer_ticked")


# And when releasing the click or the button press, we stop the timer.
func _on_button_up() -> void:
    _timer.stop()

Using the skip button with the text box

To skip conversations, we need to connect the SkipButton with the TextBox.

Open the TextBox scene and instantiate the SkipButton as a child of the TextBox node.

It should appear in the bottom-right as we set its anchors there. You might need to move it a bit though, as with the text font, it may go out of the viewport.

In the TextBox script, we want to connect to the skip button’s timer_ticked signal and call a function that advances dialogues every time the signal ticks.

We currently have the logic to move dialogues forward in the _unhandled_input() function, so we need to extract it to a function.

Open the TextBox script and make the following changes.

onready var _skip_button : Button = $SkipButton


func _ready() -> void:
    #...
    # We connect to the `time_ticked` signal.
    _skip_button.connect("timer_ticked", self, "_on_SkipButton_timer_ticked")


func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_accept"):
        # We move the lines that were here into a new `advance_dialogue()` function,
        # as you can see below.
        advance_dialogue()


# This function either completes the current line or shows the next dialogue
# line.
func advance_dialogue() -> void:
    if _blinking_arrow.visible:
        emit_signal("next_requested")
    else:
        _tween.seek(INF)


# Our button will cause dialogue to advance based on the `DelayTimer`'s
# `wait_time` property.
func _on_SkipButton_timer_ticked() -> void:
    advance_dialogue()

To stop the skipping process when a choice appears, we hide the button. When a control node is hidden, it does not receive mouse input anymore.

If you would like to animate the button instead of instantly hiding it, you can use its disabled property to toggle it on and off.

func display_choice(choices: Array) -> void:
    #...
    _skip_button.hide()


func _on_ChoiceSelector_choice_made(target_id: int) -> void:
    #...
    _skip_button.show()

You can test the game and, when clicking the button, you’ll notice conversations advance rapidly. Our code only affects dialogues. So it won’t affect parts of your scenes where you’re only playing animations without using the text box.

Adding a keyboard shortcut

We can click the skip button, but currently, we can’t use a key to trigger it.

To do so, we could either use code or Godot’s built-in keyboard shortcut feature. We will do the latter, so you see how it works.

First, we’ll define a new input action dedicated to skipping ahead. Open the Project -> Project Settings… and in the Input Map, add a new fast_forward input action. I’ve assigned the keyboard’s Shift key and the select button on the gamepad to it.

We can now assign that input to the button as a keyboard shortcut.

Head back to the SkipButton scene, select the SkipButton, and in the Inspector, add a new ShortCut to the Shortcut property. It takes another resource. We’ll go with an InputEventAction.

Set the Action to fast_forward and turn on Pressed so the button triggers when pressing down fast_forward.

And that’s it. You can now press your button using the keyboard. If you make plugins to extend Godot’s editor, that’s also how you’d go about adding support for keyboard shortcuts in the interface.

Note shortcuts are always active, so you have to be wary of that. When you want to prevent access to the skip button, you have to either disable it using its disabled property, hide it, or remove it from the scene tree.

You could also disable it using Godot’s built-in pause system through the SceneTree.paused property. If the button is set to stop processing when pausing the tree, it will no longer listen to its shortcut.

Finally, I’d like to mention that by default, the Mouse Filter of buttons is set to Stop.

This means that when you click a button, it consumes the click event, preventing it from reaching other control nodes behind it or a node that listens for clicks in its _unhandled_input() function.

This is why clicking the SkipButton does not cause our game dialogues and animations to advance normally. The button is handling our mouse click.

And with that, we have our fast-forward system in-place.

Code reference

Here are the complete scripts we added and modified in this lesson.

SkipButton.gd

extends Button

signal timer_ticked

onready var _timer := $DelayTimer


func _ready() -> void:
    connect("button_down", self, "_on_button_down")
    connect("button_up", self, "_on_button_up")
    _timer.connect("timeout", self, "_on_DelayTimer_timeout")


func _on_button_down() -> void:
    _timer.start()


func _on_DelayTimer_timeout() -> void:
    emit_signal("timer_ticked")


func _on_button_up() -> void:
    _timer.stop()

TextBox.gd

extends Control

signal display_finished
signal next_requested
signal choice_made(target_id)

export var display_speed := 20.0
export var bbcode_text := "" setget set_bbcode_text

onready var _rich_text_label: RichTextLabel = $RichTextLabel
onready var _name_label: Label = $NameBackground/NameLabel
onready var _blinking_arrow: Control = $RichTextLabel/BlinkingArrow
onready var _tween: Tween = $Tween
onready var _anim_player: AnimationPlayer = $AnimationPlayer
onready var _choice_selector: ChoiceSelector = $ChoiceSelector
onready var _name_background: TextureRect = $NameBackground
onready var _skip_button : Button = $SkipButton


func _ready() -> void:
    hide()
    _blinking_arrow.hide()

    _name_label.text = ""
    _rich_text_label.bbcode_text = ""
    _rich_text_label.visible_characters = 0

    _tween.connect("tween_all_completed", self, "_on_Tween_tween_all_completed")
    _choice_selector.connect("choice_made", self, "_on_ChoiceSelector_choice_made")
    _skip_button.connect("timer_ticked", self, "_on_SkipButton_timer_ticked")


func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_accept"):
        advance_dialogue()


func advance_dialogue() -> void:
    if _blinking_arrow.visible:
        emit_signal("next_requested")
    else:
        _tween.seek(INF)


func display(text: String, character_name := "", speed := display_speed) -> void:
    set_bbcode_text(text)

    if speed != display_speed:
        display_speed = speed

    if character_name == ResourceDB.get_narrator().display_name:
        _name_background.hide()
    elif character_name != "":
        _name_background.show()
        if _name_label.text == "":
            _name_label.appear()

        _name_label.text = character_name


func set_bbcode_text(text: String) -> void:
    bbcode_text = text
    if not is_inside_tree():
        yield(self, "ready")

    _blinking_arrow.hide()
    _rich_text_label.bbcode_text = bbcode_text
    call_deferred("_begin_dialogue_display")


func fade_in_async() -> void:
    _anim_player.play("fade_in")
    _anim_player.seek(0.0, true)
    yield(_anim_player, "animation_finished")


func fade_out_async() -> void:
    _anim_player.play("fade_out")
    yield(_anim_player, "animation_finished")


func display_choice(choices: Array) -> void:
    _name_background.hide()
    _rich_text_label.hide()
    _blinking_arrow.hide()
    _name_background.disappear()
    _skip_button.hide()

    _choice_selector.display(choices)


func _begin_dialogue_display() -> void:
    var character_count := _rich_text_label.get_total_character_count()
    _tween.interpolate_property(
        _rich_text_label, "visible_characters", 0, character_count, character_count / display_speed
    )
    _tween.start()


func _on_Tween_tween_all_completed() -> void:
    emit_signal("display_finished")
    _blinking_arrow.show()


func _on_ChoiceSelector_choice_made(target_id: int) -> void:
    emit_signal("choice_made", target_id)
    _name_background.show()
    _rich_text_label.show()
    _name_background.appear()
    _skip_button.show()


func _on_SkipButton_timer_ticked() -> void:
    advance_dialogue()