Gameplaycom.zoa.ai · v0.1.0

ZOA AI Framework

Definition-driven behaviour trees, batched perception, and tactical services for autonomous agents.

The AI package splits cleanly in two. ZOA.AI.Core is an engine-free behaviour tree kernel: nodes, states, a blackboard, a context, and a service that ticks registered agents. It has no reference to UnityEngine at all, which is why the tree logic is testable in a plain edit-mode fixture without a scene. ZOA.AI.Unity is everything that touches the engine: the agent component, the perception pipeline, locomotion and combat bridges, spawners, and the tactical services.

Behaviour is authored as data rather than written as code. A BehaviorTreeDefinition is a ScriptableObject holding a flat list of BehaviorNodeData records, each naming an archetype id such as "movement.pursue_target" or "tactical.cover_when_los", carrying a string parameter map and a list of child ids. BehaviorTreeResolver walks that graph and produces a live IBehaviorNode hierarchy. Composites and decorators become real node instances with real semantics; every leaf becomes a single DefinitionActionLeaf that carries its archetype id forward to an executor at runtime. Adding a new AI capability therefore means adding an archetype and a dispatch case rather than a new node class the designer has to learn.

Around that core sit the systems that make a crowd of agents affordable and interesting. Perception can hand off from per-agent linecasts to a batched RaycastCommand service. Spawning is driven by AiSpawnerDefinition rosters placed in AiSpawnZone volumes. Cover, vantage scoring, and suppression are three separate services resolved through FoundryServiceRegistry, so a scene that never installs them still runs, it just has agents that never take cover.

How it works

Concepts

The behaviour model runs on states

Every node implements IBehaviorNode, which is three members: an Id, an Evaluate that takes an IAiContext and returns a BehaviorState, and a Reset. BehaviorState has five values, Idle, Running, Success, Failure and Suspended, and the composites interpret them in the ordinary way. A Sequence evaluates children left to right and returns immediately on anything that is not Success. A Selector evaluates children left to right and returns immediately on anything that is not Failure. A Parallel evaluates every child each tick and compares the success count against a threshold parameter, returning Running while the outcome is still undecided.

The tree is evaluated from the root on every tick. There is no resumption bookmark, no coroutine, no stack of suspended nodes. A node that needs to remember something across ticks keeps that state internally, which is exactly what the Cooldown, Timeout and Repeater decorators do, or writes it to the blackboard, which is what movement leaves do with their current patrol point. This makes tick cost proportional to the depth actually reached and makes an interrupt free: a high-priority branch that starts succeeding simply wins on the next tick, because the tree is asked again from the top.

Nodes report their result to BehaviorNodeTracer at the end of Evaluate. When BehaviorNodeTracer.Enabled is false, which is the default, that call is a single boolean check. The debug HUD flips it on while it is open so it can render the last state of every node without re-evaluating the tree, which would be unsafe because evaluation mutates cooldown timers and blackboard entries.

How definitions drive behaviour

BehaviorNodeArchetypeRegistry is a static catalogue of every node kind the editor can place. Each BehaviorNodeArchetype carries an id, a display name, a description, a BehaviorNodeCategory, a BehaviorNodePortConfig telling the graph editor whether it is a Root, Composite, SingleChild or Leaf, and a list of BehaviorNodeParamDef describing its typed parameters. The registry populates itself in a static constructor across ten category groups: FlowControl, Decorator, Sensor, Movement, Combat, Formation, Survival, Social, Utility and Tactical.

Resolution branches on PortConfig, not on the archetype id, for structure. Composite archetypes become SequenceNode, SelectorNode, ParallelNode or RandomSelectorNode. SingleChild archetypes become InverterNode, RepeatNode, ForceStateNode, CooldownNode, TimeoutNode or ConditionGuardNode. Everything with a Leaf port config becomes a DefinitionActionLeaf holding the archetype id and the resolved parameter dictionary. An unknown archetype id logs a warning and resolves to a FailureLeaf, so a definition authored against a newer archetype set degrades to a failing branch instead of throwing at load.

At tick time the leaf calls DefinitionActionExecution.Evaluate, which looks for an IAiDefinitionActionExecutor on the blackboard under the reserved key AiRuntimeBlackboardKeys.ActionExecutor. If one is present, the leaf's archetype id, parameter map and context are handed to it. If none is present, a deterministic fallback path handles the common sensor and utility archetypes directly from context data, so headless tests of authored trees are possible. AiDefinitionActionExecutor is the Unity implementation: it holds references to the locomotion, combat, animation, audio and coordination bridges on the same GameObject and turns each archetype id into concrete work against them.

