Notifications
Article
Single Fire Components
Updated 2 months ago
88
0
Design Patterns / ECS
Cover Photo by rawpixel
(Originally published here)
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.

Tags:ECS
Cameron MacFarland
Software Engineer - Programmer
1
Comments