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)
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.
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")
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)