Coding the BattlerAnim class

It is time to write the code that is going to control our BattlerAnim scene. We have three nodes for animation: two animation players, and one tween node. We want to code a script that will encapsulate those details and make it relatively easy to control animations from the battler class.

Several of our classes’ methods will wrap around AnimationPlayer’s functions to tailor them to our needs. The goal here is to have all the methods we need on BattlerAnim so we don’t have to dig into its child nodes from the outside.

Add a new script to the BattlerAnim node. Let’s start with its state and signals.

# We use the tool mode here so the node's `scale` updates when we change the battler's direction. See `direction` below.
tool
# Hold and plays the base animation for battlers.
class_name BattlerAnim
extends Position2D

# Forwards the animation players' `animation_finished` signal.
signal animation_finished(name)
# Emitted by animations when a combat action should apply its next effect, like dealing damage or healing an ally.
signal triggered

# There are two directions a battler can look: left or right. This enum represents that.
enum Direction { LEFT, RIGHT }

# Controls the direction in which the battler looks and moves.
# In `set_direction()`, we change the node's `scale.x` based on this property's valie.
export (Direction) var direction := Direction.RIGHT setget set_direction

# We store the node's start position to reset it using the `Tween` node.
var _position_start := Vector2.ZERO

As usual, we store references to all the nodes we use in this script in onready variables. We will access both animation player nodes and the tween node.

onready var anim_player: AnimationPlayer = $Pivot/AnimationPlayer
onready var anim_player_damage: AnimationPlayer = $Pivot/AnimationPlayerDamage
onready var tween: Tween = $Tween

In the _ready() callback, we store the node’s initial position when it enters the scene tree.

func _ready() -> void:
    _position_start = position

Here are the three wrapper methods that almost just use AnimationPlayer’s functions as-is, but not quite.

# Functions that wraps around the animation players' `play()` function, delegating the work to the
# `AnimationPlayerDamage` node when necessary.
func play(anim_name: String) -> void:
    if anim_name == "take_damage":
        anim_player_damage.play(anim_name)
        # Seeking back to 0 restarts the animation if it is already playing.
        anim_player_damage.seek(0.0)
    else:
        anim_player.play(anim_name)


# Wraps around `AnimationPlayer.is_playing()`
func is_playing() -> bool:
    return anim_player.is_playing()


# Queues the animation and plays it if the animation player is stopped.
func queue_animation(anim_name: String) -> void:
    anim_player.queue(anim_name)
    if not anim_player.is_playing():
        anim_player.play()

Here come the two methods that rely on the Tween node: move_forward() and move_back().

# The following two functions use the tween node to move the character forward and back.
# We'll use this to emphasize the start and end of a battler's turn.
func move_forward() -> void:
    # The call below animates the node's position forward over `0.3` seconds.
    tween.interpolate_property(
        self,
        "position",
        position,
        # We use `scale.x` to control the direction the node is facing.
        position + Vector2.LEFT * scale.x * 40.0,
        0.3,
        Tween.TRANS_QUART,
        Tween.EASE_IN_OUT
    )
    # Don't forget to start a tween or a series of tweens after defining them.
    # No animation happens until you do so.
    tween.start()


# Moves the node to `_position_start`
func move_back() -> void:
    # Note that you may want to stop other tweens affecting the node's `position` in a different context.
    # In general, you want to ensure that previous tweens affecting a given property ended before starting a new one.
    # Due to the game's turn-based nature and how we will sequence animations, we don't need to stop
    # previous tweens here.
    tween.interpolate_property(
        self, "position", position, _position_start, 0.3, Tween.TRANS_QUART, Tween.EASE_IN_OUT
    )
    tween.start()

All that’s left is our animation_finished signal. In the 2D workspace, connect each animation player nodes’ animation_finished signal to BattlerAnim. I connected them both to the same callback function, _on_AnimationPlayer_animation_finished().

func _on_AnimationPlayer_animation_finished(anim_name: String) -> void:
    emit_signal("animation_finished", anim_name)

Using our animations

Now, the BattlerAnim is ready to use. Let’s add an instance of the scene to our battlers. Open Battler.tscn and drag and drop BattlerAnim.tscn onto the Battler node.

We are going to access the BattlerAnim in two places: Battler and the AttackAction. We want to avoid accessing it elsewhere because the battler should encapsulate its animation. We still use it from the action objects because the command pattern they use represents a function call from the battler as an object.

