We already have our own Signals framework as described in this post. It’s basically a messaging system where the parties, dispatcher and handlers, don’t have references to each other. Think of it as an elaborate Observer Pattern.

This current system, however, is implemented in OOP. As we are slowly inching towards Unity’s Pure ECS where reference types can’t be used, I needed a Signals framework that can be used in this environment. In other words, I needed a Signals system that is implemented in ECS.

In this post, I’ll show how I did it, albeit a bit more simplified. I’ll only show the non jobified version so it’s easier to understand.

Dispatching Signals

We can model a signal as an entity with a Signal component and another component that will act as the parameters of the signal. This other component will also act as a filter to the system that will handle such signal. We can define it in code like this:

// Signal is just a tag component public struct Signal : IComponentData { public static void Dispatch<T>(EntityManager entityManager, T signalComponent) where T : struct, IComponentData { Entity entity = entityManager.CreateEntity(); entityManager.AddComponentData(entity, new Signal()); entityManager.AddComponentData(entity, signalComponent); } // An EntityCommandBuffer could also be used public static void Dispatch<T>(EntityCommandBuffer buffer, T signalComponent) where T : struct, IComponentData { Entity entity = buffer.CreateEntity(); buffer.AddComponent(entity, new Signal()); buffer.AddComponent(entity, signalComponent); } }

Handling Signals

Signal handlers or responders are implemented as a system. Here’s an example:

// Say this is the signal parameter struct TakeDamage : IComponentData { public Entity target; public int damageAmount; } // This is the signal handler system class TakeDamageSystem : ComponentSystem { private EntityQuery query; protected override void OnCreate() { this.query = GetEntityQuery(typeof(Signal), typeof(TakeDamage)); } protected override void OnUpdate() { this.Entities.With(this.query).ForEach(delegate(ref TakeDamage parameter)) { // Deal the damage to the target here } } }

Making the Framework

From this modelling, we can define some common code that we could reuse every time we deal with signals. One of the utility classes I’ve made is the following which I use inside systems that needs to handle signals:

public class SignalHandler<T> where T : struct, IComponentData { private readonly EntityQuery query; private ArchetypeChunkEntityType entityType; private ArchetypeChunkComponentType<T> componentType; private readonly ComponentSystemBase system; public delegate void Listener(Entity entity, T component); private readonly List<Listener> listeners = new List<Listener>(1); public SignalHandler(ComponentSystemBase system, EntityQuery query) { this.system = system; this.query = query; } public void AddListener(Listener listener) { this.listeners.Add(listener); } public void Update() { this.entityType = this.system.GetArchetypeChunkEntityType(); this.componentType = this.system.GetArchetypeChunkComponentType<T>(); NativeArray<ArchetypeChunk> chunks = this.query.CreateArchetypeChunkArray(Allocator.TempJob); for (int i = 0; i < chunks.Length; ++i) { Process(chunks[i]); } chunks.Dispose(); } private void Process(ArchetypeChunk chunk) { NativeArray<Entity> entities = chunk.GetNativeArray(this.entityType); NativeArray<T> components = chunk.GetNativeArray(this.componentType); int count = chunk.Count; for (int i = 0; i < count; ++i) { Publish(entities[i], components[i]); } } private void Publish(Entity entity, T component) { for (int i = 0; i < this.listeners.Count; ++i) { this.listeners[i].Invoke(entity, component); } } }

It uses delegates as signal handler. They are maintained in a list so it can hold multiple handlers. Here is how it can be used:

