Selecting a target

In this part, we’ll code an arrow to select battlers regardless of their position on the battlefield.

The arrow needs to go in front of battlers, and we need to get that position reliably regardless of the battler’s skin size. To do so, we can add anchor points that a designer or animator can offset for each battler individually.

As it’s related to the battler’s visuals, we’ll place it in the BattlerAnim scene.

We’re going two add two anchor positions right away:

  1. In front of the battler for the selection arrow.
  2. Above the battler for the damage labels we’ll add at the end of the UI series.

Open the BattlerAnim scene and add two Position2D nodes named FrontAnchor and TopAnchor respectively. They should be children of the BattlerAnim node.

Here, we use Position2D nodes instead of Node2D to visualize them in the editor easily.

By default, the cross is too small for my taste. You can select both nodes and set the Gizmo Extents to 30 or 40 pixels in the Inspector to increase their size.

Where you place them in the BattlerAnim scene doesn’t matter much as you’ll need to tweak the position for each battler. Just try to find a default position that’ll roughly work for your game’s sprites.

Let’s update the BattlerAnim.gd script to access these positions from the code. Open the script and add the following lines.

#...
# onready var tween: Tween = $Tween
onready var _anchor_front: Position2D = $FrontAnchor
onready var _anchor_top: Position2D = $TopAnchor


func get_front_anchor_global_position() -> Vector2:
    return _anchor_front.global_position


func get_top_anchor_global_position() -> Vector2:
    return _anchor_top.global_position

I’ve chosen to make the nodes pseudo-private and to expose a public method to access their position, for encapsulation.

Now, given a Battler object, you can write something like battler.battler_anim.get_front_anchor_global_position() to get the front anchor’s world position. We’ll use it in a moment with the selection arrow.

To place the anchors for a given battler, open their scene and right-click on BattlerAnim. In the context menu, toggle Editable Children.

Here’s where I moved the anchors for the Bear.

Coding the battler selection arrow

This arrow takes an array of target battlers and gives the player visual feedback to select one. It uses vectors and angles to know which battler to select based on the player’s input.

While it looks like the action menu’s arrow, it doesn’t work exactly the same. Because of that, we need to duplicate the UIMenuSelectArrow scene.

In the FileSystem dock, right-click on UIMenuSelectArrow.tscn and select Duplicate. Name the copy UISelectBattlerArrow.tscn. Open the new scene and name the root node UISelectBattlerArrow as well. Also, remove the script that’s attached to it and create a new one.

Attach a new script to the UISelectBattlerArrow node.

# In-game arrow to select target battlers. Appears when the player selected an action and has to pick a target for it.
class_name UISelectBattlerArrow
extends Node2D

# Emitted when the player presses "ui_accept" or "ui_cancel".
signal target_selected(battler)

onready var _tween = $Tween

# Duration of the arrow's move animation in seconds.
export var move_duration: float = 0.1

# Targets that the player can select. It's an array of Battler objects.
var _targets: Array
# Battler at which the arrow is currently pointing. If the player presses `ui_accept`, this battler will be selected.
# The setter calls the `_move_to()` function that moves the arrow to the target using a tween.
var _target_current: Battler setget _set_target_current


# Similarly to the other arrow, we want this one to move independently of its parents.
func _init() -> void:
    set_as_toplevel(true)


func _unhandled_input(event: InputEvent) -> void:
    # When the user presses enter or escape, we emit the "target_selected" signal.
    if event.is_action_pressed("ui_accept"):
        emit_signal("target_selected", [_target_current])
    elif event.is_action_pressed("ui_cancel"):
        # In the case the player cancels the selection, we emit an empty array for the targets,
        # which will cause the turn to restart and the game to re-open the action menu.
        emit_signal("target_selected", [])

    # If the player presses a direction input, we try to find a target in that direction.
    var new_target: Battler
    # We start by storing the direction as a Vector2.
    var direction := Vector2.ZERO
    if event.is_action_pressed("ui_left"):
        direction = Vector2.LEFT
    elif event.is_action_pressed("ui_up"):
        direction = Vector2.UP
    elif event.is_action_pressed("ui_right"):
        direction = Vector2.RIGHT
    elif event.is_action_pressed("ui_down"):
        direction = Vector2.DOWN

    # If the direction is not Vector2.ZERO, we try to find the closest target in that direction.
    # See `_find_closest_target()` below for more information.
    if direction != Vector2.ZERO:
        new_target = _find_closest_target(direction)
        if new_target:
            _set_target_current(new_target)