A definition is therefore portable. The same BehaviorTreeDefinition asset drives a fully rigged agent with a NavMeshAgent and a weapon holder, and also drives a bare test fixture where only the fallback executor exists, without either side knowing about the other.

Blackboard, context, and reserved keys

IAiBlackboard is a typed string-keyed store with Set, TryGet, Remove, Contains, Clear and a Keys collection. IAiContext wraps it with the entity id, the current behaviour id, the frame delta, and GetSensorData for reading the tick's sensor samples by AiSensorType. Nodes only ever see the context, which is the seam that keeps them independent of the agent implementation.

AiAgent uses two blackboards at once and this catches people out. AgentBlackboard is a strongly typed record of the agent's own runtime values, MoveSpeed, AttackRange, DistanceToPlayer and so on, and AgentBlackboardAdapter presents it through the generic IAiBlackboard contract so behaviour tree nodes can read it. The Core type ZOA.AI.Core.AiBlackboard is the general dictionary implementation, a different type with a different name.

AiRuntimeBlackboardKeys names every key the runtime writes. Three are internal plumbing prefixed with a double underscore: ActionExecutor, Agent and SelfTransform. The rest are the contract with authored trees. targetTransform, targetPosition, health, healthPercent, ammoCount, alert, alertPosition, lastHeardPosition, investigationPoint, coverPosition, faction, and the four tactical keys tactical.vantagePoint, tactical.losBreakPoint, tactical.coverReservation and tactical.coverAnchorPosition. AiAgent.Tick refreshes the health, distance and target keys before evaluating the tree, so a Check Blackboard node always reads current values.

Perception: two paths, one blackboard contract

AiPerceptionSensor is the per-agent path. It samples vision with a field-of-view cone and an optional occlusion linecast, retains a last-seen position for VisionMemorySeconds after losing sight, and reads sound stimuli off AiSoundStimulusBus against a hearing range and intensity threshold. It writes canSeeTarget, canHearSound and lastSeenPosition, and pushes AiSensorData entries into the context for the tick.

IAiPerceptionBatchService is the scale path. Agents register once with an AiPerceptionConfig snapshot, which pre-computes the cosine of the FOV half-angle so the per-tick prefilter avoids a trigonometric call, and the service owns all line-of-sight raycasts through RaycastCommand.ScheduleBatch on pre-allocated NativeArrays. It completes the previous frame's JobHandle at the start of the next Update, distributes results, then schedules the following batch, which trades one frame of perception latency for a main thread that never blocks on physics. The tick interval is adaptive: roughly 20 Hz below ten agents, 10 Hz below fifty, 5 Hz beyond that.

The two paths write the same keys. AiAgent.Initialize calls TryRegisterBatchPerception on the sensor; if a batch service is installed the sensor short-circuits its own Sample and the service owns perception for that agent from then on. If no service is registered nothing changes and the per-agent path keeps running. Authored trees do not know which one is active.

IPerceptionService is a third and separate thing. It is the cross-package memory of who saw what and when, a per observer-target pair cache of last-known position plus a realtime timestamp, plus stateless CanSee and CanHear helpers. Mission triggers and squad logic read it without holding a reference to the sensor that produced the sighting, and callers decide for themselves whether a stale timestamp still counts.

Tactical services and role tuning

Three interfaces cover the tactical layer and all three are resolved through FoundryServiceRegistry by their own scene installers, so each is independently optional. ICoverProvider hands out reservations against CoverPointAnchor components. TryRequestCover takes the agent's position, the threat position and the agent GameObject, and returns a CoverReservation carrying the anchor, a slot index within the anchor's capacity, the owner, the issue time and a generation counter that lets the provider reject a stale release after the slot has been re-issued. An anchor only qualifies when the threat falls inside its ArcDegrees half-arc, so the wall ends up between the agent and the shooter rather than beside it.

ITacticalEvaluator scores candidate positions. ScoreVantage returns a VantageScore with four normalised components, HeightAdvantage, LoSClarity, Concealment and Reachability, plus the weighted Total. FindBestVantage walks the HighGroundMarker components in radius and returns the highest scorer, and FindNearestLosBreak finds the closest position that breaks line of sight from a threat, which is what a wounded or suppressed agent uses to disengage.

ISuppressionTracker accumulates near-miss pressure per agent and decays it over time. Crossing the registered threshold flips IsSuppressed and raises SuppressionChanged; every near miss raises NearMissRecorded for HUD and audio consumers that want the raw signal.

