Actions with the command pattern

This lesson uses the command programming pattern to code actions battlers can take during encounters. An action could be anything like a basic attack, a spell, using an item, and more.

In this part, we’re only going to write the base Action class. We will code concrete attacks later because to do so, we need the battler’s animations and combat formulas in place.

The command pattern consists of turning an action into an object. In other words, it’s a way of representing function calls as objects. Doing so lets you store them in memory, pass them around your code, change their properties, and execute them later. We use the command pattern in object-oriented languages like C++ or GDScript because we can’t easily store references to functions in variables. In GDScript’s case, we can’t nest functions either, an alternative to commands in languages like Python and JavaScript.

In Godot 4.0, GDScript is getting first-class functions. This feature allows you to store references to functions in variables, like any other value. This improvement will reduce the command pattern’s need, although, with a command object, you can sometimes do more than with functions.

There are many ways to implement commands. The general idea is to have a class with a method named something like execute() or apply(), regardless of what it does. Extended classes override the function to make it do something specific like attacking a target, moving, consuming an item, or anything you could imagine.

Let’s get to code to see one way to implement commands in practice. We’re going to write four classes. We have code to define actions on the one hand, and two resource types to store data:

  1. Action, an abstract base class1 so all specific commands, like an AttackAction or a ConsumeItemAction share the same interface2.
  2. AttackAction, a concrete action type for battler attacks. It can be anything that deals damage or inflicts a status effect.
  3. ActionData, a resource that stores the data an action requires, like how much energy it costs to use.
  4. AttackActionData, a resource that holds attack-specific data, like how much base damage it inflicts.

We need the two resource classes because we’re using resources to create and edit data in Godot, using the inspector.

Creating an abstract Action class

Create a new GDScript file named Action.gd and open it in the script editor.

The Action class takes a battler that performs the action, an array of targets, some data, and applies the action to each target in the array.

class_name Action
# Reference is the default type you extend in Godot, even if you omit this line.
# Godot allocates and frees instances of a Reference from memory for you.
extends Reference

# Emitted when the action finished playing.
signal finished

var _data: ActionData
var _actor
var _targets := []


# The constructor allows you to create actions from code like so:
# var action := Action.new(data, battler, targets)
func _init(data: ActionData, actor, targets: Array) -> void:
    _data = data
    _actor = actor
    _targets = targets

Before we add methods to the class, let’s write ActionData. Create a new GDScript file for it. I’m including all the properties we’ll need for the rest of the course to avoid future round-trips to this file.

class_name ActionData
extends Resource

# We will define this enum several times in our codebase.
# Having it in the file allows us to use it as an export hint and to have a
# drop-down menu in the inspector. See `element` below.
enum Elements { NONE, CODE, DESIGN, ART, BUG }

# The following two properties are for the user interface.
# We will use them to represent the action in menus.
export var icon: Texture
export var label := "Base combat action"

# Amount of energy the action costs to perform.
export var energy_cost := 0
# Elemental type of the action. We'll use it later to add bonus damage if
# the action's target is weak to the element.
export (Elements) var element := Elements.NONE

# The following properties help us filter potential targets on a battler's turn.
export var is_targeting_self := false
export var is_targeting_all := false

# The amount of readiness left to the battler after acting.
# You can use it to design weak attacks that allow you to take turn fast.
export var readiness_saved := 0.0


# Returns `true` if the `battler` has enough energy to use the action.
func can_be_used_by(battler) -> bool:
    return energy_cost <= battler.stats.energy

One advantage of using resources in Godot is defining methods on them, like can_be_used_by() above. You can use them to validate or constrain their data.

Back to Action.gd, let’s add the one method that makes it a command:

# Applies the action on the targets, using the actor's stats.
# Returns `true` if the action succeeded.
func apply_async() -> bool:
    # Ok, I lied, there are two methods here.
    # You don't have to add this indirection, but the convention for abstract
    # methods in Godot is they should start with an underscore, but public
    # methods should not. That's why I separated the two.
    return _apply_async()


# Notice that the function's name includes the suffix "async".
# This indicates the function should be a coroutine. That's because in our case,
# finishing an action involves animation.
func _apply_async() -> bool:
    # In the abstract base Action class, we don't do anything!
    emit_signal("finished")
    return true

That’s one implementation of the command pattern for you. And as explained above, you only need one function if you prefer.

Let’s add one method to define whether the action targets opponents or party members:

# Returns `true` if the action should target opponents by default.
func targets_opponents() -> bool:
    return true

To access important values in the pseudo-private _data from the outside, I’ve decided to define public methods.

# The battler needs to know how much readiness they should retain after 
# performing the action.
func get_readiness_saved() -> float:
    return _data.readiness_saved


# Exposing the energy cost will allow us to highlight energy points an action
# will use in the energy bar.
func get_energy_cost() -> int:
    return _data.energy_cost

Alternatively, you could use properties.

The next logical step would be to implement attacks, but we’re going to implement damage formulas first, on which they depend.


  1. An abstract class is a class you’re not meant to instantiate directly. Instead, it’s here only to define the intended behavior of derived types, that is to say, methods and properties. We use them as a base to create extended classes that share the same interface.↩︎

  2. In a programming language like GDScript, an interface is the functions and properties intended for you to access on an object. In general, in programming, an interface is a collection of public methods and properties you can access on a given object.↩︎