一个基于observer模式的游戏事件分发系统

来源:互联网 发布:c语言四种基本数据类型 编辑:程序博客网 时间:2024/06/05 07:37

转自:http://blog.csdn.net/popy007/article/details/8242787


-潘宏

-2012年11月

-本人水平有限,疏忽错误在所难免,还请各位高手不吝赐教

-email: popyy@netease.com

-weibo.com/panhong101



在游戏引擎以及产品开发中,程序员需要让大量的系统模块相互进行通信。在大多数时候,模块的数量是巨大的。在最坏的情况下,N个相互通信的模块需要N*(N-1)/2种依赖关系。就算是最简单的游戏产品,N的数目也是可观的。



如果设计者放任这种相互依赖的模块关系于不顾,整个系统将高度耦合,代码牵一发而动全身。对一个模块的修改,将导致多个模块无法工作,必须进行相应调整。这将导致程序员的焦躁情绪,从而很容易让局面难以控制。 此外,还需要忍受长时间的重新编译、长时间加班修补bug。这些是OOP设计模式(指GoF的经典著作所描述的)所要努力避免的。


一个设计合理、高效的系统,需要对模块进行解耦——使得模块间的依赖关系减少。同时开发者将获得更好的代码维护性和扩展性,并节省大量的编译时间。本文描述了一种基于observer模式的事件分发系统(Event Dispatch System,EDS),用来进行游戏模块的相互通信。在一般的情况下,N个模块的相互通讯只需要N种依赖关系(借助于EDS)。


我们通过描述基本知识来逐步构成我们对系统的深层理解。


Observer模式


Observer模式也叫publish/subscribe模式或listener模式,它实际上是一种利用callback方法进行通信的机制。依照GoF的描述,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。如下图所示,



我把上面的图帮你解释一下(如果你是该模式的专家,可以忽略解释):一个Subject关联任意多个Observer,这在多态下也一样,一个子类ConcreteSubject也可以关联任意多个Observer。而Observer也自成体系,但Subject体系一般只操作基类Observer。开发者可以通过Subject::Attach和Subject::Detach将目标Observer绑定到它所关心的Subject身上,当Subject产生任何变化需要通知Observer的时候,Subject::Notify方法会通知所有被绑定的Observer。同时,得到通知的Observer都可以访问到它们所关心的Subject,得到它们相应的状态。


通俗地说,一个模块A如果想在模块B进入某种状态的时候,做一些事情,我们需要知道B进入状态的时机,并在该时机通知A。这引出我们所说的通知模型概念,在软件开发中,有两种通知模型被使用:Poll和Callback。


Poll通知模型


一种最简单的通知方法叫作轮询(poll)——模块A需要在每个CPU周期中查询B是否进入了该状态——这其实是一种主动的自我通知。比如:


[cpp] view plain copy
  1. class B  
  2. {  
  3. public:  
  4.     //...  
  5.     bool isFinished() const { return m_isFinished; }  
  6.     void cycle()  
  7.      {   
  8.         //...  
  9.     }  
  10. private:  
  11.     // ...  
  12.     bool m_isFinished;  
  13.   
  14. };  
  15.   
  16. class A  
  17. {  
  18. public:  
  19.     void poll( B& b )  
  20.     {  
  21.         if( b.isFinished() )  
  22.         {  
  23.             // Do something  
  24.         }  
  25.     }  
  26. };  
  27.   
  28.   
  29.   
  30. int main()  
  31. {  
  32.     A a;  
  33.     B b;  
  34.   
  35.     for(;;)  
  36.     {  
  37.   
  38.         b.cycle();  
  39.         a.poll( b );  
  40.     // ...  
  41. 
   }  
  42.     return 0;  
  43. }  


A类的class object a,在每一个主循环周期中通过A::poll对B类的class object b进行状态查询,当发现B::isFinished返回true,也就是条件满足,则进行相应的操作。


这就是状态轮询惯用法——简单、直接,也有些野蛮!它有两个致命的缺点:


1)轮询代码是查询方主动调用,查询方不知道对方何时完成,因此如果需要对查询结果进行快速反应,需要在每个周期中进行查询调用,大量的CPU周期被浪费在无效的查询指令中。

2)当模块的数量提高,这种轮询方式将产生代码急剧膨胀,每两个有依赖关系的模块,都需要在周期中进行相互查询。这样的代码将难以甚至无法维护。


因此,轮询一般只在规模不大的几个模块间调用,并且模块状态不能太多。