TacticalConfig is the knob that ties the three together, and it is serialised inside AiAgentDefinition. It carries a TacticalRole, a PreferHighGround flag, RepositionUnderFireSeconds, RetreatHealthThreshold, SuppressionThreshold and a StealthBias in zero to one. ResolveVantageWeights picks a VantageWeights basis from the role, Sniper, Scout or the default mix, then linearly nudges weight from LoS toward Concealment in proportion to StealthBias. Five static presets exist, DefaultAssault, DefaultSniper, DefaultSupport, DefaultScout and DefaultBoss, and the Boss preset is suppression immune with an infinite reposition time so a boss holds its ground to death.

Ticking a crowd

AiAgent ticks itself by default. Update accumulates real time and runs Tick at a fixed TickInterval, capped at four catch-up iterations per frame so a hitch cannot cascade into a spiral. Awake seeds the accumulators with a random offset up to TickPhaseJitter so a wave of agents spawned on the same frame does not evaluate their trees in lockstep. Sensor sampling runs on its own separate SensorTickInterval, typically slower than the behaviour tick.

AiAgentBatchScheduler is the alternative for large crowds. It discovers agents in the scene, disables their auto tick, and time-slices the roster so at most MaxAgentsPerSlice agents evaluate per step. The agents themselves are unchanged; the scheduler is simply the thing calling Tick.

AiAgent works without an authored tree. When Initialize receives a null BehaviorTreeDefinition, the agent runs a built-in chase and attack loop routed through the NavMeshAgent, so an agent dropped into a scene does something observable before any content exists. LastBehaviorState reports Success in that case, as a neutral marker, so the debug HUD does not flag every fallback agent as broken.

In the editor

Screens

Screenshot pending

/screenshots/ai-behavior-tree-editor.png

The GraphView window showing a loaded BehaviorTreeDefinition: root selector fanning into engage, investigate and patrol branches, with the categorised toolbox panel on the left and the New / Load / Save / Export / Templates toolbar along the top.

The Behavior Tree Editor with an authored graph.

Screenshot pending

/screenshots/ai-workbench-module.png

The workbench with the AI capability strip visible, one of the definition browsers open showing a list of AiAgentDefinition assets with the inspector pane on the right.

The AI module inside the ZOA Workbench.

Screenshot pending

/screenshots/ai-bt-debug-hud.png

Play mode with the F4 HUD open, showing the scene roster with per-agent BT health badges and a selected agent's tree rendered as an indented outline with live node states.

The F4 behaviour tree debug overlay in play mode.

Setup

Workflow

  1. 01

    Author a tree

    Open the Behavior Tree Editor from Tools, ZOA, Advanced, Define, Characters, AI, Behavior Tree Editor, or from the BT Editor capability inside the AI Workbench module. Start from the Templates button rather than an empty graph: BtTemplateLibrary ships ten fully wired archetypes including Enemy Grunt, Elite Soldier, Sniper, Boss, Swarm Drone, Harvester, Guard, Patrol Scout, Medic and Berserker. Drag nodes from the toolbox, wire ports, set parameters, and give the asset a TreeId you will resolve by later.

  2. 02

    Author the archetype

    Create an AiAgentDefinition and set AgentTypeId to a stable lowercase slug such as grunt or swarm.drone. Tune health, speed, ranges and cooldowns, then set the two performance intervals: TickInterval governs how often the tree evaluates and SensorTickInterval how often perception samples, and the sensor interval is normally the slower of the two. Pick a TacticalRole, which decides vantage weighting, suppression immunity and retreat threshold in one move.

  3. 03

    Build the prefab

    Run the AI Entity Wizard or Build AI Prefab from Tools, ZOA, Advanced, Build, Characters, AI. Both produce a runtime-wired prefab carrying AiAgent, the perception sensor, the action executor, and the locomotion, combat, animation and audio bridges, plus an AiAgentRuntimeBootstrap that calls Initialize on Start. Build AI Prefab additionally scans the chosen model's folder neighbourhood and offers to bind the animator controller, avatar and sound profile it finds.

  4. 04

    Install the scene services you actually want

    Each service has its own installer component and each is optional. ZOAAiPerceptionBatchSceneInstaller plus an AiPerceptionTickPump for batched perception, ZOAPerceptionSceneInstaller for sighting memory, ZOACoverSceneInstaller, ZOATacticalSceneInstaller and ZOASuppressionSceneInstaller for the tactical trio, and ZOAAiSpawnSceneInstaller for spawning. Installers are idempotent and honour a pre-existing registration, and they unregister on destroy only when their own instance is still the registered one.

  5. 05

    Place spawn zones

    Add an AiSpawnZone to a scene object, assign an AiSpawnerDefinition, and pick a sphere or box volume. The spawn installer auto-discovers zones already in the scene and also hooks sceneLoaded, so zones in an additively loaded gameplay scene register after the persistent scene has finished waking. Leave autoActivateOnAwake off for waves an external trigger should start.

  6. 06

    Scatter tactical geometry

    Drop CoverPointAnchor components where cover should exist, setting the facing to the direction threats come from, the height to Low or Full, the capacity if a long wall should fit more than one agent, and the arc beyond which a flanking threat defeats the cover. Add HighGroundMarker components at vantage candidates with a prominence and a plateau radius, since candidates with too small a plateau are filtered out.

  7. 07

    Tune with the HUDs

    Press F3 for the AI scene HUD, which lists every agent with archetype, health and alive state and drills into blackboard, perception and position. Press F4 for the behaviour tree HUD, which shows a per-agent health badge, HasTree running, HasTree stuck on failure, NoTree on fallback, or dead, and renders the selected agent's live tree as an indented outline.

Surface

Key types

IBehaviorNode

interface

The entire runtime contract for a behaviour tree node: three members, so the tree kernel stays engine-free and testable in a plain fixture.

  • BehaviorId Id { get; }
  • BehaviorState Evaluate(IAiContext context)
  • void Reset()

IBehaviorNodeChildren

interface

Opt-in topology accessor implemented by composites and decorators. Inspection surfaces walk tree shape through this rather than reflecting into private fields; a node that does not implement it is a leaf.

  • IReadOnlyList<IBehaviorNode> Children { get; }

IAiContext

interface

Everything a node is allowed to see during evaluation: identity, blackboard, current behaviour, frame delta, and this tick's sensor samples.

  • string EntityId { get; }
  • IAiBlackboard Blackboard { get; }
  • BehaviorId CurrentBehavior { get; }
  • float DeltaTime { get; }
  • IReadOnlyList<AiSensorData> GetSensorData(AiSensorType sensorType)

IAiBlackboard

interface

Typed string-keyed state store shared between nodes for the life of an agent.

  • void Set<T>(string key, T value)
  • bool TryGet<T>(string key, out T value)
  • bool Remove(string key)
  • bool Contains(string key)
  • IReadOnlyCollection<string> Keys { get; }

IAiService

service

Engine-free registry of agents and their trees. Use it when you want to own tick scheduling yourself rather than letting each AiAgent tick itself.

  • void RegisterAgent(string entityId, IBehaviorNode behaviorTree)
  • void UnregisterAgent(string entityId)
  • void TickAgent(string entityId, float deltaTime)
  • void TickAll(float deltaTime)
  • BehaviorState GetAgentState(string entityId)
  • event AgentStateChangedEventHandler AgentStateChanged
  • event BehaviorCompletedEventHandler BehaviorCompleted

BehaviorState

enum

Idle, Running, Success, Failure, Suspended. Composites branch on these; Running keeps a multi-tick action alive across frames.

BehaviorTreeResolver

class

Compiles a BehaviorTreeDefinition asset into a live IBehaviorNode hierarchy. Composites and decorators become real nodes; leaves become DefinitionActionLeaf carrying their archetype id forward.

  • static IBehaviorNode Resolve(BehaviorTreeDefinition definition)

BehaviorNodeArchetypeRegistry

class

Static catalogue of every placeable node kind across ten categories. The graph editor builds its toolbox from this and the resolver reads port config from it.

  • static IReadOnlyList<BehaviorNodeArchetype> All { get; }
  • static IReadOnlyDictionary<BehaviorNodeCategory, IReadOnlyList<BehaviorNodeArchetype>> ByCategory { get; }
  • static BehaviorNodeArchetype Get(string id)

BehaviorTreeRegistry

class

Resolves BehaviorTreeDefinition assets by their authored TreeId, so serialised level data can say 'spawn archetype X with tree patroller' without holding an asset reference. Case and whitespace insensitive; duplicates keep the first and log an error.

  • static BehaviorTreeRegistry BuildFromCollection(IEnumerable<BehaviorTreeDefinition> trees)
  • bool TryGet(string treeId, out BehaviorTreeDefinition tree)
  • IReadOnlyList<BehaviorTreeDefinition> All { get; }

AiAgent

component

The runtime agent. Owns health, the blackboard and context, the compiled tree, tick scheduling with phase jitter, damage intake through IDamageReceiver, and corpse configuration on death. Implements IAiSaveable.

  • void Initialize(AiAgentConfig config, BehaviorTreeDefinition treeDef, Transform playerTarget, IFoundryEventBus eventBus = null)
  • void Tick(float deltaTime)
  • void SetTarget(Transform target)
  • void SetTacticalConfig(TacticalConfig tactical)
  • void SetFrozen(bool frozen)
  • void ReceiveAlert(Vector3 alertPosition)
  • float Receive(in DamageContext ctx)
  • IBehaviorNode BehaviorRoot { get; }
  • IAiContext Context { get; }
  • BehaviorState LastBehaviorState { get; }
  • event Action<AiAgent> OnDeath

