Effective Event Handling in C++

本篇是GameDev上的一篇文章「Effective Event Handling in C++ by Szymon Gatner」,這裡做個簡單(隨意)的翻譯,但是其中有些部分實在不知道怎樣翻譯(或者很怪),就用原文帶過啦~哈哈!!如果文中有誤煩請指教>w<!!

—–

很多時候,遊戲中的各個物件(不只是實體上的不同,可能是本質上的相異)都需要互相溝通。為了讓各類別得以溝通,使用一個通用的資訊系統是必要的。

第一個方案(或者可以稱之為古老的C方案):

其中最常見的方案之一,這種系統會使用一個整數代表著特定訊息(事件)。依照這些代號,在各種環境下會有不同的意義。例如:

void VeryBadMonster::handleEvent(Event* evnt)
{
  switch(evnt->type)
  {
  case MSG_EXPLOSION:
    handleExplosion(evnt->position, evnt->damage); 
    //event is union
    //or handleEvent( (ExplosionMsg*) event) if event is struct
  case MSG_TRAP:
    // etc
  }
}

雖然這樣簡潔又快速,但這種方式仍有一些嚴重的缺陷。首先,識別符「MSG_XXXX」定義的來源。引用該標頭檔需要相當小心,避免重複定義。每當有新的訊息加入,則要逐一修改"汙染"到的文件,並且重新編譯!這就是代表著所有類別毫無意義的依賴這些毫無相關的訊息,卻又不能脫離它們。第二,需要為每個驅動物件的事件(訊息)改寫Switch,也就容易出現撰寫的錯誤。此外,展示的程式碼只有極少的物件導向,善用其可重複使用的特性,即可減少複製貼上的動作和危機。

第二方案(也可稱C++方案 – 累加放在後面,代表比C更好,但是仍然會存取C):

這方法利用C++的「執行時期形態資訊(RTTI)」的特性,做更好的處置。主要處理資訊的函式會使用dynamic_cast<>判定資訊的實際用途。

Event類別:

class Event
{
protected:
    virtual ~Event() {};
};

沒啥特別的,但我們所有的事件都需要由這個衍生。如下,要讓兩個不相干的類別互相溝通,這兩個類別所知曉的共用程式碼。

void onEvent(Event* event)
{
  if(Explosion* explosion = dynamic_cast< Explosion* >(event))
  {
    onExplosion(explosion);
  }
  else if(EnemyHit* hit = dynamic_cast< EnemyHit* >(event))
  {
    onEnemyHit(hit);
  }
}

經過連串的if-else,就會呼叫對應的方法。在上面的範例,它會依照事件的形態去啟動對應的方法,利用重載(overloading)和模板(template)可以更簡化程式碼:

template < class EventT >
bool tryHandleEvent(const Event *event)
{
  if(const EventT* concreteEvent = dynamic_cast< const EventT* >(event))
  {
    return handleEvent(concreteEvent);
  }
  return false;
}

void onEvent(const Event* event)
{
  if(tryHandleEvent< Explosion >(event)) return;
  else if(tryHandleEvent< EnemyHit >(event)) return;
}

這個方案比前一個好上很多:轉型(cast)會檢查其合法性,整數(MSG_XXXX)型態識別符以及不相干的類別依賴性都掰了!同時,Event也成為全面性的物件了。但是也有一些缺點在:使用if-else代替switch代表event的型態定義複雜度變為線性(複雜成長度?)而非固定。另一方面,dynamic_cast<>的效率也取決於編譯器和event類別的結構深度。最後也最重要的,選取類型的順序必須非常小心,例如(圖1):如果Event指標一開始動態轉型為Explosion,但它實際上是指向一個NukeExplosion物件的話,程式碼將會誤判它的類型,以至於呼叫到處理Explosion的方法。這個if-else決策鍊仍然有著switch語法的缺陷:它無法重複使用。

fig1[1]

圖1

++C方案(或稱為我們所愛的虛擬函數和模版,而沒有C):

上一個方案是快速的(介於1和2之間),安全的(高於2),切割兩個毫無關係的事件處理類別(如同2)以及完全的重複使用特性。唯一的事情是event處理類別需要知道的,一個穩定的(未曾改變)基層event類別和固定的資訊類別。

這概念是在EventHandler裡面,從基層的Event類別衍生固定的event類別,並註冊成員函式處理事件。EventHandler是負責依照事件的型別找出對應的方法處理事件。

