Minimalists – AI & Node Control
Codebase Breakdown • Unity • ScriptableObjects • FSM AI
Overview
A minimalist real-time strategy prototype where houses, turrets and helipads collaborate through dynamic node control and streamlined AI. Built with Unity and data-driven ScriptableObjects to stay highly moddable and fast.
Architecture
Core systems are divided between client input, game loop, and combat/economy services.
Unit Command Sequence
Data Model
The game leans on data-driven ScriptableObjects so designers can tune balance without touching code.
Field | Type | Description |
---|---|---|
constructName | string | Display name of the structure |
upgradeCost | int | Units required to upgrade |
maxUnitCapacity | int | Population provided by this construct |
unitsPerSecond | float | Passive unit generation rate |
Key APIs / Contracts
Game Manager
public class GameManager : MonoBehaviour
{
public void registerUnit(UnitController unit);
public void unregisterUnit(UnitController unit);
}
Construct Controller
public class ConstructController : MonoBehaviour
{
public void SendUnits(ConstructController target, float percentage);
}
Representative Code Snippets
Map analysis assigns roles to each node – uses distance percentages to classify frontline, support and economic nodes.
Show code
@AIManager7.cs lines 66-103
private void AnalyzeMapAndAssignRoles(List<ConstructController> myNodes)
{
nodeRoles.Clear();
var enemyNodes = GameManager.Instance.allConstructs
.Where(c => c.Owner != aiFaction && c.Owner != GameManager.Instance.unclaimedFaction)
.ToList();
// If no enemies exist or we have too few nodes for complex roles, they are all economic.
if (!enemyNodes.Any() || myNodes.Count <= 2)
{
myNodes.ForEach(n => nodeRoles[n] = NodeRole.Economic);
return;
}
Vector3 enemyCenter = GetFactionCenter(enemyNodes);
// Sort nodes by their distance to the enemy's center of mass.
var sortedNodes = myNodes.OrderBy(n => Vector3.Distance(n.transform.position, enemyCenter)).ToList();
int nodeCount = sortedNodes.Count;
// Designate roles by percentage: closest 30% are Frontline, next 40% are Support, rest are Economic.
int frontlineCount = Mathf.Max(1, Mathf.CeilToInt(nodeCount * 0.3f));
int supportCount = Mathf.CeilToInt(nodeCount * 0.4f);
for (int i = 0; i < nodeCount; i++)
{
ConstructController currentNode = sortedNodes[i];
if (i < frontlineCount)
{
nodeRoles[currentNode] = NodeRole.Frontline;
}
else if (i < frontlineCount + supportCount)
{
nodeRoles[currentNode] = NodeRole.Support;
}
else
{
nodeRoles[currentNode] = NodeRole.Economic;
}
}
}
Streaming units between nodes – validates targets and spawns units along the current movement system.
Show code
@ConstructController.cs lines 415-450
public void SendUnits(ConstructController target, float percentage)
{
bool isValidTarget = true;
foreach (ConstructController construct in targetConstructs)
{
if (construct == target)
{
isValidTarget = false;
break;
}
}
if (!isValidTarget) return;
int unitsToSend = Mathf.FloorToInt(UnitCount * percentage);
if (unitsToSend == 0 && UnitCount > 0)
{
unitsToSend = 1;
}
if (unitsToSend > 0)
{
if (currentConstructData is HelipadData)
{
targetConstructs.Add(target);
StartCoroutine(SpawnUnitsRoutine(unitsToSend, target));
}
else
{
if (navmeshIndex == target.navmeshIndex)
{
targetConstructs.Add(target);
StartCoroutine(SpawnUnitsRoutine(unitsToSend, target));
}
}
}
}
Performance Notes
Profiling is ongoing and formal benchmarks are not yet published.
Testing & Validation
Currently verified through manual playtesting; no automated tests are in place.
Trade-offs & Next Steps
- Finite state AI is easy to reason about; behavior trees could scale variety later.
- Current movement/streaming approach is lightweight but has limited dynamic-obstacle handling, only works reliably for static obstacles.
- Data-driven design simplifies scalability but can potentially be detrimental for systems that dont necessarily need it.