IAiDefinitionActionExecutor

interface

The seam between data-only leaf nodes and concrete gameplay. One method takes an archetype id and a parameter map and returns the tick's BehaviorState.

  • BehaviorState Execute(string nodeId, string archetypeId, IReadOnlyDictionary<string, string> parameters, IAiContext context)

AiDefinitionActionExecutor

component

The Unity implementation. Holds the locomotion, combat, animation, audio and coordination bridges on the agent and dispatches each archetype id to concrete work against them.

  • void Bind(AiAgent agent)
  • void ApplyConfig(AiAgentConfig config)
  • void ResolveReferences()

AiRuntimeBlackboardKeys

class

The reserved key names the runtime writes and authored trees read. Reach for these constants rather than typing the strings, especially the tactical set.

  • TargetTransform, TargetPosition, Health, HealthPercent, AmmoCount
  • Alert, AlertPosition, LastHeardPosition, InvestigationPoint, CoverPosition, Faction
  • VantagePoint, LosBreakPoint, CoverReservation, CoverAnchorPosition

AiPerceptionSensor

component

Per-agent vision, hearing and proximity sampling with a memory window. Short-circuits itself once the batched perception service takes ownership of the agent.

  • void Sample(Transform self, Transform target, IAiContext context)
  • void ApplyConfig(AiAgentConfig config)
  • void TryRegisterBatchPerception(AiAgent agent)

IAiPerceptionBatchService

service

Scene-wide batched perception. Owns all line-of-sight raycasts through RaycastCommand.ScheduleBatch on pre-allocated buffers, with an adaptive tick rate that falls off as the agent count rises.

  • void RegisterAgent(AiAgent agent, AiPerceptionConfig config)
  • void UnregisterAgent(AiAgent agent)
  • bool IsRegistered(AiAgent agent)
  • void Tick(float deltaTime)
  • int RegisteredAgentCount { get; }
  • int LastBatchSize { get; }
  • float CurrentTickInterval { get; }

IPerceptionService

service

Cross-package sighting memory plus stateless cone and audibility checks. Lets mission triggers and squad logic ask what an observer last knew without referencing the sensor that produced it.

  • bool CanSee(Transform observer, Transform target, float fovDeg, float range, LayerMask losMask)
  • bool CanHear(Vector3 observer, Vector3 source, float radius, float minVolume)
  • void RecordSighting(GameObject observer, GameObject target, Vector3 at)
  • bool TryGetLastKnown(GameObject observer, GameObject target, out Vector3 position, out float realtimeSeconds)
  • void ClearObserver(GameObject observer)

IAiSpawnService

service

Zone-driven spawn coordination. Tick-driven so tests can advance simulated time deterministically; the AiSpawnTickPump drives it in production.

  • void RegisterZone(AiSpawnZone zone)
  • void ActivateZone(AiSpawnZone zone)
  • void DeactivateZone(AiSpawnZone zone)
  • void Tick(float deltaTime)
  • void DespawnAll()
  • IReadOnlyList<AiAgent> ActiveAgents { get; }
  • event Action<AiAgent, AiSpawnZone> AgentSpawned
  • event Action<AiAgent, AiSpawnZone> AgentKilled

ICoverProvider

service

Reservation-aware cover resolution against CoverPointAnchor components. Called from behaviour tree leaves at up to about 30 Hz across every live agent, so implementations are expected to use a spatial grid rather than a linear scan.

  • bool TryRequestCover(Vector3 from, Vector3 awayFromThreat, GameObject agent, out CoverReservation reservation)
  • void ReleaseCover(CoverReservation reservation)
  • IReadOnlyList<CoverPointAnchor> AllInRadius(Vector3 from, float radius)
  • void Tick(float deltaTime)

ITacticalEvaluator

service

Scores standing positions against a target using per-role VantageWeights, and finds the nearest position that breaks line of sight for disengaging.

  • VantageScore ScoreVantage(Vector3 candidate, Vector3 target, GameObject fromAgent, in VantageWeights weights, LayerMask losBlockingMask)
  • Vector3? FindBestVantage(GameObject agent, Vector3 target, float searchRadius, in VantageWeights weights, LayerMask losBlockingMask)
  • Vector3? FindNearestLosBreak(GameObject agent, Vector3 fromTarget, float searchRadius, LayerMask losBlockingMask)

ISuppressionTracker

service

Per-agent near-miss pressure with decay. Crossing the registered threshold flips the suppressed flag and raises an event; immune agents still accumulate pressure but never read as suppressed.

  • void RegisterAgent(GameObject agent, float threshold, bool suppressionImmune)
  • void RecordNearMiss(GameObject agent, Vector3 origin, float severity)
  • float GetPressure(GameObject agent)
  • bool IsSuppressed(GameObject agent)
  • event Action<SuppressionEvent> SuppressionChanged

