Event System – Dispatcher is a powerful, but easy-to-use tool that allows for different GameObjects to communicate with one another by sending messages through a broker. This is can be used in a variety of ways, including damage systems, dynamic game events, and more!
Event and message dispatching is a critical part of any game. As games get more and more complex, so too are the interactions between objects. Message dispatching ensures that game objects are able to communicate in a consistent, reliable, and efficient way.
Dispatcher does this and makes it easy too! Simply tell the Dispatcher what an object wants to listen for. When another object sends that message to the Dispatcher, the Dispatcher will ensure all the “listeners” are notified.
With this dispatcher, you can create your own custom messages and take advantage of the message pool for super-fast performance.
- Creating and sending custom message types
- Sending messages containing custom data
- Sending messages to everyone
- Sending messages to specific objects based on their name
- Sending messages to specific objects based on their tag
- Sending messages to specific objects based on a custom string filter
- Sending messages immediately
- Sending messages at the next frame
- Scheduling messages for a certain point in the future
- Connecting multiple listeners to a single message
- High performance message pooling
The event system is composed of 4 primary parts:
- This is a static or “Singleton” that takes messages and either sends them immediately or holds onto them and sends them later.
- Listeners are simply any GameObjects or classes that expect to receive one or more types of messages from the dispatcher. Listeners must be registered with MessageDispatcher.AddListener(…). More on that method below.
- Upon receiving a message, the dispatcher calls a method on your listener that you specified for that message type. That method is called the “MessageHandler” and can be any method with a single IMessage argument.
- Senders are simply any GameObjects or classes that send messages to the dispatcher (and therefore listeners). Senders do not have to be registered with the dispatcher. Any GameObject or class can simply use MessageDispatcher.SendMessage(…) to send a message and be considered a “sender”. More on that method below.
- Messages are simply the data you want the senders to send and the listeners to listen for. You may choose that simply receiving a message is enough to perform an action (such as opening a door), or you may decide that a message needs to contain lots of data (such as changing a player’s health, stamina, and armor status). The examples will make this clearer.
Here’s how you’ll typically use them together:
1. Register listeners with the dispatcher. Listeners needs two things: some method to be used as the “message handler” and MessageDispatcher.AddListener(…), which links incoming messages of a specific type to the the “message handler” method.
In this case, GameObject B will be our listener, and registers messages of type “Test” to call the handler foo().
2. Any GameObject can send a message of any type to the dispatcher. Senders are not registered with the dispatcher, any script can send a message to the dispatcher.
In this case, GameObject A will be our sender, sending a message of type “Test”. This message does not contain data, but you can configure messages to contain just about anything.
3. The dispatcher relays incoming messages to GameObjects registered to receive messages of the same type. The dispatcher also has the ability to hold onto messages for an amount of time before sending if desired.
In this case, our listener received the message from the dispatcher.
4. Finally, the listener calls the handler method associated with the message type. The message is passed as an argument to the message handler so that data can be taken from the message.
In this case, the message arriving was all we needed, and the debug statement is printed.
The Dispatcher is a code-based asset. That means to take advantage of its capabilities, you need to access it through C#. There are no inspectors or scripts to add to Game Objects (other than the game logic you need for your specific situation).
So, setup is pretty simple: Open a scene, and import the package files from the Unity Asset Store.
When you add a listener to the dispatcher, the listener isn’t typically added right away. Instead, we cache the request until the end of the frame. I do this so you can add listeners as a response to an incoming message.
However, this means you can’t immediately send a message unless you tell the dispatcher to ignore the cache. You can see how to do this by looking at the different signatures of the AddListener() method; rImmediate set to true will immediately add the listener to the dispatcher.
When a message comes into the dispatcher, the dispatcher sends it off to the listeners. Under the hood, what the dispatcher is doing is calling a function that the listener said to call. In the example above, the “OnStarted” function is that function.
Notice, the function is based on a delegate and has a special signature:
public delegate void MessageHandler(IMessage rMessage);
The signature shows it has one argument rMessage. This message object contains all the information about the message the listener is getting, such as type, sender, delay, data, etc.
It also allows the listener to set the property IsHandled (more on this later)
So, at its most basic form, AddListener simply says what type of message to listen for and what function will be called when that type of message comes in.
Most of the time, you probably won’t need to worry about filters as the Message Type will determine what listeners get what messages. However, for advanced users, there is a filter value that can be set. Filters can be used in a couple of different ways:
- As a basic string to add another level to the message type
- As a GameObject’s name to specify a certain recipient
- As a GameObject’s tag value to specify a certain recipient
For the last two, the filter is based on the “RecipientType” property of the MessageDispatcher. This property says that when a GameObject filter is used, it’s either treated as the recipient’s “name” or “tag”.
See below for how these filters are actually used.
There are several different overloads of AddListener on the MessageDispatcher class. They all do the same thing, but give you plenty of options without writing extra code. If you’re using a modern IDE, the argument descriptions will pop up so you can tell the different between each one.
AddListener(string rMessageType, MessageHandler rHandler)
AddListener(string rMessageType, MessageHandler rHandler, bool rImmediate)
rMessageType: String representing the message type to look for. You can set this to anything you want.
rHandler: The function that will be called when the listener is being told about an incoming message.
rImmediate: Tells the dispatcher not to cache the add, but to do it immediately.
Most of the time, you’ll probably add listeners using one of the two methods above. With that said, there are more advanced features you may find useful, such as:
AddListener(string rMessageType, string rFilter, MessageHandler rHandler)
AddListener(string rMessageType, string rFilter, MessageHandler rHandler, bool rImmediate)
AddListener(UnityEngine.Object rOwner, string rMessageType, MessageHandler rHandler)
AddListener(UnityEngine.Object rOwner, string rMessageType, MessageHandler rHandler, bool rImmediate)
rFilter: String representing a second layer to determine if this listener will get the message.
rOwner: The GameObject whose name/tag will be used to determine if this listener will get the message.
Just as you can add a listener, you can remove a listener so it will no longer get messages.
The signatures for RemoveListener pair with those of AddListener. So, I’m not going to list them all over again. Needless to say, for each AddListener there is a RemoveListener function.
Like AddListener, RemoveListener is cached until the end of the frame. This way, you can remove a listener in response to a message and not cause an error. Again, you can remove a listener immediately with the rImmediate argument set to true.
Once you have listeners setup, you’re ready to send messages. Like with AddListener, there are several variations of the function in order to simplify code. However, they all work pretty much the same.
When you send a message, the most important argument is typically the message type. This is just a string that you can set to any value you’d like and it acts as a basic filter.
In the example above under Architecture, I created a listener to listen out for messages of type “Test” and then I sent a message whose type was “Test”. This is what allows the dispatcher to know which listener gets which message.
A word on String types
Some developers hate Strings and prefer to use hashed values or simple int values. While it’s true that there is a performance benefit to doing this, we’re typically talking tiny fractions of a millisecond. So long as we don’t manipulate the strings, there’s little to no impact on the garbage collector as well.
Since not everyone is a hard-core programmer, I decided to go with strings for ease of use. Even with this approach, I find that the Event System – Dispatcher is 30% to 100% faster than the standard Unity event system.
Messages can be sent with a delay. This allows you to send a message to the dispatcher immediately, but not have it delivered until the next frame or for a specific number of seconds.
To send with a delay, use one of the following as the argument for rDelay:
- “0” to send the message immediately
- “-1” to send the message on the next frame
- A value >0 to send the message in that number of seconds
Along with the message itself, you can add data to the message. Since this argument is an “object”, you can add any value you want. The listener can then interrogate the message and use the data as needed.
As a mentioned in the AddListener section, advanced users can use filters to help determine which listeners get which messages. Filters can be used in a couple of different ways:
- As a basic String to add another level to the message type
- As a GameObject’s name to specify a certain recipient
- As a GameObject’s tag value to specify a certain recipient
For advanced users, the Event System – Dispatcher allows you to create entirely new message structures.
I use a C# interface (IMessage) to define the basic message type, this is what allows us to set the type, delay, etc. However, you may have a situation where you want to create a whole new message structure. All you need to do is inherit from either the Message class or the IMessage interface. Once you do this, you can send messages using the basic SendMessage(IMessage
SendMessage(string rType, string rFilter)
rType: String that determines which listener gets the message.
rFilter: Additional string to help determine which listener gets the message.
SendMessage(string rType, float rDelay)
SendMessage(string rType, string rFilter, float rDelay)
rDelay: Seconds to wait before sending the message (See Delay).
SendMessage(object rSender, string rType, object rData, float rDelay)
SendMessage(object rSender, object rRecipient, string rType, object rData, float rDelay)
SendMessage(object rSender, string rRecipient, string rType, object rData, float rDelay)
rData: Any form of data you would like to send to the listeners
rSender: Object who is sending the message.
rRecipient: String to use as an additional filter to determine who gets the message.
In the end, all the SendMessage() signatures call this version.
You can use this if you have a custom message structure. Simply set your values on the IMessage object and then pass the message to the dispatcher using this function.
Once the dispatcher sends off a message, it needs to clean that message up. This is especially true for delayed messages that the dispatcher held on to.
The way it does this is that it looks at two flags on the message: IsSent and IsHandled.
While processing, if the dispatcher sees the message flag IsSent is true or the flag IsHandled is true, the message will be deleted.
IsSent is set by the Dispatcher when it sends the message. IsHandled can be set by you as the listener is processing the message. I also find setting the IsHandled flag is helpful when you’ve got multiple listeners handling the same message. If one of them sets this flag, the other listener doesn’t need to process the message.
This kind of “IsHandled” logic is for you to code as needed.
This section will be in reference to the scripts and scenes under ootii/Dispatcher Examples, and will provide additional explanation as to how each feature works. I recommend that as you read these you have each scene open and follow along.
Example 1: Sending Simple Messages
Open ootii/Dispatcher Examples/1. Sending Simple Messages/Scene.unity. You’ll notice there’s a script for each object. The RedSphereSend script is tied to the red sphere, and the RedCubeListen script is tied to the red cube.
RedCubeListen simply tells the dispatcher that it wants to receive messages of type “Red Bounce”, and once it does receive a message, make the red cube do a little jump.
RedSphereSend simply sends the dispatcher a blank message of type “Red Bounce” every time it bounces (collides) with something such as the ground plane. There is no delay specified, so as soon as the red ball sends the message, the dispatcher immediately relays the message to the red cube and it pops up.
The blue cube, blue sphere, BlueCubeListen script, and BlueSphereSend script are all functionally identical from their red counterparts except for one key difference: the BlueSphereSend script sends the “Blue Bounce”-type message with a delay of half a second. The blue sphere collides with the ground and sends the message to the dispatcher. The dispatcher then holds onto the message for the specified half-second, and only then relays the message to the blue cube, causing it to jump. The red objects are entirely separate from the blue ones.
Notice that no data is sent, as the act of receiving the message is all we need to tell the cubes to pop up.
Example 2: Sending Data
Open ootii/Dispatcher Examples/2. Sending Data/Scene.unity. There are two scripts: one for the senders (the spheres), and one for the listeners (the cube). As shown in example 1, simply receiving a message may be all you need, but in other cases you may want messages to contain data such as a number for health, or in this case, a reference to an object like a material.
Upon colliding, each sphere simply calls this:
MessageDispatcher.SendMessage(gameObject, “Color”, gameObject.GetComponent<MeshRenderer>().material, 0);
The third argument is the data, and the method signature requires it be of type “object”. This gives the user the flexibility to make the data quite literally anything they desire. This exact line of code sends a message of type “Color” to the dispatcher, where the data is a reference to the material that the sphere is using. The dispatcher relays the message immediately, as per the “0” delay.
The ListenForColor script is attached to the cube. The 3 spheres are the only objects that send messages of type “Color”, and we know that the message’s data field is always a reference to a material. So, upon receiving the message, ColorHandler() tells the cube to change its material to incomingMessage.Data.
Every time a sphere bounces, it sends a reference to its material to the cube, which applies the material to itself.
Example 3: Filters
Open ootii/Dispatcher Examples/3. Filters/Scene.unity. Filters may seem counter-intuitive at first, but hopefully this explanation will clear things up.
Each button corresponds to their respective method in the ButtonControl script, which is attached to the Canvas object. This makes the Canvas object the sender.
Obviously, pressing the first button makes just the sphere pop up. No surprises there.
Pressing the second button calls MessageDispatcher.SendMessage(“Cube”); You may be expecting all 3 cubes to jump up, however only the white one does. You’ll see why in a second.
Pressing the third and fourth button calls
This makes both the colored cube and the white cube pop up because filters are enforced by the listener, not the sender. Take a look at each shape’s script.
– The sphere listens for messages of type “Sphere” regardless of filter.
– The white cube listens for messages of type “Cube” regardless of filter.
– The red cube listens for messages of type “Cube” AND the filter “Red”.
– The blue cube listens for messages of type “Cube” AND the filter “Blue”.
If you have any comments, questions, or issues, please don’t hesitate to email me at firstname.lastname@example.org. I’ll help any way I can.