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:
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.
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())
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.