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
- Add Controller parameters such as Float
Speed, BoolGrounded, IntStanceor TriggerAttack, then configure transitions/BlendTrees. - 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 asBase Layer/Idle#0; inspectGetStateMetadata(i).Keyon the descriptor. - Declare only the exact keys your client needs.
Speedwrites a parameter;Attackwrites a Trigger only when that parameter exists. Direct state playback uses its complete state key. - 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:
- Retain gameplay intent and its unique request. String APIs return a request; typed writers use client.NextRequest().
- Submit in CharacterAnimationBridgeSystemGroup. A successful write does not confirm that a skill started.
- 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.
- Accepted confirms command acceptance. Trigger consumption and visual events happen separately. Repair, retry or cancel conflicts/invalid requests according to gameplay policy.
- 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:
- Create one client per producer/asset. Resolve and cache typed handles with TryGet; keep gameplay intent in your IComponentData.
- Call TryReserveTargets(count, out writer, out reservationDependency) once per batch. Retain intent and retry if it fails.
- Combine input dependencies with reservationDependency. Use writer.Begin(index, target) for each unique target and write parameters/requests.
- 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.
- Confirm receipts in the Event group, borrowing the read-only buffer once per batch.
- 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.
- Resolve clip events by function name, such as ECSAnimatorEvent. A string parameter such as event.attack.contact is payload, not an automatic event key.
- Resolve once through prepared.Bindings.TryResolveEvent, then enumerate TryGetEventOccurrences in the Event group. Process repeated occurrences separately; use generation/ordinal to prevent duplicates. See NewcomerAnimationEventBridgeSystem.
- No MonoBehaviour SendMessage or StateMachineBehaviour callbacks are invoked. Trigger sounds, particles and UI from your presentation event bridge.
- Lifecycle records include StateEnter, StateExit, TransitionStart and TransitionEnd. Explicitly add CharacterLifecycleSubscription for per-update StateUpdate. Move parameter writes, movement and damage logic from old state scripts into gameplay systems; listening for events alone is not necessarily equivalent.
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.