In the last post about multiple scene development, I described a signal system briefly that we used for inter scene communication. This post will be about that system in full detail. I don’t claim that this is the best method. It works for us at the least but it can definitely be improved.

For objects in different scenes to communicate, I wanted something where objects don’t need to have the reference of the object in the other scene. I don’t want objects to have direct references of objects in another scene as much as possible (can’t completely be avoided). The rationale is that I wanted to avoid missing references when excluding loading of certain scenes at certain times. The game should still work even when some scenes don’t exist. I also wanted it to be simple and straightforward to use. This system will be exposed for software modding that we’re going to support later on.

Usage Sample

The gist of the system is that you have Signal instances which you can dispatch. Other parts of the game can then add listeners to a signal. When a signal is dispatched, the listeners will be executed. Parameters can also be added to a signal which can be queried by the listeners. Think of it as an observer pattern without a particular subject.

In our games, we maintain a static class where all game signals are collected:

public static class GameSignals { public static readonly Signal CLOSE_ALL_PANELS = new Signal("CloseAllPanels"); public static readonly Signal HOUR_CHANGED = new Signal("HourChanged""); public static readonly Signal DAY_CHANGED = new Signal("DayChanged"); public static readonly Signal REQUEST_SCENE_TRANSITION = new Signal("RequestSceneTransition"); ... // The rest of the other signals }

Any component in any scene can add listeners to any of these signals. For example:

public class CashflowManager : MonoBehaviour { void Awake() { GameSignals.DAY_CHANGED.AddListener(ProcessCashflow); } void OnDestroy() { GameSignals.DAY_CHANGED.RemoveListener(ProcessCashflow); } private void ProcessCashflow(ISignalParameters parameters) { // Detailed cashflow processing implementation here } }

During gameplay, one of the systems in the game (could be in any scene) will then dispatch the signal:

public class DayTimer : MonoBehaviour { void Update() { ... // Some code if(this.hour == 24) { // New day GameSignals.DAY_CHANGED.Dispatch(); } ... // Some code } }

Some signals require parameters. Dispatching signals with parameters looks like this:

Signal signal = GameSignals.ADD_FUNDS; signal.ClearParameters(); signal.AddParameter(Params.VALUE, dailyCashflow); signal.AddParameter(Params.ITEM, "CashFlow"); signal.Dispatch();

Implementation

Let’s start with parameters. Support for parameters is a must. Most of the time, it’s not used but you will definitely need them at times. Parameters are stored as string and object pair. The following is the interface for adding and getting parameters:

public interface ISignalParameters { void AddParameter(string key, object value); object GetParameter(string key); bool HasParameter(string key); }

An implementation of this interface looks like this:

class ConcreteSignalParameters : ISignalParameters { private Stack<Dictionary<string, object>> parameterStack = new Stack<Dictionary<string, object>>(); public ConcreteSignalParameters() { } public void AddParameter(string key, object value) { this.parameterStack.Peek()[key] = value; } public object GetParameter(string key) { return this.parameterStack.Peek()[key]; } public bool HasParameter(string key) { return this.parameterStack.Peek().ContainsKey(key); } public void PushParameters() { this.parameterStack.Push(NewParameterMap()); } public void PopParameters() { POOL.Recycle(this.parameterStack.Peek()); this.parameterStack.Pop(); } public bool HasParameters { get { return this.parameterStack.Count > 0; } } // Pool of parameter dictionaries private static readonly Pool<Dictionary<string, object>> POOL = new Pool<Dictionary<string, object>>(); private static Dictionary<string, object> NewParameterMap() { Dictionary<string, object> newInstance = POOL.Request(); newInstance.Clear(); return newInstance; } }

Parameters are stored in a Dictionary. Notice that we keep a pool of instances of these and there’s a stack manipulation involved. I’ll explain later.

The Signal class looks like this:

public class Signal { private readonly string name; private ConcreteSignalParameters parameters; public delegate void SignalListener(ISignalParameters parameters); private List<SignalListener> listenerList = new List<SignalListener>(); public Signal(string name) { this.name = name; this.listenerList = new List<SignalListener>(); } public void ClearParameters() { // Lazy initialize because most signals don't have parameters if (this.parameters == null) { this.parameters = new ConcreteSignalParameters(); } this.parameters.PushParameters(); } public void AddParameter(string key, object value) { // This will throw an error if ClearParameters() is not invoked prior to calling this method this.parameters.AddParameter(key, value); } public void AddListener(SignalListener listener) { this.listenerList.Add(listener); } public void RemoveListener(SignalListener listener) { this.listenerList.Remove(listener); } public void Dispatch() { try { if (this.listenerList.Count == 0) { Debug.LogWarning("There are no listeners to the signal: " + this.name); } for (int i = 0; i < this.listenerList.Count; ++i) { // invoke the listeners this.listenerList[i](this.parameters); // note that the parameters passed may be null if there was none specified } } finally { // Pop parameters for every Dispatch // We check if there was indeed parameters because there are signals that are dispatched without parameters if (this.parameters != null && this.parameters.HasParameters) { this.parameters.PopParameters(); } } } public string Name { get { return name; } } }

The Signal class keeps track of parameters and the list of listeners. Listeners are simply implemented as delegates that accept ISignalParameters. I specifically chose delegates so that listeners are easier to write. One could just write a method and that can be used already. A component may also listen to multiple signals. The listeners could just be different methods within the component. If it were implemented as an interface, writing listeners would be more tedious.

The Dispatch() method simply invokes all listeners then pops the parameters.

Why the usage of stack in ConcreteSignalParameters?

The old implementation of this class only maintains one instance of parameter dictionary. Unfortunately, this causes a bug when a listener of a signal dispatches the same signal again with new set of parameters. When this happens, the next listeners will use the new parameters instead of the original ones. We needed a way to somehow store parameters before dispatching the same signal. Thus, the usage of stack that you see here. The assumption here is Signal.ClearParameters() should always be invoked prior to Signal.AddParameter(). A new parameter dictionary is pushed whenever ClearParameters() is called. A parameter dictionary is popped in every Dispatch(). This is also the reason why we used an interface when passing parameters to listeners. Listeners need not know about the stack manipulation of parameters.

When to use?

This system is obviously less efficient than calling a method directly. We only use this for things that are not required to run every frame. This can be mitigated by moving objects to their proper scene. For example, if an object requires a reference from another scene for its Update(), maybe that object should belong to that scene. If this is not possible, then we may need that reference of the object from another scene. One can use GameObject.Find() but I try to avoid that approach. We use another system for querying objects/values which will be a topic for another day.