The turn bar

Time and turns are an important element for the player to choose a strategy in an “active time battle” system.

There are different ways to represent the turns’ order and how quickly a battler can be ready to act. The gauges in old Final Fantasy games only give you information about playable characters, not enemies.

I’ve chosen an approach that shows all battlers instead, each represented by an icon that moves along an axis.

Some titles like Grandia on PlayStation 1 used this kind of UI widget, sometimes with an extra area to represent a skill’s cast duration.

Our turn bar will rely on two elements:

  1. An icon representing a battler. Its frame shows if it’s an ally or an enemy.
  2. A bar that instantiates, displays, and updates the position of icons.

I will also show you how to keep this user interface separate from the game world and how to give it the information it needs to function.

Designing the battler icon

Let’s start with the icons as we’ll instantiate them later in the turn bar.

You’re going to use two sprites to make them: a frame and an icon representing the battler.

Create a new scene with two TextureRect nodes named UIBattlerIcon and Icon respectively. The UIBattlerIcon is our background frame, and the Icon will display the battler’s image.

In the course project, you will find the sprites portrait_bg_*.png for the UIBattlerIcon and bear.png and bugcat.png for the Icon node. Assign them to the corresponding TextureRect node.

At this stage, your scene should look like this.

We need to change the nodes’ properties to fit the icon inside the background.

Select the Icon node and in the Inspector turn on the Expand option. Doing so allows us to resize the node freely; the texture will shrink or expand to fit the node’s bounding box according to the Stretch Mode. Change the Stretch Mode to Keep Aspect Centered, so the node always retains the image’s aspect ratio. You can also check Flip H to mirror the image horizontally.

Then, to fit the texture into the background, use the Layout -> Full Rect command in the toolbar.

The icon is still too big. You can add margins relative to the parent UIBattlerIcon node by resizing the Icon in the viewport. With the Select Mode active in the toolbar (Q), click and drag the Icon’s corner handles while pressing Shift Alt on your keyboard. Those keys allow you to scale uniformly around the node’s center.

The UIBattlerIcon’s script

Let’s code the UIBattlerIcon’s script. We’re going to define two exported properties to change the background and the icon on instances easily.

Attach a script to the UIBattlerIcon node.

# Icon representing a battler on the `UITurnBar`.
# The turn bar moves the icons as the associated battler's readiness updates.
# The tool mode allows us to encapsulate the node's icon texture and see it update in the viewport.
tool
class_name UIBattlerIcon
extends TextureRect

# We use this enum in the `type` exported variable below to get a drop-down menu in the Inspector.
# In the game, we can have ally AI, enemy AI, and player-controlled battlers.
enum Types { ALLY, PLAYER, ENEMY }

# This constant maps members of the Types enum to a texture.
const TYPES := {
    Types.ALLY: preload("portrait_bg_ally.png"),
    Types.PLAYER: preload("portrait_bg_player.png"),
    Types.ENEMY: preload("portrait_bg_enemy.png"),
}

# The following two properties allow you to update the icon and the background.
# The setter functions update the TextureRect nodes. This works both in-game and in the editor
# thanks to the `tool` mode.
export var icon: Texture setget set_icon
export (Types) var type: int = Types.ENEMY setget set_type

# Range of the icon's horizontal position to move it along the turn bar.
# The vector's `x` value is the minimum position, and the `y` component is its maximum.
var position_range := Vector2.ZERO

onready var _icon_node := $Icon


# Linearly interpolates the `x` position within the `position_range`.
# The `ratio` below is a value between 0 and 1.
func snap(ratio: float) -> void:
    rect_position.x = lerp(position_range.x, position_range.y, ratio)


# Updates the icon's texture.
func set_icon(value: Texture) -> void:
    icon = value
    # Setters get called when the engine creates the object, before adding it to the scene tree. At
    # this point, the `icon` variable is `null` and we can't set the texture. We can wait for the
    # node to finish its `_ready()` callback to ensure that the `_icon_node` is set.
    if not is_inside_tree():
        yield(self, "ready")
    _icon_node.texture = icon


# Updates the background texture using our `TYPES` dictionary.
func set_type(value: int) -> void:
    type = value
    texture = TYPES[type]

Designing the turn bar

Next up is the turn bar, which will instantiate the icons we just created for each battler on the field.

Create a new scene with a Control at the root named UITurnBar, a TextureRect named Background, and an AnimationPlayer.

Select the Background node and assign it to the turn bar’s background image, turn_bar_bg.png. The sprite should appear in the top-left corner of the viewport.

The background image may be too wide by default. We can change some properties to resize it horizontally. In the Inspector, set it to Expand and change the Stretch Mode to Keep Aspect Covered. You can now resize the Background node horizontally to crop the bar.

Let’s anchor the turn bar to the top edge of the viewport. Select the UITurnBar node and apply the Layout -> Center Top command to anchor it to the top edge’s middle point.

