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:
- Any object can post any notification
- Any object can efficiently observe any notification by a direct reference to a handler
- Notification observation can target a specific sender or any sender
- Notifications can pass along an optional EventArgs parameter
- Notifications do not have to be handled, so you don’t have to check that they exist
- 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 }
This is amazing
thaaaaank you!!!
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?
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.
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?
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.
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 π
can you please make another post about your updated notifaction system? why did you change from using a event handler to a action. The method add observer in your newer one confuses me. can you explain it in detail for a noob please
Great questions. To begin with, let me make sure we are on the same page with what you consider old and new. Is the “old” one the version at this post, and the “new” one the version used in my projects such as in the Collectible Card Game? If so, then let me point out that the “old” version at this post also includes the “add observer” method along with a description of what it does. Be sure to see the part under the heading “Convenience Extensions” plus syntax for using it in the next heading, “Using the Notification Center”. You can also see this documentation on what C# extensions are https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
As a quick overview of C# extensions, it basically lets me add additional functionality to an existing type, almost as if I had written that type myself. The extensions I added for the notification center are applied to the system object, which is the base type for EVERYTHING in C# meaning that anything can post notifications or observe them.
Regarding the change from using “EventArgs” to a simple “Action”, I made that change because EventArgs is actually a class of its own. That meant that anytime I wanted to post information I would have to create a new object which inherited from EventArgs and which contained the information I wanted to pass along. With the Action handler that takes system objects, I can simply post the information I want – no need to create new wrapper objects.
Does that clear things up?
What is the use of the _invoking Hashset in the new version (the one used in tactics RPG) of the notification center? it stays private and keeps adding and removing handlers, but I can’t find where the hashset itself is actually used anywhere. What am I missing?
It is generally a bad practice to increment over a list while changing the list. That private property keeps track of the list of observers that are currently being notified, so that if any changes (adding or removing an observer) are made from within the notification, that I end up creating a new list for those changes instead.
ah, that makes sense.
I really enjoyed how decoupled that system is, how an object does not need to know about any other objects in order to post or receive notifications. My biggest concern with it is the fact that notifications are sent as strings, isn’t that a bit problematic? Working with strings can lead to typo errors, and a person programming one sender class would have to know the string defined by another person working on an obvserver class (or vice-versa)? Curious to see your thoughts about this.
Great questions. Strings aren’t a problem themselves, but how you use them can certainly create issues. For example, you should not litter your code with string literals. Instead, declare them once as a variable and use that. Some people use static constants for this purpose. This gives you the benefit of code completion and not having to worry about typo’s because the use of the variable will not compile if it isn’t recognized. Watch how notifications are used in my various projects to see this in practice.
Hello again,
I am a bit confused by the use of the word observer in the implementation of the notification center. Prior to this tutorial, I thought an observer and a listener were basically the same thing (the object that receives the notification and acts on it). Doing some research outside of this website seemed to confirm this.
Here it seems you use the word observer interchangeably with event handlers.
All of these concepts are still very blurry in my mind, do you mind clarifying a bit?
Thanks a lot for still being so present and helpful years after having posted these blog articles, you’re a fantastic teacher.
Glad to help π
You are correct that observers and listeners are the same thing. The thing that might help is to consider that this is an “architectural pattern” that doesn’t speak to a specific implementation of itself. Notifications and events are both a form of this same pattern, they just have a slightly different implementation, each of which has their own pros and cons. In general, it is just a pattern where something can broadcast messages that other objects may want to hear about.
My confusion comes from method names like GetObservers, AddObservers, etc.
It doesn’t seem to me like this method actually gets the observers, it seems to be getting the action delegates associated with the notification (the return type is an EventHandler).
In other words, if I correctly understand the word observer, do I misunderstand the word eventHandler? they seem very different things to me
The EventHandler seen in this post is really just a method signature:
https://docs.microsoft.com/en-us/dotnet/api/system.eventhandler?view=netframework-4.8
Likewise the “Handler” used in the tactics version of the notification center also defines a method signature. The main difference between the two is that the newer one uses a “looser” requirement of a “System.Object” rather than an instance of “EventArgs”.
Think of an observer as any object which has a method that conforms to the required method signature above, and then requests to use that method as the handler of a specified notification, via the “AddObserver” method. The notification center is then maintaining a reference to the specific method on the specific object instance.
Note that we must be careful not to send all notifications to all observers. We want specific control and the ability to only send notifications to the objects that requested that particular notification. When you see the method “GetObservers” what you are seeing is that my system is doing a lookup of the references it has stored that fit the criteria for the notification that is currently being posted.
Oooooh I think I get it!
I was confused because I was conceptualizing the notification center as a controller that is the main user of its AddObserver, RemoveObserver methods. But in most cases it’s only there as a structure for regular game objects (or other objects) to use.
The name AddObserver makes a whole lot more sense considering that it is the observer object itself that is subscribing to the event (it is “adding itself as an observer”). Is this then kind of similar to the event syntax “+=” that is used to subscribe to an event?
Am I on the right track?
Yep, looks like you’ve got it π