TacticalConfig

struct

Per-archetype role tuning serialised inside AiAgentDefinition. Drives vantage weighting, reposition timing, retreat threshold and suppression immunity from one block.

  • TacticalRole Role; bool PreferHighGround; float RepositionUnderFireSeconds
  • float RetreatHealthThreshold; float SuppressionThreshold; float StealthBias
  • bool IsSuppressionImmune { get; }
  • VantageWeights ResolveVantageWeights()
  • static TacticalConfig DefaultAssault / DefaultSniper / DefaultSupport / DefaultScout / DefaultBoss

AiLocomotionController

component

Movement bridge for the movement archetypes. Prefers NavMeshAgent and falls back to transform movement, and keeps the per-leaf state that makes dynamic patrol and circle-strafe read as continuous motion rather than teleporting between random offsets.

  • Vector3 CurrentDestination { get; }
  • float CurrentSpeed { get; }

AiCombatController

component

Combat bridge with a distance-based accuracy model: full base accuracy inside the falloff start, linear decay to a floor at the falloff end, plus a muzzle-height line-of-sight check so partial cover blocks shots. Owns ammo and reload timing.

AiWeaponHolder

component

Holds one equipped weapon and exposes range, damage per shot, fire rate and ammo so combat behaviour follows the weapon rather than the agent config. The definition is typed as UnityEngine.Object so the AI package takes no dependency on the weapon package.

AiAnimationBridge

component

Animator bridge with state-edge gating. Every mutator caches its last applied value and skips no-op writes, and triggers carry a cooldown, so a leaf that returns Running for many ticks does not restart the clip from frame zero each time.

AiAudioBinder

component

Plays audio events through AudioEventPlayer and emits a matching stimulus onto AiSoundStimulusBus on every call, so anything an agent does that makes noise is automatically audible to other agents' hearing sensors.

AiFacingController

component

Per-frame idle facing toward the perceived target while the agent is alert and not actively pathing. It sits outside the behaviour tree, because where an agent looks while standing still is not a decision the tree needs to make.

AiSoundStimulusBus

class

Static, short-lived sound channel read by hearing sensors. Stimuli carry position, intensity, radius, expiry and an optional source entity id, and expired entries are pruned in place.

  • static void Emit(Vector3 worldPosition, float intensity, float radius, float lifeSeconds = 0.75f, string sourceEntityId = "", string soundId = "")

AiAgentBatchScheduler

component

Scene-level time slicer for crowds. Disables per-agent auto tick and evaluates at most MaxAgentsPerSlice agents per step, rescanning the scene on an interval.

  • void RefreshAgents()

BehaviorNodeTracer

class

Opt-in per-node last-state and evaluation-count recorder. Near-zero cost when disabled; the debug HUD enables it while open so it can render tree state without re-evaluating nodes.

  • static bool Enabled
  • static void Record(IBehaviorNode node, BehaviorState state)
  • static void Clear()

AiRagdoll

component

Builds AiHitboxLimb colliders from the humanoid rig for region-targeted damage, and on death disables the animator and raycast-settles the corpse onto the terrain so the death pose lies flush with the ground.

AgentKilledEvent

struct

The canonical kill signal on the event bus, alongside AgentSpawnedEvent and AgentDamagedEvent. Carries the archetype id, an opaque instance discriminator, the position, and optionally the killer and victim GameObjects. Killer is null for unattributed deaths such as environmental damage.

IAiSaveable

interface

Save and restore contract implemented by AiAgent. Captures identity, pose, health and liveness into an AiAgentSaveState; behaviour tree execution state is not captured, and the tree restarts from the root on restore.

  • string UniqueId { get; }
  • AiAgentSaveState CaptureState()
  • void RestoreState(AiAgentSaveState state)

Surface

Authoring assets

BehaviorTreeDefinition

asset

The authored tree. A TreeId, a description, a root node id, and a flat list of BehaviorNodeData holding archetype id, parameters, child ids and graph position. Created via Assets, Create, ZOA, AI, Behavior Tree Definition.

  • string TreeId
  • string RootNodeId
  • List<BehaviorNodeData> Nodes
  • BehaviorNodeData FindNode(string nodeId)

AiAgentDefinition

asset

