RTS Demo – 01 – Scanning & Memory

The purpose of this document is to provide explanations and descriptions for how scanning 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 “01_Scanning”.

Units, and select structures, can observe other entities in their vicinity through scanning. The observed entities are stored as observations along with relevant metadata, i.e. the position of the observation, the time at which it was observed, etc. When units or structures scan other entities, they also share their observation with their Controller, which updates its shared memory if the observation is new or newer than an existing equivalent for the same entity. Additionally, units sample and store a grid of positions around themselves in order to facilitate position scoring, e.g. for worker units fleeing.

Executing Scanning and Sharing Observations

All units and some structures execute entity scanning through an AI. Unity’s Physics.OverlapSphereNonAlloc is used to get all colliders in certain layers within the entity’s scan radius. In order to get the entity associated with a hit collider, without using GetComponent which might impact performance, all entities register themselves through extensions methods to a static handler, where they are stored in a dictionary. Thus, when an entity needs to be accessed through its collider, a simple extension method called Get is utilized. For performance reasons, creating new Observation instances for each scanned entity should be avoided. Therefore, efforts were made to check whether an entity was already observed earlier, in which case the existing observation can simply be updated. Only if the entity has not been observed before will an observation be created for it. Finally, the entity also adds the observation to its Controller as a part of its scanning process. The Controller evaluates whether the observation is new, or newer than an existing for the same entity, and in those cases adds or updates the observation accordingly. There is also explicit handling of enemy observations, which should also be updated, but it simply follows the same logic as for other observations. The (abbreviated) bulk of entity scanning is shown in the following:

Enemy Observations

Observations of enemies – that is, enemy units and structures, are stored separately in their own list. This empowers the AI development and facilitates for example passing a list of only enemies to ActionWithOptions, e.g. when setting attack targets. Even though the memory consumption is increased for each entity, performance and memory is saved later on in the AI, when there is no need to use new temporary lists simply for selecting the enemies from their observation lists. Many AI actions and scorers only need to concern themselves with enemies, so rather than they each have to use temporary lists, the enemies are simply stored on the entities themselves separately. This is facilitated through the AddObservation method, which handles adding enemy observations to a separate list. See the following code snippet:

Where AddOrReplaceIfNewer simply adds the given observation to the specified list, if the timestamp is newer or the entity observation is new. As can be seen from the code, another extension method called GetEntity is used to get the entity as a specific type (or null if invalid) conveniently. The Controller uses the exact same method as units do, their AddObservation methods are equivalent.

Memory Cleanup

It is important to ensure that there are no invalid observations in the memory of units or the Controller. Thus, memory cleanup is needed to ensure that e.g. dead entities or empty resources are removed from the observation lists. Units clean their memory as a part of their EntityScan action. After adding or updating new observations, they must check that all current observations are still valid and relevant. If any are null (destroyed) or inactive (deactivated) they should be removed, as they are no longer relevant to keep in memory. Units’ cleanup code is shown in the following:

The Controller does its memory cleanup in a different way (although using equivalent code), since it does not itself scan for entities, it cannot update as a part of the scanning process. Instead, it has a specific ‘memory cleanup’ AI which runs at a set interval. Otherwise, it performs the memory cleanup in the exact same way – testing for and removing null or inactive entities.

Position Sampling

Units also sample and store a grid of positions around themselves. This facilitates using position scoring for evaluating where to move on a short-term basis, e.g. when fleeing. By continuously sampling and storing the positions, AI actions and scorers can simply utilize the at any time stored positions. They may be slightly outdated, depending on the scanning interval, but performance-wise this solution is superior compared to each action and scorer that needs positions, sampling them individually. In the RTS Demo, the same scan radius is used for entity scanning as for position scanning. However, since positions are sampled in a grid around the unit, the scan radius is multiplied by a factor to ensure that the sampled area is at least the size of the scan radius. The code for this is shown in the following:

Visualization and Debugging

It is crucial to be able to see which entities have observed other entities and how they view those observations. If there are issues with the implemented memory model, those issues will likely propagate throughout the entire AI, since it oftentimes acts on its memory. Thus, ensuring a valid and up-to-date memory is crucial. Visualization is a key tool in debugging memory issues.

In the RTS Demo project, the visualizer components also (optionally) draw Gizmos for visualizing observations, as well as outputting the count of normal observations, enemy observations and resource observations in the Unity inspector. Observations are color coded so that it is easy to see if the AI is for some reason mismatching types or friend/foe status. It is advisable to always draw lines for each observation, as drawing spheres only can make it very difficult to gauge which observations belong to who (when multiple entities are selected), or distinguishing from other gizmos drawing. The code for the Controller’s gizmo drawing of observations, units do it in the same way, can be found in the following: