嫌翻得很差的去看後面的原文吧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.