ZOA Entity Rig
One archetype composes what an entity looks like, moves like, sounds like, and how it transitions.
Entity Rig is the composition layer. Players, AI characters and animated interactables are all built the same way: an EntityArchetypeDefinition names five composable profile assets, EntityRigBuilder assembles a rig from them, and EntityRigController drives it every frame from whatever the entity's state provider reports. The only thing that distinguishes a player from a door is the EntityKind field and the state provider you hand it.
The five parts are a visual profile, an animation profile, a sound profile, a state graph, and a rig policy. All five are optional. An archetype with nothing but a visual profile still produces a living rig, because the engine ships defaults for the rest: an empty clip table, a default state graph, a default rig policy with procedural breathing, and silence. An author can bind one part at a time and always have something running.
The package is thin. Almost all the intelligence lives in com.zoa.animation, whose StateTagSet algebra and profile resolvers do the work of deciding which clip and which sound win. What Entity Rig adds is the binding: the object that holds all five references, the builder that instantiates and wires them, and the controller that samples state, resolves, and crossfades under the state graph's timing rules.
Depended on by (1)
How it works
Concepts
How an archetype binds the five profiles
EntityArchetypeDefinition holds five references and one string, and each slot answers a different question about the entity. The EntityRigVisualProfileDefinition answers what it looks like: the base skinned-mesh prefab, semantic material slots, bone sockets, LOD levels and optional overlays. The AnimationProfileDefinition answers how it moves: the tagged-clip table the resolver matches the active state against. The SoundProfileDefinition answers how it sounds: the semantic event table covering footsteps, vocals, foley and weapons. The EntityStateGraphDefinition answers how transitions are timed. The RigPolicyDefinition answers which IK constraints get generated and how the procedural fallback feels.
Because they are separate assets, they are separately shareable. A soldier, a guard and a civilian can all reference the same human sound profile and the same rig policy while differing only in visual and animation profile. Embedded data would make that sharing impossible, so the archetype holds references instead.
The EntityKind field, Player, AiCharacter, Interactable, Ambient or the Unspecified sentinel that validators flag, tells the builder which wiring template to use. DefaultLocomotionTag is a spawn-time baseline, useful for an AI archetype that should start crouched rather than standing.
What the builder actually does
EntityRigBuilder.Build is static and stateless. It takes an existing root GameObject, an archetype, a required IEntityStateProvider and an optional IEntitySoundEmitter, mutates the root, and returns an EntityRigBuildResult carrying handles to the root, controller, visual root, animator and audio player.
The sequence is five steps. It instantiates the visual profile's base prefab as a child named Visual, reusing an existing child of that name rather than duplicating it so the builder can be run repeatedly during authoring iteration. It finds the Animator in that sub-tree. It adds an EntityRigController if the root does not already have one. It attaches an AudioEventPlayer, assigns the archetype's sound profile to it if the player has none, and wraps it in an adapter implementing IEntitySoundEmitter, unless the caller supplied a custom emitter. Then it calls Rebind on the controller, which re-flattens the resolution caches.
An archetype with no visual profile or no base prefab simply gets no visual sub-tree, which is the correct outcome for an ambient sound source. The builder is the single entry point for rig composition, so new capabilities such as cloth or footprint effects extend the builder instead of being wired at each call site.
The controller's frame
Every frame the controller asks its IEntityStateProvider for a StateTagSet and compares it with last frame's. If nothing changed it does nothing, except on the very first frame where it forces a resolve so an entity whose initial state happens to be the empty set still picks a clip.
When the state has changed, the controller checks the hold window before honouring the transition. If a hold is still counting down, it finds the first dimension that differs between the old and new state, looks up that dimension's TransitionPolicy, and only allows the transition through if that policy's trigger is OnEvent. Reactions interrupt under that rule: hit and stagger sit on the Reaction dimension, whose default trigger is OnEvent with no hold, so a flinch lands immediately while a locomotion change during a posture hold waits its turn.
Resolving calls AnimationProfileResolver.ResolveBestMatch against the flattened entry list. On a hit, the controller derives a state hash from the winning clip's name and calls CrossFadeInFixedTime with the policy's crossfade duration, then arms the hold window with the policy's minimum hold. On a miss it clears the hold so the next state change is honoured immediately.
The hot path allocates nothing. The flattened entry lists for both animation and sound are built once at bind time, resolution returns an int index, and the head bone used by the procedural bob is located once during Bind rather than searched per frame.
Procedural fallback
When resolution produces no match at all, and the archetype's rig policy allows it, the controller drives the rig procedurally instead of freezing. Today that is a breath: a sine on the head bone's local Y at the policy's BreathFrequency and BreathAmplitude, accumulating phase against delta time.
Treat it as the floor of the system rather than a feature you author toward. An archetype with an empty animation profile falls straight through to it, so a rig with only a visual profile bound still looks alive. Set AllowProceduralFallback to false on the rig policy when the entity should hold its last pose instead, which is usually right for a door or a vehicle.
Sockets and where things mount
A SocketBinding pairs a semantic SocketId with a transform path inside the visual prefab, plus optional position and Euler rotation offsets for when a bone's pivot does not sit where the socket should. The runtime resolves the path once at bind time and caches the transform.
SocketId is a short canonical enum: Head, Neck, ChestUpper and ChestLower, SpineAttachUpper and SpineAttachLower, WaistRight and WaistLeft, HipBack, the two shoulders, the two hands and the two feet, EyesAnchor, MouthAnchor, WeaponPrimary and WeaponSecondary, plus Custom for anything bespoke. The list stays short because equipment and attachment systems mount against these ids, and a sprawling enum makes that contract useless.
Sockets are seeded at FBX import time by name heuristics, so a bone called hand_r seeds RightHand, and every one of them is individually overridable by the author afterwards. The same is true of material slots and LOD levels: most of the visual profile is populated by the importer and the author's job is usually confirming rather than typing.
Material slots stay pipeline neutral
A MaterialSlot names the renderer path, the slot index on that renderer, and a semantic material id. It never references a URP or HDRP material asset directly. The id resolves through the material resolver chain at runtime, so the same archetype ships against either pipeline.
VisualOverlay works the same way for add-on geometry. An overlay names an optional prefab, an optional mount socket, and a set of free-form activation tags; the runtime activates it when at least one of its tags is asserted on the entity, so an armour piece or a helmet appears without the archetype needing a variant.
In the editor
Screens
Screenshot pending
/screenshots/entityrig-archetype-wizard.png
The wizard showing the five profile object fields, visual, animation, sound, state graph and rig policy, with some slots filled and some left empty, and the four-step rail visible on the left.
Screenshot pending
/screenshots/entityrig-visual-profile.png
The inspector for an EntityRigVisualProfileDefinition showing the base prefab, a list of material slots, and the sockets list with several SocketId entries bound to bone transform paths.
Setup
Workflow
- 01
Author the sub-profiles first
An archetype is only a set of references, so there is nothing useful to compose until at least one profile exists. Create the visual profile by importing the model, which seeds the base prefab, material slots and sockets by name heuristic. Author the animation and sound profiles in the Animation package, and reuse a shared state graph and rig policy unless the archetype needs its own.
- 02
Compose the archetype
Run Build a Character from Tools, ZOA, Advanced, Build, Characters, or open the Entity Rig module in the Workbench. The wizard has four steps: Identity names the archetype, picks the EntityKind and chooses an output folder; Profiles assigns the five slots and leaves any of them empty to fall back to engine defaults; Validate runs the validator and offers quick fixes including a warning when the resolved asset path collides with an existing asset; Apply writes the asset and surfaces it in the Project view.
- 03
Implement a state provider
This is the one piece a consumer has to write, and it should be two or three field reads. Return a StateTagSet composed from whatever your entity already knows. Keep it allocation-free, because it is called once per rig update. For a quick test or a trivial entity, the builder's delegate overload wraps a lambda so you do not have to declare a type at all.
- 04
Build the rig
Call EntityRigBuilder.Build with the root GameObject, the archetype and your provider. It returns handles to everything it created. Run it again on the same root and it reuses the existing Visual child and the existing controller rather than duplicating them, so authoring iteration stays cheap.
- 05
Emit sounds through the rig
Call EmitSound on the controller with a semantic event id and an optional surface tag. The controller passes the current active state along, so the sound profile can filter on tags exactly the way the clip table does, and a footstep on gravel resolves differently from one on metal without gameplay code knowing anything about clips.
- 06
Start from a bundled archetype
Tools, ZOA, Advanced, Generate, Entity Rig, Bundled Archetypes, Ensure Assets writes three demonstration archetypes, Bundled_Civilian, Bundled_Sniper and Bundled_Boss, into the generated archetypes folder. All three are AiCharacter kind and differ in default locomotion tag, with the sniper starting crouched. The installer is idempotent and skips any asset that already exists.
Surface
Key types
EntityRigController
component
The runtime brain on the entity root. Samples the state provider each frame, resolves the best-fit clip, and crossfades under the state graph's timing. Attached by the builder, never by hand.
- void Bind(IEntityStateProvider provider, IEntitySoundEmitter emitter)
- void Rebind(EntityArchetypeDefinition newArchetype, IEntityStateProvider provider, IEntitySoundEmitter emitter)
- void EmitSound(string eventId, string surfaceTag = null)
- EntityArchetypeDefinition Archetype { get; }
- StateTagSet ActiveState { get; }
- bool IsBound { get; }
EntityRigBuilder
class
The single entry point for rig composition. Static and stateless: instantiates the visual sub-tree, attaches the controller and audio player, binds them, and hands back the pieces. Safe to run more than once on the same root.
- static EntityRigBuildResult Build(GameObject root, EntityArchetypeDefinition archetype, IEntityStateProvider provider, IEntitySoundEmitter emitter = null)
- static EntityRigBuildResult Build(GameObject root, EntityArchetypeDefinition archetype, Func<StateTagSet> stateSampler)
EntityRigBuildResult
struct
The handles the builder returns, so callers can reach into a freshly built rig without re-walking the hierarchy. IsValid is true when both root and controller are present.
- GameObject Root; EntityRigController Controller; Transform VisualRoot
- Animator Animator; AudioEventPlayer SoundEmitter
- bool IsValid { get; }
IEntityStateProvider
interface
The one thing a consumer must supply. Returns the entity's active StateTagSet for this frame. The player implementation reads controller, equipment and survival state; an AI implementation reads the behaviour tree blackboard; an interactable reads its own open or closed state.
- StateTagSet SampleActiveState()
IEntitySoundEmitter
interface
The seam between pure sound resolution and a platform audio backend. Implementations resolve the best-fit entry, pick a variant and dispatch. The default adapter forwards into AudioEventPlayer; an FMOD or Wwise backend supplies its own.
- void Emit(string eventId, StateTagSet activeState, string surfaceTag, Vector3 position)
EntityKind
enum
Unspecified, Player, AiCharacter, Interactable, Ambient. Tells the builder which wiring template applies; Unspecified is a sentinel the validators flag rather than a usable value.
SocketBinding
struct
A semantic socket id bound to a transform path inside the visual prefab, with optional position and rotation offsets. Resolved once at bind time and cached.
- SocketId Id; string TransformPath
- Vector3 PositionOffset; Vector3 RotationOffsetEuler
SocketId
enum
The canonical mount points: Head, Neck, ChestUpper, ChestLower, SpineAttachUpper, SpineAttachLower, WaistRight, WaistLeft, HipBack, ShoulderRight, ShoulderLeft, RightHand, LeftHand, RightFoot, LeftFoot, EyesAnchor, MouthAnchor, WeaponPrimary, WeaponSecondary, and Custom for anything bespoke.
MaterialSlot
struct
A renderer path, a slot index on that renderer, and a semantic material id. The id resolves through the material resolver chain, which is how one archetype serves both render pipelines.
- string RendererPath; int SlotIndex; string MaterialSemanticId
VisualOverlay
struct
An optional add-on prefab such as armour or a helmet, mounted at a socket or at the entity root, activated when at least one of its tags is asserted on the entity.
- string Name; GameObject Prefab; SocketId MountSocket; string[] ActivationTags
LodLevel
struct
One LOD step, using Unity's screen-relative transition height convention, with an optional mesh override. When the base prefab already carries a LODGroup the importer mirrors it into this list.
- float ScreenRelativeTransitionHeight; GameObject MeshOverride
Surface
Authoring assets
EntityArchetypeDefinition
asset
The composition handle. A kind, five optional profile references, and a default locomotion tag. Every profile slot may be null, and the engine supplies a default for each. Created via ZOA, Foundry, Entity Rig, Entity Archetype.
- EntityKind Kind
- EntityRigVisualProfileDefinition VisualProfile
- AnimationProfileDefinition AnimationProfile
- SoundProfileDefinition SoundProfile
- EntityStateGraphDefinition StateGraph
- RigPolicyDefinition RigPolicy
- string DefaultLocomotionTag
EntityRigVisualProfileDefinition
asset
What the entity looks like: base skinned-mesh prefab, semantic material slots, bone sockets, LOD levels and overlays. Mostly populated by the FBX importer. Created via ZOA, Foundry, Entity Rig, Visual Profile.
- GameObject BasePrefab
- IReadOnlyList<MaterialSlot> MaterialSlots
- IReadOnlyList<SocketBinding> Sockets
- IReadOnlyList<LodLevel> LodLevels
- IReadOnlyList<VisualOverlay> Overlays
Usage
Examples
using ZOA.Animation.Core.State;
using ZOA.EntityRig.Runtime;
public sealed class AiEntityStateProvider : IEntityStateProvider
{
private readonly MyAgent _agent;
public AiEntityStateProvider(MyAgent agent) => _agent = agent;
// Called once per rig update. Field reads and flag flips only:
// anything that allocates here allocates every frame, per entity.
public StateTagSet SampleActiveState()
{
var locomotion = _agent.Speed > 4f ? LocomotionTag.Run
: _agent.Speed > 0.1f ? LocomotionTag.Walk
: LocomotionTag.Idle;
var combat = CombatTag.None;
if (_agent.HasWeapon) combat |= CombatTag.ArmedRifle;
if (_agent.IsFiring) combat |= CombatTag.Firing;
var mood = _agent.HealthPercent < 0.35f ? MoodTag.Wounded
: _agent.IsAlert ? MoodTag.Alert
: MoodTag.Neutral;
return new StateTagSet(
locomotion: locomotion,
posture: _agent.IsCrouched ? PostureTag.Crouched : PostureTag.Standing,
combat: combat,
mood: mood);
}
}using UnityEngine;
using ZOA.EntityRig.Definitions;
using ZOA.EntityRig.Runtime;
public sealed class EntitySpawner : MonoBehaviour
{
[SerializeField] private EntityArchetypeDefinition _archetype;
public EntityRigController Spawn(Vector3 position, MyAgent agent)
{
var root = new GameObject(_archetype.name);
root.transform.position = position;
var result = EntityRigBuilder.Build(
root,
_archetype,
new AiEntityStateProvider(agent));
if (!result.IsValid)
return null;
// Everything the builder made is on the result: no need to
// GetComponentInChildren your way back to the Animator.
result.Animator.applyRootMotion = false;
return result.Controller;
}
}using ZOA.Animation.Core.State;
using ZOA.EntityRig.Runtime;
// No IEntityStateProvider type required: the builder wraps the lambda
// in an inline provider. Useful in edit-mode tests.
var result = EntityRigBuilder.Build(
root,
archetype,
() => new StateTagSet(
locomotion: LocomotionTag.Idle,
posture: PostureTag.Standing));
Assert.IsTrue(result.IsValid);
Assert.AreEqual(archetype, result.Controller.Archetype);using ZOA.EntityRig.Runtime;
public sealed class FootstepRelay
{
private readonly EntityRigController _rig;
public FootstepRelay(EntityRigController rig) => _rig = rig;
// The controller passes the rig's current active state through to
// the sound profile, so a sprinting footstep on gravel can resolve
// to a different variant set than a crouched one on metal without
// this call site knowing anything about clips.
public void OnFootPlanted(string surfaceTag) =>
_rig.EmitSound("footstep", surfaceTag);
public void OnHurt() => _rig.EmitSound("vocal.hurt");
}Tooling
Editor tools
Build a Character wizard
Tools / ZOA / Advanced / Build / Characters / Build a Character
Four-step archetype authoring: Identity, Profiles, Validate, Apply. Routes into the Workbench Characters hub when one is open and falls through to a standalone window otherwise. It can also load an existing archetype from the asset browser and write changes back to that asset rather than creating a new one.
Entity Rig Workbench module
ZOA Platform Workbench, Entity Rig module
Entity Definitions browses, edits, validates, duplicates and deletes archetypes and visual profiles; Overview explains how the pieces fit together.
Bundled archetype installer
Tools / ZOA / Advanced / Generate / Entity Rig / Bundled Archetypes / Ensure Assets
Writes the Bundled_Civilian, Bundled_Sniper and Bundled_Boss demonstration archetypes into the generated archetypes folder. Idempotent: existing assets are left alone.
Extensible wizard step catalog
EntityArchetypeWizardCatalog exposes the canonical steps through IEntityArchetypeWizardCatalog with orders spaced by ten, so a downstream package can wedge a step such as cloth configuration between the shipped ones without rewriting the list. The draft is a plain mutable class rather than a ScriptableObject, so cancelling costs nothing and tests can drive it without the asset database.
Read this
Notes and caveats
See also