The archetype. Combat and locomotion stats, tick and sensor intervals, perception ranges, an optional model prefab with animator controller and avatar, the TacticalConfig block, and the death and corpse-loot policy. ToRuntime produces a mutable AiAgentConfig so the asset is never mutated at play time. Created via Assets, Create, ZOA, AI, Agent Definition.

  • string AgentTypeId; string BehaviorTreeId
  • float MaxHealth, MoveSpeed, AttackRange, DetectionRange, AttackDamage, AttackCooldown
  • float TickInterval, SensorTickInterval, VisionRange, VisionFovDegrees, HearingRange, HearingThreshold, WanderRadius
  • TacticalConfig Tactical; AiDeathBehaviorMode DeathBehaviorMode; bool AutoBuildRagdollOnDeath
  • InventoryDefinition CorpseInventoryDefinition; int CorpseLootRolls; IReadOnlyList<SerializedCorpseLootEntry> CorpseLootPool
  • AiAgentConfig ToRuntime()

AiSpawnerDefinition

asset

One spawn rule. A weighted roster of archetype plus tree plus optional prefab, a cadence of initial count, per-tick count and interval, a live cap and a lifetime total, a faction tag stamped on each spawned agent, and an optional respawn delay once every agent from the budget is dead. Created via Assets, Create, Tools, ZOA, AI, Spawner.

  • string SpawnerId; IReadOnlyList<RosterEntry> Roster
  • int SpawnsPerTick, InitialSpawnCount, MaxAlive, TotalToSpawn; float SpawnInterval
  • bool AutoActivate; string FactionTag; float RespawnAfterAllDeadSeconds

Usage

Examples

Building a tree in codecsharp
using ZOA.AI.Core;

var builder = new BehaviorTreeBuilder();

var findTarget = builder.Action(new ActionId("FindTarget"), ctx =>
    ctx.GetSensorData(AiSensorType.Vision).Count > 0
        ? BehaviorState.Success
        : BehaviorState.Failure);

var closeDistance = builder.Action(new ActionId("MoveToTarget"), ctx =>
{
    // Return Running until the agent is inside attack range; the tree
    // re-enters this node from the root on every tick until it succeeds.
    return ctx.Blackboard.TryGet<float>("distanceToPlayer", out var d) && d <= 8f
        ? BehaviorState.Success
        : BehaviorState.Running;
});

var attack = builder.Action(new ActionId("Attack"), ctx => BehaviorState.Success);

IBehaviorNode root = builder
    .Sequence(findTarget, closeDistance, attack)
    .Build();

var service = new AiService();
service.RegisterAgent("grunt.01", root);
service.TickAll(Time.deltaTime);
The fluent builder is the code path. Most content goes through BehaviorTreeDefinition instead; reach for the builder in tests and for trees that are computed rather than authored.
Spawning an agent from definitionscsharp
using UnityEngine;
using ZOA.AI.Unity.Agents;
using ZOA.AI.Unity.Definitions;
using ZOA.Messaging;

public sealed class ManualAgentSpawner : MonoBehaviour
{
    [SerializeField] private AiAgentDefinition _archetype;
    [SerializeField] private BehaviorTreeDefinition _tree;
    [SerializeField] private GameObject _agentPrefab;
    [SerializeField] private Transform _target;

    public AiAgent Spawn(Vector3 position)
    {
        var go = Instantiate(_agentPrefab, position, Quaternion.identity);
        var agent = go.GetComponent<AiAgent>();

        // ToRuntime hands back a mutable copy, so the asset is never
        // touched at play time and per-spawn overrides are safe.
        var config = _archetype.ToRuntime();

        FoundryServiceRegistry.TryResolve<IFoundryEventBus>(out var bus);
        agent.Initialize(config, _tree, _target, bus);
        agent.SetTacticalConfig(_archetype.Tactical);
        return agent;
    }
}
SetTacticalConfig is applied after Initialize. AiSpawnService does the same thing, using the roster entry's archetype.
Resolving a tree by id at spawn timecsharp
using System.Collections.Generic;
using ZOA.AI.Unity.Definitions;

// Built once per session from whatever collection of trees the
// project ships, then queried by the authored TreeId.
var registry = BehaviorTreeRegistry.BuildFromCollection(allTrees);

if (registry.TryGet(archetype.BehaviorTreeId, out var tree))
{
    agent.Initialize(archetype.ToRuntime(), tree, target, bus);
}
else
{
    // No tree resolved: AiAgent runs its built-in chase-and-attack
    // fallback rather than standing inert.
    agent.Initialize(archetype.ToRuntime(), null, target, bus);
}
Serialised level data records a tree id, not an asset reference. Lookup ignores case and surrounding whitespace.
Reacting to agent lifecycle without referencing the AI packagecsharp
using System;
using UnityEngine;
using ZOA.AI.Unity.Agents;
using ZOA.Messaging;

public sealed class KillCounter : MonoBehaviour
{
    private IDisposable _sub;

