Writing a Game Engine – Pt. 2 – Messaging

This article follows from the Application Class article and assumes that you are familiar with that article.

Messaging is one of the most important subsystems in a game engine.  It allows the developer to decouple the occurrence of an event from the handling of that event.

Why Coupling Events with Event Handling is Bad

The design paradigm of responding to an event immediately can limit performance and cause the developer to write bad code.

Say you’re writing a stealth espionage game, let’s call it Plastic Gear Liquid.  The “Process Inputs” step might look conceptually like this:

  • Player presses the Shoot Gun button.
  • Game immediately triggers the character’s Shoot Gun action.

So far, so good.  But how would enemy responses work?

For example, when the main character (let’s call her Liquid Salamander) fires her gun, several things may need to happen. To name a few examples:

  • Animation System plays an animation of Liquid Salamander raising and firing her weapon.
  • Particle System causes a flash, and maybe some gun smoke, to be drawn.
  • Sound System plays an appropriate sound effect.
  • AI System alerts enemies to Liquid Salamander’s presence.

Without some kind of messaging in place, Plastic Gear Liquid would prove to be difficult to write.

Using messaging, the design is fairly straightforward:

  • During the application class’ “process user input” step:
    • Input Handler collects the user’s input (pressing the “shoot” button), and places a message on the message queue that the user pressed the shoot button.
  • During the “process commands” step:
    • Animation System looks at the “user pressed the shoot button” message and initiates Liquid Salamander’s shoot-weapon animation.
    • Particle System looks at the same message and begins the light flash/gun smoke particle effect.
    • Sound System looks at the same message and plays the appropriate sound effect.
    • AI System looks at the same message and initiates the notify-nearby-enemies routine (likely based on distance from the character and whatever other inputs).
  • During the “update actors” step:
    • Liquid Salamander’s animation advances.
    • The particle effects continue.
    • The sound effect keeps playing.
    • The AI enemies track down Liquid Salamander.

Implementing a Messaging Subsystem

There are probably many ways to implement messaging in a game.  The Falldown engine (and the WIP Gamma Engine) uses a message queue.  The message queue is, as the name suggests, a central queue of messages.  It is “central” because there is only one MessageQueue in the engine.  Any game objects (GameObjs) that participate in messaging will write to, and read from it.

The message queue interacts with Writers and Readers.  A Writer is any GameObj that leaves a message for any Reader; a Reader is any GameObj that reads messages left by any Writer.  Any GameObj can Reader, or a Writer, or both.

The key to the message queue is that messages are destinationless. Writers don’t know which specific Readers will receive their message, and similarly, Readers don’t know which Writers will leave messages of interest.  Instead, Writers publish their messages with a given Topic, and Readers subscribe to whatever Topics they’re interested in (this is called the publish/subscribe pattern).

An IRL Example

In Falldown, the message queue is implemented by a MessageQueue object.  The MessageQueue is an object that has the following:

  • a queue of Message objects
  • a collection of lists of registered Readers (in Falldown, I called them “RegisteredListeners”).

To register a listener, the MessageQueue object has a RegisterListener function that adds a specified listener to a list of listeners for a given topic.  The function looks something like this:

def RegisterListener(self, listener_name, listener, topic):
    self._registeredListeners[topic].append( { 'name': listener_name, 'ref': listener} )

The code snippet is a member function of the MessageQueue class. “_registeredListeners” is the container of lists of listeners for each message Topic..

NOTE: This function does not test for duplicate entries before adding/replacing items, but it is a good idea to do so in real life.

Once the listeners are registered, the rest of the messaging flow goes something like this:

  • During the “process inputs” step of the game loop:
    • The input handler collects keyboard presses and enqueues keypress event messages (e.g., Topic = ‘PlayerInput’) onto the MessageQueue object’s message queue.
      • Side note: To be clear, lower-case “message queue” is the concept we’re talking about; camel-case “MessageQueue” is the name of the object in the engine. The MessageQueue object contains a message queue, as well as other things.
  • During the “Process Commands” step of the game loop:
    • The MessageQueue dequeues a message off the queue, to be processed.
    • The MessageQueue then steps through the list of registered listeners for the message’s Topic.
    • For every RegisteredListener subscribed to the Topic:
      • The RegisteredListener “makes a note to self” to act on the contents of the Message. (Note that the RegisteredListener does not actually begin acting yet. That happens later.)
      • In Falldown, the Ball “makes a note to self” by setting state variables: “Moving Left = True/False”, or “Moving Right = True/False”
  • During the “Update” step:
    • The Actor processes its “notes to self” and acts
    • In Falldown, the Ball acts by actually changing its position to the left or right, depending on its state variables.

… And then after all that, we draw the scene (not pictured here)

About Message Objects

In Falldown, every Message contains:

  • A Topic, and
  • a Payload

The Topic is a string, e.g. “PlayerInput”.

The Payload is more complex.  In Falldown, the Payload contains:
* an Action, and
* Other Stuff, depending on the Action.

What constitutes “Other Stuff” depends on the programmer’s design.  It’s probably best to show a real example from Falldown:

message =
{
    'topic': 'PlayerControl'
,   'payload': {
                   'action': 'call_function'
               ,   'function_name': 'setLeftKeyPressedTrue'
               ,   'params' : ''
               }
}

In this example, the Payload contains:

  • an action, “call_function”
  • a function_name, “setLeftKeyPressedTrue”, which is the name of a function that sets a state variable for the ball.
  • params, a list of parameters to be sent into the function.

The Falldown engine uses the message above to compose a call to a function that does something useful.  For example, the above message will make the Ball call its own setLeftKeyPressedTrue() function.

In practice, you can design Messages to contain whatever Payload will be useful in your game. The design described in this article is meant to give you ideas to experiment with.

Happy gamedeving!

Leave a Reply

Your email address will not be published. Required fields are marked *