ZOA Animation
A definition-driven layer between gameplay events and animator controllers, plus tag-based clip resolution.
Animation solves two problems that look related and are actually separate. The first is that gameplay code should never contain animator parameter names. A weapon that fires does not want to know that this particular rig calls the trigger "Fire" and that one calls it "WeaponFire". The binding layer fixes that: gameplay fires a semantic AnimationBindingId and a profile maps it to a concrete parameter name, type and layer index for the rig currently in use.
The second problem is choosing which clip should play at all. The state-tag layer answers that without a hand-drawn state machine. An entity's condition is expressed as a StateTagSet, six independent bitmask dimensions covering locomotion, posture, combat, interaction, reaction and mood. An AnimationProfileDefinition is a table of TaggedClipEntry rows, each a required tag pattern plus the clip that should play when the pattern is satisfied. Resolution picks the most specific matching row, and when nothing matches it relaxes dimensions in a fixed order until something does.
ZOA.Animation.Core compiles without UnityEngine. Identifiers, binding entries, pose snapshots, the state tag algebra and the fallback walker all live there, so the resolution logic can be tested in a plain fixture. ZOA.Animation.Unity adds the ScriptableObject definitions and the resolvers that consume them. Concrete drivers live downstream, in the player and AI packages, so this package supplies contracts and data rather than a rig implementation.
Depends on (2)
How it works
Concepts
Bindings map meaning to parameters
An AnimationBindingEntry pairs an AnimationBindingId, the semantic name of a gameplay event, with a ParameterName, an AnimationParameterType of Trigger, Bool, Float or Int, and an optional LayerIndex where minus one means the base layer. The layer index puts weapon animation on an upper-body override layer without gameplay code knowing about it.
IAnimationBindingProfile is a named collection of those entries plus a set of AnimationSlotEntry rows. The entries answer "which parameter do I set"; the slots answer "which clip do I play", by pairing an AnimationSlotId with a ClipReference string, a speed multiplier and an additive flag. The clip reference is a string rather than an asset reference specifically so the contract stays engine-free: an IAnimationClipResolver turns it into a playable handle at runtime, so a project can back clips with direct references, Addressables, or a definition lookup without changing anything above.
DefaultBindings and DefaultSlots are the shipped vocabulary. Bindings cover triggers such as fire, reload, equip, holster, jump, land, dodge and hit, booleans such as is_grounded, is_aiming, is_reloading and is_crouching, floats such as speed, move_x, move_y, lean, aim_weight and ground_slope, and one integer, posture. Slots cover weapon handling, stance and locomotion. A profile authored against the shipped ids transfers to another rig; invented ids do not.
IAnimationDriver is the runtime bridge that consumes a profile. LoadProfile re-maps cached parameter hashes, FireTrigger, SetBool, SetFloat and SetInt push gameplay state in, PlaySlot plays a named clip, and ValidateProfile checks that the animator actually has every parameter the profile references. Every mutator is a documented no-op when the binding is absent from the active profile, so a rifle profile driving a rig that has no lean parameter simply does nothing rather than throwing.
State tags: six orthogonal dimensions
StateTagSet is a readonly struct holding one bitmask per StateDimension. The dimensions are Locomotion, Posture, Combat, Interaction, Reaction and Mood, and they are orthogonal: an entity can crouch-walk because posture and locomotion are separate axes, and it can aim a rifle while wounded because combat and mood are separate axes. Each axis has its own Flags enum, and those enums are int-backed rather than long-backed because Unity refuses to render a serialized enum field whose underlying type is long or ulong.
Matching is a subset test. A pattern matches the active state when every bit asserted on the pattern is also asserted on the state, dimension-wise, which is six AND operations and six comparisons with no branches. An empty pattern therefore matches everything, which is how a single untagged entry acts as a universal default. Specificity is the count of asserted bits, and it is the primary sort key during resolution.
The struct is value-equal and allocation-free, and it composes: Without clears one dimension, RestrictTo keeps only one, UnionWith merges, and the WithLocomotion, WithPosture, WithCombat, WithInteraction, WithReaction and WithMood helpers replace a single axis. The fallback walker is built on that composition.
Resolution and the fallback chain
AnimationProfileResolver.ResolveBestMatch takes a list of TaggedClipEntry rows and an active StateTagSet and returns an index, or NoMatch. It first tries the active state exactly. Within a single attempt, every entry whose pattern the state satisfies is a candidate, the highest specificity wins, ties break on Weight, and remaining ties fall to authoring order so the first row authored wins.
If nothing matches exactly, StateTagSetFallback.Walk produces a progressively relaxed chain and the resolver retries at each step. The relax order is fixed: Mood first, then Reaction, then Combat, then Interaction, then Posture. Locomotion is never cleared, because an entity that is sprinting must fall back to some sprinting clip rather than degenerating into an idle. The walker is a value-type enumerator, so the whole resolution path allocates nothing.
Profiles inherit. A profile can name a Parent, and FlattenChain walks child to parent collecting entries, keeping the first occurrence of each distinct pattern so a child row overrides the parent row with the same pattern while unrelated parent rows remain available. The flatten allocates, which is why it is done once at rebind rather than per frame; the resolver itself takes the already-flattened list.
When even the fallback chain produces nothing, the profile's own policy decides what happens. ProceduralFallbacksAllowed lets the runtime drive the rig with simple procedural motion, and DefaultIdleClip supplies a last-resort loop. With both off, the rig holds its previous pose.
State graphs replace transition drawing
EntityStateGraphDefinition is tiny, because the selection logic all lives in the resolver. What remains is timing, and that is expressed as one TransitionPolicy row per dimension: a MinHoldSeconds that prevents jitter when input hovers around a threshold, a CrossfadeSeconds blend duration where zero is an instant cut, and a TransitionTrigger of OnTagChange, OnEvent or OnInputThreshold.
Every dimension has a shipped default, exposed as the static EntityStateGraphDefinition.Default so tests and documentation can assert the baseline. Locomotion holds 50 ms and crossfades 150 ms on an input threshold. Posture holds 300 ms and crossfades 250 ms on tag change. Combat holds 150 ms and crossfades 100 ms on an event. Reaction has no hold and a 50 ms crossfade on an event, so a flinch lands immediately. Interaction holds 100 ms and crossfades 200 ms on tag change. Mood holds 500 ms and cuts with no crossfade.
ResolvePolicy returns an authored row when the graph overrides that dimension and the shipped default otherwise, so a custom graph only has to declare the dimensions it actually wants to change. Most archetypes never need one at all.
Rig policy instead of constraint graphs
RigPolicyDefinition declares which IK behaviours the runtime should generate from the rig's discovered bones, rather than requiring a hand-authored Animation Rigging constraint graph. FootPlant, LookAt, HandOnWeapon and ClimbHand are the four policies, and each is silently skipped when the rig lacks the bones it needs. A door archetype with FootPlant enabled is not a bug; there are no feet to plant, so nothing happens.
The tuning knobs are the same shape. FootPlantBlendInSpeed is the normalised locomotion speed below which foot planting fully engages, so feet stop dragging at a sprint. LookAtFovDegrees is the half-angle of the cone in which the look-at constraint activates, with targets outside it ignored and the head returning to neutral.
The procedural fallback block on the same asset covers the gap when no clip matches. BreathFrequency and BreathAmplitude drive an idle breath on the spine root, HeadBobAmplitude layers onto locomotion fallback, and AllowProceduralFallback turns the whole thing off for entities that should freeze in their last pose instead.
Sound profiles share the same tag algebra
SoundProfileDefinition applies exactly the resolution model the clip table uses, to audio. Entries bind a semantic SoundEventId string such as footstep, vocal.hurt or weapon.reload to a set of weighted AudioVariant clips, narrowed by an optional state-tag filter and an optional surface tag. SoundEventId ships canonical constants and uses dotted lowercase categories, and it never carries an asset path.
SoundProfileResolver filters by event id, keeps entries whose tag filter is a subset of the active state, keeps entries whose surface tag matches, then returns the winning entry index. It returns the entry rather than the clip; PickVariantIndex does weighted-random variant selection from a caller-supplied normalised value, which keeps the resolver pure and its tests deterministic.
Event ids fall back hierarchically. When hierarchical fallback is enabled and no entry matches the full id, the resolver strips the trailing dotted segment and retries, so a profile that authors weapon.fire will answer a request for weapon.fire.dryclick. A more specific id always beats a more general one regardless of state-tag specificity. Profiles inherit through Parent with the same override semantics as animation profiles, keyed on the event id, tag filter and surface tag triple.
Entries also carry the routing and lifecycle fields the unified audio layer needs: an AudioBusId, a Spatial flag with MaxDistance and Rolloff, a Loop flag with FadeTime, a Cooldown that silently drops re-triggers inside the window, and a PolyphonyCap that stops full-auto weapon fire from stacking into a crackle.
In the editor
Screens
Screenshot pending
/screenshots/animation-binding-wizard.png
The wizard on its parameter-bindings step, showing several rows pairing a semantic binding id with an animator parameter name and type, with the step rail on the left and the issue tray visible.
Screenshot pending
/screenshots/animation-auto-binding.png
The Auto Binding panel after a scan, listing slot rows with their top-scoring clip candidates and scores, the search roots field above and the profile set output path below.
Screenshot pending
/screenshots/animation-profile-inspector.png
The inspector for an AnimationProfileDefinition with several TaggedClipEntry rows expanded, each showing its locomotion, posture and combat tag masks alongside the assigned clip and weight.
Setup
Workflow
- 01
Decide which layer you need
The two layers are independent. If you have a hand-authored animator controller and only want gameplay to stop hardcoding parameter names, you need bindings and a driver. If you want clip selection itself to come from data instead of a state machine, you need an AnimationProfileDefinition and the state-tag layer. Entity rigs use the second; weapon and player systems typically use both.
- 02
Author a binding profile
Open the Binding Wizard from Tools, ZOA, Advanced, Define, Characters, Animation, Binding Wizard. Work through identity, parameter bindings, slot assignments and review. Bind against the DefaultBindings and DefaultSlots ids wherever one fits, since a profile built on the shipped vocabulary transfers to another rig by editing only the parameter names.
- 03
Auto-bind clips from a pack
The Auto Binding panel in the Animation Workbench module scans project animation folders through AnimationClipLibraryScanner, which enumerates embedded AnimationClip sub-assets inside FBX files so packs that ship dozens of clips per file are fully surfaced. ProjectAnimationClipAutoBinder then scores each candidate against the canonical player slots using several weak signals rather than one brittle substring: slot and weapon-profile aliases, penalties for contradictory tokens, folder context, loop settings, clip length and humanoid motion hints. Review the ranked assignments, then write them out as a PlayerAnimationProfileSetDefinition with an optional generated AnimatorOverrideController.
- 04
Build the clip table
Create an AnimationProfileDefinition and add one TaggedClipEntry per clip. Assert only the tags the clip requires, because every extra tag raises specificity and narrows when the row can win. Leave one row completely untagged as the universal default. Set OneShot for transition clips such as land or an equip flourish so they play once and hand control back to the resolver.
- 05
Layer with inheritance
Point a variant profile's Parent at a base profile and author only the rows that differ. A wounded variant of a humanoid profile overrides the locomotion rows and inherits everything else, and a female voice sound profile overrides the vocal entries of a shared human profile. Overrides are keyed on the pattern, so a child row with the same pattern replaces the parent row while unrelated parent rows stay live.
- 06
Tune timing and IK
Author an EntityStateGraphDefinition only for dimensions whose shipped defaults do not suit the archetype: a heavy boss might want a longer posture hold, a twitchy drone a shorter locomotion crossfade. Author a RigPolicyDefinition to pick which IK constraints get generated and how the procedural fallback feels when no clip matches.
- 07
Preview before you ship it
The Preview Lab capability in the Animation Workbench plays a PreviewSimulationPreset through the driver and feedback service, stepping a scripted sequence and letting you watch binding values change. IPreviewCaptureService captures a snapshot of the preset, step, binding values and active feedback channels for export as JSON, CSV or PNG.
Surface
Key types
IAnimationBindingProfile
interface
A named collection of parameter bindings and clip slots. The animation system consumes profiles without knowing their concrete definition type, which is why weapon and character packages can each supply their own.
- string ProfileId { get; }
- string DisplayName { get; }
- IReadOnlyList<AnimationBindingEntry> Bindings { get; }
- IReadOnlyList<AnimationSlotEntry> Slots { get; }
- AnimationBindingEntry GetBinding(AnimationBindingId bindingId)
- AnimationSlotEntry GetSlot(AnimationSlotId slotId)
IAnimationDriver
interface
The runtime bridge from gameplay events into an Animator. Every mutator is a no-op when the binding is absent from the active profile, so a profile that omits a parameter degrades quietly.
- IAnimationBindingProfile ActiveProfile { get; }
- void LoadProfile(IAnimationBindingProfile profile)
- void FireTrigger(AnimationBindingId bindingId)
- void SetBool(AnimationBindingId bindingId, bool value)
- void SetFloat(AnimationBindingId bindingId, float value)
- void SetInt(AnimationBindingId bindingId, int value)
- bool PlaySlot(AnimationSlotId slotId)
- bool ValidateProfile(out string error)
IAnimationEventReceiver
interface
The reverse direction from the driver. Where the driver pushes gameplay state into the animator, this receives keyframe events coming back out, so gameplay can react at the exact frame a reload completes or a hand reaches a grip.
- void OnAnimationEvent(AnimationBindingId bindingId, string eventTag)
- void OnSlotProgress(AnimationSlotId slotId, float normalizedTime)
IAnimationClipResolver
interface
Turns a slot entry's string clip reference into a playable handle. The indirection lets a project back clips with direct references, Addressables or a definition lookup without touching the profiles.
- bool TryResolve(string clipReference, out object handle)
- void Release(object handle)
StateTagSet
struct
Six orthogonal bitmask dimensions describing an entity's current condition. Value-equal, allocation-free, and cheap to compare: matching is six AND operations and six equality checks.
- bool Matches(StateTagSet pattern)
- bool IsSubsetOf(StateTagSet superset)
- int Specificity()
- StateTagSet Without(StateDimension dimension)
- StateTagSet RestrictTo(StateDimension dimension)
- StateTagSet UnionWith(StateTagSet other)
- StateTagSet WithLocomotion / WithPosture / WithCombat / WithInteraction / WithReaction / WithMood
StateDimension
enum
Locomotion, Posture, Combat, Interaction, Reaction, Mood. The declaration order matters, because fallback relaxes higher-numbered dimensions before lower-numbered ones.
StateTagSetFallback
class
Produces the progressively relaxed chain a resolver tries when the active state matches no authored entry. Relaxes Mood, then Reaction, then Combat, then Interaction, then Posture; Locomotion is never cleared.
- static Enumerator Walk(StateTagSet active)
- static int RelaxStepCount { get; }
AnimationProfileResolver
class
Stateless best-fit clip selection. Specificity first, then Weight, then authoring order, retried down the fallback chain. Allocation-free on the resolve path; FlattenChain allocates and belongs at rebind time.
- const int NoMatch
- static int ResolveBestMatch(IReadOnlyList<TaggedClipEntry> entries, StateTagSet activeState)
- static int ResolveAtExactState(IReadOnlyList<TaggedClipEntry> entries, StateTagSet state)
- static List<TaggedClipEntry> FlattenChain(AnimationProfileDefinition profile)
SoundProfileResolver
class
The audio counterpart. Resolves an event id plus active state plus surface tag to an entry, with optional hierarchical id fallback, and picks a weighted-random variant from a caller-supplied random value so tests stay deterministic.
- static int ResolveBestMatch(...)
- static List<SoundEventEntry> FlattenChain(SoundProfileDefinition profile)
- static int PickVariantIndex(AudioVariant[] variants, float randomNormalized)
AnimationBindingEntry
class
One binding: semantic id, animator parameter name, parameter type, and an optional layer index where minus one means the base layer.
- AnimationBindingId BindingId { get; }
- string ParameterName { get; }
- AnimationParameterType ParameterType { get; }
- int LayerIndex { get; }
- bool IsValid { get; }
AnimationSlotEntry
class
One clip slot: which named purpose it fills, the string clip reference to resolve, a per-weapon speed multiplier, and an additive flag for overlapping recoil and flinch clips.
- AnimationSlotId SlotId { get; }
- string ClipReference { get; }
- float SpeedMultiplier { get; }
- bool IsAdditive { get; }
AnimationBindingProfileBuilder
class
Fluent construction of a binding profile, used both by the wizard at authoring time and by code that needs to build a profile dynamically.
- AddTrigger(AnimationBindingId, string parameterName, int layer = -1)
- AddBool / AddFloat / AddInt
- AddSlot(AnimationSlotId, string clipReference)
- IAnimationBindingProfile Build()
DefaultBindings
class
The shipped binding vocabulary: triggers such as Fire, Reload, Equip, Holster, Jump, Land, Dodge and Hit; booleans such as IsGrounded, IsAiming, IsReloading, IsCrouching and IsFirstPerson; floats such as Speed, MoveX, MoveY, Lean, AimWeight and GroundSlope; and Posture as the one integer.
DefaultSlots
class
The shipped slot vocabulary, covering weapon handling from EquipTwoHand through ReloadMagazine, ReloadShell and ReloadBolt to FireSingle, FireBurst and FireAuto, the four stance slots, and the basic locomotion set.
IIKPoseProvider
interface
Implemented by equipment and weapon definitions to supply grip positions, aim offsets and per-slot poses, so different weapons place hands differently without touching the driver or the solver.
- string EquipmentId { get; }
- PoseSnapshot GetIdlePose()
- PoseSnapshot GetAimPose()
- PoseSnapshot GetSlotPose(AnimationSlotId slotId)
IIKPoseService
service
Resolves the current pose from whatever is equipped, blending between idle, aim and slot-specific poses. Providers register on equip and unregister on unequip.
- PoseSnapshot GetCurrentPose()
- void RegisterProvider(IIKPoseProvider provider)
- void UnregisterProvider(string equipmentId)
- void SetAiming(bool isAiming)
- void SetActiveSlot(AnimationSlotId slotId)
- void ClearActiveSlot()
IStanceProvider
interface
Describes which stances a character or loadout supports, whether each is currently enterable, the entry animation slot for a stance, and the IK modification the stance applies.
- IReadOnlyList<StanceId> AvailableStances { get; }
- StanceId DefaultStance { get; }
- bool CanEnterStance(StanceId stanceId)
- AnimationSlotId? GetStanceEntrySlot(StanceId stanceId)
- PoseSnapshot GetStancePoseModifier(StanceId stanceId)
PoseSnapshot
class
IK target positions and weights at a moment in time, stored as float triples rather than engine vectors so the contract layer stays engine-free. Used to blend when switching weapons or entering a stance.
- IReadOnlyList<PoseTargetData> Targets { get; }
- float Weight { get; set; }
- PoseTargetData GetTarget(IKTargetId targetId)
IPreviewSimulator
interface
Plays a PreviewSimulationPreset through a driver and feedback service, stepping a scripted sequence such as equip, idle, aim, fire, reload. Implementations are editor-side because preview is an authoring feature.
- void Play(PreviewSimulationPreset preset)
- void Pause() / Resume() / Stop()
- void Tick(float deltaTime)
- bool IsPlaying { get; }
- int CurrentStepIndex { get; }
Surface
Authoring assets
AnimationProfileDefinition
asset
The tagged clip table. A list of TaggedClipEntry rows, a procedural fallback flag, an optional default idle clip, and an optional parent profile for inheritance. Created via ZOA, Foundry, Animation, Animation Profile.
- IReadOnlyList<TaggedClipEntry> Entries
- bool ProceduralFallbacksAllowed
- AnimationClip DefaultIdleClip
- AnimationProfileDefinition Parent
EntityStateGraphDefinition
asset
Transition timing per dimension. Only the dimensions that need non-default behaviour are authored; everything else falls through to the shipped defaults. Created via ZOA, Foundry, Animation, Entity State Graph.
- IReadOnlyList<TransitionPolicy> Policies
- TransitionPolicy ResolvePolicy(StateDimension dimension)
- static TransitionPolicy Default(StateDimension dimension)
RigPolicyDefinition
asset
Which IK constraints the runtime should generate from discovered bones, how they blend, and how the procedural fallback behaves when no clip matches. Created via ZOA, Foundry, Animation, Rig Policy.
- bool FootPlant, LookAt, HandOnWeapon, ClimbHand
- float FootPlantBlendInSpeed, LookAtFovDegrees
- bool AllowProceduralFallback; float BreathFrequency, BreathAmplitude, HeadBobAmplitude
SoundProfileDefinition
asset
Semantic sound events bound to weighted clip variants, filtered by state tags and surface tag, with bus routing, spatialisation, looping, cooldown and polyphony per entry. Inherits through a parent chain. Created via ZOA, Foundry, Animation, Sound Profile.
- IReadOnlyList<SoundEventEntry> Entries
- SoundProfileDefinition Parent
PlayerAnimationProfileSetDefinition
asset
Player clips and controllers grouped by weapon class, so the rig switches to a rifle set when a rifle is armed and back to the unarmed baseline when nothing is. Created via ZOA, Foundry, Animation, Player Animation Profile Set.
- PlayerAnimationProfileType DefaultProfile
- IReadOnlyList<PlayerAnimationProfileEntry> Profiles
- bool TryGetProfile(PlayerAnimationProfileType profile, out PlayerAnimationProfileEntry entry)
- PlayerAnimationProfileEntry ResolveProfile(PlayerAnimationProfileType profile)
TaggedClipEntry
struct
One row of the clip table. Six tag masks form the required pattern, plus the clip, a Weight tie-breaker, a SpeedMultiplier, and a OneShot flag distinguishing a transition clip that plays once and yields from a clip that loops.
- LocomotionTag Locomotion; PostureTag Posture; CombatTag Combat; ReactionTag Reaction; InteractionTag Interaction; MoodTag Mood
- AnimationClip Clip; float Weight; float SpeedMultiplier; bool OneShot
- StateTagSet ToPattern()
- bool HasAnyTag { get; }
TransitionPolicy
struct
One transition rule: which dimension it governs, the minimum hold before another transition can start, the crossfade duration, and whether it fires on tag change, on an event, or on an input threshold.
- StateDimension Dimension; float MinHoldSeconds; float CrossfadeSeconds; TransitionTrigger Trigger
Usage
Examples
using ZOA.Animation.Core.Bindings;
using ZOA.Animation.Core.Models;
var profile = new AnimationBindingProfileBuilder("rifle_standard", "Standard Rifle")
.AddTrigger(DefaultBindings.Fire, "Fire")
.AddTrigger(DefaultBindings.Reload, "Reload")
.AddBool(DefaultBindings.IsAiming, "IsAiming")
.AddFloat(DefaultBindings.Speed, "Speed")
// Layer 1 is an upper-body override: gameplay never learns that.
.AddTrigger(DefaultBindings.Equip, "Equip", layer: 1)
.AddSlot(DefaultSlots.ReloadMagazine, "anim_rifle_reload_mag")
.AddSlot(DefaultSlots.EquipTwoHand, "anim_rifle_equip")
.Build();using ZOA.Animation.Core.Contracts;
using ZOA.Animation.Core.Models;
public sealed class WeaponAnimationBridge
{
private readonly IAnimationDriver _driver;
public WeaponAnimationBridge(IAnimationDriver driver) => _driver = driver;
public void OnWeaponEquipped(IAnimationBindingProfile weaponProfile)
{
_driver.LoadProfile(weaponProfile);
// Worth doing once at equip: a missing parameter is an authoring
// bug, and finding it here beats finding it as a silent no-op.
if (!_driver.ValidateProfile(out var error))
UnityEngine.Debug.LogWarning(error);
}
public void OnFired() => _driver.FireTrigger(DefaultBindings.Fire);
public void OnAimChanged(bool aiming) =>
_driver.SetBool(DefaultBindings.IsAiming, aiming);
public void OnReloadStarted() => _driver.PlaySlot(DefaultSlots.ReloadMagazine);
}using ZOA.Animation.Core.State;
using ZOA.Animation.Unity.Definitions;
// Flatten once when the profile chain changes, not every frame.
var entries = AnimationProfileResolver.FlattenChain(profile);
// The six dimensions are independent: this entity is sprinting AND
// standing AND armed with a rifle AND aiming, all at once.
var active = new StateTagSet(
locomotion: LocomotionTag.Sprint,
posture: PostureTag.Standing,
combat: CombatTag.ArmedRifle | CombatTag.Aiming,
mood: MoodTag.Alert);
int index = AnimationProfileResolver.ResolveBestMatch(entries, active);
if (index != AnimationProfileResolver.NoMatch)
{
var entry = entries[index];
PlayClip(entry.Clip, entry.SpeedMultiplier, loop: !entry.OneShot);
}
else if (profile.ProceduralFallbacksAllowed)
{
EngageProceduralFallback();
}using ZOA.Animation.Core.State;
// The walker is a value-type enumerator, so this allocates nothing.
var walker = StateTagSetFallback.Walk(active);
while (walker.MoveNext())
{
var relaxed = walker.Current;
// Locomotion is never cleared: a sprinting entity always falls back
// to some sprinting clip rather than degenerating into an idle.
if (TryResolveAt(relaxed, out var clip))
return clip;
}
return null;using ZOA.Animation.Core.Contracts;
using ZOA.Animation.Core.Models;
public sealed class ReloadSync : IAnimationEventReceiver
{
public void OnAnimationEvent(AnimationBindingId bindingId, string eventTag)
{
// The event tag is the sub-phase: "start", "complete", "cancel".
if (bindingId == DefaultBindings.Reload && eventTag == "complete")
CommitAmmoRefill();
}
public void OnSlotProgress(AnimationSlotId slotId, float normalizedTime)
{
// Detach the magazine at the frame the hand actually reaches it.
if (slotId == DefaultSlots.ReloadMagazine && normalizedTime >= 0.35f)
DropMagazineProp();
}
private void CommitAmmoRefill() { }
private void DropMagazineProp() { }
}Tooling
Editor tools
Animation Binding Wizard
Tools / ZOA / Advanced / Define / Characters / Animation / Binding Wizard
Four-step wizard for binding profiles: identity, parameter bindings, slot assignments, review. Built on the Foundry wizard shell with a step rail, issue tray and preview dock, and it can auto-bind clips for a chosen PlayerAnimationProfileType from configured search roots.
Animation Workbench module
ZOA Platform Workbench, Animation module
Capability strip covering Definitions, Auto Binding, Binding Browser, IK Targets, Preview Lab and Overview. The definition browser edits, validates, duplicates and deletes animation profiles, state graphs, rig policies, sound profiles and player profile sets in one place.
Auto Binding panel
Animation Workbench, Auto Binding capability
Scans configured roots for clips, scores them against canonical player slots, and writes the approved assignments into a PlayerAnimationProfileSetDefinition, optionally generating an AnimatorOverrideController alongside.
AnimationClipLibraryScanner
The single clip-discovery service for the whole editor toolchain. Recursive folder scan, per-FBX enumeration of every embedded AnimationClip sub-asset, preview clips and Samples paths filtered out, deterministic ordering, and results cached per root set and invalidated on import or domain reload.
ProjectAnimationClipAutoBinder
The scoring layer over the scanner. Ranks candidates against canonical slots using slot and weapon-profile aliases, contradictory-token penalties, folder context, loop settings, length ranges and humanoid motion hints, rather than a single substring match.
ModelNeighbourhoodScanner
Given a model FBX or prefab, scans its folder, conventional subfolders such as Animations, Sounds and Audio, and same-named cousin folders for related clips, avatars, controllers and masks. Pure scan with no asset writes; the player and AI build wizards present the report as a checklist and apply what the user approves.
AnimatorControllerBuilderCore
The shared AnimatorController construction plumbing every generator sits on: asset lifecycle, idempotent parameter and layer and state creation, 1D and 2D blend trees, and trigger, any-state, exit-time and bool transitions. Generators keep only their declarative spec and call these primitives.
Preview Lab
Animation Workbench, Preview Lab capability
Plays a scripted PreviewSimulationPreset through a driver and feedback service so a binding profile can be validated before it reaches a scene, with snapshot capture and export to JSON, CSV or PNG.
Read this
Notes and caveats
See also