Social Scripting Part 3

Welcome to the final post of Social Scripting. As a quick recap, Part 1 discussed several “Social” architectures offered by Unity, such as their Messaging system (both the legacy version and new version) and their new EventSystem. Part 2 discussed purely C# options including delegates and events.

The goal of this post is to create a custom Notification Center, which combines several features I like from across the board while adding a few new possibilities to boot.

The features this system will provide include:

  1. Any object can post any notification
  2. Any object can efficiently observe any notification by a direct reference to a handler
  3. Notification observation can target a specific sender or any sender
  4. Notifications can pass along an optional EventArgs parameter
  5. Notifications do not have to be handled, so you don’t have to check that they exist
  6. Handlers are only added once per notification, even if you “accidentally” add it multiple times (see some of the gotchas in working with Events in part 2)

As a side note, this Notification Center was inspired from my days in native iOS development, where I used their convenient NSNotificationCenter.

Creating the Notification Center

Let’s get started. Create a new script called “NotificationCenter.cs”. This class will use the Singleton Design Pattern:

public class NotificationCenter
{
	#region Singleton Pattern
	public readonly static NotificationCenter instance = new NotificationCenter();
	private NotificationCenter() {}
	#endregion
}

Implementation in this way guarantees that there will always be one and only one NotificationCenter, and that it will already exist before any class could try to use it. If the class inherited from MonoBehaviour this design pattern would have been trickier to implement. You can’t use Constructors for MonoBehaviours (much less private ones) and you may have to modify Unity’s script execution order (from the menu bar choose “Edit->Project Settings->Script Execution Order” to make sure that it gets created before other scripts attempt to use it. Alternatively, you could make it work by creating an initialization scene that created a GameObject with this script, marked it as DontDestroyOnLoad, and then loaded the rest of the game.

I know that there are a bunch of programmers out there who bash Singletons, but I don’t feel the need to justify myself on this point. The singleton pattern just makes sense in this case and is very easy to work with, and plus, even Apple implemented their version the same way. For anyone worried about having references to this script scattered all throughout their projects, I’ll use an extension workaround similar to what I did in the end of Part 2, which will allow you to keep the reference points to single place.

The Notification Center will essentially just be a big table that matches strings (the name of a notification) to another table matching objects (the sender of the notification) to a list of event handlers (the observer of the notification). In order to clean up the code for this, I want to show a neat trick – add this using statement to the top of your script:

using SenderTable = System.Collections.Generic.Dictionary<System.Object, System.Collections.Generic.List<System.EventHandler>>;

This allows us to use a shortcut “SenderTable” in our script everywhere that we would otherwise have had to type out the really long type of a Generic Dictionary holding an object to a list of Event Handlers. With it in place, our class can now implement its only property like this:

private Dictionary<string, SenderTable> _table = new Dictionary<string, SenderTable>();

It is as if we had defined a custom class called SenderTable that was a very specific subclass implementation of a Generic Dictionary, but we didn’t actually have to create that class. Add these convenience methods for working with our new Table and see how much easier it is to understand thanks to the using statement:

private SenderTable GetSenderTable (string notificationName)
{
	if (!_table.ContainsKey(notificationName))
		_table.Add(notificationName, new SenderTable());
	return _table[notificationName];
}

private List<EventHandler> GetObservers (SenderTable subTable, System.Object sender)
{
	if (!subTable.ContainsKey(sender))
		subTable.Add(sender, new List<EventHandler>());
	return subTable[sender];
}

The next step is adding the ability to add observers to the notification center. At a minimum, we need to know the name of a notification to observe, and the EventHandler which will be called when the notification is actually posted. I will also use a feature called function overloading so that we can choose to have an extra optional parameter for cases where users only want to handle notifications if the notification was posted by a particular object.

public void AddObserver (EventHandler handler, string notificationName)
{
	AddObserver(handler, notificationName, null);
}

public void AddObserver (EventHandler handler, string notificationName, System.Object sender)
{
	if (handler == null)
	{
		Debug.LogError("Can't add a null event handler for notification, " + notificationName);
		return;
	}

	if (string.IsNullOrEmpty(notificationName))
	{
		Debug.LogError("Can't observe an unnamed notification");
		return;
	}

	SenderTable subTable = GetSenderTable( notificationName );
	System.Object key = (sender != null) ? sender : this;
	List<EventHandler> list = GetObservers( subTable, key );
	if (!list.Contains(handler))
		list.Add( handler );
}

Note that because our SenderTable is based on a dictionary of objects as the key, there must always be a “sender” even if the users who post don’t provide one. Any notification with a null sender will be treated as if the NotificationCenter instance itself was the sender.

Removing observers will also use function overloading. Technically, the only required parameter to pass is the handler you want removed. You can optionally pass additional parameters including the name of a notification to remove from, and the sender you were observing. The more of the parameters you include, the more efficient the removal process, however, since you could use the same handler on multiple notifications and senders, the other options provide a simple means of unregistering all at once.

public void RemoveObserver (EventHandler handler)
{
	string[] keys = new string[ _table.Keys.Count ];
	_table.Keys.CopyTo(keys, 0);
	for (int i = keys.Length - 1; i >= 0; --i)
		RemoveObserver(handler, keys[i]);
}

public void RemoveObserver (EventHandler handler, string notificationName)
{
	if (handler == null)
	{
		Debug.LogError("Can't remove a null event handler from notification");
		return;
	}

	if (string.IsNullOrEmpty(notificationName))
	{
		Debug.LogError("A notification name is required to stop observation");
		return;
	}

	// No need to take action if we dont monitor this notification
	if (!_table.ContainsKey(notificationName))
		return;

	System.Object[] keys = new object[ _table[notificationName].Keys.Count ];
	_table[notificationName].Keys.CopyTo(keys, 0);
	for (int i = keys.Length - 1; i >= 0; --i)
		RemoveObserver(handler, notificationName, keys[i]);
}

public void RemoveObserver (EventHandler handler, string notificationName, System.Object sender)
{
	if (string.IsNullOrEmpty(notificationName))
	{
		Debug.LogError("A notification name is required to stop observation");
		return;
	}

	// No need to take action if we dont monitor this notification
	if (!_table.ContainsKey(notificationName))
		return;

	SenderTable subTable = GetSenderTable(notificationName);
	System.Object key = (sender != null) ? sender : this;
	if (!subTable.ContainsKey(key))
		return;
	List<EventHandler> list = GetObservers(subTable, key);
	for (int i = list.Count - 1; i >= 0; --i)
	{
		if (list[i] == handler)
		{
			list.RemoveAt(i);
			break;
		}
	}
	if (list.Count == 0)
	{
		subTable.Remove(key);
		if (subTable.Count == 0)
			_table.Remove(notificationName);
	}
}

Note that you can’t use fast enumeration (foreach) on the table because I am potentially modifying it while iterating. Whenever the last handler in a list is removed for any notification, that entry is removed.

Finally, we need to implement the ability to actually post a notification. At a minimum we only need the name of a notification to post, but as we have done a few times now we will use function overloading to allow a sender and event args as optional arguments.

public void PostNotification (string notificationName)
{
	PostNotification(notificationName, null);
}

public void PostNotification (string notificationName, System.Object sender)
{
	PostNotification(notificationName, sender, EventArgs.Empty);
}

public void PostNotification (string notificationName, System.Object sender, EventArgs e)
{
	if (string.IsNullOrEmpty(notificationName))
	{
		Debug.LogError("A notification name is required to stop observation");
		return;
	}

	// No need to take action if we dont monitor this notification
	if (!_table.ContainsKey(notificationName))
		return;

	// Post to subscribers who specified a sender to observe
	SenderTable subTable = GetSenderTable(notificationName);
	if (sender != null && subTable.ContainsKey(sender))
	{
		List<EventHandler> handlers = GetObservers( subTable, sender );
		for (int i = handlers.Count - 1; i >= 0; --i)
			handlers[i]( sender, e );
	}

	// Post to subscribers who did not specify a sender to observe
	if (subTable.ContainsKey(this))
	{
		List<EventHandler> handlers = GetObservers( subTable, this );
		for (int i = handlers.Count - 1; i >= 0; --i)
			handlers[i]( sender, e );
	}
}

Convenience Extensions

I mentioned earlier that a lot of people really hate to use singletons, if for no other reason than because they don’t want another class tightly coupled to their scripts. The following extensions would allow any object to work as if the NotificationCenter were a part of its native functionality. If at a later date you wanted to replace or remove the notification center, using these extension methods will help ease the process because all the references to NotificationCenter used in your project would be confined to a single script.

using System;
using System.Collections;

public static class ObjectExtensions
{
	public static void PostNotification (this object obj, string notificationName)
	{
		NotificationCenter.instance.PostNotification(notificationName, obj);
	}

	public static void PostNotification (this object obj, string notificationName, EventArgs e)
	{
		NotificationCenter.instance.PostNotification(notificationName, obj, e);
	}

	public static void AddObserver (this object obj, EventHandler handler, string notificationName)
	{
		NotificationCenter.instance.AddObserver(handler, notificationName);
	}

	public static void AddObserver (this object obj, EventHandler handler, string notificationName, object sender)
	{
		NotificationCenter.instance.AddObserver(handler, notificationName, sender);
	}

	public static void RemoveObserver (this object obj, EventHandler handler)
	{
		NotificationCenter.instance.RemoveObserver(handler);
	}

	public static void RemoveObserver (this object obj, EventHandler handler, string notificationName)
	{
		NotificationCenter.instance.RemoveObserver(handler, notificationName);
	}

	public static void RemoveObserver (this object obj, EventHandler handler, string notificationName, System.Object sender)
	{
		NotificationCenter.instance.RemoveObserver(handler, notificationName, sender);
	}
}

Using the Notification Center

Here are some scripts making use of the notification center. One to post and the other to observe and react.

public class Poster : MonoBehaviour
{
	void Start ()
	{
		this.PostNotification("TEST_NOTIFICATION", new MessageEventArgs("Hello World!"));
	}
}
public class Observer : MonoBehaviour
{
	void OnEnable ()
	{
		this.AddObserver(OnNotification, "TEST_NOTIFICATION");
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnNotification, "TEST_NOTIFICATION");
	}

	void OnNotification (object sender, EventArgs e)
	{
		Debug.Log("Got it! " + ((MessageEventArgs)e).message);
	}
}