class EventHandler
{
public:
  void handleEvent(const Event*);

  template < class T, class EventT >
  void registerEventFunc(T*, void (T::*memFn)(EventT*));

private:
  typedef std::map< TypeInfo, HandlerFunctionBase* > Handlers;
  Handlers _handlers;
};

template < class T, class EventT >
void EventHandler::registerEventFunc(T* obj, void (T::*memFn)(EventT*))
{
  _handlers[TypeInfo(typeid(EventT))]=
  new MemberFunctionHandler< T, EventT >(obj, memFn);
}

void EventHandler::handleEvent(const Event* event)
{
  Handlers::iterator it = _handlers.find(TypeInfo(typeid(*event)));
  if(it != _handlers.end())
  {
    it->second->exec(event);
  }
}

短小精悍!主要目的是為特定的事件註冊對應的處理方法。

class HandlerFunctionBase
{
public:
  virtual ~HandlerFunctionBase() {};
  void exec(const Event* event) {call(event);}

private:
  virtual void call(const Event*) = 0;
};

MemberFunctionHandler的作用是安全地轉換為正確的事件種類。

template < class T, class EventT >
class MemberFunctionHandler : public HandlerFunctionBase
{
public:
  typedef void (T::*MemberFunc)(EventT*);
  MemberFunctionHandler(T* instance, MemberFunc memFn) : _instance(instance), _function(memFn) {};

  void call(const Event* event)
  {
    (_instance->*_function)(static_cast< EventT* >(event));
  }

private:
  T* _instance;
  MemberFunc _function;
};

如同你所見到的,這裡是使用static_cast而非dynamic_cast,因為所有的MemberFunctionHandler實體都會由EventHandler的registerEventFunc方法自動生成。它是合法且安全的,所以不需要動態轉換了!

在這個方案中,我們擁有:安全的型別檢測(no more problems with order-of-type queries)、有邏輯的處理函式、用static_cast取代dynamic_cast並且可重複使用。

但是,如同回文者指出的,這種模版會導致「程式碼膨脹(code bloat)」。這是意味著,每當為了每個事件處理方法產生有效的MemberFunctionHandler實體時,編譯器有更多事情要做。從而導致編一時間增加,也可能造就一個龐大的執行檔(but in contrast, this is the way that one would normally implement polymorphic callbacks)。同時還有每次處理時呼叫虛擬函式的開銷。但是,測試15種不同類型的事件類別(全都直接由Event類別衍生,所以純粹是if-else v.s. std::map::find的比較)表現出的效能仍然比dynamic_casting+線性搜尋來得好(繼承的深入程度或者事件種類的多寡會得到不同的結果)。

這項技術在我目前的專案中相當有用。因為不再需要類型識別符(MSG_XXXX),就簡化了事件處理類別的程式碼,也降低了類別之間的藕合性。「demo code」展示實際應用的狀況:它產生了Monster和Tank實體,並且當我在遊戲世界中,當它們受到傷害時,可以回報特定的事件。Monster類別公開繼承自EventHandler類別,但是Tank類別是使用私有的內嵌類別,並且隱藏它處理的方法。這方法只是幾天前突然蹦出來的一個想法,或許它並不完美,但仍期望你喜歡它。隨時可和我連絡「szymon-dot-gatner-at-gmail-dot-com」。

P.S

事實上還有個技術沒有提及,它是基於「雙重分派(double dispatch)」的機制。它的執行時期複雜度就只有兩個虛擬函式的呼叫 – which is usually nothing compared to time that it takes to handle the actual event。它之所以在文中忽略是因為該技術會引起的嚴重設計問題,類別間的循環依賴。然而,對於少量事件來說,它仍然是很完美的解決方案。

參考

[1] Applied C++: Practical Techniques for Building Better Software By Philip Romanik, Amy Muntz
[2] Modern C++ Design: Generic Programming and Design Patterns Applied By Andrei Alexandrescu

 

—–

以下是我另外連結的網頁,額外參考來著,哈哈!

※「C++ Gossip: 執行時期型態資訊(RTTI)
※「實用模式:Open Closed Principle
※「Single/Double/Multiple Dispatch – Part 1
※「Double dispatch – Wiki

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 變更 )

Twitter picture

You are commenting using your Twitter account. Log Out / 變更 )

Facebook照片

You are commenting using your Facebook account. Log Out / 變更 )

Google+ photo

You are commenting using your Google+ account. Log Out / 變更 )

連結到 %s