    private void OnEnable()
    {
        var bus = FoundryServiceRegistry.Get<IFoundryEventBus>();
        _sub = bus.Subscribe<AgentKilledEvent>(OnAgentKilled);
    }

    private void OnDisable() => _sub?.Dispose();

    private void OnAgentKilled(AgentKilledEvent evt)
    {
        // Killer may be null for environmental or unattributed deaths.
        var attribution = evt.Killer != null ? evt.Killer.name : "world";
        Debug.Log(evt.AgentTypeId + " killed by " + attribution);
    }
}
Requesting cover from a custom leafcsharp
using UnityEngine;
using ZOA.AI.Core;
using ZOA.AI.Unity.Execution;
using ZOA.AI.Unity.Tactical;
using ZOA.Messaging;

// The tactical services are optional. Resolve defensively so a scene
// without the installers still runs, it just never takes cover.
if (FoundryServiceRegistry.TryResolve<ICoverProvider>(out var cover) &&
    cover.TryRequestCover(self.position, threat.position, gameObject, out var reservation))
{
    context.Blackboard.Set(AiRuntimeBlackboardKeys.CoverReservation, reservation);
    context.Blackboard.Set(
        AiRuntimeBlackboardKeys.CoverAnchorPosition,
        reservation.Anchor.Position);
}

// Later, when the agent leaves cover, dies, or repositions. The
// generation counter on the handle makes releasing a stale
// reservation a no-op rather than freeing someone else's slot.
cover.ReleaseCover(reservation);
Making noise the AI can hearcsharp
using UnityEngine;
using ZOA.AI.Unity.Execution;

// Anything that should be audible to hearing sensors emits a
// stimulus. AiAudioBinder does this automatically for every audio
// event an agent plays; gameplay code that makes noise without
// going through the binder emits directly.
AiSoundStimulusBus.Emit(
    worldPosition: transform.position,
    intensity: 0.9f,
    radius: 30f,
    lifeSeconds: 0.75f,
    sourceEntityId: "player",
    soundId: "gunshot");
Stimuli are short-lived. A sensor sampling at 10 Hz with a 0.75 second lifetime will not miss a shot, and expired entries are pruned in place on the next emit.

Tooling

Editor tools

Behavior Tree Editor

Tools / ZOA / Advanced / Define / Characters / AI / Behavior Tree Editor

GraphView-based visual editor for BehaviorTreeDefinition assets. Toolbox is built from BehaviorNodeArchetypeRegistry, with New, Load, Save, Export and Templates on the toolbar, auto-layout, and a search-window node picker. Routes into the AI Workbench module by default; OpenStandaloneWindow opens it detached.

Behavior tree template library

Templates button inside the Behavior Tree Editor

Ten prebaked archetypes that produce fully wired graphs rather than a blank canvas: Enemy Grunt, Elite Soldier, Sniper, Boss, Swarm Drone, Harvester, Guard, Patrol Scout, Medic and Berserker.

AI Entity Wizard

Tools / ZOA / Advanced / Build / Characters / AI / AI Entity Wizard

End-to-end wizard producing a behaviour tree from a template, an AiAgentDefinition, a runtime-wired prefab with sensors, executor and bridges, and optionally a scene-level AiAgentBatchScheduler for crowd-scale ticking.

Build AI Prefab

Tools / ZOA / Advanced / Build / Characters / AI / Build AI Prefab

Step-based wizard that adds model neighbourhood scanning to the same pipeline. Pick a model FBX and it scans adjacent folders for an animator controller, avatar and sound profile, and offers to bind what it finds so the generated prefab is wired up from the start.

Spawner Wizard

Tools / ZOA / Advanced / Define / Characters / AI / Spawner Wizard

Four-step wizard for AiSpawnerDefinition assets: identity, roster of archetype plus tree plus prefab, cadence and caps, then a review panel before saving.

AI Workbench module

ZOA Platform Workbench, AI module

Capability strip gathering Create AI, Build AI Prefab, Spawners, Agent Definitions, BT Editor, Behavior Trees, Agents and Overview into one surface. The definition browsers edit, validate, duplicate and delete assets in place.

Bundled behaviour tree installer

Tools / ZOA / Advanced / Build / Characters / AI / Bundled

Ensure Behavior Trees creates any missing bundled tree assets; Regenerate Behavior Trees rebuilds them from source. A sibling entry regenerates the bundled AI locomotion animator controller.

IAiAuthoringService

The headless authoring surface the wizards sit on. BuildAiAgentPrefab, BuildBehaviorTreeAsset and BuildAiEntity let generators, automation scripts and tests produce the same assets without opening an interactive window.

Read this

Notes and caveats

See also