In our game Academia, we were using this signal system before but it has a major downside: garbage. It incurs garbage whenever a value type parameter is passed (boxing) and read (unboxing) as it accepts parameters using the “object” supertype.

// value here would be boxed if it's a value type like structs // or int, float, bool, etc. public void AddParameter(string key, object value) { ... } // The returned value here would be unboxed in the calling code // if it's a value type public object GetParameter(string key) { ... }

As I was optimizing memory usage, I found out that we’re throwing lots of garbage by using this system. Something must be done. It turns out, the solution is pretty easy.

TypedSignal

I created a new signal class which I call TypedSignal. I called it as such because the parameters are no longer generalized as object. The parameters are now passed as an explicit type to the generic. Here’s the implementation:

public class TypedSignal<T> { public delegate void SignalListener(T parameter); private readonly List<SignalListener> listeners = new List<SignalListener>(1); public void AddListener(SignalListener listener) { Assertion.Assert(!this.listeners.Contains(listener)); // Prevent duplicate listeners this.listeners.Add(listener); } public void RemoveListener(SignalListener listener) { this.listeners.Remove(listener); } public void Dispatch(T parameter) { int listenersCount = this.listeners.Count; for (int i = 0; i < listenersCount; ++i) { this.listeners[i](parameter); // Invoke the delegate } } }

The generic type T here would be the type of the parameter. It could be reference types or value types. We avoid boxing/unboxing by doing it this way. The class also handles its multiple listeners represented as delegates. Here is how it’s used…

First we define the signals:

// Say you have a static class where you collect all signals // used by your game public static class GameSignals { // Bunch of signal definitions here public static readonly TypedSignal<int> DAY_CHANGED = new TypedSignal<int>(); public static readonly TypedSignal<CharacterId> ENEMY_DESTROYED = new TypedSignal<CharacterId>(); public static readonly TypedSignal<FundItem> ADD_FUNDS = new TypedSignal<FundItem>(); public static readonly TypedSignal<FundItem> USE_FUNDS = new TypedSignal<FundItem>(); } // Here are the structs that are used as parameters // Multiple values can be used by simply defining them inside // the struct public readonly struct CharacterId { public readonly int numericId; public readonly string textId; public CharacterId(int numericId, string textId) { this.numericId = numericId; this.textId = textId; } } public readonly struct FundItem { public readonly int amount; public readonly string item; public FundItem(int amount, string item) { this.amount = amount; this.item = item; } }

Here’s how to dispatch or broadcast a signal:

class DayTimer : MonoBehaviour { private void Update() { ... if(this.hour == 24) { // Change the day ++this.day; this.hour = 0; // No boxing here GameSignals.DAY_CHANGED.Dispatch(this.day); } ... } }

Then in some other parts of the game, we implement the listeners:

class CashflowHandler : MonoBehaviour { private void Awake() { GameSignals.DAY_CHANGED.AddListener(OnDayChanged); } // No unboxing here private void OnDayChanged(int day) { // Add or use funds depending on the cashflow int netFlow = ComputeCashflow(); if(netFlow >= 0) { GameSignals.ADD_FUNDS.Dispath(new FundItem(netFlow, "Cashflow")); } else { GameSignals.USE_FUNDS.Dispath(new FundItem(Mathf.Abs(netFlow), "Cashflow")); } } } // Say you have this UI handler that displays the change of funds class FundsDisplayHandler : MonoBehaviour { private void Awake() { GameSignals.ADD_FUNDS.AddListener(OnAddFunds); GameSignals.USE_FUNDS.AddListener(OnUseFunds); } private void OnAddFunds(FundItem parameter) { ShowFundChange("PositiveFundChangeView", parameter.item, parameter.amount); } private void OnUseFunds(FundItem parameter) { ShowFundChange("NegativeFundChangeView", parameter.item, parameter.amount); } private void ShowFundChange(string prefabName, string item, int amount) { // Code here to create the prefab, then initialize // it with the specified item and amount } }

Downsides?

The downside to this is that signals can no longer be a variable. For example, we can no longer do this:

class SignalDispatcher : MonoBehaviour { [SerializeField] private string signalName; // This is a signal that is resolved at runtime depending // on the value of signalName private Signal signalToDispatch; private void Awake() { this.signalToDispatch = GameSignals.Resolve(this.signalName); } // This can be used as a button callback assigned in editor public void Dispatch() { this.signalToDispatch.Dispatch(); } } // Or we can no longer do this private static readonly Dictionary<string, Signal> SIGNAL_MAPPING = new Dictionary<string, Signal>();

These can still be done provided that the type T specified to TypedSignal is the same. If signals have different parameter types, there’s no way to refer to them interchangeably.

However, this is rarely used. If we want such functionality, we’d use our old Signal implementation.

That’s all I have for now. Have a good day!