In previous tutorial, we created a warrior unit (a soldier). But if we select multiple such units, they do not follow ordered movement. So in this post, I will talk about many approaches to formations.
In real world, contingents of army stay ordered because each soldier steers himself to adjust his position among other soldiers. The overall effect, when all the soldiers do this, is that a formation system is created. Also, there could be many types of formations such as line, square, winged and so on.
In computer, this kind of self-adjustment is called steering behaviors. But it is complex. We use simpler approaches in games that are often math-based.
Breakdown of Unit Formation
There are some possible approaches:
- Approach 1: When multiple units are selected, calculate their average position by adding the position of all the units divided by the number of units. Let assume that there exists a group leader at the average position. Now calculate path from average position to the destination. This path is for the hypothetical group leader. For each (real) selected unit, set its position to be an offset from the group leader’s position. This offset is calculated based on the type of formation (square, line, etc).
- Approach 2: Set one of the units to be the group leader. All other units will have their path same as the group leader, but with an offset. This offset again is calculated based on the type of formation.
Implementing Unit Formations
The cleaner way, in my opinion is that we make marching as a separate state, or that we extend the Warrior class to make MarchingWarrior
, that adds this additional state & manage it accordingly. The condition for this state will be when multiple units are selected.
But, in my own implementation, I used a hacky way, that is, I overwritten the path of individual units to be the path of 1st unit, but with offset. This is not recommended. But the main thing I will focus on is the idea of offset around some central position.
# implement marching function
func calculate_formation_path_as_if_marching(selected_units: Array):
# if leader, return
if selected_units[0] == self:
return
# else calculate path as offset from leader, such that the formation is maintained as square
var leader = selected_units[0]
var leader_pos = leader.global_position
# Calculate offset from leader so it makes a square formation
var n = int(sqrt(selected_units.size()))
var current_unit_index = selected_units.find(self)
var offset = calculate_offset_for_index(current_unit_index, n, 8.0)
# Assign our path as the path of agent, but all points offsetted to given offset
current_agent_path.clear()
for i in range(leader.current_agent_path.size()):
var point = leader.current_agent_path[i]
var new_point = point + offset
current_agent_path.append(new_point)
# a function to loop n times, and calculate the offset for given index in square formation
func calculate_offset_for_index(index: int, n: int, separation: float):
var offset = Vector3(0, 0, 0)
var row = int(index / n)
var col = index % n
var row_offset = row * separation
var col_offset = col * separation
offset.x = col_offset
offset.z = row_offset
return offset
In above code, I set the 1st unit among the selected_units
to be the leader, and it calculates its path normally. Rest of the units use calculate_offset_for_index
function to calculate their offset from the group leader. This function takes the index of the current unit whose offset is being calculated from the group leader.
Another approach that I think is used in Age of Empires is that they assume a hypothetical group leader, whose position is calculated to be the average position of all the selected_units
. Then path will be calculated from his position to the destination, and all real units will just have an offset from that group leader.
Other Useful Sources
I found a very descent discussion on github & on unity forums as well. These discussions talk about the approaches in detail & are worth looking.