RTS Demo – High Level Overview Documentation
The purpose of this document is to provide an overview of the Apex Utility AI – RTS (Real-Time Strategy) Demo project. The demo project features a lot of game code as well as specific AI classes implemented through the Apex Utility AI. Thus, this demo is considered for ‘advanced’ users. The purpose of the demo is to showcase a range of use-cases that are solvable through the Apex Utility AI. The RTS Demo mimics popular RTS games such as StarCraft © and Age of Empires © loosely. The entire demo project is freely available on the Unity Asset Store.
The demo project features assets developed by 3DRT.com. All credits for 3D models, animations, texturing, etc. go to 3DRT.com. All 3DRT assets are found within the 3DRT folder (/Assets/3DRT/). All rights reserved.
The project thus contains 3D assets, textures, a range of Unity prefabs, Mecanim animators, custom developed particle systems, and a semi-large code base. Everything developed specifically for the demo project is located within the ‘Demo’ folder (/Assets/Demo/), which is further divided into Prefabs, Resources, Scenes and Scripts folders. The Resources folder (/Assets/Demo/Resources/) contains the developed AIs, among other things. The Scripts folder (/Assets/Demo/Scripts/) is split up into AI and General. General contains all the game code, while AI has all AI classes, including Context objects.
This demo project makes heavy use of inheritance. Thus, a number of interfaces exist to define basic entity types. Most of the implemented AI classes then utilize the interfaces as types. Due to the overhead of instantiating and removing objects in Unity, all dynamically created entities are pooled. Thus, all entities ultimately implement the IPooled interface, which only defines the needed properties for the implementation of instance pooling. However, all entities also have an EntityType for defining what kind of entity it is, as well as a few convenience properties wrapping Unity functionality. This is featured in the IEntity interface.
In this demo project there are three basic types of entities: Resources, units and structures. They are each represented by an interface. However, since structures and units share the trait that they are both mortal – they have health and can die – the IHasHealth interface is the common denominator for the two, defining properties for health and death, and methods for checking whether other entities are allies or enemies. Additionally, there are three interfaces representing visual elements, which are not entities, but are pooled. These are the decals, showing where explosions happened, doodahs, showing that workers are carrying resources, and particle systems, used for general visual effects. This overview can be seen in figure 1.
Figure 1 – This interface diagram shows an overview of the interfaces in the Apex Utility AI – RTS Demo project, as well as their relations. Please note that some interfaces were abbreviated for simplicity, and some interfaces were omitted.
In addition to the basic structure provided by the interfaces, a range of abstract base classes were implemented, in order to share common implementations and conform with the Don’t-Repeat-Yourself (DRY) principle. Each of the aforementioned interfaces are also represented by an abstract base class, starting with EntityBase, which inherits from the abstract class, PooledBase, representing pooled objects, which in turn derives from Unity’s MonoBehaviour class. For structures, there are two specialized abstract base classes – one for structures that run AI, and one for structures that spawn units. All specific structures except the EnergyGenerator inherit from one of these two base classes, while the EnergyGenerator directly extends StructureBase. The only non-entity class shown is the ‘AIController’, which represents an AI ‘Overmind’; the high-level controller mimicking a human player. Each of the visual elements are also represented by a class, deriving from the abstract PooledBase class. The full class diagram can be seen in figure 2.
Figure 2 – This simple class diagram shows an overview of the implemented classes in the Apex Utility AI – RTS Demo project, as well as their relations. Please note that this diagram only shows class names and their relations. No fields, properties or methods are shown. Additionally, only classes related to entities are shown here.
In addition to the entity classes and the Controller, there are several ‘helper’ classes that provide support systems, such as handling costs, representing grids, managing memories and issuing and executing orders. These will be discussed further in ‘Support Classes’.
As previously mentioned (in Basic Architecture), there are three types of entities: resources, structures and units, as well as the special case AI Controller, which is not an entity itself, but rather a controller of entities. Entities are understood as in-game objects (GameObjects in Unity) that may interact through AI.
Units are movable, mortal entities that run AIs. They can scan their environment, store and recall observations of other entities, receive and execute orders, and engage in combat. There are four distinct types of units implemented in this project, representing typical RTS-style units:
While all units can engage in combat, workers actively avoid doing so. They will flee at the sight of an enemy in order to stay alive for as long as possible. Additionally, by fleeing they can sometimes lure enemies away from their base, at least temporarily. Workers are the only units capable of harvesting resources and building structures. Additionally, they may serve as scouts, due to their superior scan range and movement speed, compared to other unit types. When harvesting, they can carry up to a fixed amount of resources before they need to return the gathered resources to their Center structure, which functions as a resource depot. Workers have low health however.
Melee units are simple combat units that may only engage enemies at a very short range. Melee units have high health, but relatively low damage, with a relatively high attack speed.
Ranged units have less health than melee units, but more damage, while boasting a much larger range, but a slower attack speed.
Siege units have even less health, even slower attacks, but potentially much higher damage. Additionally, siege units are special in that they have a minimum attack radius at which they can attack their enemies. Thus, melee units up close can kill a siege unit without the siege unit being able to defend itself (except by moving).
Resources come in two distinct types in this project. Harvestable metal, and generated energy. Near the base of each Controller’s Center structure there are blocks of resources, which worker units may harvest Metal from. They have a limited amount of resources and disappear (deactivate) when empty. Energy is generated by building ‘Energy Generator’ structures, which generate energy at a fixed rate as long as the generator structure exists. These two types of resources represent two different ways of designing typical RTS-style resources. These resources are in turn used for building structures and spawning units.
Structures a immovable, mortal entities that may run AIs. Structures in this project have widely different capabilities depending on their type. All structures, except the starting ‘Center’ structure, are constructed by Workers during run-time. The maximum amount of structures per Controller is defined by the size of the ‘Structure Grid’ (Read more on the Structure Grid in ‘Support Classes’). There are five distinct types of structures in this demo project, meant to represent typical types of structures in RTS games:
The Center is the structure that all Controllers start with. The Center is responsible for spawning workers and functions as a resource depot, where workers return harvested resources. The Controller has lost the game when its Center structure is destroyed. Thus, it is the goal of the game to defend the Center, while destroying the enemy’s Center. The position of the Center is also utilized as the center point of the base – the ‘position’ of the Controller.
The Factory is the spawning structure for combat units. Without a Factory, the Controller will never be able to spawn combat units and engage in war. The Factory may only spawn one unit at a time, so it may be beneficial in some cases to construct more than one Factory.
The Energy Generator is a very simple structure. All it does is add a little bit of energy resources to its controller every second, perpetually, until destroyed.
The Cannon Tower and Radar Tower both scan their surroundings and send the observations to their Controller; they have no memory of their own. However, where the Radar Tower does nothing but scan, the Cannon Tower can fire at enemies within its attack radius at a very slow rate, but with devastating damage.
The Controller (its class is called ‘AIController’) is where high-level decisions and analysis is executed. The Controller’s purpose is to simulate a human player using AI – ordering units to move, harvest, construct, engage in combat and so on. The controller manages lists of units and structures, combines individual entities’ memories in a shared memory, which its entities may in turn access. The Controller also executes analysis of the battlefield at a fixed interval, evaluating weak spots both for itself and its enemies. Each Controller has a color, which is applied as tinting to all its entities, in order to be able to distinguish entities on different ‘teams’. The Controller can have a strategy which influences its unit spawning policy (in the current implementation). The strategy may be set in the Unity inspector. If it is not, the AI will randomly select a strategy at start.
In this demo project there are a range of helper or support classes, which provide needed functionality for the AIs of entities and Controllers. The most important subset of them will be discussed briefly here in arbitrary order.
There are two types of grids in the RTS Demo; map grids and structure grids. Because of this, they share a common base class, GridBase
Map grids cover the entire map in large cells with a minimum of cell spacing. Each cell has a threat score and a timestamp for when it was last seen. Entities update the timestamps as they move around on the map grid. The threat score is calculated at a fixed interval, based on the amount of units and structures vs the enemy’s units and structures, in that cell. Each unit and structure has a threat contribution, which summed up makes up each cell’s threat score. This principle is typically known as ‘influence fields’, and in this case is used to facilitate evaluations of threatening and threatened areas for the Controllers.
Structure grids are much smaller grids with small cells and a large cellspacing (space between individual cells). The structure grids ultimately control where Workers may build structures. Only one structure may be built per structure grid cell. If the structure is destroyed, the cell becomes unoccupied. Cells that have resources in them are marked as occupied until the resources have been harvested completely, thus freeing up the structure grid cell.
Building structures and spawning units costs resources and time. As previously mentioned, there are two types of resources in the RTS Demo: Metal and energy. All structures except the Energy Generator costs energy to construct, as well as metal. All units cost metal to spawn, while Siege Units additionally cost a small amount of energy.
However, all units and structures also cost time to spawn and build. Units cost a relatively small amount of time, while structures cost a more significant amount of time. For the construction of buildings, the time cost is effectuated as idle time for the unit (where it can do nothing else). For the spawning of units, the time cost defines how long the Factory is busy before the unit is spawned and the Factory is freed up to spawn another unit.
In order to provide easy access to costs, a static ‘CostHelper’ class exists, which features static GetCost methods for units and structures. The costs are defined as simple static readonly member variables that are returned by the GetCost methods, depending on the given type of unit or structure.
In order to facilitate having a Controller capable of managing unit movement, combat, building structures, and so on, a simple Orders system was designed and implemented.
The idea is that the Controller can issue orders to any of its units, but the units choose themselves whether to follow the order or not, depending on their current situation. E.g. the Controller might order a unit to attack the enemy’s base, but if that unit is already in combat, it should resolve that situation before attempting to execute the issued attack order.
All orders inherit from an OrderBase abstract base class, which in turn implements an IOrder interface, thus facilitating using the interface type in AI. All orders have an OrderType so that it is easy and fast to query for the type of order, without e.g. casting to certain types. All orders also have an OrderCategoryType, since many orders share the same category, e.g. all offensive orders share the same ‘Offensive’ category. Additionally, all orders have a timestamp for when they were issued, as well as a priority set from the AI.
Entities which may receive orders – in the current implementation only units – implement an IHasOrders interface, which defines a current order, executing order, as well as two lists for current orders and completed orders. The current order is the order that the unit considers as its current, while the executing order is the order that the unit is actually executing at any given time. Typically the two will be the same, but in some cases the unit may have a current order which has not yet become the executing order, because the unit is otherwise engaged.
The Controller needs to be able to pass a specific target to units through orders, e.g. attack targets (enemies) or defend targets (own structures). In order to facilitate this, units’ Context object contains a range of target properties. When a unit receives an order, it can save the intended target in its Context object so that it knows what it is supposed to do. However, in order to facilitate units setting a temporary attack target, e.g. if they meet an enemy on the way to their intended target, a ‘temporary target’ property is also present, which units can set and use to engage in combat while still retaining their original intended target.
Unit groups are groups of one or more combat units. Workers cannot be added to groups. Controllers each have a list of current unit groups, where the 0th group is defined as a ‘default group’ which should not be removed ever. All combat units are automatically added to this default group when they spawn. The Controller will typically move the units from the default group to a new group when ordering them to attack or defend. When an order is issued to a group, each member of that group receives the order.
Unit groups feature methods for merging with other groups and splitting into new groups, as well as a range of properties, to make it easy to facilitate group management for the Controller’s AI. Groups define both a center of gravity – the average position of its members, as well as a ‘position’ which is actually just the 0th unit’s position. Empty groups are automatically removed, except for the default group. Units that die are automatically removed from their group when disabled.
All units and some structures have scanning capabilities. This means that they execute scanning at a fixed interval (as an AI), and for each entity within their scan radius they create or update an Observation.
The Observation class represents the memory of another entity, and features a reference to that entity, the position where the entity was observed, the collider of the entity and a timestamp for when the observation was created. The timestamp is used when an entity, that already exists as an observation in memory, is scanned. Older observations are discarded.
Whenever an entity scans another entity, they update the Controller’s shared memory, which they in turn may also access in their AIs. However, units also have their own memory, which can be used for situations where it is desirable for the units to act on their own limited information, rather than the more complete worldview of their Controller.
Observations of entities that are no longer valid, e.g. dead units or empty resources, are removed at a fixed interval, so as to keep the memory up-to-date and including only valid, live entities.
The RTS Demo project features several distinct instance pools, even though they all derive from the same generic base class, called PoolBase
The principle behind the instance pool is to instantiate a number of instances at start, so that the same instances can be reused throughout the game without (or rarely) having to instantiate new ones, which comes with a significant performance overhead. The memory usage grows as there may be a need to prepare many instances in some cases, but the CPU is unburdened when activating and deactivating instances, compared to instantiating and destroying them.
The singleton paradigm is heavily used in the RTS Demo project for all manager-types that are unique per scene. To facilitate this in a convenient way, an abstract singleton base class was implemented; SingletonMonoBehaviour
A better approach to get looser coupling and allow better support for unit testing, would be to use dependency injection. This has not been done here since that would add dependencies on third party products.
Visualization and Debugging
Several visualizers and debug classes were also implemented, in order to facilitate rapid and convenient debugging. They range from entity visualizers, i.e. for units, structures and resources, to AI action visualizers, i.e. position score visualizer, to a convenient attribute and a component for modifying time scale.
Entity visualizers are specialized components (MonoBehaviours) for a specific type of entity. E.g. there is a general UnitVisualizer for combat units, and a specific WorkerUnitVisualizer which adds visualization in relation to constructing and harvesting. Entity visualizers expose a range of interesting property values and context values to the Unity inspector for easy viewing, while also utilizing Gizmos for in-game visualization, e.g. showing a color coded line and sphere for each observation in memory. Additionally, GUI drawing is used to write what each unit is currently doing – which order is it executing, is it in combat, constructing or fleeing, etc. The Controller’s visualizer also GUI draws the current overall situation for that Controller, so that human viewers can easily evaluate which AI is doing better. Generally, only selected entities are visualized, however multiple entities can be selected simultaneously for mass visualization.
The PositionScoreVisualizer handles visualization of MoveToBestPosition AI actions being executed. That is, they show how each sampled position was scored in relation to the others, in order to identify how the position scoring evaluates each possible destination, before moving the unit to the highest scoring one.
There is an attribute named ReadOnlyAttribute, which may be used to decorate fields that should be exposed to the Unity Editor’s inspector (through SerializeFieldAttribute or by being public), but should not be editable by the user. It is used heavily, especially by the entity visualizers.
There is a component called TimeControlComponent, which facilitates in-game control of Unity’s timescale, simply by pressing plus (+) or minus (-) on the keyboard. Please note that adjusting the time scale can cause a number of issues for Unity, including its NavMesh and Physics implementations.