Battler and HUD selection

In this part, we’re going to use the battlers’ is_selected property we coded in the previous chapter to animate the corresponding Heads-Up Display.

Along with the character, the HUD will move forward and its label will turn brighter to help the player spot the energy cost preview.

In the picture, the difference is subtle, but with the animation, it’s easier to spot.

We’re going to design some animations first and then put them to use in code.

Designing the HUD’s select animations

Open the UIBattlerHUD scene again and add an AnimationPlayer to it.

We’re going to design three animations:

  1. “_setup”, the animation that allows us to reset the scene.
  2. “select”, that moves the widget forward and turns the label white.
  3. “deselect”, the opposite of the “select” animation.

Moving and animating a control node can be slightly different from a regular 2D node: you can choose to animate its Rect -> Position or its Margin properties. A Rect -> Position property track will always set both the node’s horizontal and vertical position, even if we only want to animate it on the horizontal axis.

Using the four Margin properties, we have more control. We can use them to offset the widget horizontally, even if it lives in a container.

There’s one small caveat to this method. As we list the HUDs in a VBoxContainer, if the player plays in windowed mode and manually resizes the window, the container will reset its children’s position. This is related to the UIBattlerHUDList, though, not the animations themselves.

Create a “_setup” animation that has the minimum duration, that’s set to autoplay, and add keys for:

  1. The UIBattlerHUD’s Margin -> Left and Margin -> Right.
  2. The Label’s Self Modulate.

Duplicate this animation by clicking the Animation menu, the Duplicate, and rename the copy to “select”.

I went with a duration of 0.5 seconds and the following timings.

Notice how the text’s tinting animation starts after the horizontal motion. I’ve also used the following easing curve on the margin’s leftmost keys.

It makes the animation start fast and ease out. Here’s the result.

Duplicate the “select” animation and rename it to “deselect”. I’ve made it a bit softer than the “select” one by removing some keys and changing the easings. The duration is still 0.5 seconds.

The margin’s leftmost keys have a different easing, both in and out. To get the curve below, you can click and drag towards the left, producing a negative value.

You can also right-click on the curve to open the preset menu. There, you’ll find an In-Out entry.

Using the animations in code

Using our animations is fairly straightforward. Open UIBattlerHUD.gd and update the code like so.

onready var _anim_player: AnimationPlayer = $AnimationPlayer


func setup(battler: Battler) -> void:
    # We already get a reference to the battler so all we have to do is connect to its
    # `selection_toggled` signal.
    battler.connect("selection_toggled", self, "_on_Battler_selection_toggled")
    #...


# And we play an animation based on the boolean we received from the signal callback.
func _on_Battler_selection_toggled(value: bool) -> void:
    if value:
        _anim_player.play("select")
    else:
        _anim_player.play("deselect")

And with that, your HUD will animate when a playable character’s turn starts, making it easier to see which is selected.

Here’s the complete UIBattlerHUD code for reference.

# Displays a party member's name, health, and energy.
class_name UIBattlerHUD
extends TextureRect

onready var _life_bar: TextureProgress = $UILifeBar
onready var _energy_bar := $UIEnergyBar
onready var _label := $Label
onready var _anim_player: AnimationPlayer = $AnimationPlayer


func _ready() -> void:
    Events.connect("combat_action_hovered", self, "_on_Events_combat_action_hovered")
    Events.connect("player_target_selection_done", self, "_on_Events_player_target_selection_done")


# Initializes the health and energy bars using the battler's stats.
func setup(battler: Battler) -> void:
    battler.connect("selection_toggled", self, "_on_Battler_selection_toggled")

    _label.text = battler.ui_data.display_name

    var stats: BattlerStats = battler.stats
    _life_bar.setup(stats.health, stats.max_health)
    _energy_bar.setup(stats.max_energy, stats.energy)

    stats.connect("health_changed", self, "_on_BattlerStats_health_changed")
    stats.connect("energy_changed", self, "_on_BattlerStats_energy_changed")


func _on_BattlerStats_health_changed(_old_value: float, new_value: float) -> void:
    _life_bar.target_value = new_value


func _on_BattlerStats_energy_changed(_old_value: float, new_value: float) -> void:
    _energy_bar.value = new_value


func _on_Battler_selection_toggled(value: bool) -> void:
    if value:
        _anim_player.play("select")
    else:
        _anim_player.play("deselect")


func _on_Events_combat_action_hovered(battler_name: String, energy_cost: int) -> void:
    if _label.text == battler_name:
        _energy_bar.selected_count = energy_cost


func _on_Events_player_target_selection_done() -> void:
    _energy_bar.selected_count = 0