Skip to content

Instantly share code, notes, and snippets.

@jbroadway
Created August 7, 2016 22:49
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbroadway/b94b971d224332f9158988a66f35f22d to your computer and use it in GitHub Desktop.
Save jbroadway/b94b971d224332f9158988a66f35f22d to your computer and use it in GitHub Desktop.
Delegates and yielding in Unity

Delegates and yielding in Unity

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.

@michaelybecker
Copy link

Thank you for this - an excellent tip! (And the only example I could find of GetInvocationList() usage in a Unity context!)

@HongjianTang
Copy link

Thanks for sharing this!

@sourencho
Copy link

you saved me from hours of confusion, thanks <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment