RTS Demo – 05 – Combat Engagement

The purpose of this document is to provide explanations and descriptions for how combat engagement for units was handled in the Apex Utility AI RTS (Real-Time Strategy) Demo project. It is a part of a series of use-cases showcased through the demo project. If you haven’t already, we recommend reading the High Level Overview Document first. The scene associated with this document is called “05_CombatEngagement”.

When combat units see an enemy, they should attempt to defeat that enemy. Regardless of whether combat units are already moving towards a designated target or destination, they must be able to respond to threats as they encounter them while moving. Additionally, combat units that are standing still, e.g. guarding or simply waiting, must also be able to respond to threats they see dynamically, without the necessity for their Controller to order them to attack. An overview of the implemented AI for combat engagement can be seen in figure 1.

Figure 1 – This image shows the AI for handling dynamic combat engagement. A so-called ‘Temporary Target’ is utilized in order to keep a designated attack target in memory, i.e. issued through orders from the Controller.

Being Attacked

Combat units have different scan and attack radii. This means that in some cases, a siege unit may see a melee unit and attack it, without the melee unit being able to see its attacker within its scan radius. This, when unhandled, results in very stupid behaviour for the melee unit – standing still, receiving damage and eventually dying, all while seemingly not even noticing it.

Therefore, units and structures (mortal entities) have a notion of their last attacker, as well as when they were attacked, since it would be detrimental to react to old attacks that may no longer be relevant. They have a special behaviour for reacting to being attacked, including the way they set their attack target. The special behaviour associated with being attacked results in the attacked unit stopping its movement and evaluating the entire Controller’s memory for setting an attack target, while under normal circumstances it would only search its own memory when setting the target. This means that it has a chance of actually retaliating the attack, since the last attacker is always added as one of the options. Additionally, it will score its last attacker very highly, in order to bias its AI towards engaging the last attacker. It will also attempt to avoid workers, if there are combat units present, and finally pick lower health enemies in cases where multiple target candidates scored very similarly. This can be seen in figure 2.

Figure 2 – This screenshot shows the composition of scorers for when a combat unit is attacked. The most dominant scorer is the proximity scorer, ensuring that units should pick the nearest attacker first and foremost. In this case it will also score its last attacker highly, avoid picking workers as targets, and as a tiebreaker pick units with low health, as they are quicker to kill.

Setting a Target

When combat units have an enemy within their scan radius – and they were not attacked, they have a slightly different AI behaviour for engaging in combat with visible enemies. In these cases, they will favorize the nearest enemies, still avoiding workers if there are other candidates and placing much less emphasis on the last attacker. The exact implemented AI scoring can be seen in figure 3.

Figure 3 – This screenshot shows the composition of scorers when setting a temporary attack target under normal circumstances – that is, when they are not being attacked, but rather they have seen an enemy within their scan radius. The dominant scorer here is the proximity scorer, ensuring that the nearest enemy is chosen as the attack target.

The “Set Best Temp Target” action is implemented as an ActionWithOptions operating on IHasHealth options. The code is quite simple. It has an option for using the Controller’s memory or the unit’s own memory. In normal cases it will use its own memory, while when being attacked it uses its Controller’s memory. Based on this option, it will select all the mortal entities in the observations list, possibly adding the last attacker, and pass them to the GetBest method, which runs the options through the scorers and returns the highest scoring option. The code can be seen in the below snippet:


The actual attacking is carried out when the combat unit has identified and set a target. As long as the target is alive, they need to move to it so that the target is within their attack radius, and then actually attack. However, since siege units also have a minimum attack radius, giving them a special attacking behaviour, this has to be accounted for in the implementation of AI. Thus, two different contextual scorers are needed for siege units, and for other combat units. Ultimately the AI simply calls an Attack method that is implemented in the game code for each combat unit.

In the game code, the implementation of attacking is shared between melee and ranged units, while siege units have a slightly different behaviour, in that they need to consider their minimum attack radius. Unity’s Physics.SphereCastNonAlloc is used for figuring out which entities are hit by an attack. Steps are taken to ensure that hits are valid, mortal, alive, enemy entities. In the RTS demo there is no friendly fire. The hits returned from the sphere cast are sorted on distance, so that nearest hits are processed first. Since only one entity is hit per attack, this ensures that the nearest enemy is indeed hit. The code for this can be seen below:

Visualization and Debugging

For combat debugging, visualization can be an extremely helpful tool. First off, just being able to see the observations for any unit at any given time is an important step, for more on this see 01 Scanning. Additionally, seeing current targets through gizmo drawing is also very helpful, this is also mentioned in 04 Fleeing.

What is actually also crucial for gauging whether the AI is behaving as intended, is visual feedback. Using animations, particle systems, health bars and similar means that a viewer can easily see whether an attack is being carried out, whether it hits (health decreases) or misses. Debug logging can go some way, but especially with multiple units the log can be difficult to sift through. Proper in-game visualization of such states can thus be extremely helpful. Such a scenario can be seen in figure 4.

Figure 4 – This screenshot shows actual combat in progress. Units can be seen to be midst animations, and a particle system shows that a ranged unit is firing. Health bars show the health levels for each unit.