Callback通知模型


和轮询相对的,是让B主动的通知A,A被动等待B的完成并通知,在这段等待时间中A可以继续做其他的事情,不用浪费CPU周期无谓地查询目标状态,这叫作callback——回调,也是observer模式的实现策略。很明显,callback克服了poll模型的两个致命缺点,但代码相对复杂一些。在C++ OOP中,我们一般有三种方式来实现callback。


1 使用静态成员函数


[cpp] view plain copy
  1. class A  
  2. {  
  3. public:  
  4.     static void onBFinished( void* param )  
  5.     {  
  6.         A* _this = static_cast<A*>(param);  
  7.         // ... Do something using _this  
  8.     }  
  9.     void cycle()  
  10.     {  
  11.         // Do something...  
  12.     }  
  13. };  
  14.   
  15.   
  16. typedef void (*FPtrOnBFinished)( void* param );  
  17. class B  
  18. {  
  19. public:  
  20.     void setCallback( FPtrOnBFinished callback, void* param ) { m_callback = callback;                      m_param = param; }  
  21.     void cycle()  
  22.     {  
  23.         // Finished in some cases  
  24.         m_callback(m_param); // Notify the subscriber  
  25.     }  
  26. private:  
  27.   
  28. FPtrOnBFinished   m_callback;  
  29. void* m_param;  
  30. };  
  31.   
  32. int main()  
  33. {  
  34.   
  35.     A a;  
  36.     B b;  
  37.     b.setCallback( A::onBFinished, &a );  
  38.   
  39.     for(;;)  
  40.     {  
  41.         a.cycle();  
  42.         b.cycle();  
  43.     }  
  44.   
  45.     return 0;  
  46. }  

可以看到B存储了A的静态方法地址以及A class object指针,该指针将作为参数传递给A的静态方法,以决定到底是谁——哪个对象对该事件感兴趣,并通知它。


该指针实际上充当了非静态成员函数中this指针的作用——我们给static方法显示地放置了一个“this指针”——实参param,编译器不需要、也不可能干预static成员函数的this指针安插。这可以通过A::onBFinished看出,实参param被转换为A*,然后针对该指针(也就是subscriber本身——希望得到通知的观察者)进行相应操作。


很容易想到,class B的subscriber可以是多个,这也是observer的设计初衷。你可以扩展B的类接口来增加、减少subscriber和它们相应的callback。将它们妥善保存在一个容器里面,并在B::cycle中通过迭代通知每一个subscriber。


回调方法中的形参类型void*保证了subscriber对于publisher是类型无关的,只要它能够在相应的callback方法中被正确的cast成真实的类型,就可以了。这提供了很大的灵活性,是这种回调方法的中心思想。另一方面,这种方式也会导致潜在的类型安全问题——这要求客户程序员必须保证subscriber类型的一致性。如果把一个错误的类型对象作为param给了publisher,程序仍可以经过编译,但在运行期subscriber的callback方法中的cast会以失败告终,这通常是危险的。


2 使用指向成员函数的指针


[cpp] view plain copy
  1. class A  
  2. {  
  3. public:  
  4.     void onBFinished();  
  5.     void cycle()  
  6.     {  
  7.         // Do something...  
  8.     }  
  9. };  
  10.   
  11. typedef void (A::*onBFinished)();  
  12. class B  
  13. {  
  14. public:  
  15.     void setCallback( onBFinished callback, A* a ) { m_callback = callback; m_a = a; }  
  16.     void cycle()  
  17.     {  
  18.         // ...  
  19.         // Finished in some cases  
  20.         (m_a->*m_callback)();  
  21.     }  
  22.   
  23. private:  
  24.   
  25.     onBFinished m_callback;  
  26.     A* m_a;  
  27. };  
  28.   
  29. int main()  
  30. {  
  31.     A a;  
  32.     B b;  
  33.     b.setCallback( &A::onBFinished, &a );  
  34.     for(;;)  
  35.     {  
  36.         a.cycle();  
  37.         b.cycle();  
  38.     }  
  39.   
  40.     return 0;
}  


static member function被替换成了指向成员函数的指针。


这对类型安全性起到了一定促进作用:在B::cycle中对指向成员函数的指针进行调用的时候,编译器会进行type-checking,从而将错误的类型匹配拦截在编译期。


另一方面,由于必须指定指向成员函数指针的类作用域,比如A::,B需要知道所有subscriber的类型。这在一定程度上增加了类的耦合性:系统每增加一个subscriber类,就需要在publisher类中定义相应的callback typedef,这将带来维护开销,并增加重编译时间。而这个问题在上面的static方法中是不存在的。