class TakeDamageSystem : ComponentSystem { private EntityQuery signalQuery; private SignalHandler<TakeDamage> signalHandler; protected override void OnCreate() { this.signalQuery = GetEntityQuery(typeof(Signal), typeof(TakeDamage)); this.signalHandler = new new SignalHandler<T>(this, this.signalQuery); this.signalHandler.AddListener(HandleSignal); } private void HandleSignal(Entity entity, TakeDamage parameter) { // Deal the damage to the target here } protected override void OnUpdate() { this.signalHandler.Update(); } }

Template Signal Handler System

After writing so many signal handler systems, I realized I could use a Template Pattern to simplify creation of these systems. Ideally, only one signal should be handled by a signal handler system, anyway. So I made this template system class.

public abstract class SignalHandlerComponentSystem<T> : ComponentSystem where T : struct, IComponentData { private EntityQuery signalQuery; private SignalHandler<T> signalHandler; protected override void OnCreate() { this.signalQuery = GetEntityQuery(typeof(Signal), typeof(T)); this.signalHandler = new SignalHandler<T>(this, this.signalQuery); this.signalHandler.AddListener(OnDispatch); } protected abstract void OnDispatch(Entity entity, T signalComponent); protected override void OnUpdate() { this.signalHandler.Update(); } }

TakeDamageSystem can now be simplified to:

private class TakeDamageSystem : SignalHandlerComponentSystem<TakeDamage> { protected override void OnDispatch(Entity entity, TakeDamage signalComponent) { // Deal the damage to the target here } }

Signal Entity Destruction

The ideal implementation is that signal handler systems should no longer need to destroy the signal entities. Destroying signal entities is not a good idea anyway because there may be multiple handlers for a certain signal.

What we can do is create another system whose only purpose is to destroy signals. This can be easily made as:

public class DestroySignalsSystem : ComponentSystem { private EntityQuery query; protected override void OnCreate() { this.query = GetEntityQuery(typeof(Signal)); } protected override void OnUpdate() { this.EntityManager.DestroyEntity(this.query); } }

Then our template class should have this attribute so it will run before DestroySignalsSystem:

[UpdateBefore(typeof(DestroySignalsSystem))] public abstract class SignalHandlerComponentSystem<T> : ComponentSystem where T : struct, IComponentData { ... }

Handler Runs Before Dispatcher

There’s a case that we need to handle. Let’s say our project got bigger with lots of systems. At some point, there may be a case that a system dispatches a signal but its handler system is executed before it. What happens is the signal would be destroyed without having it handled by the handler system. That’s a potential bug that could be hard to trace.

// Say this is the execution order SystemA SystemB DestroySignalsSystem // If SystemB dispatches a signal that is handled by SystemA, // it may no longer be handled since it will be destroyed // before the next frame begins.

What we need to do is we should not destroy signals on the same frame. We should let the signal live for another frame. We can do this by adding another tag component that lets DestroySignalsSystem know that such signal has already passed another frame. We only destroy signals that have this tag component. We need a new component and a system that adds this component:

// A tag component that identifies a signal entity that it has passed a single frame public struct SignalFramePassed : IComponentData { } // Adds SignalFramePassed component to signal entities // that doesn't have this component yet [UpdateAfter(typeof(DestroySignalsSystem))] public class SignalFramePassedSystem : ComponentSystem { private EntityQuery query; protected override void OnCreate() { this.query = GetEntityQuery(typeof(Signal), ComponentType.Exclude<SignalFramePassed>()); } protected override void OnUpdate() { this.PostUpdateCommands.AddComponent(this.query, typeof(SignalFramePassed)); } }

We then edit the entities query of DestroySignalsSystem to only include entities that has the SignalFramePassed component

public class DestroySignalsSystem : ComponentSystem { private EntityQuery query; protected override void OnCreate() { this.query = GetEntityQuery(typeof(Signal), typeof(SignalFramePassed)); } protected override void OnUpdate() { this.EntityManager.DestroyEntity(this.query); } }

As we are doing this, we’re introducing another problem. Since signals live in 2 frames, it may happen that they will be handled by signal handlers more than once. That’s another cause for bugs.

This can be solved in the template class. We can add another tag component that identifies a signal that it’s already handled by the signal handler system.

[UpdateBefore(typeof(DestroySignalsSystem))] public abstract class SignalHandlerComponentSystem<T> : ComponentSystem where T : struct, IComponentData { private EntityQuery signalQuery; private SignalHandler<T> signalHandler; // Tag that identifies a signal entity that it has been already processed public struct ProcessedBySystem : IComponentData { } protected override void OnCreate() { this.signalQuery = GetEntityQuery(typeof(Signal), typeof(T), ComponentType.Exclude<ProcessedBySystem>()); this.signalHandler = new SignalHandler<T>(this, this.signalQuery); this.signalHandler.AddListener(OnDispatch); } protected abstract void OnDispatch(Entity entity, T signalComponent); protected override void OnUpdate() { this.signalHandler.Update(); this.PostUpdateCommands.AddComponent(this.signalQuery, typeof(ProcessedBySystem)); } }

Now, we only process signals that doesn’t have the ProcessedBySystem component yet. After executing the handler, we add the ProcessedBySystem so it will no longer be processed by the signal handler system. This solves the problem of having a handler system process a signal more than once.

That’s it! I hope that was simple enough and useful.