A game is usually made of several different entities that interact with each other. Those interactions tend to be very dynamic and deeply connected with gameplay. This tutorial covers the concept and implementation of a message queue system that can unify the interactions of entities, making your code manageable and easy to maintain as it grows in complexity.
Introduction
A bomb can interact with a character by exploding and causing damage, a medic kit can heal an entity, a key can open a door, and so on. Interactions in a game are endless, but how can we keep the game code manageable while still able to handle all those interactions? How do we ensure the code can change and continue to work when new and unexpected interactions arise?
As interactions are added (especially the unexpected ones), your code will look more and more cluttered. A naive implementation will quickly lead to you asking questions like:
"This is entity A, so I should call method damage()
on it, right? Or is it damageByItem()
? Maybe this damageByWeapon()
method is the right one?"
Imagine that cluttering chaos spreading to all your game entities, because they all interact with each other in different and peculiar ways. Luckily, there is a better, simpler, and more manageable way of doing it.
Message Queue
Enter the message queue. The basic idea behind this concept is to implement all game interactions as a communication system (which is still in use today): message exchange. People have communicated via messages (letters) for centuries because it is an effective and simple system.
In our real-world post services, the content of each message can differ, but the way they are physically sent and received remains the same. A sender puts the information in an envelope and addresses it to a destination. The destination can reply (or not) by following the very same mechanism, just changing the "from/to" fields on the envelope.
By applying that idea to your game, all interactions among entities can be seen as messages. If a game entity wants to interact with another (or a group of them), all it has to do is send a message. The destination will deal with or react to the message based on its content and on who the sender is.
In this approach, communication among game entities becomes unified. All entities can send and receive messages. No matter how complex or peculiar the interaction or message is, the communication channel always remains the same.
Throughout the next sections, I'll describe how you can actually implement this message queue approach in your game.
Designing an Envelope (Message)
Let's start by designing the envelope, which is the most basic element in the message queue system.
An envelope can be described as in the figure below:
The first two fields (sender
and destination
) are references to the entity that created and the entity that will receive this message, respectively. Using those fields, both the sender and the receiver can tell where the message is going to and where it came from.
The other two fields (type
and data
) work together to ensure the message is properly handled. The type
field describes what this message is about; for instance, if the type is "damage"
, the receiver will handle this message as an order to decrease its health points; if the type is "pursue"
, the receiver will take it as an instruction to pursue something—and so on.
The data
field is directly connected to the type
field. Using the previous examples, if the message type is "damage"
, then the data
field will contain a number—say, 10
—which describes the amount of damage the receiver should apply to its health points. If the message type is "pursue"
, data
will contain an object describing the target that must be pursued.
The data
field can contain any information, which makes the envelope a versatile means of communication. Anything can be placed in that field: integers, floats, strings, and even other objects. The rule of thumb is that the receiver must know what is in the data
field based on what is in the type
field.
All that theory can be translated into a very simple class named Message
. It contains four properties, one for each field:
Message = function (to, from, type, data) { // Properties this.to = to; // a reference to the entity that will receive this message this.from = from; // a reference to the entity that sent this message this.type = type; // the type of this message this.data = data; // the content/data of this message };
As an example of this in use, if an entity A
wants to send a "damage"
message to entity B
, all it has to do is instantiate an object of the class Message
, set the property to
to B
, set the property from
to itself (entity A
), set type
to "damage"
and, finally, set data
to some number (10
, for instance):
// Instantiate the two entities var entityA = new Entity(); var entityB = new Entity(); // Create a message to entityB, from entityA, // with type "damage" and data/value 10. var msg = new Message(); msg.to = entityB; msg.from = entityA; msg.type = "damage"; msg.data = 10; // You can also instantiate the message directly // passing the information it requires, like this: var msg = new Message(entityB, entityA, "damage", 10);
Now that we have a way to create messages, it's time to think about the class that will store and deliver them.
Implementing a Queue
The class responsible for storing and delivering the messages will be called MessageQueue
. It will work as a post office: all messages are handed to this class, which ensures they will be dispatched to their destination.
For now, the MessageQueue
class will have a very simple structure:
/** * This class is responsible for receiving messages and * dispatching them to the destination. */ MessageQueue = function () { this.messages = []; // list of messages to be dispatched }; // Add a new message to the queue. The message must be an // instance of the class Message. MessageQueue.prototype.add = function(message) { this.messages.push(message); };
The property messages
is an array. It will store all the messages that are about to be delivered by the MessageQueue
. The method add()
receives an object of the class Message
as a parameter, and adds that object to the list of messages.
Here's how our previous example of entity A
messaging entity B
about damage would work using the MessageQueue
class:
// Instantiate the two entities and the message queue var entityA = new Entity(); var entityB = new Entity(); var messageQueue = new MessageQueue(); // Create a message to entityB, from entityA, // with type "damage" and data/value 10. var msg = new Message(entityB, entityA, "damage", 10); // Add the message to the queue messageQueue.add(msg);
We now have a way to create and store messages in a queue. It's time to make them reach their destination.
Delivering Messages
In order to make the MessageQueue
class actually dispatch the posted messages, first we need to define how entities will handle and receive messages. The easiest way is by adding a method named onMessage()
to each entity able to receive messages:
/** * This class describes a generic entity. */ Entity = function () { // Initialize anything here, e.g. Phaser stuff }; // This method is invoked by the MessageQueue // when there is a message to this entity. Entity.prototype.onMessage = function(message) { // Handle new message here };
The MessageQueue
class will invoke the onMessage()
method of each entity that must receive a message. The parameter passed to that method is the message being delivered by the queue system (and being received by the destination).
The MessageQueue
class will dispatch the messages in its queue all at once, in the dispatch()
method:
/** * This class is responsible for receiving messages and * dispatching them to the destination. */ MessageQueue = function () { this.messages = []; // list of messages to be dispatched }; MessageQueue.prototype.add = function(message) { this.messages.push(message); }; // Dispatch all messages in the queue to their destination. MessageQueue.prototype.dispatch = function() { var i, entity, msg; // Iterave over the list of messages for(i = 0; this.messages.length; i++) { // Get the message of the current iteration msg = this.messages[i]; // Is it valid? if(msg) { // Fetch the entity that should receive this message // (the one in the 'to' field) entity = msg.to; // If that entity exists, deliver the message. if(entity) { entity.onMessage(msg); } // Delete the message from the queue this.messages.splice(i, 1); i--; } } };
This method iterates over all messages in the queue and, for each message, the to
field is used to fetch a reference to the receiver. The onMessage()
method of the receiver is then invoked, with the current message as a parameter, and the delivered message is then removed from the MessageQueue
list. This process is repeated until all messages are dispatched.
Using a Message Queue
It's time to see all of the details of this implementation working together. Let's use our message queue system in a very simple demo composed of a few moving entities that interact with each other. For the sake of simplicity, we'll work with three entities: Healer
, Runner
and Hunter
.
The Runner
has a health bar and moves around randomly. The Healer
will heal any Runner
that passes close by; on the other hand, the Hunter
will inflict damage on any nearby Runner
. All interactions will be handled using the message queue system.
Adding the Message Queue
Let's start by creating the PlayState
which contains a list of entities (healers, runners and hunters) and an instance of the MessageQueue
class:
var PlayState = function() { var entities; // list of entities in the game var messageQueue; // the message queue (dispatcher) this.create = function() { // Initialize the message queue messageQueue = new MessageQueue(); // Create a group of entities. entities = this.game.add.group(); }; this.update = function() { // Make all messages in the message queue // reach their destination. messageQueue.dispatch(); }; };
In the game loop, represented by the update()
method, the message queue's dispatch()
method is invoked, so all messages are delivered at the end of each game frame.
Adding the Runners
The Runner
class has the following structure:
/** * This class describes an entity that just * wanders around. */ Runner = function () { // initialize Phaser stuff here... }; // Invoked by the game on each frame Runner.prototype.update = function() { // Make things move here... } // This method is invoked by the message queue // to make the runner deal with incoming messages. Runner.prototype.onMessage = function(message) { var amount; // Check the message type so it's possible to // decide if this message should be ignored or not. if(message.type == "damage") { // The message is about damage. // We must decrease our health points. The amount of // this decrease was informed by the message sender // in the 'data' field. amount = message.data; this.addHealth(-amount); } else if (message.type == "heal") { // The message is about healing. // We must increase our health points. Again the amount of // health points to increase was informed by the message sender // in the 'data' field. amount = message.data; this.addHealth(amount); } else { // Here we deal with messages we are not able to process. // Probably just ignore them :) } };
The most important part is the onMessage()
method, invoked by the message queue every time there is a new message for this instance. As previously explained, the field type
in the message is used to decide what this communication is all about.
Based on the type of the message, the correct action is performed: if the message type is "damage"
, health points are decreased; if the message type is "heal"
, health points are increased. The number of health points to increase or decrease by is defined by the sender in the data
field of the message.
In the PlayState
, we add a few runners to the list of entities:
var PlayState = function() { // (...) this.create = function() { // (...) // Add runners for(i = 0; i < 4; i++) { entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random())); } }; // (...) };
The result is four runners randomly moving around:
Adding the Hunter
The Hunter
class has the following structure:
/** * This class describes an entity that just * wanders around hurting the runners that pass by. */ Hunter = function (game, x, y) { // initialize Phaser stuff here }; // Check if the entity is valid, is a runner, and is within the attack range. Hunter.prototype.canEntityBeAttacked = function(entity) { return entity && entity != this && (entity instanceof Runner) && !(entity instanceof Hunter) && entity.position.distance(this.position) <= 150; }; // Invoked by the game during the game loop. Hunter.prototype.update = function() { var entities, i, size, entity, msg; // Get a list of entities entities = this.getPlayState().getEntities(); for(i = 0, size = entities.length; i < size; i++) { entity = entities.getChildAt(i); // Is this entity a runner and is it close? if(this.canEntityBeAttacked(entity)) { // Yeah, so it's time to cause some damage! msg = new Message(entity, this, "damage", 2); // Send the message away! this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong. } } }; // Get a reference to the game's PlayState Hunter.prototype.getPlayState = function() { return this.game.state.states[this.game.state.current]; }; // Get a reference to the game's message queue. Hunter.prototype.getMessageQueue = function() { return this.getPlayState().getMessageQueue(); };
The hunters will also move around, but they will cause damage to all runners that are close. This behavior is implemented in the update()
method, where all entities of the game are inspected and runners are messaged about damage.
The damage message is created as follows:
msg = new Message(entity, this, "damage", 2);
The message contains the information about the destination (entity
, in this case, which is the entity being analyzed in the current iteration), the sender (this
, which represents the hunter that is performing the attack), the type of the message ("damage"
) and the amount of damage (2
, in this case, assigned to the data
field of the message).
The message is then posted to the destination via the command this.getMessageQueue().add(msg)
, which adds the newly created message to the message queue.
Finally, we add the Hunter
to the list of entities in the PlayState
:
var PlayState = function() { // (...) this.create = function() { // (...) // Add hunter at position (20, 30) entities.add(new Hunter(this.game, 20, 30)); }; // (...) };
The result is a few runners moving around, receiving messages from the hunter as they get close to each other:
I added the flying envelopes as a visual aid to help show what is going on.
Adding the Healer
The Healer
class has the following structure:
/** * This class describes an entity that is * able to heal any runner that passes nearby. */ Healer = function (game, x, y) { // Initializer Phaser stuff here }; Healer.prototype.update = function() { var entities, i, size, entity, msg; // The the list of entities in the game entities = this.getPlayState().getEntities(); for(i = 0, size = entities.length; i < size; i++) { entity = entities.getChildAt(i); // Is it a valid entity? if(entity) { // Check if the entity is within the healing radius if(this.isEntityWithinReach(entity)) { // The entity can be healed! // First of all, create a new message regaring the healing msg = new Message(entity, this, "heal", 2); // Send the message away! this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong. } } } }; // Check if the entity is neither a healer nor a hunter and is within the healing radius. Healer.prototype.isEntityWithinReach = function(entity) { return !(entity instanceof Healer) && !(entity instanceof Hunter) && entity.position.distance(this.position) <= 200; }; // Get a reference to the game's PlayState Healer.prototype.getPlayState = function() { return this.game.state.states[this.game.state.current]; }; // Get a reference to the game's message queue. Healer.prototype.getMessageQueue = function() { return this.getPlayState().getMessageQueue(); };
The code and structure are very similar to the Hunter
class, except for a few differences. Similarly to the hunter's implementation, the healer's update()
method iterates over the list of entities in the game, messaging any entity within its healing reach:
msg = new Message(entity, this, "heal", 2);
The message also has a destination (entity
), a sender (this
, which is the healer performing the action), a message type ("heal"
) and the number of healing points (2
, assigned in the data
field of the message).
We add the Healer
to the list of entities in the PlayState
the same way we did with the Hunter
and the result is a scene with runners, a hunter, and a healer:
And that's it! We have three different entities interacting with each other by exchanging messages.
Discussion About Flexibility
This message queue system is a versatile way to manage interactions in a game. The interactions are performed via a communication channel that is unified and has a single interface that is easy to use and implement.
As your game grows in complexity, new interactions might be needed. Some of them might be completely unexpected, so you must adapt your code to deal with them. If you are using a message queue system, this is a matter of adding a new message somewhere and handling it in another.
For example, imagine you want to make the Hunter
interact with the Healer
; you just have to make the Hunter
send a message with the new interaction—for instance, "flee"
—and ensure that the Healer
can handle it in the onMessage
method:
// In the Hunter class: Hunter.prototype.someMethod = function() { // Get a reference to a nearby healer var healer = this.getNearbyHealer(); // Create message about fleeing a place var place = {x: 30, y: 40}; var msg = new Message(entity, this, "flee", place); // Send the message away! this.getMessageQueue().add(msg); }; // In the Healer class: Healer.prototype.onMessage = function(message) { if(message.type == "flee") { // Get the place to flee from the data field in the message var place = message.data; // Use the place information flee(place.x, place.y); } };
Why Not Just Send Messages Directly?
Although exchanging messages among entities can be useful, you might be thinking why the MessageQueue
is needed after all. Can't you just invoke the receiver's onMessage()
method yourself instead of relying on the MessageQueue
, as in the code below?
Hunter.prototype.someMethod = function() { // Get a reference to a nearby healer var healer = this.getNearbyHealer(); // Create message about fleeing a place var place = {x: 30, y: 40}; var msg = new Message(entity, this, "flee", place); // Bypass the MessageQueue and directly deliver // the message to the healer. healer.onMessage(msg); };
You could definitely implement a message system like that, but the use of a MessageQueue
has a few advantages.
For instance, by centralizing the dispatchment of messages, you can implement some cool features like delayed messages, the ability to message a group of entities, and visual debug information (such as the flying envelopes used in this tutorial).
There is room for creativity in the MessageQueue
class, it's up to you and your game's requirements.
Conclusion
Handling interactions between game entities using a message queue system is a way to keep your code organized and ready for the future. New interactions can be easily and quickly added, even your most complex ideas, as long as they are encapsulated as messages.
As discussed in the tutorial, you can ignore the use of a central message queue and just send messages directly to the entities. You can also centralize the communication using a dispatch (the MessageQueue
class in our case) to make room for new features in the future, such as delayed messages.
I hope you find this approach useful and add it to your game developer utility belt. The method might seem like overkill for small projects, but it will certainly save you some headaches in the long run for bigger games.