# Like all our UI components, we use a `setup()` function to initialize the arrow.
# I've decided to pass and store the battlers in this class for convenience because
# we need to know which battler the player selected. And due to the absence of buttons, we can't
# bind a reference to a signal callback like we did before.
func setup(battlers: Array) -> void:
    show()
    _targets = battlers
    _target_current = _targets[0]

    # You can use the arrow to select either an enemy or a party member.
    # We scale the node horizontally to flip it if targetting an ally.
    scale.x = 1.0 if _target_current.is_party_member else -1.0
    # This places the arrow in front of the current target battler.
    global_position = _target_current.battler_anim.get_front_anchor_global_position()


# This function is similar to the one in `UIMenuSelectArrow` from the previous lesson.
# In this case though, we don't want other nodes to call it so we prepend the function with
# an underscore to mark it as pseudo-private.
func _move_to(target_position: Vector2):
    if _tween.is_active():
        _tween.stop_all()
    _tween.interpolate_property(
        self,
        'position',
        position,
        target_position,
        move_duration,
        Tween.TRANS_CUBIC,
        Tween.EASE_OUT
    )
    _tween.start()


# Returns the closest target in the given direction.
# Returns null if it cannot find a target in that direction.
func _find_closest_target(direction: Vector2) -> Battler:
    # We define our return variable and another to compare against battlers.
    var selected_target: Battler
    var distance_to_selected: float = INF

    # First, we find battlers in the given `direction`.
    var candidates := []
    for battler in _targets:
        # This filters out the `_target_current`.
        if battler == _target_current:
            continue
        # Then, we check if the target is at an angle of less than PI / 3 radians compared to
        # the `direction`. If so, it's a candidate for the current selection.
        var to_battler: Vector2 = battler.global_position - position
        if abs(direction.angle_to(to_battler)) < PI / 3.0:
            candidates.append(battler)

    # We then find the closest battler among the `candidates`. That's the battler we want to select.
    for battler in candidates:
        var distance := position.distance_to(battler.global_position)
        if distance < distance_to_selected:
            selected_target = battler
            distance_to_selected = distance

    return selected_target


func _set_target_current(value: Battler) -> void:
    _target_current = value
    _move_to(_target_current.battler_anim.get_front_anchor_global_position())

Using the arrow in the ActiveTurnQueue

As we did in the previous lesson, we’re going to instantiate the arrow in ActiveTurnQueue.gd. Open the file and update the _player_select_targets_async() function.

# export var UIActionMenuScene: PackedScene
export var SelectArrow: PackedScene

#...

func _player_select_targets_async(_action: ActionData, opponents: Array) -> Array:
    # We instantiate the arrow, add it as a child, and call its `setup()` function.
    var arrow: UISelectBattlerArrow = SelectArrow.instance()
    add_child(arrow)
    arrow.setup(opponents)
    # We then wait for it to return a target, which means the player either selected a target
    # or cancelled the operation.
    var targets = yield(arrow, "target_selected")
    # If the player cancelled, the `_play_turn()` function's loop will reopen the action menu,
    # then create a new arrow.
    arrow.queue_free()
    return targets

Be sure to head to the CombatDemo scene and to assign the UISelectBattlerArrow.tscn scene to the new Select Arrow property.

And with that, if you run your game, you can now both select actions and targets interactively during the player’s turn.