MasterNin ECSAnimator
On this page

Connect Gameplay to Animation

Read Supported Boundaries and Unsupported Operations first. Build success does not establish support for omitted behavior or arbitrary Prefab combinations.

ECSAnimator accepts parameter/state requests and evaluates the converted Controller. Input, AI, movement, collision and combat rules belong to the game. Start with a working EntityDesc from Getting started; no direct GPU writes are required.

1. Choose an integration route

Game structure Recommended route
Verify Speed and movement on one character Quick Start's SimpleCharacterExample; disable Read Keyboard and call SetVelocity
One character with an existing C# / MonoBehaviour controller The complete GameAnimationDriver below
Crowds, AI, behavior trees or Burst Jobs Store intent in gameplay components and submit once through an AnimationBridge System; see NewcomerAnimationBridgeSystem.cs

Do not send commands to the source Prefab's UnityEngine.Animator; it does not control the spawned Entity. Changing Speed changes animation, not position. Your movement system writes the character root's LocalTransform, with one position owner per character.

2. Use Controller binding keys directly

  1. Add Controller parameters such as Float Speed, Bool Grounded, Int Stance or Trigger Attack, then configure transitions/BlendTrees.
  2. Save and rebuild <PrefabName>_EntityDesc.asset. Parameter keys match the Controller's case-sensitive names. Layer keys match layer names. State keys are the complete exported paths, including the ordinal suffix, such as Base Layer/Idle#0; inspect GetStateMetadata(i).Key on the descriptor.
  3. Declare only the exact keys your client needs. Speed writes a parameter; Attack writes a Trigger only when that parameter exists. Direct state playback uses its complete state key.
  4. Rebuild after changing names/types, then recreate prepared data and clients. Missing, duplicate or ambiguous keys fail registration with a diagnostic. Float, Int, Bool and Trigger APIs must match the compiled type.

No gameplay alias table, automatic short state aliases or alternate-name mapping is generated. For layer weights, declare the actual layer name, then call TrySetLayerWeight. The typed prepared.Bindings.TryResolveFloat/TryResolveState/TryResolveLayer APIs remain available for explicit-domain integration.

3. Complete single-character example

The Controller must define Speed (Float) and Attack (Trigger), and the Controller must use them. Create a gameplay asmdef referencing ECSAnimator.Runtime.Product, Unity.Entities and Unity.Mathematics. Save this as GameAnimationDriver.cs, add it to an empty GameObject and assign the EntityDesc. Set Speed from gameplay and call RequestAttack once on an attack input. The script controls animation only and does not use legacy input.

using ECSAnimator.Runtime.Product.Authoring;
using ECSAnimator.Runtime.Product.Data;
using ECSAnimator.Runtime.Product.ECS;
using Unity.Mathematics;
using UnityEngine;

public sealed class GameAnimationDriver : MonoBehaviour
{
    public CharacterRuntimeAsset EntityDesc;
    public CharacterSpawnerWorldSelection WorldSelection = CharacterSpawnerWorldSelection.DefaultWorld;
    public string WorldName = "";
    public float Speed;

    private CharacterRuntime runtime;
    private CharacterPreparedAsset prepared;
    private CharacterSpawnScope scope;
    private CharacterAnimationClient client;
    private CharacterInstanceHandle character;
    private bool attackPending;

    public void RequestAttack() => attackPending = true;

    private void OnEnable()
    {
        if (!CharacterWorldSelector.TryResolve(WorldSelection, WorldName, out var world, out var error) ||
            !CharacterRuntime.TryGet(world, out runtime, out error) ||
            !runtime.TryPrepare(EntityDesc, out prepared, out error) ||
            !runtime.TryCreateScope(1, out scope, out error) ||
            !runtime.TryCreateAnimationClient(prepared, "game.player." + GetInstanceID(),
                new[] { "Speed", "Attack" }, out client, out error) ||
            !runtime.TrySpawn(prepared, new CharacterSpawnRequest((float3)transform.position,
                (quaternion)transform.rotation, 1f),
                scope, null, out var spawned, out error))
        {
            Debug.LogError(error, this);
            StopCharacter();
            return;
        }
        character = spawned.SpawnId;
    }

    private void Update()
    {
        if (client == null) return;
        if (!client.IsValid || !runtime.TryResolve(character, out _))
        {
            StopCharacter();
            return;
        }
        if (!client.TryBind(character, out var animator, out _)) return;
        if (!animator.TrySetFloat("Speed", Speed, out var error))
            Debug.LogError(error, this);
        if (attackPending)
        {
            if (animator.TrySetTrigger("Attack", out _, out error))
                attackPending = false; // Written, not yet accepted, played or applied as damage.
            else
                Debug.LogError(error, this);
        }
    }

    private void OnDisable() => StopCharacter();
    private void OnDestroy() => StopCharacter();

