> 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/gameplay-code.md).

# Writing gameplay code

Your game talks to the ECS runtime in three ways. It **asks**: it queues activations, effects, ends, commits and gameplay events. It **reads**: attributes, tags, ability slots and active effects. It **reacts**: to activation results and to hits. This page shows each one, says which system group your code belongs in, and ends with the direct calls that act at once instead of through a queue.

**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): a catalog and an actor in a SubScene.
* You know the catalog, the queues and the frame order from [How it works](/docs/gameplay-ability-toolkit-ecs/build-with-it/concepts.md). This page does not repeat them.

## Do you need code?

An ability that only affects its owner needs no code, and neither does one that hits other actors in an area, an arc, the nearest or with a projectile: [Abilities that reach other actors](/docs/gameplay-ability-toolkit-ecs/build-with-it/delivery.md) makes those from data, with the **New Ability** window. This page is for what data cannot say - the rules of your game around the abilities.

## Where your systems go

```csharp
[UpdateInGroup(typeof(GasRequestSystemGroup))]   // runs just before the runtime: ask here
public partial struct PlayerAbilitySystem : ISystem { ... }

[UpdateInGroup(typeof(GasResultSystemGroup))]    // runs just after the runtime: read results here
public partial struct AbilityResultSystem : ISystem { ... }
```

| Your code                                                     | Goes in                 | What you get                                                                                                 |
| ------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| Queues an activation, an effect, an end, a commit or an event | `GasRequestSystemGroup` | The runtime serves it in the same frame. Its result can be read in `GasResultSystemGroup` in the same frame. |
| Reads results, attributes, tags, hits                         | `GasResultSystemGroup`  | Attributes already include this frame's effects.                                                             |
| Queues something from `GasResultSystemGroup`                  | `GasResultSystemGroup`  | The runtime serves it on the next frame.                                                                     |

An ability that a trigger starts (a gameplay event, or a tag being added or removed) is served on the next frame, because triggers run last in the runtime.

