Here’s a solution to our challenge.
There are two cases where the code needs to change: when moving the cursor and confirming movement.
We have two signal callbacks from the Cursor in GameBoard.gd
: _on_Cursor_accept_pressed()
and _on_Cursor_moved()
. That’s where I started looking.
In those callbacks, we pass a cell to another function that will either draw a path or move the unit to the highlighted cell. Instead of directly passing the cursor’s position, we can modify it and find another, more appropriate cell in the context.
To do so, I wrote a new function, find_closest_walkable_cell()
. It relies on our GameBoard._active_unit
variable at the moment.
It looks at the current cell and, if there’s a unit there, it loops over the surrounding cells and finds the one closest to the _active_unit
. Note that as the game evolves, we may want to remove the use of _active_unit
and use another parameter instead to make the function more flexible. But for now, it keeps the code simple.
## If the cursor's over a unit, returns the closest cell in front of the enemy. ## Otherwise, returns the current cell. func find_closest_walkable_cell(cell: Vector2) -> Vector2: # We only need to process and modify the output cell if the cursor is over a unit. # If that's not the case, we keep the current cell. if not _units.has(cell): return cell var closest_cell := cell # To find the closest cell, we need to store the smallest distance we've had so far to the # cells we looked at. We start with "infinite". var min_distance := INF for direction in DIRECTIONS: var target: Vector2 = cell + direction # For each neighboring cell we look at, we ensure it's not also occupied by a unit. if _units.has(target) and not _units[target] == _active_unit: continue # We then compare the squared distance to every cell and always take the smallest one. var distance := target.distance_squared_to(_active_unit.cell) if distance < min_distance: min_distance = distance closest_cell = target return closest_cell
With the function above, if other units surround the target unit, we can’t move to it and keep our previous behavior.
Now, we update both _on_Cursor_moved()
and _on_Cursor_accept_pressed()
to process the cursor’s cell with our new function.
func _on_Cursor_accept_pressed(cell: Vector2) -> void: #... elif _active_unit.is_selected: # We process the cell by calling `find_closest_walkable_cell()` _move_active_unit(find_closest_walkable_cell(cell)) ## Updates the interactive path's drawing if there's an active and selected unit. func _on_Cursor_moved(new_cell: Vector2) -> void: if _active_unit and _active_unit.is_selected: # We process the new cell by calling `find_closest_walkable_cell()` var target_cell = find_closest_walkable_cell(new_cell) _unit_path.draw(_active_unit.cell, target_cell)
And with that, when hovering a unit, the cursor offers to move to a cell in front of it.
If the cell in question is occupied, we fall back to another empty cell.
Here is the complete GameBoard.gd
script with the changes from this solution.
class_name GameBoard extends Node2D const DIRECTIONS = [Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN] export var grid: Resource var _units := {} var _active_unit: Unit var _walkable_cells := [] onready var _unit_overlay: UnitOverlay = $UnitOverlay onready var _unit_path: UnitPath = $UnitPath func _ready() -> void: _reinitialize() func _unhandled_input(event: InputEvent) -> void: if _active_unit and event.is_action_pressed("ui_cancel"): _deselect_active_unit() _clear_active_unit() func _get_configuration_warning() -> String: var warning := "" if not grid: warning = "You need a Grid resource for this node to work." return warning func is_occupied(cell: Vector2) -> bool: return _units.has(cell) func get_walkable_cells(unit: Unit) -> Array: return _flood_fill(unit.cell, unit.move_range) func _reinitialize() -> void: _units.clear() for child in get_children(): var unit := child as Unit if not unit: continue _units[unit.cell] = unit func _flood_fill(cell: Vector2, max_distance: int) -> Array: var array := [] var stack := [cell] while not stack.empty(): var current = stack.pop_back() if not grid.is_within_bounds(current): continue if current in array: continue var difference: Vector2 = (current - cell).abs() var distance := int(difference.x + difference.y) if distance > max_distance: continue array.append(current) for direction in DIRECTIONS: var coordinates: Vector2 = current + direction if is_occupied(coordinates): continue if coordinates in array: continue stack.append(coordinates) return array func _move_active_unit(new_cell: Vector2) -> void: if is_occupied(new_cell) or not new_cell in _walkable_cells: return _units.erase(_active_unit.cell) _units[new_cell] = _active_unit _deselect_active_unit() _active_unit.walk_along(_unit_path.current_path) yield(_active_unit, "walk_finished") _clear_active_unit() func _select_unit(cell: Vector2) -> void: if not _units.has(cell): return _active_unit = _units[cell] _active_unit.is_selected = true _walkable_cells = get_walkable_cells(_active_unit) _unit_overlay.draw(_walkable_cells) _unit_path.initialize(_walkable_cells) func _deselect_active_unit() -> void: _active_unit.is_selected = false _unit_overlay.clear() _unit_path.stop() func _clear_active_unit() -> void: _active_unit = null _walkable_cells.clear() func _on_Cursor_accept_pressed(cell: Vector2) -> void: if not _active_unit: _select_unit(cell) elif _active_unit.is_selected: _move_active_unit(find_closest_walkable_cell(cell)) func _on_Cursor_moved(new_cell: Vector2) -> void: if _active_unit and _active_unit.is_selected: var target_cell = find_closest_walkable_cell(new_cell) _unit_path.draw(_active_unit.cell, target_cell) func find_closest_walkable_cell(cell: Vector2) -> Vector2: if not _units.has(cell): return cell var closest_cell := cell var min_distance := INF for direction in DIRECTIONS: var target: Vector2 = cell + direction if _units.has(target) and not _units[target] == _active_unit: continue var distance := target.distance_squared_to(_active_unit.cell) if distance < min_distance: min_distance = distance closest_cell = target return closest_cell