Creating and extending AI
Starting with version 2.0, the AI is config-driven and modular: an AIData asset holds tunable settings for seven always-active behaviour concerns (Construction, Production, Squad, Scouting, Economy, Utility, Defense), and an AIController ticks one executor per concern each match. Most tuning — difficulty, build order, squad composition, targeting — is done entirely from the Inspector, without writing any code. Code is only needed if you want a genuinely new kind of behaviour, or your own AI/data subclass.
Creating an AI preset
Create an AIData asset via Assets > Create > RTS Starter Kit > AI Settings. It has a top-level Think Time (delay between AI decisions) and seven fixed fields, each always active — leave a concern’s fields at their default/empty values to effectively turn it off:
public class AIData : ScriptableObject
{
public float ThinkTime;
public ConstructionModuleData Construction;
public ProductionModuleData Production;
public SquadModuleData Squad;
public ScoutingModuleData Scouting;
public EconomyModuleData Economy;
public UtilityModuleData Utility;
public DefenseModuleData Defense;
}
Targeting units by role, not by asset (multi-faction AI)
ConstructionModuleData.BuildingRules and ProductionModuleData.UnitsToBuild (both covered below) don’t have to point at a fixed UnitData asset - by default they do (Direct mode), but each entry is actually an AIUnitReference, which also supports By Role mode: instead of a fixed asset, you give it a Role string tag, resolved at runtime against the current player’s own faction (FactionData.OwnProductionCategories). This is what lets one AI preset be reused across several factions - as long as each faction tags its own matching unit/building with the same role, the preset doesn’t need any faction-specific copy.
Roles are free-form string tags, not a fixed enum - tag a unit or building via UnitData.AIRole (the same field Squad composition uses, see below). The Inspector shows a dropdown of already-known roles (the project-wide master list, GameSettings.AIRoles) with a “+” button to add a new one inline, right from the field. If a role is later renamed or removed from that list while still assigned somewhere, the field keeps its (now stale) value instead of silently clearing it, and shows a warning instead.
If a faction has several units/buildings sharing the same Role (e.g. Barracks of different tiers), Tier (also on UnitData) disambiguates which one a By Role reference resolves to:
Left at the default
-1(auto), it picks the highest Tier among candidates that are currently unlocked (UnitData.RequiredBuildingToUnlocksatisfied) - so the AI naturally starts with a low-tier unit/building and switches to a higher-tier one of the same role as it becomes available, without needing a separate rule per tier.Pinned to a specific value (0+), it requires an exact Tier match instead - use this when a rule needs its own tuning (weight, time-gate, etc.) distinct from other tiers of the same role.
Construction — building the base
ConstructionModuleData.BuildingRules is a weighted list of buildings the AI can construct; the highest-weight eligible rule wins each time the AI decides to build something. Each rule points at a building via Building (an AIUnitReference - Direct or By Role, see above), and can also be:
Time-gated (
TimeGate/TimeValue) — always eligible, or only before/after a certain match time.Capped (
MaxCount) — counted against buildings the AI has ever finished of that type, so a destroyed building can always be rebuilt once it’s below the cap again.Focus-scaled (
FocusAffinity/FocusInfluence) — its weight is multiplied depending on the AI’s current Economy/Military focus (see Utility AI below). LeaveFocusAffinityatNoneto keep the weight constant.Electricity-aware (
ElectricityDeficitPriorityMultiplier) — multiplies the rule’s weight whenever the AI is at or over its electricity capacity (only checked if Electricity is enabled in Game Settings). Set this above 1 on your Power Plant rule so the AI reliably prioritizes power once it runs short.
Each building is queued into the production line matching its own category (e.g. Defense buildings go to the Defense line), so one busy line never blocks a building from an unrelated, idle line.
Production — buying combat units
ProductionModuleData.UnitsToBuild is a list of unit waves (UnitsList: a list of units - each an AIUnitReference, Direct or By Role, see above - plus an optional BuildTimeType/TimeValue time gate, same semantics as Construction’s). The AI cycles through waves in order, looping back to the start unless StopOnListEnd is set.
The AI only ever has one unit “in flight” per wave slot at a time — it won’t select the next wave entry until the previously ordered unit has actually finished building, so it never piles up a backlog in one production line.
Squad — grouping and attacking
Newly built combat units are grouped into squads, then sent to attack once a squad is ready. Units currently on scouting duty (see Scouting below) are never grouped into a squad.
Composition (
SquadModuleData.Composition, optional) — a list ofSquadRoleWeight(a Role string tag - the same free-form tag used by By RoleAIUnitReference, see above - plus a desired count). A squad fills each listed role up to its desired count before its first attack. Units are tagged with a role viaUnitData.AIRole— units whose role isn’t listed inCompositionat all still join a still-forming squad instead of being stranded alone.If
Compositionis empty, squads are just grouped by flatGroupSizeinstead. SetGroupSizeMaxaboveGroupSizeto have each squad roll its own random target size in that range once, when it’s formed, instead of every squad always using the same fixed size.Adaptive composition — each
SquadRoleWeightentry can setMaxAirCounterBonus(default 0, off): its effective Desired Count grows (up to Desired Count + this bonus) proportionally to how much of the enemy’s spotted combat force turns out to be flying. Set this on an AntiAir role entry so squads bring more AA once the enemy is confirmed fielding aircraft, without needing to hand-tune composition per matchup. HonorsRequireVisibleTargetsthe same way theArmyRatioUtility AI consideration does (see below).Once a squad has attacked at least once, it keeps fighting with whatever survivors it has left as soon as it’s free again (e.g. its target died) — only a squad’s very first attack waits for full composition/size.
RequireVisibleTargets— if enabled, the AI only attacks enemy units currently within its own units’ vision range (honest targeting), instead of always knowing exact enemy positions regardless of Fog of War. When enabled, target selection also considers any enemy building previously spotted by Scouting (see below), even if nothing of the AI’s currently sees it - withoutRequireVisibleTargets, the AI already “cheats” and sees every enemy directly, so remembered sightings don’t add anything in that mode.Target scoring weights (
TargetDistanceWeight/TargetValueWeight/TargetThreatWeight) tune which enemy unit a squad picks: closer, tougher (higher max health), and armed targets are preferred, each independently weighted.Target coordination — when picking a target, a squad prefers one no other squad is already attacking, only falling back to an already-covered target if nothing else is visible. This spreads simultaneous squads across different targets instead of all of them piling onto the single best-scoring one.
Scouting — finding the enemy
ScoutingModuleData keeps a small pool of units permanently wandering the map to locate the enemy, and remembers enemy buildings once spotted, so Squad (above) can send an attack at one even after it’s no longer in sight:
MaxScoutingUnits— how many units this AI keeps on permanent scouting duty at once. Only freshly spawned units are ever considered for scouting duty, before Squad gets a chance to group them — a unit is decided once, at spawn, to be either a scout or an army unit for its whole lifetime, never both, so existing squads are never pulled apart to fill the quota, and there’s no tug-of-war between a scouting order and a squad order for the same unit.GridResolution— the map is divided into aGridResolutionxGridResolutiongrid; scouts cycle through a shuffled pass over the grid’s NavMesh-sampled cell centers, so multiple scouts spread across different areas instead of clumping on the same point, and the whole map gets covered before any cell repeats.WaypointTimeout— if a scout hasn’t reached its current waypoint within this many seconds (e.g. stuck on pathing), it’s redirected to the next one instead of waiting forever.StartDelay— delay from game start before units start being assigned to scouting duty.
Spotting an enemy building isn’t limited to units actively scouting - any of the AI’s own units can add one to memory just by having it within vision range. Scouting only decides who goes looking for the enemy; vision itself is inherent to every unit. A remembered building is forgotten as soon as it’s destroyed, and also - if MemoryDuration is set above 0 - once it hasn’t been re-seen for that many seconds (0, the default, means never expire; a building’s position doesn’t itself go stale, but this keeps a very old unconfirmed sighting from being a permanent target while RequireVisibleTargets is on, and bounds memory growth on very long matches).
Economy — keeping harvesters up
EconomyModuleData replenishes harvesters: TargetHarvesterCount/BuyDelay control how many it tries to keep alive and how often it checks. Unlike the other concerns, it has no unit reference of its own - the harvester unit to build comes from the current player’s FactionData.Harvester, so the same AIData preset works across every faction that sets one. Leave FactionData.Harvester empty on a faction to keep harvester replenishment off for it.
It also auto-repairs buildings: any of this AI’s own buildings whose health drops below AutoRepairHealthThreshold (a fraction of max health) gets a Repair component attached automatically - the same mechanism the player’s Repair button uses, so it spends the same resources at the same rate and removes itself once the building is back to full health. Set AutoRepairHealthThreshold to 0 to disable auto-repair entirely.
Finally, it can grant a straightforward difficulty handicap, separate from any of the behaviour tuning above: StartingResourceBonus is added once, effectively at match start, and PeriodicResourceGrant (if above 0) is added repeatedly every GrantInterval seconds. Both default to 0 - no effect unless you opt in, e.g. to make an “Easy” preset weaker or a “Brutal” one richer without touching how it actually plays.
Defense — reacting to being attacked
DefenseModuleData redirects nearby idle/available combat units to fight off an enemy that comes within DetectionRadius of one of this AI’s own buildings - without it, the AI only ever attacks outward via Squad and never reacts to being attacked at home.
ResponseRadius— how far from the threatened building this AI will pull units from to respond. Keeps a small raid near one base from draining units out of an ongoing offensive elsewhere on the map - only units already nearby (mostly idle ones) get pulled in.MaxDefendersPerThreat— caps how many units respond to a single attacker at once.Every threatened building is handled the same tick (not one-at-a-time like Construction’s building placement) - a defense response never waits behind an unrelated, simultaneous threat elsewhere. Units already responding to the same attacker aren’t re-ordered every tick, and scouting units are never pulled off scouting duty to defend.
Utility AI — dynamic Economy/Military focus
UtilityModuleData is an optional decision layer that continuously scores configured Actions and writes the result into two shared 0..1 values, EconomyFocus/MilitaryFocus (both default to the neutral 0.5, so a preset without any Utility Actions behaves exactly as if this concern didn’t exist). Other modules read those focus values back:
EconomyModule/ProductionModulescale their buy delay by focus (faster when the matching focus is high, slower when low).ConstructionModulescales aBuildingRule’s weight by focus, if that rule opted in viaFocusAffinity.
An Action (AIUtilityActionData) targets one focus axis (AIFocusTarget.Economy or .Military) and holds one or more Considerations (AIConsideration): each consideration reads one raw game-state input (AIConsiderationInput — IncomeRate, HarvesterSaturation, ResourceStockpileRatio, ArmyRatio, TimeSinceMatchStart), normalizes it by NormalizationReference, and maps it to a 0..1 score via a designer-tunable ResponseCurve. An action’s score is the product of all its considerations’ scores; the highest-scoring action for each axis wins that axis every EvaluationInterval seconds, and the focus value smoothly moves toward it by FocusSmoothing.
For example, an Economy-axis action with an IncomeRate consideration whose curve scores low income highly will push EconomyFocus up whenever the AI’s income drops — which in turn speeds up EconomyModule’s harvester buying and boosts any BuildingRule with FocusAffinity: Economy (like a Power Plant or Refinery rule).
RequireVisibleArmyCount extends the same Fog-of-War honesty as Squad’s RequireVisibleTargets, but for the ArmyRatio consideration.
Per-player AI presets (difficulty/personality)
An AI preset can be selected per player in the lobby, independently of difficulty being baked into one shared asset. GameSettings.AIPresets lists every selectable preset (populated automatically from all AIData assets), and the lobby’s AI preset dropdown writes the chosen one into PlayerSettings.SelectedAIPreset. If a player leaves it unset, MapData.DefaultAIPreset is used instead.
Debugging: the AI Inspector
RTS Starter Kit > AI Inspector (Editor menu) is a Play Mode-only tool for picking one AI player and watching its live state update continuously: current resources (including electricity used/total), every production building’s queues (click a queue’s header to select its building in the scene), and the current Utility AI state (income rate, Economy/Military focus values). It has no effect outside Play Mode and shows a status message if the game hasn’t finished booting yet.
Extending with your own AI logic
Adding a whole new concern (an eighth module alongside Construction/Production/Squad/Scouting/Economy/Utility/Defense): create a data class in the Data assembly and its matching executor in the AI assembly, then wire it into your own AIController subclass.
// Data assembly - fields only, no gameplay logic
[Serializable]
public class MyModuleData : BaseAIModuleData
{
public float SomeSetting = 1f;
}
// AI assembly - the logic that reads MyModuleData
public class MyModule : BaseAIModule
{
readonly MyModuleData data;
public MyModule(MyModuleData data) => this.data = data;
public override void Tick(AIContext context)
{
// context gives you Units, GameSettings, SelfPlayer, Enemies,
// SelfCommandCenter, EconomyFocus/MilitaryFocus, etc.
}
}
// Your own AIData subclass with the new field
public class MyAIData : AIData
{
public MyModuleData MyExtra = new ();
}
// Your own AIController subclass
public class MyAIController : AIController
{
protected MyAIData MyData => (MyAIData)aiData; // aiData is protected, cast once here
protected override void BuildModules()
{
base.BuildModules();
modules.Add(new MyModule(MyData.MyExtra));
}
}
Create a MyAIData asset instead of a plain AIData one — Unity’s Inspector accepts it anywhere an AIData reference is expected (GameSettings.AIPresets, MapData.DefaultAIPreset, etc.), no other change needed.
Using your own AIController subclass: override AISystem’s factory method so it creates your subclass instead of the base one - the rest of AISystem (updating, disposing, registering with AIService) works with any AIController subtype unchanged:
public class MyAISystem : AISystem
{
protected override AIController CreateAIController() => new MyAIController();
}
Then register MyAISystem instead of the stock AISystem in your own IBootData.MakeGameModules() (see the Dependency Injection tutorial for how the boot module list works).
Other useful overrides on AIController (without a subclass of your own module list): UpdateAction() runs every frame regardless of Think Time, HandleActions() runs on the Think Time cadence after all modules ticked, OnHarvesterSpawned() fires when this AI’s harvester is spawned, OnAIAddedToPlayer() fires once the controller is set up for its player.