Social Scripting Part 2

In Part 1 of this series we discussed several means by which Unity allows you to get your scripts talking back and forth between each other. That included direct references, their legacy and new message system, and their new event system as well.

In this post we will examine the options available to you as a language feature of C#, just in case you don’t want to rely on the options Unity provided. Although their event system is quite powerful and easy to use, keeping your events native will allow your code to be more easily reused in other projects or ported to other engines. I consider this part of the series intermediate level, and will expect you to have a working knowledge of C#.

Delegates

Create a new scene. Add three GameObjects: “Controller”, “Foo”, and “Bar”. Parent Foo and Bar to Controller and then create and assign a new C# script for each object according to its name. The contents of each script are provided below:

using UnityEngine;
using System.Collections;

public class Controller : MonoBehaviour
{
	void Start ()
	{
		Foo foo = GetComponentInChildren<Foo>();
		Bar bar = GetComponentInChildren<Bar>();

		foo.doStuff = bar.OnDoStuff;
		foo.TriggerStuffToDo();
	}
}
using UnityEngine;
using System.Collections;

public delegate void MyDelegate ();

public class Foo : MonoBehaviour
{
	public MyDelegate doStuff;

	public void TriggerStuffToDo ()
	{
		if (doStuff != null)
			doStuff();
	}
}
using UnityEngine;
using System.Collections;

public class Bar : MonoBehaviour
{
	public void OnDoStuff ()
	{
		Debug.Log("I did stuff");
	}
}

The delegate was globally defined above the declaration of the Foo class within the “Foo.cs” script. You can tell because it uses the “delegate” keyword. Within that line we show a few important things. We are basically defining a method that will be implemented elsewhere in a way similar to the method declaration you might put inside of an interface. The return type and parameters of the delegate declaration must be followed exactly in any method that tries to become the observer for this delegate, although they do not need to use the same method name. You can see this for yourself because the “OnDoStuff” method of Bar was able to be assigned to the “MyDelegate” definition – this is because they both returned void and did not take any parameters.

The name assigned to the delegate definition is still important. It is used inside the Foo class as a Type from which to declare a property. You are basically saying you want a pointer to a method, and since it is public, it can be assigned at a later point. The delegate can be assigned like any other property, by referencing only the name of a method to assign – don’t use the parenthesis. To actually invoke the method, you just treat the delegate property as if it was a method in your class – you do use the parenthesis and pass along any required parameters.

Run the sample, and Bar should do some work, logging “I did stuff” to the console. At the moment this sample seems like a lot of extra work to do something we could have accomplished in the Foo script alone. However, the beauty of this system is that we now have more options. By delegating work that needs to be done, the way that work is fulfilled can change – even at run time. It’s also possible we don’t want any work to be done, in which case we simply don’t assign the delegate.

The other benefit of this system is that it is loosely coupled. The scripts Foo and Bar are completely ignorant of each other, making them very reusable, and yet they work together as efficiently as if they had direct references to each other. The only script which is not loosely coupled is our controller script, but controller scripts are almost never reusable anyway and this is to be expected.

There are a few gotchas when working with delegates. The first issue to point out is that they keep strong pointers to objects – this means that they can keep an object alive that you thought would have gone out of scope. To demonstrate why this is a problem, modify the Controller script so that you destroy the Bar object after assigning it as a delegate but before triggering the delegate call. Note that I had to modify the Start method to an Enumerator so that I could wait a frame – Unity waits one frame before actually destroying GameObjects.

IEnumerator Start ()
{
	Foo foo = GetComponentInChildren<Foo>();
	Bar bar = GetComponentInChildren<Bar>();

	foo.doStuff += bar.OnDoStuff;
	GameObject.Destroy(bar.gameObject);
	yield return null;
	foo.TriggerStuffToDo();
}

Run the example now, and you will see that the Bar object still performs its work even though the GameObject has already been destroyed. This isn’t necessarily an issue in the demo right now, but if the Bar script tried to reference its GameObject or any Components that had been on it, such as Transform, then you will get a “MissingReferenceException: The object of type ‘Bar’ has been destroyed but you are still trying to access it.

Revert all of your changes to the original version of the script. Uncomment line 12 of Foo.cs so that our script doesn’t compare doStuff to null, and then uncomment line 11 of Controller.cs where we actually assign it. Run the scene again. This demonstrates that you should always check that a delegate exists before calling it, or else you risk a NullReferenceException.

Revert your changes. Now modify line 11 of Controller.cs to:

