Skip to content
Ilan edited this page Apr 8, 2024 · 10 revisions

The Problem

When coding, we often reference a script to another. For example, the MechaController script uses the Unity Rigidbody script and decides when the method function should be called. This works fine and is what is done most of the time. However, it creates a dependency between MechaController and Rigidbody, one needs the other to work. If we remove the Rigidbody script, the MechaController would not work as expected or even crash crash.

Of course, to avoid dependency, we can make the field nullable and check that it is assigned before using it. While it works, it is not very pretty. And worse, imagine a script A that relies on script B that relies on script C, which itself relies on the script A. This create a circular dependency.

This project have hundreds of file, if we are not careful, it could end up as a web with files dependent on another and another and another. And when we just wanted to test a single script, we end up forced to another 10 mores.

The solution

What is it?

There are various way to tackle this problem but my favorite so far is definitely events.

The events paradigm is composed of two parts:

  • The subscriber: It listens to the given event and receive a notification when the event is triggered.
  • The emitter: Triggers the event. All the subscriber will receive the notification and handle the event as they wish.

To better understand events, let's make an analogy. As a user on social networks, you follow your favorite artist. When they publish a new art or song, you receive the notification because you are subscribed. If one day you decide to unsubscribe, you won't receive anything the notification if a new song is published. But other followers will! And both the artist and you will be able to keep living your life. And the best part: you can follow them again whenever you want!

How we use it? (In development)

Unity has its own delegate for events, called UnityEvent which has a nice editor, useful for scripts. So we are using it!

As mentioned before, to avoid dependencies, all the scripts will create a single dependency with a class called EventManager (in EventManager.cs). This class has multiple available static methods (which in fact call a Singleton).

public static void AddListener(string eventName, UnityAction listener) // subscribe to the non-typed event
public static void AddListener(string eventName, UnityAction<object> listener) // subscribe to the typed event

and its counter part

// Do nothing if there is no eventName/listener
public static void RemoveListener(string eventName, UnityAction listener)
public static void RemoveListener(string eventName, UnityAction<object> listener)

These methods are used to subscribe to the given eventName. An eventName can have as many listeners as needed. There is an overload to pass additional parameters. You may have noticed the second parameter is of type UnityAction it is of type object. This is a delegate for functions of type void () and void (object). The object type is a lazy type. Basically, any class can be passed as a parameter of it. To then get the passed object as a parameter, simply cast it as (MyClass)obj. This may be dangerous as someone may not pass the expected type so we will see below how to properly do it.

When you want to trigger your events, use the following methods:

public static void TriggerEvent(string eventName) // Triggers all the non-typed events of name "eventName"
public static void TriggerEvent(string eventName, object data) // Triggers all the typed events of name "eventName"

⚠️ Depending on the overload you chose, the other type won't be triggered. The typed and non-typed events are distinct.

Lisibility

You may have noticed that the event name is a string. However, since it is a string, you may do some typo or want to change the event name at some point without having to change it for all the scripts.

That is why that we have a class called Constants in Constants.cs that have all the name constants of the events. It contains the sub-classes Events and TypedEvents with many constant string.

Hence, instead of writting

TriggerEvent("OnPause")

you can do

TriggerEvent(Constants.Events.OnPause)

It is way more explicit, avoid typos and, on some IDE, you can CTRL+LCLICK on the constant name and have a pop-up with all the scripts that refenrece this constant. Useful to see which scripts listen/trigger the given event. If you see an event name hardcoded as a string, this is because it hasn't been migrated yet. So if you have time to change it, please do so.

TLDR: Do use constants as done above.

Examples

Here is a simple case to illustrate the usage.

Non-typed Events

MyListener.cs

public class MyListener : MonoBehaviour
{
    // we subscribe when the gameobject becomes "Active"
    private void OnEnable()
    {
         //                             notice here that we don't put parenthesis: we don't want to call the method, just reference it
         EventManager.AddListener("myEvent", OnEventTriggered)
    }
    // we unsubscribe when it becomes "Inactive
    // because otherwise, it will keep listening and do some actions we may not want to do in this state (such as shooting?)
    private void OnDisable()
    {
         EventManager.RemoveListener("myEvent", OnEventTriggered)
    }

    public void OnEventTriggered()
    {
        Debug.Log("The event was triggered!!!")
    }
}

MyEmitter.cs

public class MyEmitter : MonoBehaviour
{
    private void Start()
    {
        EventManager.TriggerEvent("myEvent")
    }
}  

Output

MyListener: The event was triggered!!!

Typed Events

Let's keep the previous classes defined aboved and add a new one.

MyTypedListener.cs

public class MyTypedListener : MonoBehaviour
{
    private void OnEnable()
    {
         // notice how here it the same as MyListener.cs even though it is a typed event
         EventManager.AddListener("myTypedEvent", OnEventTriggered)
    }
    
    private void OnDisable()
    {
         // notice how here it the same as MyListener.cs even though it is a typed event
         EventManager.RemoveListener("myTypedEvent", OnEventTriggered)
    }
    
    // the object parameter is what makes it a typed event
    public void OnEventTriggered(object data)
    {
        // here we safely cast the object type to int or skip if its not an integer
        if (data is int number)
            Debug.Log("The event contains " + number + " !!!")
        // same here but as a List
        if (data is List list)
            Debug.Log("The event contains a list of size " + list.Length + " !!!")
        // we could add an "else" section and throw an Exception if it is not the expected type. 
    }
}

MyEmitter.cs

public class MyEmitter : MonoBehaviour
{
    private void Start()
    {
        EventManager.TriggerEvent("myEvent") // 1 listener
        EventManager.TriggerEvent("myEvent", 86) // 0 listener for this typed event
        EventManager.TriggerEvent("myTypedEvent") // 0 listener for this non-typed event
        EventManager.TriggerEvent("myTypedEvent", 86) // 1 listener
        EventManager.TriggerEvent("myTypedEvent", new List()) // 1 listener
        EventManager.TriggerEvent("myTypedEvent", null) // 1 listener. This doesn't print anything as it is not of type int nor List but we do 
enter in the method.
        
        EventManager.AddListener("myEvent", () => Debug.Log("Second Listener!) // Add a second listener with a lambda function
        EventManager.TriggerEvent("myEvent") // 2 listener
    }
}  

Output

MyListener: The event was triggered!!!
MyTypedListener: The event contains 86 !!!
MyTypedListener: The event contains a list of size 0 !!!")
MyListener: The event was triggered!!!
MyEmitter: Second Listener!

Previous     •     Home     •     Next
Clone this wiki locally