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.

Architecture diagram
High-level architecture

Unit Command Sequence

Unit command sequence diagram
Sequence from input → validation → spawn/stream → resolve

Data Model

The game leans on data-driven ScriptableObjects so designers can tune balance without touching code.

FieldTypeDescription
constructNamestringDisplay name of the structure
upgradeCostintUnits required to upgrade
maxUnitCapacityintPopulation provided by this construct
unitsPerSecondfloatPassive 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;
        }
    }
}

View on GitHub (lines 66–103)

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));
            }
        }
    }
}

View on GitHub (lines 415–450)

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.