foo.doStuff += bar.OnDoStuff;

Notice that the line looks almost identical, we merely added a “+” in front of the assignment operator. With this method of assignment you are essentially stacking objects into the single delegate property, and when it is invoked, all delegates on the stack will be called. To see this in action, you can duplicate line 11 so that there are several additions of bar.OnDoStuff. If you run the scene now you will see a log for each time you added the delegate.

After your last delegate assignment, add another line:

foo.doStuff -= bar.OnDoStuff;

This time we added a “-” before the assignment operator which tells the delegate to remove an object from its delegate stack. If you run the scene now you will see that there is one less log than there had previously been, although if you added more than you removed it should still be logging something. This is important to note, because if you ever get out of balance on adding and removing delegates, you may find yourself executing code more frequently than you anticipated.

Note that there are no negative consequences to attempting to unregister a delegate beyond the number of times that you had registered it. This is helpful because you could for example, unregister any delegate that might have been registered whenever you prepare to dispose of an object, without needing to check if you actually had registered it. In the case of a Monobehaviour, the OnDisable or OnDestroy methods would be great opportunities to unregister all of your listeners. Note also that you can not rely on the Destructor of a native C# object to remove a delegate, because the delegate itself is keeping the object alive.

If you assign a delegate using only the assignment operator “=” without a plus or minus, the entire stack of delegate objects will be replaced with whatever you assign the new value to. For example, you can add “+=” multiple copies of bar.OnDoStuff and then assign “=” a single copy of bar.OnDoStuff, and now only the newly assigned handler will do any work. You can also assign null to the delegate which is an easy way of saying, “Hey remove all of the listeners from this”. The assignment of a delegate can be a double edged sword – it is nice to be able to easily remove all listeners, however, you risk other scripts removing or replacing a delegate which you intended to keep registered. The solution to this issue is to turn your delegate into an event.

Before I finish discussing delegates, there is one last bit of information I want to share. Because it is very common to need to define delegates, C# has predefined several to save you the effort. To use them you will need to reference the System namespace – add a “using System;” line to your script. See below for several use cases of “Action”, a generic delegate with a return type of void, and “Func” which is another delegate with a non void return type (the last parameter type in its generic definition is the type to be returned):

public Action doStuff;
public Action<int> doStuffWithIntParameter;
public Action<int, string> doStuffWithIntAndStringParameters;
public Func<bool> doStuffAndReturnABool;
public Func<bool, int> doStuffWithABoolAndReturnAnInt;

and here are sample methods that could observe them:

public void OnDoStuff () {}
public void OnDoStuffWithIntParameter (int value) {}
public void OnDoStuffWithIntAndStringParameters (int age, string name) {}
public bool OnDoStuffAndReturnABool () { return true; }
public int OnDoStuffWithABoolAndReturnAnInt (bool isOn) { return 1; }

Events

All events are delegates, but not all delegates are events. To make a delegate an event you must add the word “event” to its property declaration:

public delegate void MyDelegate ();

public class Foo : MonoBehaviour
{
	public event MyDelegate doStuff;

	public void TriggerStuffToDo ()
	{
		if (doStuff != null)
			doStuff();
	}
}

When the delegate is registered as an event in this way, you can no longer use the assignment operator directly. You can now only increment “+=” and decrement “-=” the listeners. This forces the scripts which add listeners to be responsible for themselves and stop listening to the event when they can.

When using events it is common to use a particular pre-defined delegate called an “EventHandler”. This delegate is also generic but not in the same way as “Action” and “Func”. The EventHandler always passes exactly two parameters, the first being an “object” representing the sender of the event and the second being an “EventArgs” which will hold any information relevant to the event. If you reference the generic version of the EventHandler you are defining what subclass of EventArgs is going to be passed along. Following are some examples of their use:

using UnityEngine;
using System;
using System.Collections;

public class MyEventArgs : EventArgs {}

public class Foo : MonoBehaviour
{
	// Define EventHandlers
	public event EventHandler doStuff;
	public event EventHandler<MyEventArgs> doStuff2;

	// These methods can be added as observers
	public void OnDoStuff (object sender, EventArgs e) {}
	public void OnDoStuff2 (object sender, MyEventArgs e) {}

	public void Start ()
	{
		// Here we add the method as an observer
		doStuff += OnDoStuff;
		doStuff2 += OnDoStuff2;

		// Here we invoke the event
		if (doStuff != null) doStuff( this, EventArgs.Empty );
		if (doStuff2 != null) doStuff2( this, new MyEventArgs() );
	}
}

