The energy bar

The energy bar is going to offer a discrete representation of the player’s energy. As the player has few energy points, we will have each as a separate node in our bar. Doing so will allow us to animate the points as you saw in the previous lesson.

Our energy bar needs two components:

  1. The individual point with its animations.
  2. The bar that instantiates and manages the points.

The energy point

Let’s start with the individual point scene. Create a new scene with a TextureRect named UIEnergyPoint. Add another TextureRect named Fill as a child of it as well as a Tween node.

Assign the energy_point_bg.png file to the UIEnergyPoint’s Texture and energy_point_fill.png to the Fill node. This way, the fill image displays in front of the background. Also, lower the opacity of the Fill using its Visibility -> Modulate property.

It should be slightly visible.

Also, set the UIEnergyPoint’s Stretch Mode to Keep Centered to center the texture in the node’s bounding box.

Attach a script to UIEnergyPoint to define its behavior. We will control it from the bar instance, so it only defines public methods that another node can call.

# Represents one energy point. This node has two animations and functions to make it appear and
# disappear smoothly.
extends TextureRect

# This represents a target offset when selecting the point and tweening the node's position.
const POSITION_SELECTED := Vector2(0.0, -6.0)

# All our animations affect the `Fill` node.
onready var _fill: TextureRect = $Fill
onready var _tween: Tween = $Tween

# We store the start modulate value of the `Fill` node because it's semi-transparent.
# This way, we can animate the color from and to this value.
onready var _color_transparent := _fill.modulate


func appear() -> void:
    # We animate the `Fill` node's color from `_color_transparent` to a fully opaque white for `0.3
    # seconds`.
    _tween.interpolate_property(_fill, "modulate", _color_transparent, Color.white, 0.3)
    _tween.start()


func disappear() -> void:
    # This is the opposite of the appear animation, going from the fill's current `modulate` value
    # to `_color_transparent`.
    _tween.interpolate_property(_fill, "modulate", _fill.modulate, _color_transparent, 0.3)
    _tween.start()


func select() -> void:
    # This animation offsets the node to `POSITION_SELECTED`. The "out" easing means the animation
    # starts at full speed and slows down as it reaches the target position.
    _tween.interpolate_property(_fill, "rect_position", Vector2.ZERO, POSITION_SELECTED, 0.2, Tween.TRANS_CUBIC, Tween.EASE_OUT)
    _tween.start()


func deselect() -> void:
    # This is the opposite of the select animation.
    _tween.interpolate_property(_fill, "rect_position", POSITION_SELECTED, Vector2.ZERO, 0.2, Tween.TRANS_CUBIC, Tween.EASE_OUT)
    _tween.start()

The energy bar

The bar’s mostly going to be code. Create a new scene with an HBoxContainer as its root and name it UIEnergyBar.

We’re going to instantiate the points as a child of this node, and the container will align them horizontally.

Attach a script to the node with the following code.

# Bar representing energy points. Each point is an instance of UIEnergyPoint.
extends HBoxContainer

# We preload the energy point to instantiate it in `setup()`
# Once again, you need `UIEnergyPoint.tscn` to be in the same directory for the path to work.
const UIEnergyPoint: PackedScene = preload("UIEnergyPoint.tscn")

# The following properties are in the same logic as Godot's built-in progress bar, which extends the
# `Range` class. While we could also extend `Range`, it works with floating-point values, while our
# energy is integer-based, and it would have more unnecessary features.
var max_value := 0

var value := 0 setget set_value
var selected_count := 0 setget set_selected_count


# The setup function creates `max_energy` instances of the energy point scene.
func setup(max_energy: int, energy: int) -> void:
    max_value = max_energy
    value = energy
    # This is a shorthand for `range(max_value)`.
    for i in max_value:
        var energy_point: TextureRect = UIEnergyPoint.instance()
        # The points will be children of the bar, allowing us to manipulate them with the
        # `get_child()` function.
        add_child(energy_point)


func set_value(amount: int) -> void:
    var old_value := value
    value = int(clamp(amount, 0.0, max_value))
    # The code below makes points appear and disappear when the value changes. As each point is a separate object, we need to loop over them and call the corresponding function on each instance.
    if value > old_value:
        # If we have more points, we need to play the "appear" animation on the added points only.
        # That's why we generate a range of indices from `old_value` to `value`.
        for i in range(old_value, value):
            get_child(i).appear()
    else:
        # If we lost points, we generate a range of indices going down, using `step`, the function's third argument.
        for i in range(old_value, value, -1):
            get_child(i - 1).disappear()


# This function follows the logic of `set_value()` but plays the "select" and "deselect" animations
# on the points instead of "appear" and "disappear". We will use it in the next lesson to preview
# the energy cost of actions in the player's turn.
func set_selected_count(amount: int) -> void:
    var old_value := selected_count
    selected_count = int(clamp(amount, 0.0, max_value))
    if selected_count > old_value:
        for i in range(old_value, selected_count):
            get_child(i).select()
    else:
        for i in range(old_value, selected_count, -1):
            get_child(i - 1).deselect()

You may have noticed I like to use setter functions. Since Godot 3, the engine uses properties a lot more than it used to, and I like to use properties too so the code style stays somewhat consistent with it.

With this, we can control the bar by changing its value and its selected_count. We will do so in the next lesson, where we’ll build the HUD.