I discovered something interesting in C#'s event delegate system when combined with coroutines.
First, I started with a simple event delegate example:
using UnityEngine;
using System.Collections;
public class DelegateTest : MonoBehaviour {
public delegate void NonYieldingEventHandler ();
public static event NonYieldingEventHandler OnNonYieldTestEvent;
void OnEnable () {
OnNonYieldTestEvent += TestNonYieldEventHandler1;
OnNonYieldTestEvent += TestNonYieldEventHandler2;
}
void Update () {
if (Input.GetKeyDown (KeyCode.X)) {
if (OnNonYieldTestEvent != null) {
OnNonYieldTestEvent ();
}
}
}
public void TestNonYieldEventHandler1 () {
Debug.Log ("TestNonYieldEventHandler1()");
}
public void TestNonYieldEventHandler2 () {
Debug.Log ("TestNonYieldEventHandler2()");
}
}
Here we have two methods being called each time X is pressed, each outputting a line to the debug console. Whenever you press X, you should see the following two entries appended to your console output:
TestNonYieldEventHandler1()
TestNonYieldEventHandler2()
But what if we want to make our event handlers into coroutines because they might be doing something that can slow things down. Can't we just modify the delegates to return IEnumerator
and pass them to StartCoroutine()
?
Here's a modification of the previous example with this change:
using UnityEngine;
using System.Collections;
public class DelegateTest : MonoBehaviour {
public delegate IEnumerator YieldingEventHandler ();
public static event YieldingEventHandler OnYieldTestEvent;
void OnEnable () {
OnYieldTestEvent += TestYieldEventHandler1;
OnYieldTestEvent += TestYieldEventHandler2;
}
void Update () {
if (Input.GetKeyDown (KeyCode.X)) {
if (OnYieldTestEvent != null) {
StartCoroutine (OnYieldTestEvent ());
}
}
}
public IEnumerator TestYieldEventHandler1 () {
Debug.Log ("TestYieldEventHandler1() started");
yield return new WaitForSeconds (2f);
Debug.Log ("TestYieldEventHandler1() ended");
}
public IEnumerator TestYieldEventHandler2 () {
Debug.Log ("TestYieldEventHandler2() started");
yield return new WaitForSeconds (2f);
Debug.Log ("TestYieldEventHandler2() ended");
}
}
This runs successfully, but if you look at the console, it shows different output than we expected.
Based on the first example, it's fair to assume the output would look like this:
TestYieldEventHandler1() started
TestYieldEventHandler2() started
// 2 second pause
TestYieldEventHandler1() ended
TestYieldEventHandler2() ended
Instead, we get the following:
TestYieldEventHandler2() started
// 2 second pause
TestYieldEventHandler2() ended
The first event handler that was added is never fired.
To work around this, we can use the GetInvocationList()
method to fire each one independently:
void Update () {
if (Input.GetKeyDown (KeyCode.X)) {
if (OnYieldTestEvent != null) {
foreach (YieldingEventHandler handler in OnYieldTestEvent.GetInvocationList ()) {
StartCoroutine(handler.Invoke ());
}
}
}
}
Now it produces the output we expected.
This also highlights that there's more we can do with event handlers than invoking them, for example we can add error handling around them:
void Update () {
if (Input.GetKeyDown (KeyCode.X)) {
if (OnYieldTestEvent != null) {
foreach (YieldingEventHandler handler in OnYieldTestEvent.GetInvocationList ()) {
try {
StartCoroutine(handler.Invoke ());
} catch (Exception e) {
Debug.Log (e.Message);
}
}
}
}
}
Now even if one handler fails, the others will continue to be called, and we have the option to act on the one that failed.
Event delegates give you a lot of flexibility in building event-driven apps in Unity, and coroutines give you a lot of control over the division of work between frames. In practice, you sometimes have to manage the balance of the two with care.
Thank you for this - an excellent tip! (And the only example I could find of GetInvocationList() usage in a Unity context!)