50 Components Course: InputBuffer

< Back to Component List

Table of contents

Demo & Sources

InputBuffer component in action.
InputBuffer component in action.

You can try it yourself live here.

The source code is available on GitHub:

Design

What is the best way to design a component? It can be a bit tricky to understand it at first, but here's a checklist:

  • rule #1: it only does ONE thing!
  • rule #2: it is generic, so you can use it for any game.
  • rule #3: it has as little dependencies as possible.
  • rule #4: it can be used from the outside, by its owner.

Enforcing rule #1 makes sure you do not overscope your components.

Rule #2 makes sure you can reuse your components, in- and outside your current game project. Compare a Sprite2D node and an RPG talent tree and skill system. Sprite2D does one thing, displays an image. You can use that for any 2D game to display images. The RPG talent tree and skill system is not a component because it is highly specific to RPG games.

Rule #3 helps keeping your components flexible. Fewer dependencies mean that the component can be used in different contexts. Too many dependencies often suggest a too specific use-case.

Rule #4 establishes a standard communication line in your component-based games. There are 3 main ways of communication.

  • A component can notify the outside world of an event happening with a signal (outside world is almost always the owner)
  • The owner of the component can use the components functionality through its public method(s).
  • The owner of the component can affect the behaviour of a component through changing its properties.

To make the process of designing a component easier, ask these three questions.

What is the ONE thing this component does?

It captures and holds a specific input action within a short physics-timed window. This  prevents "eaten inputs" by caching a button press right before an actor is ready to act (e.g., pressing jump 0.1 seconds before touching the ground).

What DATA the component needs to do its thing?

  • action_name: identifying the engine input action to listen for (StringName)
  • buffer_time: how long the input remains valid, in seconds (float)

How does it communicate with the outside world?

This component provides two public methods for outside communication:

  • is_buffered(): a bool method which returns true if the action was pressed within the valid buffer time window.
  • consume(): clears the buffer window immediately. Should be called after using the buffer for the desired action.

Implementation

@icon("input_buffer.svg")
class_name InputBuffer
extends Node

First, a custom icon is provided which makes the component feel like it is part of the Godot eco system when you add it to the SceneTree.

Then, a custom class_name is set and the class is extended from the base Node class.

You can read more about the @icon annotation here.

@export var action_name: StringName = &"jump"
@export var buffer_time: float = 0.15

An export variable is defined for the input action of this buffer component (StringName). These can be found at Project Settings -> Input Map.

Then, a buffer time variable is defined for how long the input remains valid (in seconds).

var _buffer_timer: float = 0.0

The current remaining time for the buffer is stored in a float variable.

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed(action_name):
		_buffer_timer = buffer_time

Inside _unhandled_input(), we check if the registered InputEvent is the action defined as an export variable.

If it is, the buffer time window needs to be started. The private _buffer_timer variable is set to the current value of the buffer_time export variable.

func _physics_process(delta: float) -> void:
	if _buffer_timer > 0.0:
		_buffer_timer -= delta

Next, the built-in _physics_process() method is overrode.

If the buffer timer is currently running, we subtract the delta time elapsed since the last frame.

func is_buffered() -> bool:
	return _buffer_timer > 0.0

The is_buffered() method is defined next. It returns true if the action was pressed within the valid buffer time window. Note: this does not clear or consume the buffer!

This method can be used from the outside, to check if the buffer window is currently open for the input action.

func consume() -> void:
	_buffer_timer = 0.0

Finally, the consume() method is defined. It clears the buffer window immediately by setting the remaining time to 0.0.

This should be called after successfully executing the action to prevent double-firing.

Example Use Case

Take a look at this Scene setup:

Scene setup for the InputBuffer demo
Scene setup for the InputBuffer demo

There is a CharacterBody2D for the jumping character, with an extra InputBuffer component attached to it. It is set to the fire InputAction which is bound to spacebar in this demo.

There are three buttons for changing Engine.time_scale so it is easier to see the InputBuffer in action. There is also an HSlider in the UI which is used to change the buffer_time export variable for the InputBuffer component.

Here is the code needed for this demo:

extends Node2D


func _ready() -> void:
	%Speed25.pressed.connect(_set_speed.bind(0.25))
	%Speed50.pressed.connect(_set_speed.bind(0.5))
	%Speed100.pressed.connect(_set_speed.bind(1.0))
	
	%BufferSlider.value_changed.connect(
		func(value: float) -> void:
			%BufferLabel.text = "Buffer time: %ss" % value
			%Character/InputBuffer.buffer_time = value
	)


func _set_speed(speed: float) -> void:
	Engine.time_scale = speed


func _process(_delta: float) -> void:
	if %Character/InputBuffer.is_buffered():
		%Status.text = "Jump buffered for:\n%.2fs" % [%Character/InputBuffer._buffer_timer]
	else:
		%Status.text = "Press Space\nto Jump"


func _physics_process(delta: float) -> void:
	_handle_gravity(delta)
	_handle_jump()
	%Character.move_and_slide()
	_handle_animations()


func _handle_gravity(delta: float) -> void:
	if not %Character.is_on_floor():
		%Character.velocity.y += 980 * delta


func _handle_jump() -> void:
	var jump_pressed: bool = Input.is_action_pressed("fire")
	var is_jump_buffered: bool = %Character/InputBuffer.is_buffered()
	
	if %Character.is_on_floor() and (jump_pressed or is_jump_buffered):
		%Character.velocity.y = -600
		%Character/InputBuffer.consume()


func _handle_animations() -> void:
	if %Character.is_on_floor():
		%Character/AnimatedSprite2D.play("idle")
	elif %Character.velocity.y > 0:
		%Character/AnimatedSprite2D.play("fall")
	else:
		%Character/AnimatedSprite2D.play("jump")

First, inside _ready(), signals are connected for the 3 buttons and the slider.

For the buttons, the pressed signal is connected to a method called _set_speed(), which sets Engine.time_scale to the required percentage float value.

The slider's value_changed signal is connected to an anonymous function which updates the Label's text above the slider and changes the InputBuffer component's buffer_time export variable.

Then, inside _process(), the disabled status button's text is updated:

  • if there is a jump buffered, the remaining buffer time is displayed
  • if not, the text is changed back to "Press Space to Jump"

Finally, the character's movement and jumping is handled inside _physics_process().

  1. handle the gravity applied to the character
  2. handle jumping
  3. move the character with CharacterBody2D.move_and_slide()
  4. finally, handle updating the AnimatedSprite2D's animations based on the current velocity.

_handle_gravity()_handle_jump() and _handle_animations() are self-explanatory. For the jumping and buffering:

  • we want the character to jump in two cases: either when the character is on the floor and pressed the jump action in this frame OR when the character is on the floor and there is a jump action buffered within the given time window.
  • at any case, the Y velocity is updated for the jump and InputBuffer.consume() needs to be called. Why? Consuming the InputBuffer makes sure that we cannot execute double jumps when both conditions would be satisfied.

That wraps up the InputBuffer component! :)

< Back to Component List