遊戲Entity層通訊機制:超越虛擬函式

nightmare發表於2011-10-30
    物件導向的程式設計模式無疑是成功的,虛擬函式機制對構造複雜系統無疑是非常有用的。但面對足夠複雜的問題時,我們往往會需要更加強大的機制。遊戲的實體層的構造,就是其中一種可能的情況。

    有人會驚詫道:哪尼,傳說中的XXL號的軟體都可以完美地用OO實現,一個遊戲邏輯會更復雜?莫急莫急,待我娓娓道來。話說從前有個程式,它有很多可能的事件需要子類定製處理,於是它就定義了很多的虛擬函式,但是每個子類通常只關心一小撮事件的處理,於是大部分的時間浪費在呼叫不能內聯的空的虛擬函式上了,它的人生很空虛,於是它在和其它程式的比武中敗陣下來。這就是最不重要的一個原因:空的虛擬函式開銷。

    用虛擬函式機制最大的問題是耦合性,因為基類裡必須為所有可能的事件定義虛擬函式,也就意味著本應該對遊戲具體邏輯最無知的基類必須和遊戲具體設計相關。相比而言,訊息機制則有更大的靈活性,基類只需一個虛擬函式來處理一個訊息,而不需要知道具體是什麼訊息,而且可以方便地廣播分發給多個實體。用C寫過Windows或XWindow程式的讀者會立刻覺得很熟悉。但是,對高階一點的語言和應用開發框架,比如MFC/WinForm/WPF,這些API看起來是標準的一個事件一個虛擬函式的方式啊?這就是機制和表象的區別了。在底層,它是一套訊息機制,在高層,可以把它包裝成容易按OO方式理解的形式。MFC是用巨集將訊息對映到成員函式。WinForm是根據訊息觸發事件虛擬函式,基類Control只對映通用的事件虛擬函式,子類會對映自己專用的事件虛擬函式。WPF不使用作業系統的訊息機制,而是有自己的訊息系統,核心元件是Dispatcher類,它的訊息定義和一個函式呼叫非常的類似,訊息資料就是函式和呼叫引數。

    類似的,遊戲實體層設計中,也有不同的訊息機制實現。有人喜歡簡單的switch列舉量(訊息作為資料),有人喜歡把它包裝得象函式呼叫(訊息作為操作)。定義訊息最原始的方式是:

struct Message
{
    MsgCode msg;
    int param1;
    int param2;
}

    缺點很明顯:引數個數有限且需要型別轉換。更OO的方式是定義一個訊息型別繼承系統:

class Message
{
public:
    virtual MsgCode getMsgCode() const = 0;
    virtual ~Message() {}
}

class UpdateMessage : public Message
{
public:
    MsgCode getMsgCode() { return MSG_UPDATE; }
    float TimeInterval;
}

    當然也有用C++流方式的,省去了為每個訊息定義型別的麻煩,但也失去了型別檢查:

Message msg;
msg << MSG_UPDATE << timeInterval;
SendMessage(msg);

    託管語言可以簡單地用物件陣列傳遞引數,將訊息對映到類成員函式,而同時享有執行時引數型別檢查:
MessageQueue.SendMessage(MsgCode.Update, timeInterval);

    指令碼語言可以動態dispatch:
MessageQueue.SendMessage("Update", timeInterval);

    儘管方式各有不同,訊息佇列都是必定要有的,因為訊息機制的另一個重要好處是可以支援非同步處理和優先順序。非同步的極致是可以讓遊戲邏輯分配到多個執行緒並行,甚至多個server並行,並可以根據負載動態調整任務分配。非同步不止意味著並行(Parallel),還有併發(Concurrent)。遊戲邏輯中,經常有併發的需求,比如角色A走到角色B面前,和B說話,之後去桌上拿一本書,同時遊戲其它部分照常執行,這可以通過響應訊息的狀態機實現,或者看起來更直觀的coroutine。訊息的優先順序支援,還可以擴充套件到指定訊息n秒後傳送,以實現延時操作的功能。

    到此,我們回憶一下基本的物件導向概念中物件的幾個基本元素:屬性(property)、方法(method)、訊息(message)。訊息?!沒錯。我們過於熟悉C++等主流程式語言,以致忘記了OO的本來面貌。看看Objective C或Smalltalk等純OO語言,就會發現訊息在其中佔的比重是多麼的大。

相關文章