> For the complete documentation index, see [llms.txt](https://morningheartgames.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://morningheartgames.gitbook.io/docs/gameplay-ability-toolkit-ecs/build-with-it/presentation.md).

# Cues and presentation

The ECS runtime decides what happens; it does not draw, play sounds or spawn particles. It tells your presentation code what happened in three ways: **cue events**, raised when effects with cues run; **montage state**, which says what pose a character should hold; and the actors' **buffers**, which UI can read. This page shows how to turn each into visuals, sound and UI.

**Before you start**

* You have an ECS scene that runs, such as the one from [Getting started](/docs/gameplay-ability-toolkit-ecs/start/getting-started.md).
* To use cues, an effect needs **Cues** tags. To use montages, an ability needs montages; see [Authoring](/docs/gameplay-ability-toolkit-ecs/build-with-it/authoring.md#montages).
* This page shows presentation code in Entities systems and in a `MonoBehaviour`. Use whichever suits what you draw with.

## Cue events

An effect's **Cues** tags are raised as data. Each one becomes a `GasCueEvent` in the `GasCueEvent` buffer of the actor the effect is on:

| Field                  | Meaning                                                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `Target`               | The actor the effect is on.                                                                  |
| `Source`, `Instigator` | Who applied the effect, and who is behind it.                                                |
| `CueTagIndex`          | The cue tag's catalog index. Compare it with an index from `FindTagIndex`.                   |
| `EventType`            | What happened; see below.                                                                    |
| `EffectLevel`          | The level the effect was applied at.                                                         |
| `EffectHandle`         | The effect instance, for matching an `OnActive` with its `Removed`. 0 for an instant effect. |
| `GameplayEffectActive` | Whether the effect instance was active when the cue was raised.                              |

| `EventType`   | Raised when                                                                       |
| ------------- | --------------------------------------------------------------------------------- |
| `Executed`    | an instant effect is applied, and each time a periodic effect runs its period     |
| `OnActive`    | a duration or infinite effect with the cue becomes active                         |
| `WhileActive` | right after `OnActive`, once per effect instance: start a looping visual here     |
| `Removed`     | the effect ends, by expiry, removal or cancellation: stop the looping visual here |

These follow the GameObject runtime's rules. A fresh effect raises `OnActive` and then `WhileActive`. A later `WhileActive` for the same actor, cue and effect instance is not raised again, so a looping particle is started once however long the effect lasts. An effect switched off by its ongoing requirements keeps its cue showing, as on the GameObject runtime. When it switches back on, you get `OnActive` again but no second `WhileActive`.

### Reading them

Read cue events in `PresentationSystemGroup`, or anywhere after the simulation, and clear each buffer once read. A change filter skips the chunks whose cue buffers were not written since the system last ran, so reading cues costs next to nothing on the frames nothing happens:

```csharp
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class BurnCuePresenter : SystemBase
{
    int _burn = -1;

    protected override void OnUpdate()
    {
        if (_burn < 0)
        {
            var catalog = SystemAPI.GetSingleton<GasCatalog>().Value;
            _burn = catalog.Value.FindTagIndex("Cue.Burn");
        }

        foreach (var (cues, transform) in SystemAPI
                     .Query<DynamicBuffer<GasCueEvent>, RefRO<LocalToWorld>>()
                     .WithChangeFilter<GasCueEvent>())
        {
            foreach (var cue in cues)
            {
                if (cue.CueTagIndex == _burn && cue.EventType == GasCueEventType.Executed)
                {
                    // Play a pooled particle and a sound at transform.ValueRO.Position.
                }
            }

            cues.Clear();
        }
    }
}
```

A managed `SystemBase` is the natural place for this, because particles, audio sources and pools are managed objects.

The query asks for `LocalToWorld`, so the actor needs a transform. `GasEntityAuthoring` does not add one by itself, although a catalog that lists delivery abilities gives its actors one. Otherwise a baker of your own on the same GameObject, or any component that asks for one, adds it with `GetEntity(TransformUsageFlags.Dynamic)`. Leave `LocalToWorld` out of the query if you do not need a position.

If several systems read cues, order them and let the last one clear. A buffer nobody clears grows every frame; the Dashboard's ECS Runtime page shows how many cue events are waiting.

### Matching the GameObject runtime's cue library

On the GameObject runtime, a `GameplayCueLibrary` maps a cue tag to a notify prefab, falling back to the tag's parent when a tag has no entry of its own. The ECS runtime leaves that to you, because notifies are GameObjects. To reuse a library, look up each tag it lists with `FindTagIndex` when your presenter starts, and keep a table from tag index to whatever you spawn. The catalog's `Tags` table gives each tag's `Name` and `ParentIndex`, so a presenter can fall back to a parent the same way. For the library itself, see the toolkit's [Gameplay Cues](https://morningheartgames.gitbook.io/docs/gameplay-ability-toolkit/subsystems/gameplay-cues).

### With Netcode for Entities

A predicting client simulates some ticks more than once: again after a rollback, and in part before it has the whole tick. The runtime raises cues on every pass. `GasPredictedCueDropSystem` takes back the cues of every pass except a tick's first full prediction, which is Netcode's own rule for one-off presentation. Read cues after the prediction loop, as above, and each cue arrives once. A cue can arrive up to a tick after the first partial prediction showed its effect, and a cue for an effect the server then refuses has already played; both are the usual trade of client prediction.

Only cue events are handled this way. Other events the runtime raises, such as activation results and montage events, are not filtered, and a re-simulated tick raises them again.

In a server world nothing clears cue events, so a server with cue-bearing effects collects them for as long as it runs. A server with no presentation needs a small system of its own that clears them:

```csharp
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(GasResultSystemGroup), OrderLast = true)]
public partial struct ServerCueClearSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var cues in SystemAPI.Query<DynamicBuffer<GasCueEvent>>().WithChangeFilter<GasCueEvent>())
        {
            cues.Clear();
        }
    }
}
```

## Posing a character from its montage

An actor that plays montages carries a `GasMontagePlayback` buffer, one element per montage slot: which montage, how far in, which section, and a `Sequence` that counts starts, so a montage started again from its beginning can be told apart from one still playing. `GasMontageUtility.TryGetClip` turns an element into what an animation system needs

* the name hash of the clip the montage shows now, the time within that clip, and the clip's place in the montage - and `GasMontageUtility.GetBlendWeight` gives the slot's weight, rising over the montage's blend-in and falling over its blend-out:

```csharp
// catalog is the BlobAssetReference<GasCatalogBlob> in the GasCatalog component's Value.
var playbacks = entityManager.GetBuffer<GasMontagePlayback>(actor, isReadOnly: true);
foreach (var playback in playbacks)
{
    float weight = GasMontageUtility.GetBlendWeight(ref catalog.Value, playback);
    if (weight > 0f
        && GasMontageUtility.TryGetClip(ref catalog.Value, playback, out uint clip, out float clipTime, out int slot))
    {
        // Pose the slot's layer (playback.SlotNameHash): the clip whose name hashes to `clip`, at `clipTime`,
        // blended in at `weight`.
    }
}
```

The blend-out starts before the montage's last frame, when the time left reaches the montage's blend-out time, so a character that fades back to locomotion over it arrives there as the montage ends. A montage with **Enable Auto Blend Out** turned off (`GasMontageEntry.HoldsAtEnd` in the catalog) instead stays at its end time, playing, until it is stopped or jumps, so the pose holds its last frame.

The name hash is `GasMontageUtility.NameHash(clip.name)`. Because the pose comes from the simulation's time, the frame a notify fires in the simulation is the frame the animation shows it, on every machine. The ECS Swarm demo poses its hero this way on the GPU, from animations baked into a texture; an Animator with a Playables graph sampling the clip at that time works the same way. With Netcode for Entities a client that only interpolates the actor reads the same state from `GasGhostMontage`.

### Notifies and root motion

A montage's notifies are reported on the actor as `GasMontageEvent` elements: `Kind` is `GasMontageEventKind.Notify`, `NotifyBegin`, `NotifyEnd`, `SectionChanged`, `BlendingOut`, `Started`, `Ended` or `Interrupted`, and `NotifyNameHash` is `GasMontageUtility.NameHash` of the notify's name. They last one runtime update, so read them in `GasResultSystemGroup`, in the frame they happen:

```csharp
[UpdateInGroup(typeof(GasResultSystemGroup))]
public partial class FootstepPresenter : SystemBase
{
    uint _footstep;

    protected override void OnCreate()
    {
        _footstep = GasMontageUtility.NameHash("Footstep");
    }

    protected override void OnUpdate()
    {
        foreach (var events in SystemAPI.Query<DynamicBuffer<GasMontageEvent>>())
        {
            foreach (var montageEvent in events)
            {
                if (montageEvent.Kind == GasMontageEventKind.Notify && montageEvent.NotifyNameHash == _footstep)
                {
                    // Play the footstep sound.
                }
            }
        }
    }
}
```

When the simulation should react to a notify, bind it to a gameplay event on the ability instead (**Montage Events** in [Authoring](/docs/gameplay-ability-toolkit-ecs/build-with-it/authoring.md#montages)). Root motion works the other way round: the runtime writes how far the montages moved the actor into `GasMontageRootMotion`, and a system of yours applies it to the transform. [Authoring](/docs/gameplay-ability-toolkit-ecs/build-with-it/authoring.md#montages) describes both.

## Showing state in UI

UI reads the actors' buffers directly. From a `MonoBehaviour`, keep an `EntityQuery` and read with `isReadOnly: true`. This health bar follows the hero of [Getting started](/docs/gameplay-ability-toolkit-ecs/start/getting-started.md):

```csharp
public sealed class HealthBar : MonoBehaviour
{
    [SerializeField] UnityEngine.UI.Slider m_Slider;

    EntityQuery _heroes;
    int _health = -1;

    void Update()
    {
        var world = World.DefaultGameObjectInjectionWorld;
        if (world == null || !world.IsCreated)
        {
            return;
        }

        var entityManager = world.EntityManager;
        if (_health < 0)
        {
            using var catalogs = entityManager.CreateEntityQuery(typeof(GasCatalog));
            if (catalogs.CalculateEntityCount() != 1)
            {
                return;
            }

            var catalog = catalogs.GetSingleton<GasCatalog>().Value;
            _health = catalog.Value.FindAttributeIndex("HeroAttributes", "Health");
            if (_health < 0)
            {
                return;
            }

            _heroes = entityManager.CreateEntityQuery(typeof(Hero));
        }

        if (_heroes.CalculateEntityCount() != 1)
        {
            return;
        }

        var values = entityManager.GetBuffer<GasAttributeValue>(_heroes.GetSingletonEntity(), isReadOnly: true);
        m_Slider.value = values[_health].CurrentValue / 100f;
    }
}
```

The same pattern reads ability cooldowns from `GasAbilitySlot.CooldownRemaining`, and tags with `GasTagUtility.HasTag`. The ECS Swarm demo's `SwarmHud` draws player health and mana and each ability's cooldown this way.

For drawing thousands of actors, read their state in a job instead. The demo's `SwarmVisualStateSystem` turns each enemy's tags into a visual state (normal, burning, chilled or stunned) in a Burst job, and its `SwarmCrowdRenderer` draws every enemy with `Graphics.RenderMeshIndirect`. Entities Graphics works as well; the runtime does not depend on either.

## Debug views

* **Dashboard > ECS Runtime** shows the catalog, the actors, the effects on them, running waits, and every queue's depth, live in Play mode.
* **Tools > Gameplay Ability Toolkit > ECS > Actor Debugger** shows one actor's attributes, tags, abilities, effects, waits and montage by name.
* `GasRuntimeSnapshot.Capture(entityManager)` returns the same numbers to code, for an in-game debug panel or a test.
* `GasSimulationStats`, when a world has one, holds what the runtime did on its last update: actors considered and recalculated, attributes recalculated, effect instances scanned, periodic executions, waits evaluated. Create one entity with the component to turn the counters on; the ECS Swarm demo does this for its Performance panel.

## Next steps

* [Writing gameplay code](/docs/gameplay-ability-toolkit-ecs/build-with-it/gameplay-code.md): ask for abilities and effects, and react to results and hits.
* [Authoring](/docs/gameplay-ability-toolkit-ecs/build-with-it/authoring.md#montages): montages, notifies, sections and root motion.
* [ECS Swarm demo](/docs/gameplay-ability-toolkit-ecs/start/swarm-demo.md): a complete presenter, with a crowd, a skinned hero, damage numbers and a HUD.
* [Netcode for Entities](/docs/gameplay-ability-toolkit-ecs/build-with-it/netcode-for-entities.md): what changes for presentation on a predicting client.