In case you wondered about the MessageEventArgs class, I used this:

public class MessageEventArgs : EventArgs
{
	public readonly string message;

	public MessageEventArgs (string m)
	{
		message = m;
	}
}

Note that neither script has a reference to the other, or even the Notification Center, and even better still, we did not need a controller script to connect them as we did with our event based examples in Part 2. I can’t imagine anything much easier to use.

Note that you do still need to remove your handlers as you would with an event. I accomplished this by using the OnEnable and OnDisable methods of MonoBehaviour to register and unregister my event handlers.

The only last complaint about this demonstration that I have is that the chance of a typo on the notification name is rather high, and could result in some hard to track down bugs. In order to remedy this problem, one suggestion is to use another class which holds all of your notification names as properties. Then you can use autocomplete as well as have confidence that all string names match accordingly. For example, you could use the following:

public static class Notifications
{
	public const string TEST_NOTIFICATION = "TEST_NOTIFICATION";
}

and then use “Notifications.TEST_NOTIFICATION” anywhere you are adding, removing, or posting this notification.

For convenience sake, the complete NotificationCenter script is shown below:

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using SenderTable = System.Collections.Generic.Dictionary<System.Object, System.Collections.Generic.List<System.EventHandler>>;

public class NotificationCenter
{
	#region Properties
	private Dictionary<string, SenderTable> _table = new Dictionary<string, SenderTable>();
	#endregion