Order your systems inside a group against each other as usual, with `[UpdateAfter]` and `[UpdateBefore]`. Do not order a system against `GasSimulationSystemGroup` from outside the three groups. With Netcode for Entities the runtime moves into the prediction loop, and that order stops holding; see [Netcode for Entities](/docs/gameplay-ability-toolkit-ecs/build-with-it/netcode-for-entities.md#where-your-code-goes). Code that draws or plays sounds belongs in `PresentationSystemGroup`; see [Cues and presentation](/docs/gameplay-ability-toolkit-ecs/build-with-it/presentation.md).

## Finding indices

Requests and buffers use catalog indices. Look them up by name once and keep them:

```csharp
public void OnUpdate(ref SystemState state)
{
    if (!_ready)
    {
        var catalog = SystemAPI.GetSingleton<GasCatalog>().Value;
        _fireball = catalog.Value.FindAbilityIndex("GA_Fireball");
        _burning = catalog.Value.FindTagIndex("State.Burning");
        _health = catalog.Value.FindAttributeIndex("HeroAttributes", "Health");
        _ready = true;
    }
    ...
}
```

The names are the asset names for abilities and effects, the full dotted name for tags, and the attribute set definition's asset name plus the attribute name for attributes. Each lookup returns -1 when nothing has that name. The lookups compare fixed strings, so they also work inside Burst-compiled code. Keep the catalog in a local variable before taking a reference into it, as above; a reference into a temporary copy does not compile.

A system that needs several indices can store them in a component, as the ECS Swarm demo's `SwarmIndices` does, so jobs and other systems can read them too.

## Asking

### Activate an ability

```csharp
GasEntityFactory.RequestActivation(state.EntityManager, owner, _fireball);
```

| Parameter       | Meaning                                                                                                                                                                                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `target`        | The actor that owns the ability and runs it.                                                                                                                                                                                                               |
| `abilityIndex`  | The ability's catalog index. The actor must have been granted it.                                                                                                                                                                                          |
| `level`         | The level to activate at. 0 or less means 1. An ability started by a trigger uses the level on the actor's slot instead.                                                                                                                                   |
| `source`        | Who is behind the activation. Its tags are what the ability's source requirements check, and it is recorded as the source of the ability's cost and granted effects. When there is none, the owner is the source, and the source requirements see no tags. |
| `abilityTarget` | Who the ability is aimed at. Its tags are what the ability's target requirements check. Leave it out for an ability that is not aimed.                                                                                                                     |

The call returns `false` when the world has no activation queue, which means it has no catalog yet. Every activation request gets one result; see [Reacting to results](#reacting-to-results).

### Apply an effect

```csharp
GasEntityFactory.RequestInstantEffect(state.EntityManager, target, _burn, level: 1, source: caster);
```

Despite the name, the effect queue applies effects of every type. A duration or infinite effect becomes an active instance on the target, with its modifiers, granted tags and cues, until it expires or is removed. Pass the actor that caused the effect as `source`. The effect's modifiers, cues and later removal by source use it.

There is no queue for removing an effect. Use the direct calls in [Doing it now instead of queueing](#doing-it-now-instead-of-queueing).

### Set-by-caller values

An effect whose magnitude is **set by caller** takes its value from the request. The values live in a pool on the request queue, and a request points at a range of it with `SetByCallerStart` and `SetByCallerCount`. One value can serve any number of requests. This applies the same burn damage to every enemy in a radius:

```csharp
GasEntityFactory.TryGetQueue<GasInstantEffectQueue>(state.EntityManager, out var queue);
int start = GasEntityFactory.AppendSetByCaller(state.EntityManager, queue, "Damage.Burn", -8f);

var requests = SystemAPI.GetBuffer<GasInstantEffectRequest>(queue);
foreach (var enemy in enemiesInRadius)
{
    requests.Add(new GasInstantEffectRequest
    {
        Target = enemy,
        Source = caster,
        EffectIndex = _burn,
        Level = 1,
        SetByCallerStart = start,
        SetByCallerCount = 1,
    });
}
```

The name is the one the effect's modifier was authored with; the overload that takes an `int` supplies a value by tag index instead. The runtime copies the values onto the effect when it is applied, so a periodic effect uses the same value on every tick, and the pool itself is cleared when the queue has been served.

An activation can carry values too, for abilities whose effects are set by caller. Append them to the activation queue's pool (`TryGetQueue<GasAbilityActivationQueue>`), and pass the range to the `RequestActivation` overload that takes the queue entity: its last two parameters are `setByCallerStart` and `setByCallerCount`.

### End or cancel an ability

```csharp
GasEntityFactory.RequestEnd(state.EntityManager, owner, _channel);                     // end
GasEntityFactory.RequestEnd(state.EntityManager, owner, _channel, wasCancelled: true); // cancel
```

Ending removes the tags the ability's activation added and unblocks what it blocked. If several executions of the ability are running, one ends. Ending an ability that is not running does nothing. The two calls above end the ability the same way today; the flag is there to match the GameObject runtime.

### Commit an explicit-commit ability

An ability whose commit policy is **Explicit** starts without paying its cost or starting its cooldown. Commit it with a request:

```csharp
GasEntityFactory.TryGetQueue<GasAbilityCommitQueue>(state.EntityManager, out var commits);
SystemAPI.GetBuffer<GasAbilityCommitRequest>(commits).Add(new GasAbilityCommitRequest
{
    Target = owner,
    AbilityIndex = _charge,
});
```

Committing twice pays once. Ending the ability before it commits costs nothing. A commit is refused when the owner cannot pay. The ability's **Activation Owned Tags** and **Block Abilities With Tags** are added when it commits, not when it activates. Its granted effects still apply at activation.

### Send a gameplay event

```csharp
GasEntityFactory.SendGameplayEvent(state.EntityManager, _parryTag, instigator: attacker, target: defender);
```

An event reaches the actor it names: abilities triggered by the tag start, and `GameplayEvent` waits on the tag complete. An event with no target reaches every actor. The event's instigator is the source of the ability it triggers, and its target is that ability's target.

### Asking from a job

The queues are single buffers, so never append to one from a parallel job. Let a parallel job decide who asks, and append afterwards, from the main thread or from a single job. The ECS Swarm demo's enemies do this: a parallel Burst job collects the enemies close enough to bite into a `NativeQueue<Entity>`, and a second, single job then appends one activation request per entry.

## Reading

Read the runtime's buffers **read-only**. A read-write access marks the chunk as changed, and the runtime then recalculates every actor in it.

```csharp
var values = SystemAPI.GetBufferLookup<GasAttributeValue>(isReadOnly: true);
float health = values[hero][_health].CurrentValue;
```

| To read                                  | Use                                                                                                                                                                                                                        |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| An attribute                             | `GasAttributeValue[attributeIndex]`: `CurrentValue` with modifiers applied, `BaseValue` without.                                                                                                                           |
| Whether an actor has a tag               | `GasTagUtility.HasTag(ref catalog, tags, tagIndex, GasTagKind.Owned)`. Owning `State.Stunned.Hard` counts as having `State.Stunned`. `HasTagExact(tags, tagIndex, kind)` needs no catalog and does not look at child tags. |
| An ability's cooldown or whether it runs | The actor's `GasAbilitySlot` for the ability: `CooldownRemaining`, `ActiveCount`, `Level`. `GasAbilityActivationUtility.FindSlot(slots, abilityIndex)` returns its index in the buffer, or -1.                             |
| The effects on an actor                  | `GasActiveEffect`: `EffectIndex`, `StackCount`, `Duration`, `StartTime`, `Handle`.                                                                                                                                         |

In an `IJobEntity`, take the buffers as `in` parameters:

```csharp
[BurstCompile]
partial struct SlowJob : IJobEntity
{
    [ReadOnly] public BlobAssetReference<GasCatalogBlob> Catalog;
    public int Stunned;

    void Execute(ref LocalTransform transform, in DynamicBuffer<GasTagCount> tags)
    {
        if (GasTagUtility.HasTag(ref Catalog.Value, tags, Stunned, GasTagKind.Owned))
        {
            return;
        }
        // ...
    }
}
```

From a `MonoBehaviour`, for UI, use an `EntityQuery` and `EntityManager.GetBuffer<T>(entity, isReadOnly: true)`. [Cues and presentation](/docs/gameplay-ability-toolkit-ecs/build-with-it/presentation.md#showing-state-in-ui) has an example.

To see the same numbers while you work, open **Tools > Gameplay Ability Toolkit > ECS > Actor Debugger** in Play mode.

## Reacting to results

Each activation request gets one `GasAbilityActivationEvent` in the `GasAbilityActivationEventQueue` singleton:

```csharp
var results = SystemAPI.GetBuffer<GasAbilityActivationEvent>(
    SystemAPI.GetSingletonEntity<GasAbilityActivationEventQueue>());

foreach (var result in results)
{
    if (result.Succeeded && result.AbilityIndex == _fireball)
    {
        // Place the fireball, play the cast, ...
    }
}

results.Clear();
```

Each result has `Target`, `Source`, `AbilityIndex`, `Level`, `Succeeded` and, when it did not succeed, a `FailureReason`:

| `FailureReason`         | Means                                                                                                                                                                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cooldown`              | The ability is cooling down.                                                                                                                                                                                                       |
| `Cost`                  | The owner cannot pay the cost effect, or the payment failed.                                                                                                                                                                       |
| `TagRequirements`       | A required tag is missing, a blocked tag is present, or a tag query failed, on the owner, the source or the ability target. Another running ability that blocks this one, through its **Block Abilities With Tags**, fails it too. |
| `InvalidRequest`        | The ability index is outside the catalog, the actor was not granted it, it is already running and cannot run twice, or Netcode for Entities is not simulating the actor on this client.                                            |
| `AbilitySystemNotFound` | The target is not an actor.                                                                                                                                                                                                        |

The runtime appends results and never clears them. Clear the buffer in one place, after every system that reads it. In a world with delivery abilities, the delivery module reads the results first and clears them last in `GasResultSystemGroup`, so read results inside that group; see [Activation results](/docs/gameplay-ability-toolkit-ecs/build-with-it/delivery.md#activation-results). A trigger's activation writes no result when the ability is already running and cannot run twice, as on the GameObject runtime.

What an ability's success means beyond its own effects is up to your game. The ECS Swarm demo's Firestorm, for example, has no effects of its own: its result system sees the success and launches meteors at the aim point, and each meteor applies damage and a burn to the enemies it lands among.

## Handling hits

A hit is an effect that lands on another actor. Who gets hit depends on where the ability finds its targets.

**Targets your code finds.** When your own system decides who was hit - a physics overlap, a melee trace, a projectile you move yourself - queue the effect for each one, as in the set-by-caller example above. Pass the attacker as `source`. Queued in `GasRequestSystemGroup`, the effect is applied in the same frame. Read the result in `GasResultSystemGroup`: the new attribute values, the tags the effect granted, or the cue events it raised (see [Cues and presentation](/docs/gameplay-ability-toolkit-ecs/build-with-it/presentation.md#cue-events)).

**Targets a delivery ability finds.** A delivery ability (area, arc, nearest or projectile; see [Abilities that reach other actors](/docs/gameplay-ability-toolkit-ecs/build-with-it/delivery.md)) asks for its hit effects itself. After it does, it reports each hit as a `GasDeliveryHitEvent` with the `AbilityIndex`, the `Caster`, the `Target` and the `Level`. Use these for what data cannot say: a knock-back, a score, a sound.

```csharp
using GameplayAbilityToolkit.Entities;
using GameplayAbilityToolkit.Entities.Delivery;
using Unity.Entities;

[UpdateInGroup(typeof(GasResultSystemGroup))]
public partial struct KnockbackOnHitSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<GasDeliveryState>();
    }

    public void OnUpdate(ref SystemState state)
    {
        // Finish the jobs that write what this reads.
        state.CompleteDependency();

        foreach (var hit in SystemAPI.GetSingletonBuffer<GasDeliveryHitEvent>(true))
        {
            // hit.Caster hit hit.Target with ability hit.AbilityIndex at level hit.Level.
        }
    }
}
```

Hit events last one frame: the next frame's delivery apply system clears them, so read them in the frame they happen, in `GasResultSystemGroup` or in `GasRequestSystemGroup` after `GasDeliveryApplySystem`. They report that the effects were asked for. A target that fails an effect's requirements still produces a hit event, so check the target's attributes or tags if the outcome matters. The **New Ability** window can also write a hook script that does this, ready to fill in. It runs in `GasRequestSystemGroup`, after `GasDeliveryApplySystem`.

## Doing it now instead of queueing

For editor tools, tests and main-thread code that needs an answer on the spot, the runtime's operations are also static calls. They apply the same rules as the queues, and keep the change tracking right. The example runs inside a system. From other code, read the `GasCatalog` component with an `EntityQuery`.

```csharp
var catalog = SystemAPI.GetSingleton<GasCatalog>().Value;

// Apply an effect, and find out why when it does not apply.
GasEntityFactory.TryGetWorldTime(entityManager, out float now);
bool applied = GasEffectUtility.TryApplyEffect(
    entityManager, ref catalog.Value, target, _swordBonus, level: 1, now, source: player, instigator: player,
    default, out GasEffectFailureReason reason, out uint handle);

// Later: take it off again, every stack.
GasEffectUtility.RemoveEffectInstance(entityManager, ref catalog.Value, target, handle, stacks: -1);
```

| Call                                                                        | Does                                                                                                                                                                                                                      |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GasEffectUtility.TryApplyEffect`                                           | Applies an effect. The short overload reads the clock for you; the long ones report a `GasEffectFailureReason` and the new instance's handle.                                                                             |
| `GasEffectUtility.RemoveEffectInstance`                                     | Removes stacks from the instance with a handle.                                                                                                                                                                           |
| `GasEffectUtility.RemoveEffect`                                             | Removes stacks from the first instance of an effect.                                                                                                                                                                      |
| `GasEffectUtility.RemoveEffectsFromSource`                                  | Removes every instance a source or instigator applied.                                                                                                                                                                    |
| `GasEffectUtility.RemoveEffectsWithTag`                                     | Removes every instance whose asset or granted tags hold a tag or a child of it.                                                                                                                                           |
| `GasEffectUtility.RemoveAllEffects`                                         | Removes every instance.                                                                                                                                                                                                   |
| `GasAbilityActivationUtility.TryActivate`                                   | Activates an ability from a `GasAbilityActivationRequest` and returns its result. Pass the clock as `now` (see `GasEntityFactory.TryGetWorldTime`). Its default is 0, which starts granted duration effects at time zero. |
| `GasAbilityActivationUtility.EndAbility` / `EndAllExecutions` / `TryCommit` | Ends one execution of an ability, ends every running execution, or commits an ability.                                                                                                                                    |

The handle that `TryApplyEffect` reports is zero for an instant effect, which leaves no instance. A stack count of 0 or below removes every stack, and so does a count as large as the instance's stacks. Removing an instance takes its modifiers, granted tags, granted abilities and cues with it, as an expiry does.

| `GasEffectFailureReason`        | Means                                                             |
| ------------------------------- | ----------------------------------------------------------------- |
| `InvalidEffectIndex`            | The index is outside the catalog.                                 |
| `TargetCannotReceiveEffects`    | The target is not an actor.                                       |
| `ApplicationRequirementsNotMet` | The target failed the effect's required tags or query.            |
| `TargetNotSimulated`            | Netcode for Entities is not simulating the target on this client. |
| `InvalidEffectData`             | The baked effect cannot be used.                                  |

Inside a system's update, prefer the queues. A direct call acts in the middle of whatever is running, and a queued request is served in the runtime's order.

## Next steps

* [Cues and presentation](/docs/gameplay-ability-toolkit-ecs/build-with-it/presentation.md): turn cue events and montages into visuals and sound, and show state in UI.
* [Abilities that reach other actors](/docs/gameplay-ability-toolkit-ecs/build-with-it/delivery.md): the data-driven way to hit other actors, and how to replace its target search.
* [Netcode for Entities](/docs/gameplay-ability-toolkit-ecs/build-with-it/netcode-for-entities.md): the same code in a server-authoritative game with client prediction.
* [Troubleshooting](/docs/gameplay-ability-toolkit-ecs/reference/troubleshooting.md): why a request does nothing.
* [API reference](/docs/gameplay-ability-toolkit-ecs/reference/api-reference.md): the public types.