下面的方法则解决了类型安全和类耦合两个问题(这也是observer模式的一种正规实现方式)。


3 使用Listener类


建立一个监听器基类,使用多态方法进行通知。

[cpp] view plain copy
  1. class Listener  
  2. {  
  3. public:  
  4.     virtual ~Listener() {}  
  5.     virtual void onSomeEventReceive() = 0;  
  6. };  
  7.   
  8. class A : public Listener  
  9. {  
  10. public:  
  11.     virtual void onSomeEventReceive()  
  12.     {  
  13.         //...Do something  
  14.     }  
  15. };  
  16.   
  17. class B  
  18. {  
  19. public:  
  20.     void setListener( Listener* l ) { m_listener = l; }  
  21.     void cycle()  
  22.     {  
  23.         // Finished in some cases  
  24.         m_listener->onSomeEventReceive();  
  25.     }  
  26.   
  27. private:  
  28.     Listener* m_listener;  
  29.   
  30. };  
  31.   
  32. int main()  
  33. {  
  34.     A a;  
  35.     B b;  
  36.     b.setListener( &a );  
  37.   
  38.     for(;;)  
  39.     {  
  40.         b->cycle();  
  41.     }  
  42.   
  43.     return 0;  
  44. }  


这种方法中,B不需要知道listener的具体类,它只需处理listener抽象类。系统通过多态来高效地复用基类代码。这很好地解决了上述的类型安全和类耦合两个问题:


a)至少在B的层面上,B处理的必须是listener类,B只调用它的方法,编译器保证了这一点。

b)增加任何一个新的listener子类,B都不需要关心,它只处理listener类。B的代码不需要重新编译。


该方法有缺点吗?当然。缺点主要在于它使用了C++的virtual机制,这是C++的性能落后于C的主要开销之一——通过增加间阶层来提高设计抽象性而带来的开销。但,这样的性能损失相对于该结构所带来的设计优势而言,是可以接受甚至忽略的。


通过以上三个方法,我们看到了每种回调方式的优缺点。后面我们会使用listener方式来构建我们的事件系统,因为对于我们的系统来说,它更易于维护、扩展,更符合OOP精神。


游戏事件系统


现在我们回到游戏消息系统主题中来。首先我们已经决定使用回调方式来进行消息通知,更进一步来说,我们打算采用listener方案。接下来我们要考虑我们如何定义模块结构。


为了避免文章开头所说的N个模块的最差依赖关系N*(N-1)/2,同时为了保证每两个模块之间都可以进行通信,我们的事件系统将采用一个中介模块,这实际上是一个mediator模式(如果对mediator模式感兴趣,请参考GoF的书)的简化版本。整个系统结构如图所示:




A到H每个模块都和mediator通信,Mediator会把相应的消息发给相应的模块。这样,N个模块的依赖关系只有N个——每个模块只和mediator相依赖。


为了设计出这个系统,我们开始结合observer模式思考,在listener例子里面,我们把subscriber作为一个listener指针存在于publisher中,我们说这个subscriber对这个publisher要发布的内容感兴趣——它等待着publisher的一个特定的条件产生,从而通知subscriber,它会做它该做的事情。然而,在那个例子中,两个模块是直接依赖的,并不适用于我们现在设计的这个系统,我们要实现的系统将提供一种“多路分派”——可以有多个subscriber接收同一个事件。在该系统中,需要把“特定条件的产生”变成一个实体用来在模块和mediator之间传递来进行间接依赖,一个很自然的方案就是command模式( 如果对command模式感兴趣,请参考GoF的书),它封装着通信接收者需要完成的工作信息,将工作延迟到真正的目标。


这里,我不想牵扯太多的模式,因为这样会导致大量的题外细节。因此,我们暂时不使用标准的command模式,改用一个简单的事件结构体:


struct GameEvent

{

int m_eventID;

int m_params[6];

};


一个整型事件ID和若干个自定义整型参数。对,没错,只有整型参数。那我们如何传递其他类型的东西呢?比如一个字符串什么的。一般来说,需要采用辅助数据结构,这和你的游戏引擎结构有很大的关系。比如用一个字符串池,用m_param来传递池索引等等类似的方案。


一个game event的数据类似这样:


#define GAME_EVENT_ID_CLICK_BUTTON   1000

GameEvent event;

