The ability to send game events as messages is a very useful thing indeed. In our projects, we produce different game systems at different times. Sound might be last, or we might add in a scoring system long after the initial game logic is completed. Messages are ideal for this type of scenario.
For instance, take the event of a raptor dying in Raptor Safari. One option–a bad one–is to explicitly reference all neccesary managers in the raptor scripts themselves. So, to add sound we would have to add a call to our sound manager, to add scoring we call a function on our scoring manager, and so on. The problem is we shouldn’t have to edit older scripts to enable functionality in new managers. This is a recipe for errors and programmer collisions (it will be harder to add in real raptor-specific functionality if there is a constant stream of edits to support other parts of the game).
A better paradigm is to have the raptor broadcast a message about its death. This message includes all relevant information: What killed the raptor, where it died, a reference to the raptor object itself, and so on. The message is broadcast with a specific category, like “raptor”, and interested parties sign up to consume messages about this category.
This means we can add functionality about an event without editing the original script. We could add a sound for our raptor’s death, include it in our scoring systems, spawn a particle effect, and more–all without touching the original raptor logic. Perfect! But how do we accomplish this is UnityScript?
We need a single object to keep track of our lists of listeners. When a script wants to listen to some classification of events, like “item” events, it has to call a function on our messenger. The messenger then keeps track of which scripts should recieve messages for which types of events. This looks like:
The actual collection of listeners is very basic. We use a Hashtable of ArrayLists. The Listen() function looks like:
* Adds a listener for a particular type of message
function Listen(listenerType:String, go:GameObject)
// if there’s no array for this tracking category, make a new one
if(listeners[listenerType] == null)
listeners[listenerType] = new ArrayList();
var listener:ArrayList = listeners[listenerType];
// only add to the array if it isn’t already being tracked
* Register implicitly with a Component instead of GameObject
function Listen(listenerType:String, component:Component)
We use a second function signature that accepts a Component to allow scripts to use this to register as a listener.
Message Class Structure
All messages are their own classes. To send a message you simply instantiate the class. In Minotaur China Shop, a broadcast of the item delivered message looks like:
In this particular case, the message contains the item we delivered to a custom (the first argument), and the customer to which it was delivered (the second argument). Let’s take a look at the actual MessageItemDelievered.js file:
class MessageItemDelivered extends Message
// the item that was collected
// which customer we delievered it to
function MessageItemDelivered(item:Item, customer:BaseCustomer)
this.item = item;
this.customer = customer;
We use our message objects to store multiple references and values. Many times we will compute additional values. For instance, for a raptor death event we may calculate the impact of the event as a scalar from 0..1 and store that in the message. If other scripts want to know the severity of the impact they can use this value, while leaving us the flexibility to tune it in a single location. This is one reason delegates wouldn’t serve us very well.
The super(“item”) line calls the constructor on the parent class, Message. This is where we stick our category identifier. You can think of the line as saying “our message is now ready, send out to anyone interested in the item category”.
You’ll notice we don’t actually define the function name the message is calling anywhere. We do this implicitly based on the class name. The naming standards we use are messages look like MessageItemDelivered and their corresponding function names look like _ItemDelivered(). The leading underscore lets us know at a glance that the function will be called by the messenger.
The messenger calls the function and sends it the instance of the message. This allows messages to contain as much data as we want, and also to precompute anything expensive once (at message composition instead of per-listener). A listening object might look like:
* Score an item being delievered
// do something here
Debug.Log(“customer purchase # “ + msg.customer.itemsPurchased);
One of the nice side effects of using one class per message is visible organization. We keep all of our messages in one folder, so it’s very easy to see which messages are available in a project. This allows collaborators to get an idea of how much functionality is exposed by other systems. If we need to create a new message–the sound designer would like to hook into something else–it’s easy to go in and add new messages, which may prove useful for other functionality later.
Adding New Messages
Adding a new message is relatively painless. We must:
- Create a new MessageSomething class (we copy/paste an existing message)
- Specify the category in the super() call
- Listeners register with the Messenger singleton
- Listeners implement _Something(msg:MessageSomething) function
Our actual broadcast functionality uses Unity’s SendMessage() function. This isn’t nearly as fast as calling functions directly with your code, but in practice it’s fast enough. You wouldn’t want to be calling multiple messages every frame, but for game events–enemy spawned, enemy killed, powerup acquired–it’s performant enough to be practical.
In our current implementation we also let null values accumulate in the ArrayLists of listeners. This hasn’t caused any problems, because we flush the Messenger along with scene changes. If you are incredibly performance-conscious, or developing for the iPhone, you may want to skip a messaging system altogether. The point of a messaging system isn’t programming performance, but production performance; it’s a fast and efficient way to get things done.
It’s not as complicated as it sounds, actually. In practice we create new messages in just a few minutes, and we haven’t updated the actual Messenger logic in two projects. Here’s a sample project to get you started. If you make any improvements or changes to the project, please let us know by leaving a comment!