Open Battler.gd and add the following code to it.

# Emitted when an animation from `battler_anim` finished playing.
signal animation_finished(anim_name)

onready var battler_anim: BattlerAnim = $BattlerAnim


func act(action) -> void:
    # ...
    # yield(action.apply_async(), "completed")
    battler_anim.move_back()


func set_is_selected(value) -> void:
    # ...
    # is_selected = value
    if is_selected:
        battler_anim.move_forward()


func _take_damage(amount: int) -> void:
    #...
    if stats.health > 0:
        battler_anim.play("take_damage")

We should also connect the animation_finished signal to the battler so we can make it available to the action, that will rely on it.

In Battler.gd, add.

# Emitted when an animation from `battler_anim` finished playing.
signal animation_finished(anim_name)

#...

func _on_BattlerAnim_animation_finished(anim_name) -> void:
    emit_signal("animation_finished", anim_name)

Head to AttackAction.gd so we can put our animations to use.

func _apply_async() -> bool:
    # We're going to access the BattlerAnim node directly from the action. We could define
    # a `Battler.play()` method instead to encapsulate it completely, but the action
    # is an object representing a method call on the battler, in a sense.
    var anim = _actor.battler_anim
    for target in _targets:
        # ...
        # var hit := Hit.new(damage, hit_chance)

        # Here's how we use the animations' `triggered` signal. We bind the target and each hit
        # to the `_on_BattlerAnim_triggered()` callback.
        anim.connect("triggered", self, "_on_BattlerAnim_triggered", [target, hit])
        # Then, we play the animation, which will later emit the `triggered` signal.
        anim.play("attack")
        yield(_actor, "animation_finished")
    return true


func _on_BattlerAnim_triggered(target, hit: Hit) -> void:
    # On each animation trigger, we apply the corresponding hit.
    target.take_hit(hit)

There’s one last place where we can use the battler’s animation: ActiveTurnQueue.gd:

func _play_turn(battler: Battler) -> void:
    #...
    # Makes every battler step forward at the start of their turn.
    # With player-controlled battlers, this helps to highlight the active character.
    battler.is_selected = true
    #...

With your animations and code in place, you should be able to play automated battles already. The characters’ can’t die yet, but they can damage each other and play simple animations.

Playing the death animation

Our BattlerAnim scene features a die animation but we’re not using it yet. It makes the character disappear completely and is intended for monsters, that will not be revived in our demo.

We can play it in the battler, in the _on_BattlerStats_health_depleted() callback.

In Battler.gd, add the following.

func _on_BattlerStats_health_depleted() -> void:
    #...
    if not is_party_member:
        #...
        # We only play the animation for monsters as it makes them disappear from the battlefield.
        battler_anim.queue_animation("die")

The code so far

Here is the complete BattlerAnim.gd script.

tool
class_name BattlerAnim
extends Position2D

signal animation_finished(name)
signal triggered

enum Direction { LEFT, RIGHT }

export (Direction) var direction := Direction.RIGHT setget set_direction

var _position_start := Vector2.ZERO

onready var anim_player: AnimationPlayer = $Pivot/AnimationPlayer
onready var anim_player_damage: AnimationPlayer = $Pivot/AnimationPlayerDamage
onready var tween: Tween = $Tween


func _ready() -> void:
    _position_start = position


func play(anim_name: String) -> void:
    if anim_name == "take_damage":
        anim_player_damage.play(anim_name)
        anim_player_damage.seek(0.0)
    else:
        anim_player.play(anim_name)


func is_playing() -> bool:
    return anim_player.is_playing()


func queue_animation(anim_name: String) -> void:
    anim_player.queue(anim_name)
    if not anim_player.is_playing():
        anim_player.play()


func move_forward() -> void:
    tween.interpolate_property(
        self,
        "position",
        position,
        position + Vector2.LEFT * scale.x * 40.0,
        0.3,
        Tween.TRANS_QUART,
        Tween.EASE_IN_OUT
    )
    tween.start()


func move_back() -> void:
    tween.interpolate_property(
        self, "position", position, _position_start, 0.3, Tween.TRANS_QUART, Tween.EASE_IN_OUT
    )
    tween.start()


func set_direction(value: int) -> void:
    direction = value
    scale.x = -1.0 if direction == Direction.RIGHT else 1.0


func _on_AnimationPlayer_animation_finished(anim_name: String) -> void:
    emit_signal("animation_finished", anim_name)