Then, apply Layout -> Center to the Background node. It centers inside the parent control node. Finally, resize the UITurnBar vertically to move the turn bar closer to the top edge.

The fade animations

We’re going to use animations to fade various UI widgets in and out. Select the AnimationPlayer and create the following three animations.

As animations directly change node properties’ values in Godot, even in the editor, I like to have a “setup” animation to reset everything. This is an animation with only one frame that resets properties other animations modify. Here, we only need one key in the UITurnBar’s Modulate property.

The fade-in animation uses two keys to take the node from fully transparent to fully opaque. Notice that the animation is set to Autoplay On Load (icon highlighted in yellow below).

To animate the node’s opacity, you want to modify the alpha channel of the Modulate property in the Inspector.

The fade-out animation is a copy of the fade-in but with the two keys swapped.

Coding the turn bar

We now have everything our turn bar needs to work, so let’s move to code. Attach a new script to the UITurnBar node.

# Timeline representing the turn order of all battlers in the arena.
# Battler icons move along the timeline as their readiness updates.
extends Control

# You can preload scenes with relative paths when they're in the same directory. The line below will
# work as long as `UIBattlerIcon.tscn` is in the same directory as `UITurnBar.gd`.
# When there's only one scene meant to be instanced by another, I like to preload it like that
# instead of using an `export var`.
const BattlerIcon := preload("UIBattlerIcon.tscn")

onready var _background: TextureRect = $Background
onready var _anim_player: AnimationPlayer = $AnimationPlayer


# To initialize the turn bar, we pass all the battlers we want to display.
func setup(battlers: Array) -> void:
    for battler in battlers:
        # We first calculate the right icon background using the `Types` enum from `UIBattlerIcon`.
        # Below, I'm using the ternary operator. It picks the first value if the condition is `true`,
        # otherwise, it picks the second value.
        var type: int = (
            UIBattlerIcon.Types.PLAYER
            if battler.is_party_member
            else UIBattlerIcon.Types.ENEMY
        )
        # We create an instance of the `UIBattlerIcon` scene using a function and add it as a child.
        var icon: UIBattlerIcon = create_icon(type, battler.ui_data.texture)
        # We get to use the `Battler.readiness_changed` signal again to move the icons along the turn bar.
        # Once again, we bind the icon to the callback for each battler, so we don't have to worry
        # about which battler corresponds to which icon later.
        battler.connect("readiness_changed", self, "_on_Battler_readiness_changed", [icon])
        _background.add_child(icon)


# Creates a new instance of `UIBattlerIcon`, initializes it, adds it as a child of `background`, and
# returns it.
func create_icon(type: int, texture: Texture) -> UIBattlerIcon:
    var icon: UIBattlerIcon = BattlerIcon.instance()
    icon.icon = texture
    icon.type = type
    # We calculate where to position the icon, ranging between the left and right limits of the
    # `background` texture. Note this range is only in the X axis: the vector's X and Y components are
    # the minimum and the maximum icon's X position.
    icon.position_range = Vector2(
        # TextureRect nodes have their pivot and origin in the top-left corner, so we need to offset
        # the scale by half the icon's size.
        -icon.rect_size.x / 2.0,
        -icon.rect_size.x / 2.0 + _background.rect_size.x
    )
    return icon


# The following two functions encapsulate animation playback and the animation player.
func fade_in() -> void:
    _anim_player.play("fade_in")


func fade_out() -> void:
    _anim_player.play("fade_out")


# This is where we update the position of each icon. Every time a battler emits the
# "readiness_changed" signal, we use this value to snap the corresponding icon to a new location.
func _on_Battler_readiness_changed(readiness: float, icon: UIBattlerIcon) -> void:
    # We call the `UIBattlerIcon.snap()` function we defined previously. I recommend using a
    # function or dedicated property here because polishing the game, you might want to animate the
    # icon's movement.
    icon.snap(readiness / 100.0)

Instantiating the bar in the battle

To initialize the turn bar, we need an instance of it in our CombatDemo scene. We also need to pass it the list of battlers in the encounter by calling UITurnBar.setup().

To keep UI widgets above the battlefield at all times, I recommend to add a CanvasLayer node. In the image below, I’ve named it UI and added an instance of UITurnBar.tscn as a child of it.

We’re going to use the UI and the ActiveTurnQueue’s common parent, the CombatDemo node, to pass data to the UITurnBar. Attach a new script to CombatDemo.

extends Node2D

# We're going to get the battlers from the `ActiveTurnQueue` node.
onready var active_turn_queue := $ActiveTurnQueue
onready var ui_turn_bar := $UI/UITurnBar


# We set up the turn bar when the node is ready, which ensures all its children also are ready.
func _ready() -> void:
    var battlers: Array = active_turn_queue.battlers
    ui_turn_bar.setup(active_turn_queue.battlers)

And this is all we need to get the turn bar working. All the rest is encapsulated in our UITurnBar and UIBattlerIcon classes.