	#region Singleton Pattern
	public readonly static NotificationCenter instance = new NotificationCenter();
	private NotificationCenter() {}
	#endregion

	#region Public
	public void AddObserver (EventHandler handler, string notificationName)
	{
		AddObserver(handler, notificationName, null);
	}

	public void AddObserver (EventHandler handler, string notificationName, System.Object sender)
	{
		if (handler == null)
		{
			Debug.LogError("Can't add a null event handler for notification, " + notificationName);
			return;
		}

		if (string.IsNullOrEmpty(notificationName))
		{
			Debug.LogError("Can't observe an unnamed notification");
			return;
		}

		SenderTable subTable = GetSenderTable( notificationName );
		System.Object key = (sender != null) ? sender : this;
		List<EventHandler> list = GetObservers( subTable, key );
		if (!list.Contains(handler))
			list.Add( handler );
	}

	public void RemoveObserver (EventHandler handler)
	{
		string[] keys = new string[ _table.Keys.Count ];
		_table.Keys.CopyTo(keys, 0);
		for (int i = keys.Length - 1; i >= 0; --i)
			RemoveObserver(handler, keys[i]);
	}

	public void RemoveObserver (EventHandler handler, string notificationName)
	{
		if (handler == null)
		{
			Debug.LogError("Can't remove a null event handler from notification");
			return;
		}

		if (string.IsNullOrEmpty(notificationName))
		{
			Debug.LogError("A notification name is required to stop observation");
			return;
		}

		// No need to take action if we dont monitor this notification
		if (!_table.ContainsKey(notificationName))
			return;

		System.Object[] keys = new object[ _table[notificationName].Keys.Count ];
		_table[notificationName].Keys.CopyTo(keys, 0);
		for (int i = keys.Length - 1; i >= 0; --i)
			RemoveObserver(handler, notificationName, keys[i]);
	}

