Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

How to Implement and Use a Message Queue in Your Game

$
0
0

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?

Interactions in a game tend to grow in complexity very quickly
Interactions in a game tend to grow in complexity very quickly.

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. 

Interactions made using a message queue system
Interactions made using a message queue system.

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:

Structure of a message
Structure of a message.

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:

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):

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:

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:

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:

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 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:

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:

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:

The result is four runners randomly moving around:

Adding the Hunter

The Hunter class has the following structure:

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:

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:

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:

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:

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:

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?

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.


Viewing all articles
Browse latest Browse all 728

Trending Articles