Why Pluggable Factories Rock My Multiplayer World
IntroductionI've developed a nasty habit over the years. Whenever I come across a business programming article, I instinctively assume that it won't be relevant to anything cool. My initial reaction is usually "OK, wow, this is great for writing middleware, but probably useless in game programming." Most of the time this turns out to be true (when was the last time you used a SQL database to store saved games?), however, there are always a few articles that describe something that can be useful for game programming. One of those articles recently appeared in the magazine "C++ Report." (http://www.creport.com). Timothy R. Culp wrote an article entitled "Industrial Strength Pluggable Factories." In it, he describes a very valuable trick, not only in the business world, but in game programming as well. This article is an attempt to take Mr. Culp's work and bring it down into the scary mosh pit of game development. Before continuing, head over to the C++ Report website and read the pluggable factories article. I'm not going to duplicate what's already been said; I'm going to assume you've read the article and know the basics, and I'm going to dive straight into showing how Pluggable Factories can be used to simplify DirectPlay communications. The ProblemNetworked multiplayer apps today must deal with a wide variety of messages. There's the standard set of DirectPlay messages (Create Player, Delete Player, Chat, etc.), as well as the army of messages your game needs to communicate. All of these messages have their own data items, and they all must be able to send themselves through a DirectPlay connection and reassemble themselves on the other side. It's your job as a network game programmer to sort everything out so that your game has an elegant way to send and receive its information. The obvious way to do it in C++ is to use classes to represent the different messages. These classes contain all of the data for a particular message, as well as methods that serialize and deserialize the data into a byte stream (suitable for sending over a DirectPlay connection). Also, since all of the messages have certain data elements in common (like, who the message was from, and who it's going to), it makes sense to implement an abstract base class and then derive each different message type from it, like so: // the net_message base class class net_message { public: net_message() { } ~net_message() { clear(); } void clear(void) { } virtual int serializeto(byte *output) { return(0); } virtual void serializefrom(byte *fromdata, int datasize) { } DPID getfrom(void) { return(m_from); } DPID getto(void) { return(m_to); } protected: void setfrom(DPID id) { m_from = id; } void setto(DPID id) { m_to = id; } DPID m_from; DPID m_to; }; // a specific message derived from the base class – this // example corresponds to DPSYS_CREATEPLAYERORGROUP. class net_message_createplayerorgroup : public net_message { public: int serializeto(byte *output); void serializefrom(byte *fromdata, int datasize); uti_string getplayername(void) { return(m_playername); } bool isgroup(void) { return(m_isgroup); } net_byteblob &getdata(void) { return(m_data); } private: net_byteblob m_data; uti_string m_playername; bool m_isgroup; }; // convert a directplay message into our class void net_message_createplayerorgroup::serializefrom(byte *fromdata, int datasize) { LPDPMSG_CREATEPLAYERORGROUP lp = reinterpret_cast Sending these messages isn't a problem – if the client wants to send a certain message, it instantiates the appropriate class, fills up the class with the data it wants to send, and then calls the serializeto() method, which squishes everything into a byte stream, which is then sent using IDirectPlay->Send(). So far, so good. The problem is on the receiving end. Developing this class-based approach to messaging means that when we receive a message, our program will have to conjure up the appropriate class using nothing but the ID byte contained within the received message. In other words, our receive code must be able to look at a message and say, "OK, that's ID ___… that's a ____ message, so I need to construct a class of type ____." Then, we must deserialize the data back into the members of the class. Why Pluggable Factories RockPluggable factories are a solution to that problem. Imagine you write a new message class that you want to use in your program. Now, imagine that you can add support for your custom messages by simply adding the source files to the project. That's right – you don't change any lines in your networking engine… you simply add your files to the project and recompile. Sound too good to be true? It's not. Pluggable factories use a few C++ tricks, but it's not rocket science. Meet Your Maker"Blessed are the game programmers, for they shalt not have to deal with legacy file formats." The pluggable factory relies on two key C++ tricks: polymorphism (derived classes and virtual functions), and static class members. Let's look at some code. This code is straight from the networking engine of my upcoming multiplayer puzzle game, Quaternion (see my homepage for more information). I've called my base pluggable factory net_message_maker; by convention, pluggable factories usually have the word "maker" somewhere in their class name. This not only quickly tells any programmer what they are, but it also allows us writers to amuse ourselves by creating clever names for the sections of our articles. class net_message_maker { public: net_message_maker(int type) { m_registry.insert(std::make_pair(type, this)); } static net_message *constructmessage(byte *data, int datasize); protected: typedef std::map For its power, net_message_maker is a fairly simple little class. The constructmessage() function is the one we're interested in; this function takes a raw byte stream and creates the appropriate net_message derivative instance. Note that this function is static, so you don't need to actually instantiate a net_message_maker to use it (simply say net_message_maker::constructmessage(…)). Notice the makemessage() pure virtual function. makemessage() is not the same thing as constructmessage(); makemessage() is only implemented in the derivitive classes, and is responsible for newing the message and deserializing it. We have one constructor, which takes one argument – the type of message (i.e. DPSYS_SESSIONLOST, etc.) Notice that this constructor simply hands off to the base class constructor, which takes the message type, pairs it with a pointer to itself, and inserts the pair into a map (if you're not familiar with STL, you might want to learn about maps before continuing). Notice that the map the constructor inserts into – m_registry -- is static, which means it's shared by all classes, and by all derivative classes as well. That's all there is to the base maker class. One static map, one static function, one pure virtual function. Now let's look at a maker derivation. You'll need to derive a different maker for each message you want to support – you can either use templates, or some old-fashioned #define trickery, or even (horror of horrors) cut and paste to create them. class net_message_createplayerorgroup_maker : public net_message_maker { public: net_message_createplayerorgroup_maker() : net_message_maker(DPSYS_CREATEPLAYERORGROUP) { } private: net_message *makemessage(byte *data, int datasize) const { net_message_createplayerorgroup *msg = NULL; try { // construct the appropriate message type msg = new net_message_createplayerorgroup; // tell the message to populate itself using the byte stream msg->serializefrom(data, datasize); } catch(...) { // handle errors! } return(msg); } static const net_message_createplayerorgroup_maker m_registerthis; }; Notice the m_registerthis variable. This is one of the tricks Mr. Culp pointed out, and I hinted at eariler. The C++ language says that static members of classes are initialized at program startup. So, if this code is part of the program when it starts up, the constructor for the m_registerthis variable is going to get called. The m_registerthis constructor calls the base net_message_maker class constructor, which pairs the this pointer with the ID given (in this case, DPSYS_CREATEPLAYERORGROUP). We never explicitly use m_registerthis anywhere else in the code; it's sole purpose is to trick the compiler into running the constructor at program startup. (Granted, if we have multiple static variables, the C++ spec doesn't specify in which order the constructors are called, but that doesn't matter to us). What this means is that before the first line of our WinMain() is executed, the m_registry member is going to contain a valid map, linking all registered message_makers to their message IDs. This is how it's possible to add support for a new message without changing one line of the networking code. How It WorksNow let's take a look at the heart of the whole system: the function that takes a message ID and returns the appropriate class. net_message *net_message_maker::constructmessage(byte *data, int datasize) { // cast the raw memory to a generic message to determine its type LPDPMSG_GENERIC lpMsg = (LPDPMSG_GENERIC)data; try { // find the appropriate factory in the map of factories... net_message_maker *maker = (*m_registry.find(lpMsg->dwType)).second; // use that factory to construct the net_message derivative return maker->makemessage(data, datasize); } catch(...) { err_printf("net_message_maker::constructmessage: logic error, I don't know how to (or can't) construct message ID %d!", lpMsg->dwType); } return(NULL); } Let's say I've just received a big blob of data from DirectPlay's receive function, and now I want to convert that blob of data into the appropriate net_message derivative. I call net_message_maker::constructmessage(), giving it the blob of data, and the size of the blob of data. The first thing constructmessage() does is cast the raw data to a generic message. This is the sort of "blind casting" that should make any good C++ programmer freeze in terror, but it's a necessary evil. The DirectX docs even tell us to do it this way. Once we've cast the blob, we know the type of the message: lpMsg->dwType. We look in our m_registry variable, and pull out the correct pair. Then we get the second member of that pair, which is really the this pointer that the constructor registered at program start. (If we can't find the type, m_registry.find() is going to return NULL (or, in debug, 0xcdcdcdcd), which will generate an exception on the next line, and will land us in the exception handler for the function. Not the cleanest way to do things, but it gets the job done). Assuming nothing goes wrong, the local variable "maker" now points to the appropriate factory we should use to construct the message. We then call the makemessage() function of that factory (we can do so, because we have access to the private methods of other instances of ourselves). makemessage() is a pure virtual function, so we'll end up inside of the appropriate maker. makemessage() news up the appropriate net_message derivative, and then tells that instance to deserialize itself from the provided byte blob. Now we have a perfect net_message, all ready to go. From here, you can do whatever you want. Maybe your networking system is like mine, and stores all of the incoming messages in a vector… or maybe you've got some thread action happening, and have a secondary thread processing the messages. That really doesn't matter – what matters is that with one simple function call, constructmessage(), you've transformed a byte blob into a C++ class. ConclusionCongratulations, you now know about pluggable factories. Keep in mind that this technique, as Mr. Culp explains, isn't just for networking messages. Basically any place in your code where you need to turn an ID byte into a class is a great place for pluggable factories. There's a lot more power contained in this pattern than I'm illustrating; the purpose of this article was to show you how to apply a theoretical concept directly to your code. And, just maybe, to make you think twice before you cast off that "business programming journal" as useless. :) Mason McCuskey is the leader of Spin Studios, an indie development group looking to break into the industry by creating a great game, Quaternion, and getting it published. He can be reached via the Spin Studios website (http://www.spin-studios.com), and doesn't mind answering your questions by email at mason@spin-studios.com. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|