	public void RemoveObserver (EventHandler handler, string notificationName, System.Object sender)
	{
		if (string.IsNullOrEmpty(notificationName))
		{
			Debug.LogError("A notification name is required to stop observation");
			return;
		}

		// No need to take action if we dont monitor this notification
		if (!_table.ContainsKey(notificationName))
			return;

		SenderTable subTable = GetSenderTable(notificationName);
		System.Object key = (sender != null) ? sender : this;
		if (!subTable.ContainsKey(key))
			return;
		List<EventHandler> list = GetObservers(subTable, key);
		for (int i = list.Count - 1; i >= 0; --i)
		{
			if (list[i] == handler)
			{
				list.RemoveAt(i);
				break;
			}
		}
		if (list.Count == 0)
		{
			subTable.Remove(key);
			if (subTable.Count == 0)
				_table.Remove(notificationName);
		}
	}

	public void PostNotification (string notificationName)
	{
		PostNotification(notificationName, null);
	}

	public void PostNotification (string notificationName, System.Object sender)
	{
		PostNotification(notificationName, sender, EventArgs.Empty);
	}

	public void PostNotification (string notificationName, System.Object sender, EventArgs e)
	{
		if (string.IsNullOrEmpty(notificationName))
		{
			Debug.LogError("A notification name is required to stop observation");
			return;
		}

		// No need to take action if we dont monitor this notification
		if (!_table.ContainsKey(notificationName))
			return;

		// Post to subscribers who specified a sender to observe
		SenderTable subTable = GetSenderTable(notificationName);
		if (sender != null && subTable.ContainsKey(sender))
		{
			List<EventHandler> handlers = GetObservers( subTable, sender );
			for (int i = handlers.Count - 1; i >= 0; --i)
				handlers[i]( sender, e );
		}

		// Post to subscribers who did not specify a sender to observe
		if (subTable.ContainsKey(this))
		{
			List<EventHandler> handlers = GetObservers( subTable, this );
			for (int i = handlers.Count - 1; i >= 0; --i)
				handlers[i]( sender, e );
		}
	}
	#endregion

