Unity: Events vs Interfaces

“Events are evil!” is a common opinion among Unity developers. What are the reasons behind this statement and are interfaces an alternative to events? Example implementation at the bottom.

 

 

Problems

A common source for event bugs are timing issues. For an event to work correctly you have to subscribe before it’s triggered and unsubscribe on destroy/disable. Sounds easy?

 

 

Inactive Objects

You can’t get an event before you subscribed. This implies that a script can’t be enabled based on an event if it hasn’t been active before and subscribed.

 

 

Subscribe

Subscribing in Awake could already be too late. Example: Script A triggers an initialized event on awake and script B subscribes in Awake. B will never receive the event when the Awake of script A is called before script B. The order depends on the script execution order which will behave unpredictably. Never depend on the script execution order for your events to arrive. 

 

Most beginners will try to fix this issue by firing the event in Start and subscribing in Awake. Or they add both scripts to the script execution order settings. 

 

This will mask the symptom of the problem but wound solve the cause. They just introduced an implicit temporal coupling between the two scripts. This is bad because an unrelated piece of code can break when the event is fired earlier/later.

 

 

Unsubscribe

If you subscribe to an event you have to unsubscribe. This is especially important if you subscribe regularly. For example, if you subscribe in OnEnable you must unsubscribe in OnDisable. If not you will continue to add listeners each time the object is enabled which will cause your listener to be executed multiple times. If you are unsure unsubscribe more often than you need. Unsubscribing without subscribing first will cause no problems. 

 

 

Listener Execution

The order in which event listeners are executed is random and there is no way to change it. Events can’t be used if the order of the listeners’ execution is important. This would also be temporal coupling -> bad practice.

 

 

Solution

These problems can be avoided by using interfaces instead of events.

In this approach, an interface is defined for each event and the calling code searches for the implementers and executes them. The most basic approach to finding the implementers would be a FindObjectsOfType. If you know where your listeners are located you could also use GetComponentsInChildren and the like. You can also sort the implementers before calling them if the order is important.

 

Advantages

  • No need to subscribe/unsubscribe
  • Can be used to enable GameObjects
  • The execution order of the implementers can be defined

Example Implementation

using System;
using UnityEngine;

public interface IExampleHandler
{
  int ExecutionOrder { get; }

  void OnExampleExecuted();
}

public class InterfaceCaller : MonoBehaviour
{
  void Start()
  {
    IExampleHandler[] handlers =
      (IExampleHandler[])FindObjectsOfType(typeof(IExampleHandler), includeInactive: true);

    // You can sort the handlers if you need to.
    Array.Sort(handlers, (h1, h2) => h1.ExecutionOrder.CompareTo(h2.ExecutionOrder));

    foreach (var handler in handlers)
      handler.OnExampleExecuted();
  }
}

public class InterfaceListener : MonoBehaviour, IExampleHandler
{
  public int ExecutionOrder => executionOrder;

  // Makes the execution order configurable per instance inspector.
  [SerializeField]
  private int executionOrder = 1;

  public void OnExampleExecuted()
  {
    // Return if you don't want the script
    // to receive the call when it's disabled.
    if (enabled == false)
      return;

    Debug.Log("Example executed!");
  }
}