One last note is that your events can be made static. Static events are ones which do not exist within the instance of a class, but within the class itself. You might consider this pattern when there are multiple objects you wish to listen to events from, without wanting to get a reference to each one. For example, you could have a game controller listen to a static enemy event called “diedEvent”. Each event would pass the enemy which died along as the sender and the game controller could know about each one even though it only had to register to listen to this event a single time. See below for an example:

using UnityEngine;
using System;

public class Controller : MonoBehaviour
{
	void OnEnable ()
	{
		Enemy.diedEvent += OnDiedEvent;
	}

	void OnDisable ()
	{
		Enemy.diedEvent -= OnDiedEvent;
	}

	void OnDiedEvent (object sender, EventArgs e)
	{
		// TODO: Award experience, gold, etc.
	}
}
using UnityEngine;
using System;

public class Enemy : MonoBehaviour
{
	public static event EventHandler diedEvent;

	void OnDestroy ()
	{
		if (diedEvent != null)
			diedEvent(this, EventArgs.Empty);
	}
}

Pros and Cons of Events

Events are a powerful design pattern, one which makes it very easy to write flexible and reusable code which is also very efficient. They do require a certain level of responsibility to use correctly, or you may suffer some unexpected consequences, but most of these can be easily anticipated by making sure you have a corresponding unregister statement to clean up each of your register statements.

Delegates and Events are a great solution to solve scenarios where you want one-to-one communication and one-to-many communication scenarios. They do not offer efficient solutions to many-to-many or many-to-one scenarios (where the “many” is implemented as many different classes). For example, in the TextBased RPG example I have been working on, it would be nice to allow any script, whether it is based on MonoBehaviour or native C# script to post an event that some text should be logged to the interface. This could happen anywhere in my program as a result of any kind of object and action.

There are two obvious solutions to this problem, but I don’t recommend you use either. The first solution, using events, would be to make the controller which knows to listen for messages to be posted subscribe to the event of each and every class that will actually post a message. What a nightmare. The larger your project grows the harder that would be to maintain.

Another approach would not use events, and would have the objects that wish to post a message acquire a reference to the object which displays the message (perhaps you would make it a singleton) and invoke a method on it directly. This is slightly better than the first solution, but now all of your scripts are tightly coupled to the object displaying a message. We might want to reuse these scripts later with a full-blown visual RPG that doesn’t have a text window, and in that case we would have to manually disconnect that bit of logic in a potentially large number of scripts across your project.

The solution I would pick is a custom NotificationCenter, which I will present in part 3, but since we haven’t gotten that far, I will show one last option.

using System;

public class TextEventArgs : EventArgs
{
	public readonly string text;
	public TextEventArgs (string text)
	{
		this.text = text;
	}
}

public static class ObjectExtensions
{
	public static event EventHandler<TextEventArgs> displayEvent;

	public static void Display (this object sender, string text)
	{
		if (displayEvent != null)
			displayEvent(sender, new TextEventArgs(text));
	}
}

This demonstration uses something called “extensions” which is a way to add functionality to a class that hadn’t previously been there. Extensions must be defined in a static class. The functionality you are adding will be a static method, and the first parameter (begins with “this”) determines what class you are adding functionality to. In my example I added the functionality to System.object which means that ANYTHING whether inheriting from MonoBehaviour or not, can now trigger a display text event. See below for an example of a script posting the event, and another script listening to it.

public class Enemy : MonoBehaviour
{
	void OnDestroy ()
	{
		this.Display(string.Format("The {0} died.", this.GetType().Name));
	}
}
public class UIController : MonoBehaviour
{
	void OnEnable ()
	{
		ObjectExtensions.displayEvent += OnDisplayEvent;
	}

	void OnDisable ()
	{
		ObjectExtensions.displayEvent -= OnDisplayEvent;
	}

	void OnDisplayEvent (object sender, TextEventArgs e)
	{
		// TODO: a more complete sample would have a reference
		// to an interface object and append the text there
		Debug.Log(e.text);
	}
}

And now we have an event based solution for many-to-one or many-to-many communication! The solution I will probably be using for the rest of this project is the Notification Center which will be presented in Part 3, although the event based architecture presented here is capable of completing any professional level project. It is really a matter of personal preference.

2 thoughts on “Social Scripting Part 2

Leave a Reply

Your email address will not be published. Required fields are marked *