	#region Private
	private SenderTable GetSenderTable (string notificationName)
	{
		if (!_table.ContainsKey(notificationName))
			_table.Add(notificationName, new SenderTable());
		return _table[notificationName];
	}

	private List<EventHandler> GetObservers (SenderTable subTable, System.Object sender)
	{
		if (!subTable.ContainsKey(sender))
			subTable.Add(sender, new List<EventHandler>());
		return subTable[sender];
	}
	#endregion
}

6 thoughts on “Social Scripting Part 3

  1. How is it your are able to call the below function from a script that extends only monobehavior?

    this.PostNotification(“TEST_NOTIFICATION”, new MessageEventArgs(“Hello World!”));

    You mentioned having no direct references between the scripts, but I don’t know what feature is making that possible. Would you elaborate, please?

    1. Sure, I am using a feature called an “Extension” which makes it seem like I have added functionality to a class that didn’t used to be there. In this lesson I created a class called “ObjectExtensions” where the functionality I chose to add was appended to the “object” class which is the base class for everything in C#, including Unity classes like MonoBehaviour. This is a very powerful feature which could easily be abused, so tread carefully especially if you are working on very large projects or are working with a team of programmers.

  2. Hey Jon,

    I’ve been trying to wrap my head around these lessons, and there’s still something I don’t quite get: in AddObserver, I know you can specify whether you want a listener to receive notifications from a specific sender or from anything at all (in which case the NotificationCenter class seems to act as the sender). The former case still confuses me a bit, maybe because I haven’t seen an example in action.

    How about this: taking the Poster and Observer classes you used above, let’s say I only want to receive “TEST_NOTIFICATION” from the Poster class (maybe I create an class called “Impostor” which does the same thing as Poster so I can verify). I know I could declare a public Poster (maybe I call it “poster”) and link a game object in my hierarchy to it, and then I would be receiving notifications from that instance alone. But what if I wanted to receive notifications from the class and not any particular instance? I can’t simply use the Poster class as a parameter in AddObserver, so what’s the correct way to do it?

  3. Hey Alex,

    Good question… I didn’t design the notification system to work in the way that you are asking. In my own projects, if I design something to observe a generic notification that multiple classes can post, then I intend to be able to act on it regardless of what class it is. You can make a notification that is unique to a class, perhaps by putting it in a class namespace such as “Poster.TEST_NOTIFICATION” and “Imposter.TEST_NOTIFICATION” and then you don’t need to take any action to determine the type of the object because you were already only listening to what you wanted.

    On the other hand, you can sort of get what you want by allowing the observer to check the type of the sender after it receives a notification, like “sender == typeof(Poster)”.

    I haven’t tried it, but you might also get exactly what you wanted if you post and observe the notifications this way:

    // Post
    typeof(Poster).PostNotification(“TEST_NOTIFICATION”)
    // or
    this.GetType().PostNotification(“TEST_NOTIFICATION”)

    // Observe
    this.AddObserver(handler, “TEST_NOTIFICATION”, typeof(Poster))

    Of course if you do this, then you don’t have a reference to the true instance that actually was responsible for the event (normally this would have been the sender), so you may want to pass it along as an argument. If you try it, I’d be interested to know if it works or not.

    1. I tried your last suggestion, and it works! I’m also able to pass the object as an event argument to see which instance posted the notification. I can definitely use this, the only downside being that I would have to use “GetType()” instead of “this” for the notification to actually be observed correctly, but that’s probably fine. I really appreciate all of your help! I’ve learned a ton thanks to you.

      There is one more minor thing I would suggest, and it’s really just for debugging purposes for forgetful people like me: whenever you pass in a null sender into AddObserver (maybe you forgot to link an object, or some other bit of code didn’t work), it’s treated exactly as if your sender is the NotificationCenter. In other words, you’ll still get notifications, but they could come from anywhere, not just a specific sender like you intended. I modified the script so that it would log an error when I tried passing in a null sender to AddObserver or RemoveObserver. I find that these preventative measures often come in handy, so maybe someone else will, too, even though this a pretty old post 🙂

Leave a Reply

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