event.m_eventID = GAME_EVENT_ID_CLICK_BUTTON;

event.m_params[0] = 15;

event.m_params[1] = 2666;


上面的event可能在游戏中解释为:一个按下按钮的事件,按下的按钮索引为15,这将产生ID为2666的脚本执行。具体的事件参数由接收它的模块来进行解释。


有了这样一个结构,我们就可以把它和对它感兴趣的模块进行关联。比如对GAME_EVENT_ID_CLICK_BUTTON事件感兴趣的是模块A、C、E、F。则在mediator中可以建立一个关联数组保存下面的关联:


{GAME_EVENT_ID_CLICK_BUTTON, {A, C, E, F}}


每当一个publisher发送了一个GAME_EVENT_ID_CLICK_BUTTON事件给mediator,它都会把该消息转发给{A, C, E, F}模块组。这样,在系统运行的某个时刻,mediator将保存一个映射表,描述了注册的游戏事件以及要响应的模块,比如:


{GAME_EVENT_ID_1, {A, B}}

{GAME_EVENT_ID_5, {C}}

{GAME_EVENT_ID_6, {A, M}}

{GAME_EVENT_ID_11, {F, K, M, S}}

{GAME_EVENT_ID_99, {D, B, P, T, X, Z}}

...


系统结构如下所示:




对该系统的实现


接下来我打算给该系统一个简单实现。给模块设计的类为:


中介模块Mediator: EventDispatcher

通信模块A-H: EventListener


[cpp] view plain copy
  1. EventDispatcher* dispatcher;  
  2. class EventDispatcher  
  3. {  
  4. public:  
  5.     EventDispatcher() { dispatcher = this; }  
  6.     void dispatchEvent( const GameEvent& event )  
  7.     {  
  8.         ListenerGroup::iterator it = m_listeners.find( event.m_eventID );  
  9.         if( it != m_listeners.end() )  
  10.         {  
  11.             list< EventListener* >& listeners = it->second;   
  12.             for( list< EventListener* >::iterator it2 = listeners.begin(); it2 != listeners.end(); ++it2 )  
  13.             {  
  14.                 it2->handleEvent( event );  
  15.             }  
  16.         }  
  17.     }  
  18.   
  19.   
  20. private:  
  21.     typedef map< int, list< EventListener* > > ListenerGroup;  
  22.   
  23. private:  
  24.     ListenerGroup m_listeners;  
  25.   
  26.     friend class EventListener;  
  27. };  
  28.   
  29. class EventListener  
  30. {  
  31. public:  
  32.     virtual ~EventListener() {}  
  33.   
  34.     void addListener( int eventID, EventListener* listener )  
  35.     {  
  36.         EventDispatcher::ListenerGroup::iterator it = dispatcher->m_listeners.find( eventID );  
  37.         if( it == dispatcher->m_listeners.end() )  
  38.         {  
  39.             list<EventListener*> l;  
  40.             l.insert( l.begin, listener );  
  41.             dispatcher->m_listeners.insert( make_pair(eventID, l ) );  
  42.         }  
  43.         else  
  44.         {  
  45.   
  46.             list<EventListener>::iterator it2;  
  47.             for( it2 = it->second.begin(); it2 != it->second.end(); ++it2 )  
  48.             {  
  49.                 if( *it2 == listener )  
  50.                 continue;  
  51.             }  
  52.   
  53.             it->second.insert( it->second.begin(), listener );  
  54.         }  
  55.     }  
  56.   
  57.     void removeListener( int eventID, EventListener* listener )  
  58.     {  
  59.         EventDispatcher::ListenerGroup::iterator it = dispatcher->m_listeners.find( eventID );  
  60.         if( it != dispatcher->m_listeners.end() )  
  61.         {  
  62.             for( list<EventListener*>::iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2 )  
  63.             {  
  64.                 if( *it2 == listener )  
  65.                 {  
  66.                     it->second.erase(it2);  
  67.                     break;  
  68.                 }  
  69.             }  
  70.             if( it->second.empty() )  
  71.                 dispatch->m_listeners.erase(it);  
  72.         }  
  73.     }  
  74.   
  75.     virtual void handleEvent( const GameEvent& event ) = 0;  
  76.   
  77. private:  
  78.   
  79. };  
  80.   
  81. class A : public EventListener  
  82. {  
  83. public:  
  84.     A() { addListener( 5, this ); }  
  85.     virtual ~A() { removeListener( 5, this ); }  
  86.     virtual void handleEvent( const GameEvent& event )  
  87.     {  
  88.         assert( event.m_eventID == 5 );  
  89.         assert( event.m_params[0] = 1 );  
  90.         assert( event.m_params[1] = 2 );  
  91.     }  
  92. };  
  93.   
  94. int main()  
  95. {  
  96.     EventDispatcher eventDispatcher;  
  97.     A a;  
  98.     GameEvent event;  
  99.     event.m_eventID = 5;  
  100.     event.m_params[0] = 1;  
  101.     event.m_params[1] = 2;  
  102.     eventDispatcher.dispatchEvent( event );  
  103.     return 0;  
  104. }  


(为了阅读方便,我把类的方法实现直接和声明放到了一起,在实际的开发中,除了需要inline的方法,应该将实现和声明分离)


EventDispatch类即mediator保存了事件-模块映射表m_listeners。EventDispatch::dispatchEvent方法用来处理相应事件。


EventListener类是一个监听器基类,任何一个想成为subscriber的模块都该继承于它。两个方法


EventListener::addListener

EventListener::removeListener


用来将subscriber增加到事件-模块映射表中,或将模块从表中删除。


EventListener::handleEvent方法是一个纯虚方法,subscriber子类应该实现该方法来处理它感兴趣的事件。比如class A就是一个subscriber,它将自己和ID为5的事件绑定——表明它对该事件感兴趣。当在main中通过EventDispatcher发送了一个5号事件的时候,A::handleEvent就会被callback并将5号事件发送给它进行处理。这里的模块传递为:


main模块 -> event dispatcher(mediator) -> class A object



改进方向


以上实现只是一个基本结构——它完成基本的功能,但要让该事件系统达到工程级别并使用,还需要进一步的努力。以下几个改进留给感兴趣的读者当作练习完成。


1 将事件分发方法改为异步方式


这个改进是必须的!问题在于我们在EventDispatcher::dispatchEvent中使用了STL的容器迭代器进行事件分发,当一个事件被EventListener的子类模块通过EventListener::handleEvent进行处理的时候,可能会产生另一个event(比如在A::handleEvent里面再用EventDispatch分发一个事件),这会导致EventDispatcher::dispatchEvent中的迭代器被重复使用,STL不允许这种情况产生,程序崩溃!


解决方案是在EventDispatcher::dispatchEvent中只把event存入一个pending容器中而不调用EventListener::handleEvent。然后给EventDispatcher安插一个flushEvents的接口,在里面统一调用EventListener::handleEvent来处理pending events然后清除掉它们。


2 将EventDispatcher设计为singleton模式


EventDispatcher应该只有一个,并应该被各个通信模块方便地访问。这意味着把它写成一个singleton模式是理所当然。就这么做吧!(关于singleton模式,请参考GoF的书)


3 增加通道


为了在不同的游戏界面中采用不同的事件表布局,可以给监听器组增加不同的通道,简单来说就是把EventDispatcher::m_listeners变成一个数组


EventDispatcher::m_listeners[N]


然后增加一个变量


EventDispatcher::m_curChannel


来表示当前使用的是数组的哪个维度——哪个通道。这样,我们就能够保留不同的界面事件布局而不用频繁的重新初始化。


4 增加Listener的优先级属性


可以给listener增加优先级,这样在EventDispatcher::dispatchEvent中对listener进行调用的时候将按照listener的优先级调用。这种特性可以用来实现一个很酷的功能——消息独占模块:把一个模块的优先级调成最大,然后给该listener增加一个“事件处理后中断后续listener处理”标志,可以让dispatcher在EventDispatcher::dispatchEvent的循环中只调用该优先级最大的listener的事件处理方法,然后终止掉循环。一个标准的模态对话框就可以用该特性实现。


总结


以上我们循序渐进地实现了一个基本的事件分发系统,它实现了对模块的解耦——通过一个中介模块event dispatcher。在一个普通的游戏中,事件的数量是很大的,但该方案保证了增加游戏事件的便利性。它保证了最小的耦合度——事件是完全独立的。理论上,发送者和接收者彼此都不需要了解彼此,这更像是一个广播系统,收听者可以随时打开或关闭收音机,广播电台根本不知道谁在收听广播。


这个系统已经在我们自己的多个项目中被使用,它确实提供给开发者一定的便利性和健壮性。希望你能够从这个系统中有所收获。

[cpp] view plain copy
  1. <pre name="code" class="cpp"></pre><pre name="code" class="cpp"></pre>  

0 0
原创粉丝点击