The Right Way To Do Message Type Resolution
By Kyle Wilson
Tuesday, May 28, 2002Shortly after I left Cyan, they scrapped Plasma and did a total rewrite of the engine for Myst Online. Message passing is central to Plasma 2.0. Cyan's motivation for using message passing the way they do is that they want to make maintaining consistency in network games easier. The idea is that an object's state can only be changed by receipt of a message, and messages know how to write themselves to a network stream. They enforce this decision by allowing an object to only publicly expose const member functions, except for the message handling function.
Coincidentally, I discovered when I got to iROCK that message passing was also integral to RFEngine, the game engine used on Savage Skies. iROCK's motivation was different, though. Here, the idea was to reduce interdependencies between different engine modules. If modules communicate through message passing, they only need to know about the messages and the message broadcasting system, not about each other. It wasn't clear what operations should use message passing and which should use direct function calls, though, and the code did little to force coders to use message passing, so the system deteriorated over time. The lesson there is, if you're the designer on a game engine and expect your design decisions to last, make sure that you use the physical and logical structure of the engine to encourage compliance.
But that's an article for another day. For now, just note that message passing is a useful feature of game engines. Capable programmers realize that a wide range of problems can be solved by adding another level of indirection, and message passing does just that on a grand scale. The way the system generally works is that you have something like this:
class BaseMsg
{
void Send();
};class DerivedMsgOne : public BaseMsg
{
/* class-specific data */
};class DerivedMsgTwo : public BaseMsg
{
/* class-specific data */
};class BaseListener
{
virtual void Receive(BaseMsg* msg);
};Listeners derived from BaseListener register themselves in a list, either a static member of BaseMsg or a singleton Broadcaster object. One way or another, directly or indirectly, BaseMsg::Send is responsible for calling BaseListener::Receive for every registered listener. The various overriding functions of BaseListener::Receive process each message and... do what? We have, at this point, lost all the information about what kind of message it is, and what subclass-specific data it contains.
iROCK handled this problem the old C way. There's a central enum of message types. Each message knows its type. Receivers check the enum, cast to the appropriate derived type, and proceed on their way.
Cyan handled things a little more safely, if a little less efficiently. In their implementation, BaseMsg has a virtual GetAsDerivedMsgXXX() function for each derived class which returns NULL. Derived classes override their own GetAs function to return their this pointer instead. (Or at least that's the gist of it... their roll-it-yourself RTTI system is actually more complicated, and uses a lot of C-style #define macros to keep things manageable.) The whole thing is less efficient than just checking enums and casting, but at least it's typesafe. The efficiency is only really a problem when, as is often the case, you end up with two or three dozen message types and you have and if ... else clause for each, calling all those virtual functions every time you get a message.
I think that the right way to handle message reception is with something like the visitor pattern, or Scott Meyers' double-dispatch idea from More Efficient C++ (which is similar to visitor). The basic idea is that you have a virtual function call back into the message that resolves its type. So you have a class structure like this:
class BaseMsg
{
void Send();
virtual void SendActual(BaseListener* listener) = 0;
};class DerivedMsgOne
{
virtual void SendActual(BaseListener* listener) { listener->ReceiveActual(this); }
};class DerivedMsgTwo
{
virtual void SendActual(BaseListener* listener) { listener->ReceiveActual(this); }
};class BaseListener
{
void Receive(BaseMsg* msg) { msg->SendActual(this); }
virtual void ReceiveActual(DerivedMsgOne* msg) { }
virtual void ReceiveActual(DerivedMsgTwo* msg) { }
};class DerivedListener
{
virtual void ReceiveActual(DerivedMsgOne* msg) { /* do something */ }
};The benefits are that everything's type-safe, it's easier to maintain and understand than a switch statement, and the cost to resolve each message is exactly two virtual function calls (into BaseMsg::SendActual, then back to one of the BaseListener::ReceiveActual functions). To handle a particular type of message, all a class has to do is implement its own ReceiveActual function for that message type. If it wants to ignore a message, it can just trust the ReceiveActual in the base type to do nothing.
I see two downsides. First, there's a circular dependency between BaseMsg and BaseListener. Both are pretty lightweight interface classes, though, so I wouldn't expect compile/link times to be hurt too much. Second, BaseListener changes every time you add a new derived message class. Thus, any time you add a new message type, you have to recompile all other messages and all listeners. If messages are lightweight and all defined in the same header, though, you're doing the same thing now.
I'm Kyle Wilson. I've worked in the game industry since I got out of grad school in 1997. Any opinions expressed herein are in no way representative of those of my employers.