    private void StopCharacter()
    {
        client?.Dispose();
        client = null;
        if (runtime != null && runtime.IsValid)
        {
            if (scope != null) runtime.DestroyScope(scope);
            if (prepared != null) runtime.Release(prepared);
        }
        runtime = null;
        prepared = null;
        scope = null;
        character = default;
        attackPending = false;
    }
}

This minimal example merges multiple unsubmitted attacks into one pending request and clears it when writing succeeds. It demonstrates integration, not a skill queue, receipt retry policy or combat confirmation. Do not call RequestAttack every frame instead of responding to a press.

TryBind returns false while the channel is busy; retry next update. Other errors still require diagnosis. Inputs must be finite, binding keys correct and the World valid. Bind a client only once per frame; use the returned animator only during that update. For crowds, use the batch route.

4. Parameters, direct playback and one-shot requests

Call these on the animator obtained by the current TryBind. Declare the corresponding keys when creating the client.

Intent Call Notes
Continuous speed TrySetFloat("Speed", speed, out error) Write the final value. No Animator dampTime overload is provided; smooth in gameplay.
Stance TrySetInt("Stance", stance, out error) Requires an Int parameter
Ground contact TrySetBool("Grounded", grounded, out error) Requires a Bool parameter
Attack Trigger TrySetTrigger("Attack", out request, out error) Uses Controller conditions; written does not mean consumed
Reset Trigger TryResetTrigger("Attack", out error) Does not undo an entered state or an event
Layer weight TrySetLayerWeight("layer.upper", 0.5f, out error) Layer/LayerWeight binding, weight in 0–1
Enter a state TryPlay("action.attack", 0f, out request, out error) State binding, not a Trigger
Seek by seconds TryPlayFixedTime("action.attack", 0.2f, out request, out error) Seconds
Normalized transition TryCrossFade("action.attack", 0.1f, 0f, out request, out error) Normalized duration/offset, not milliseconds
Timed transition TryCrossFadeFixedTime("action.attack", 0.15f, 0f, out request, out error) Duration/offset in seconds

Submit Play/CrossFade when intent changes. Sending Play to zero every frame repeatedly restarts the animation.

For reliable one-shot handling:

  1. Retain gameplay intent and its unique request. String APIs return a request; typed writers use client.NextRequest().
  2. Submit in CharacterAnimationBridgeSystemGroup. A successful write does not confirm that a skill started.
  3. In the same update's CharacterEventSystemGroup, call TryGetReceipts and use client.Matches(receipt, target, request, binding, operation) to match the full identity; then inspect receipt.Status == Accepted.
  4. Accepted confirms command acceptance. Trigger consumption and visual events happen separately. Repair, retry or cancel conflicts/invalid requests according to gameplay policy.
  5. Do not retain receipt buffers across frames. Copy only needed history values. Quick Start's NewcomerAnimationReceiptBridgeSystem demonstrates matching a Play request to its receipt.

One character may have only one gameplay animation writer per frame. Resolve Death/Hit/Attack priority first. Two clients writing the same character conflict; later writes do not override earlier ones.

5. ECS, AI and batch Jobs

Within a Simulation update, use:

IntentProducer (AI/input/movement) > Transform update > AnimationBridge (commands) > Animation evaluation > Event (receipts/presentation feedback).

Use CharacterAnimationIntentProducerSystemGroup for gameplay intent, CharacterAnimationBridgeSystemGroup for submission and CharacterEventSystemGroup for feedback. MonoBehaviour.Update does not guarantee consumption in the same Simulation update.

Adapt the existing bridge sample to your Controller:

  1. Create one client per producer/asset. Resolve and cache typed handles with TryGet; keep gameplay intent in your IComponentData.
  2. Call TryReserveTargets(count, out writer, out reservationDependency) once per batch. Retain intent and retry if it fails.
  3. Combine input dependencies with reservationDependency. Use writer.Begin(index, target) for each unique target and write parameters/requests.
  4. Immediately call client.RegisterProducer(jobHandle) after scheduling and combine the handle into system Dependency. Do not Complete merely to register. Missing registration breaks command consumption and release ordering.
  5. Confirm receipts in the Event group, borrowing the read-only buffer once per batch.
  6. During shutdown, complete owned Jobs, unregister the bridge, dispose the client, destroy the scope, release prepared resources and dispose owned NativeArrays.

The sample's Register/Unregister are internal to its assembly, not a general plugin service. Copy/rename gameplay bridge code or implement the same public call sequence in your own assembly. Resolve handles again after a World rebuild or Prepare; never share them across Worlds. See custom components.

6. Events, observations and StateMachineBehaviour migration

CharacterAnimationObservation exposes layers, states, tags, Trigger consumption and event records. Read current-frame results after evaluation; do not replace server skill timing or damage authority with animation observation.

7. Integration checks

Verify Speed=0 gives Idle, speed above the threshold gives Run, one Attack enters and exits its state, playback is not reset every frame, AI and keyboard do not compete, disable/re-enable releases and recreates correctly, and World rebuilds do not reuse stale handles.

Then test your events and attachments. Keep API errors, declared binding keys, Controller types and the failing stage; use Troubleshooting.