嫌翻得很差的去看后面的原文吧react
————————————————————————————————————————————————————————————ios
问题程序员
今天的网络多人游戏必须处理大量不一样的消息。有标准的消息(建立玩家、删除玩家、聊天等等),也有游戏中特定的消息。全部这些消息都有它们本身的数据项,它们必须可以经过一个链接发送出去并在另外一端从新组装。做为网络游戏开发者,你的工做就是梳理一切以便你的游戏可以以一种优雅的方式发送和接收消息。web
在C++中最明显的作到这一点方式就是用类来表示不一样的消息。这些类包括一个特殊消息的全部数据,也包括将这些数据序列化和反序列化到一个字节流的方法。然而,既然全部的消息都包含一些公有的数据元素(例如,从哪里来,到那里去),实现一个抽象的基类以便每一个不一样的消息能够从它继承是有意义的,像下面这样:网络
// 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; }; // convert a directplay message into our class void net_message_createplayerorgroup::serializefrom(byte *fromdata, int datasize) { LPDPMSG_CREATEPLAYERORGROUP lp = reinterpret_cast(fromdata); m_data.setdata(lp->lpData, lp->dwDataSize); m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP); namestructtoplayer(lp->dpnName, m_playername); }
// another derivation. class net_message_destroyplayerorgroup : public net_message { public: int serializeto(byte *output) { output = NULL; return(-1); } void serializefrom(byte *fromdata, int datasize); uti_string getplayername(void) { return(m_playername); } bool isgroup(void) { return(m_isgroup); } private: bool m_isgroup; uti_string m_playername; }; // convert a directplay message into our class void net_message_destroyplayerorgroup::serializefrom(byte *fromdata, int datasize) { LPDPMSG_DESTROYPLAYERORGROUP lp = reinterpret_cast(fromdata); m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP); namestructtoplayer(lp->dpnName, m_playername); }
发送这些消息不是问题。若是客户端想发送某个消息,它实例化某个适当的类,填充它想要发送的数据,而后调用serializeto()方法,这个方法将全部数据都插入到一个字节流中,而后将这个字节流发送出去。到目前为止,一切都很好。app
问题出如今接收端,这种基于类的表示不一样消息的实现方式意味着当咱们收到一个消息,咱们的程序将只能使用包含在消息中的ID类型来判断具体是哪个类的消息被收到了。换句话说,咱们的接收代码必须可以看到这个消息而且说“好吧,这是一个ID___,这是一个___消息,因此我须要构造一个___类型的类。”而后,咱们必须反序列化数据到这个类的成员中去。less
为何要用可插拔工厂ide
可插拔工厂是这个问题的一个解决方案。想象一下你写了一个新的消息类。如今,想象一下你能够经过简单的在你的项目中添加源码来支持你的自定义消息。这很好,你没必要更改你的网络引擎的任何代码,你简单的将你的文件加入项目并从新编译。函数
听起来好像不是真实的?不,可插拔的工厂使用一些C ++技巧,但它不是火箭科学。ui
可插拔类依赖于两个关键的C++技巧:多态和静态类成员。
让咱们来看一些代码。这个代码直接来自于我即将完成的多人解密游戏中的网络引擎。我将个人基本的可插拔工厂命名为net_message_maker;按照惯例,可插拔工厂一般用单词maker做为类名的一部分,这样可以快速告诉程序员他们是什么。
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 net_message_maker_map; static net_message_maker_map m_registry; virtual net_message *makemessage(byte *data, int datasize) const = 0; };
net_message_make是一个至关简单,至关小的类。constructmessage()方法是咱们感兴趣的;这个方法用一个原始的字节流建立适当的net_message的继承类。注意这个方法是静态的,因此没必要实际的实例化一个net_message_make来使用它。
注意makemessage()纯虚方法。makemessage()与constructmessage()不一样;makemessage()只在继承类中实现,用于生成消息并反序列化它。
咱们有一个构造方法,这个构造方法有一个表示消息类型(例如DPSYS_SESSIONLOST等等)的参数。注意这个构造方法只是简单的将消息类型和消息自己组成一对插入到一个map中。注意构造方法插入的map名为m_registry是静态的,这意味着它被全部的类共享,固然也被全部的继承类共享。
这就是maker基类全部的:一个静态map,一个静态方法和一个纯虚方法。
如今来看看maker的继承类。你要为你想支持的每一个消息建立一个不一样的maker。你可使用模板,也可使用旧风格的#define技巧,甚至能够经过剪切和复制来建立他们。
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; };
注意m_registerthis变量。这是Culp先生指出的一个技巧,我在前面已经暗示过。C++语言程序开始的时候初始化类的静态变量。因此,若是这个代码在程序开始的时候执行,m_registerthis得构造方法就会被调用。m_registerthis的构造方法调用基类net_message_maker的构造方法,这样这个指针将与指定的ID(在这种状况下是DPSYS_CREATEPLAYERORGROUP)组成一对。咱们历来没必要在代码的任何地方显示使用m_registerthis;它惟一的目的就是骗编译器在程序开始的时候运行构造方法。(固然,若是咱们有多个静态变量,C++规范没有明确规定,其中构造函数的调用顺序,但对咱们来讲这没关系)。
这意味着在WinMain()代码的第一行执行以前,m_registry的成员已经包含了一个有效的map,连接到全部已注册的message_maker到它们的消息ID。这就是为何可以在不改动网络代码的前提下能够支持新的消息。
如何工做的
如今看一下整个系统的核心:方法使用一个消息ID返回合适的类。
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); }
比方说,我从receive方法中收到了一大块数据,如今我想把这块数据转成合适的net_message继承类。我调用net_message_maker::constructmessage(),将数据和数据的大小给它。
constructmessage()作的第一件事是将原始字节转换成通用的消息。一旦完成了转型,咱们就知道了消息的类型:lpMsg->dwType。咱们看一下m_registry变量,取出正确的键值对,而后获得程序开始时放入的指针。(若是咱们不能找到这种类型,m_registry.find()将返回NULL,下一行将会产生一个异常,进而进入异常处理,不是很干净的处理方式,可是它能完成任务)。
假设一切正常,本地变量maker将指向合适的工厂类(咱们用这个工厂类来构造消息)。而后咱们调用工厂类的makemessage()方法(咱们能这么作,由于咱们能够访问咱们本身的娶她实例的私有方法)。makemessage()是一个纯虚方法,因此咱们最终会在适当的maker内部结束掉它。
makemessage()实例化合适的net_message的继承类,而后告诉这个实例从给定的字节块中反序列化其自身。如今咱们有了一个完整的net_message,一切准备就绪。
到这,你能够作任何你想作的事了。可能你的网络系统和我同样,在一个vector中存储全部到来的消息;或者作一些线程动做,在另外一个线程中处理消息。这都不是问题。最要紧的是,只要一个简单的方法调用,constructmessage(),你就能将字节块转换为一个C++类。
—————————————————————— 原文在此 ————————————————————————————————————————————
Introduction
I'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 Problem
Networked 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(fromdata);
m_data.setdata(lp->lpData, lp->dwDataSize);
m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);
namestructtoplayer(lp->dpnName, m_playername);
}
// another derivation.
class net_message_destroyplayerorgroup : public net_message
{
public:
int serializeto(byte *output) { output = NULL; return(-1); }
void serializefrom(byte *fromdata, int datasize);
uti_string getplayername(void) { return(m_playername); }
bool isgroup(void) { return(m_isgroup); }
private:
bool m_isgroup;
uti_string m_playername;
};
// convert a directplay message into our class
void net_message_destroyplayerorgroup::serializefrom(byte *fromdata, int datasize)
{
LPDPMSG_DESTROYPLAYERORGROUP lp = reinterpret_cast(fromdata);
m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);
namestructtoplayer(lp->dpnName, m_playername);
}
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 Rock
Pluggable 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 net_message_maker_map;
static net_message_maker_map m_registry;
virtual net_message *makemessage(byte *data, int datasize) const = 0;
};
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 Works
Now 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.
Conclusion
Congratulations, 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.