Single Fire Components

3rd June 2018 in Design Patterns ECS #Design Patterns #ECS
Photo by rawpixel

Since using Entity Component System architecture for game design I’ve noticed a number of common patterns that are useful for solving different types of problems the “ECS” way. In this article I want to talk about what I’m calling the Single Fire Component pattern.

Here’s a scenario. Your character is walking around the screen and they enter a trigger. In an ECS world how do you deal with this?

The answer is to provide a mechanism so that when the entity (the character) enters the trigger, a single use component is added to that entity. Then a system that is watching for that component fires, and at the end of that update that system (or a dedicated cleanup system) takes that component off the entity.

Components don’t always need to contain data. Sometimes they can just act as flags for a system to either respond to, or ignore, a given entity. In this case we want to use the component as a positive flag so the system knows when to act on an entity.

Teleport System

Let’s go through an example. In the game I’m working on I needed a way to teleport things once they entered a trigger. The teleport is triggered from a collision, and moves the position of the entity to a fixed point.

Adding the component

First, I need a way to add the component to the entity on a trigger. This is done with a MonoBehaviour as right now there’s no pure ECS way to use physics or colliders.

[Serializable]
public struct Transition : IComponentData
{
    public float2 Location;
}

[RequireComponent(typeof(Collider2D))]
public class AddTransitionComponentOnTrigger : MonoBehaviour
{
    // The location to set the transform too.
    [SerializeField] private Transform location;

    void OnTriggerEnter2D(Collider2D collider)
    {
        // Get the entity part of the object that collided with the trigger.
        var entityComponent = collider.gameObject.GetComponent<GameObjectEntity>();

        // Add the component to the entity.
        entityComponent.EntityManager.AddComponentData(entityComponent.Entity,
            new Transition {
              Location = new float2(location.position.x, location.position.y)
            });
    }
}
Using the component

Now that the entity has a component added to it, a system can take over and perform some task. In our case we want the system to update the entities position based on the data in the Transition component.

[UpdateAfter(typeof(MoveForward2DSystem))]
public class TransitionSystem : JobComponentSystem
{
}

First, we want the system to copy the Transition data from the component to the Position2D of the entity.

[ComputeJobOptimization]
struct CopyTransition : IJobProcessComponentData<Transition, Position2D>
{
    public void Execute(
        [ReadOnly]ref Transition transition, 
        ref Position2D position) => 
        position = new Position2D { Value = transition.Location };
}

Then, we want to remove the Transition component from the entity.

struct RemoveTransition : IJob
{
    [ReadOnly] public EntityArray entities;
    public EntityCommandBuffer entityCommandBuffer;

    public void Execute()
    {
        for (int i = 0; i < entities.Length; i++)
            entityCommandBuffer.RemoveComponent<Transition>(entities[i]);
    }
}

Why do we need to use a command buffer to remove the component? When you’re in a job, you can’t access the entity manager, so you cannot remove a component using that. A job must use an EntityCommandBuffer to remove the component instead.

And now we schedule those jobs in the TransitionSystem.

[UpdateAfter(typeof(MoveForward2DSystem))]
public class TransitionSystem : JobComponentSystem
{
    [ComputeJobOptimization]
    struct CopyTransition : IJobProcessComponentData<Transition, Position2D> { ... }
    struct RemoveTransition : IJob { ... }

    EndFrameBarrier endFrameBarrier;
    ComponentGroup componentGroup;

    protected override void OnCreateManager(int capacity)
    {
        endFrameBarrier = World.GetOrCreateManager<EndFrameBarrier>();
        componentGroup = GetComponentGroup(
            ComponentType.ReadOnly(typeof(Transition)), 
            typeof(Position2D));
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var entities = componentGroup.GetEntityArray();

        var copyTransitionJob = new CopyTransition();
        var copyTransitionJobHandle = copyTransitionJob.Schedule(this);

        var removeComponentsJob = new RemoveTransition
        {
            entities = entities,
            entityCommandBuffer = endFrameBarrier.CreateCommandBuffer()
        };
        var removeComponentsJobHandle = 
            removeComponentsJob.Schedule(copyTransitionJobHandle);
        return removeComponentsJobHandle;
    }
}

Notice how the CopyTransition job is chained to the RemoveTransition job so they run in that order.

Extracting the pattern

This single-fire idea seems useful. So let’s take our system and make it reusable.

public abstract class SingleFireSystem<TComponent, TBarrier> : JobComponentSystem
    where TComponent : IComponentData
    where TBarrier : BarrierSystem
{
    struct RemoveComponentJob : IJob
    {
        [ReadOnly] public EntityArray entities;
        public EntityCommandBuffer entityCommandBuffer;

        public void Execute()
        {
            for (int i = 0; i < entities.Length; i++)
                entityCommandBuffer.RemoveComponent<TComponent>(entities[i]);
        }
    }

    protected ComponentGroup group;
    protected BarrierSystem barrier;

    protected abstract JobHandle CreateJobHandle(JobHandle inputDeps);

    protected override void OnCreateManager(int capacity)
    {
        barrier = World.GetOrCreateManager<TBarrier>();
        group = GetComponentGroup(ComponentType.ReadOnly(typeof(TComponent)));
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = CreateJobHandle(inputDeps);

        var removeJob = new RemoveComponentJob
        {
            entities = group.GetEntityArray(),
            entityCommandBuffer = barrier.CreateCommandBuffer()
        };
        var removeJobHandle = removeJob.Schedule(jobHandle);

        return removeJobHandle;
    }
}

And now we can reuse this to create single-fire systems.

[UpdateAfter(typeof(MoveForward2DSystem))]
public class TransitionSystem : SingleFireSystem<Transition, EndFrameBarrier>
{
    [ComputeJobOptimization]
    struct CopyTransition : IJobProcessComponentData<Transition, Position2D>
    {
        public void Execute(
            [ReadOnly]ref Transition transition, 
            ref Position2D position) => 
            position = new Position2D { Value = transition.Location };
    }

    protected override JobHandle CreateJobHandle(JobHandle inputDeps)
    {
        var job = new CopyTransition();
        return job.Schedule(this, inputDeps);
    }
}

Conclusion

Now we’ve got a reusable system for handling tasks when we need something that only works for one update. A useful system for triggers, or collisions, etc.


comment: