2011-08-30 27 views
7

一个朋友和我一直玩pygame一些和使用pygame碰到this tutorial for building games。我们真的很喜欢它是如何将游戏转化为模型 - 视图 - 控制器系统的,其中事件作为中介,但是代码使重复使用isinstance检查事件系统。Python鸭子打字的pygame MVC事件处理

实施例:

class CPUSpinnerController: 
    ... 
    def Notify(self, event): 
     if isinstance(event, QuitEvent): 
      self.keepGoing = 0 

这导致一些极其unpythonic代码。有没有人有任何建议如何可以改善?或者实现MVC的另一种方法?


这是一段代码我写基于@马克 - 希尔德雷思答案(我怎么链接的用户?)没有任何人有什么好的建议?在选择一个解决方案之前,我打算再开放一天左右。

class EventManager: 
    def __init__(self): 
     from weakref import WeakKeyDictionary 
     self.listeners = WeakKeyDictionary() 

    def add(self, listener): 
     self.listeners[ listener ] = 1 

    def remove(self, listener): 
     del self.listeners[ listener ] 

    def post(self, event): 
     print "post event %s" % event.name 
     for listener in self.listeners.keys(): 
      listener.notify(event) 

class Listener: 
    def __init__(self, event_mgr=None): 
     if event_mgr is not None: 
      event_mgr.add(self) 

    def notify(self, event): 
     event(self) 


class Event: 
    def __init__(self, name="Generic Event"): 
     self.name = name 

    def __call__(self, controller): 
     pass 

class QuitEvent(Event): 
    def __init__(self): 
     Event.__init__(self, "Quit") 

    def __call__(self, listener): 
     listener.exit(self) 

class RunController(Listener): 
    def __init__(self, event_mgr): 
     Listener.__init__(self, event_mgr) 
     self.running = True 
     self.event_mgr = event_mgr 

    def exit(self, event): 
     print "exit called" 
     self.running = False 

    def run(self): 
     print "run called" 
     while self.running: 
      event = QuitEvent() 
      self.event_mgr.post(event) 

em = EventManager() 
run = RunController(em) 
run.run() 

这是另一个使用@Paul示例的版本 - 非常简单!

class WeakBoundMethod: 
    def __init__(self, meth): 
     import weakref 
     self._self = weakref.ref(meth.__self__) 
     self._func = meth.__func__ 

    def __call__(self, *args, **kwargs): 
     self._func(self._self(), *args, **kwargs) 

class EventManager: 
    def __init__(self): 
     # does this actually do anything? 
     self._listeners = { None : [ None ] } 

    def add(self, eventClass, listener): 
     print "add %s" % eventClass.__name__ 
     key = eventClass.__name__ 

     if (hasattr(listener, '__self__') and 
      hasattr(listener, '__func__')): 
      listener = WeakBoundMethod(listener) 

     try: 
      self._listeners[key].append(listener) 
     except KeyError: 
      # why did you not need this in your code? 
      self._listeners[key] = [listener] 

     print "add count %s" % len(self._listeners[key]) 

    def remove(self, eventClass, listener): 
     key = eventClass.__name__ 
     self._listeners[key].remove(listener) 

    def post(self, event): 
     eventClass = event.__class__ 
     key = eventClass.__name__ 
     print "post event %s (keys %s)" % (
      key, len(self._listeners[key])) 
     for listener in self._listeners[key]: 
      listener(event) 

class Event: 
    pass 

class QuitEvent(Event): 
    pass 

class RunController: 
    def __init__(self, event_mgr): 
     event_mgr.add(QuitEvent, self.exit) 
     self.running = True 
     self.event_mgr = event_mgr 

    def exit(self, event): 
     print "exit called" 
     self.running = False 

    def run(self): 
     print "run called" 
     while self.running: 
      event = QuitEvent() 
      self.event_mgr.post(event) 

em = EventManager() 
run = RunController(em) 
run.run() 
+0

顺便说一句,'Event.__ init__'中的'name'参数是不必要的。该类的名称已由Python存储。打印'QuitEvent .__ name__'可以看到。 :)另外,如果你有一个对象实例,你可以使用'obj .__ class __.__ name__'来获得它的类的名字字符串。 –

+0

一切似乎都没错。但是不要忘记在你的RunController对象被破坏之前(或者什么时候)从事件管理器中移除侦听器!除此之外,我没有看到任何问题。我仍然认为你应该在RunController__init__里面创建WeakBoundMethod,而不是在Eventmanager.add里面。事件管理器应该不知道它收到的监听者的类型。 –

+0

回复:*'这实际上是否做了什么?'* - 不,但我想在我的'__init__'函数中清楚地说明这个类有哪些属性。该行清楚表明self._listeners是一个字典,它具有对象作为键和列表作为值。 –

回答

12

处理事件的一种更简洁的方式(并且速度更快,但可能消耗更多内存)是在代码中具有多个事件处理函数。沿着这些线的东西:

所需界面

class KeyboardEvent: 
    pass 

class MouseEvent: 
    pass 

class NotifyThisClass: 
    def __init__(self, event_dispatcher): 
     self.ed = event_dispatcher 
     self.ed.add(KeyboardEvent, self.on_keyboard_event) 
     self.ed.add(MouseEvent, self.on_mouse_event) 

    def __del__(self): 
     self.ed.remove(KeyboardEvent, self.on_keyboard_event) 
     self.ed.remove(MouseEvent, self.on_mouse_event) 

    def on_keyboard_event(self, event): 
     pass 

    def on_mouse_event(self, event): 
     pass 

这里,__init__方法接收一个EventDispatcher作为参数。现在,EventDispatcher.add函数将采用您感兴趣的事件类型和侦听器。

这有益处的效率,因为听众永远只能被调用为它感兴趣的事件,这也导致更多的通用代码的EventDispatcher本身内。

EventDispatcher实施

class EventDispatcher: 
    def __init__(self): 
     # Dict that maps event types to lists of listeners 
     self._listeners = dict() 

    def add(self, eventcls, listener): 
     self._listeners.setdefault(eventcls, list()).append(listener) 

    def post(self, event): 
     try: 
      for listener in self._listeners[event.__class__]: 
       listener(event) 
     except KeyError: 
      pass # No listener interested in this event 

但这个实现有一个问题。里面NotifyThisClass你这样做:

self.ed.add(KeyboardEvent, self.on_keyboard_event) 

问题是与self.on_keyboard_event:它是一个绑定的方法,你传递给EventDispatcher。绑定方法持有对self的引用;这意味着只要EventDispatcher有绑定的方法,self将不会被删除。

WeakBoundMethod

您将需要创建一个WeakBoundMethod类,只持有一个弱引用self(我看你已经知道了弱引用),使得EventDispatcher并不妨碍self删除。

另一种方法是在删除对象之前调用一个NotifyThisClass.remove_listeners函数,但这不是最干净的解决方案,我觉得它非常容易出错(容易忘记)。

WeakBoundMethod的实施将是这个样子:

class WeakBoundMethod: 
    def __init__(self, meth): 
     self._self = weakref.ref(meth.__self__) 
     self._func = meth.__func__ 

    def __call__(self, *args, **kwargs): 
     self._func(self._self(), *args, **kwargs) 

这里的a more robust implementation我张贴在代码审查,并在这里是的你会如何使用类的一个示例:

from weak_bound_method import WeakBoundMethod as Wbm 

class NotifyThisClass: 
    def __init__(self, event_dispatcher): 
     self.ed = event_dispatcher 
     self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event)) 
     self.ed.add(MouseEvent, Wbm(self.on_mouse_event)) 

Connection对象(可选)

从管理器/分派器中删除监听器时,不要将EventDispatcher不必要通过监听器搜索,直到找到合适的事件类型,然后在列表中搜寻,直到找到合适的监听器,你可以有这样的事情:

class NotifyThisClass: 
    def __init__(self, event_dispatcher): 
     self.ed = event_dispatcher 
     self._connections = [ 
      self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event)), 
      self.ed.add(MouseEvent, Wbm(self.on_mouse_event)) 
     ] 

这里EventDispatcher.add返回Connection对象,知道哪里在EventDispatcher的列表字典它居住。当一个NotifyThisClass对象被删除时,self._connections也会被删除,这将调用Connection.__del__,这将从EventDispatcher中删除侦听器。

这可以使您的代码既快速又易于使用,因为您只需要明确添加函数,它们会自动删除,但是由您决定是否要执行此操作。如果你这样做,请注意EventDispatcher.remove不应该存在了。

+0

@pual,谢谢你的特殊答案。我已经在答案中发布了一个完整的impl。你怎么看?我试图保持代码简单。 – Petriborg

1

给每个事件一个方法(甚至可能使用__call__),并传入Controller对象作为参数。然后“调用”方法应该调用控制器对象。例如...

class QuitEvent: 
    ... 
    def __call__(self, controller): 
     controller.on_quit(self) # or possibly... controller.on_quit(self.val1, self.val2) 

class CPUSpinnerController: 
    ... 
    def on_quit(self, event): 
     ... 

你使用路由的事件的任何代码到你的控制器将调用__call__方法与正确的控制器。

2

我偶然发现了SJ布朗过去制作游戏的教程。这是一个很棒的页面,是我读过的最好的一页。但是,像你一样,我不喜欢对isinstance的调用,或者所有监听器都接收所有事件的事实。

首先,isinstance比检查两个字符串是否相等要慢,所以我最终在我的事件中存储了一个名称,并测试了名称而不是类。但是,如果电池的通知功能仍在痒痒我,因为它感觉像是浪费时间。我们可以在这里做两个优化:

  1. 大多数收听者只对几种类型的事件感兴趣。出于性能方面的考虑,当发布QuitEvent时,只有对其感兴趣的监听器应该被通知。事件管理器跟踪哪个听众想要听哪个事件。
  2. 然后,为了避免在单个通知方法中经历大量的如果陈述,我们将对每种类型的事件有一个方法。

例子:

class GameLoopController(...): 
    ... 
    def onQuitEvent(self, event): 
     # Directly called by the event manager when a QuitEvent is posted. 
     # I call this an event handler. 
     self._running = False 

因为我想开发者尽量少打字,我做了如下的事情:

当一个监听器被注册到事件管理,事件管理器扫描侦听器的所有方法。当一个方法以'on'开头(或者你喜欢的任何前缀)时,它会查看其余部分(“QuitEvent”)并将此名称绑定到此方法。稍后,当事件管理器抽取其事件列表时,它会查看事件类名称:“QuitEvent”。它知道这个名字,因此可以直接直接调用所有相应的事件处理程序。开发人员无需做任何事情,只需添加onWhateverEvent方法即可使其工作。

它也有一些缺点:

  1. 如果我做的处理程序(而不是“onPhysicsRanEvent”“onRunPhysicsEvent” 例如“)的名称拼写错误,然后我的处理程序将 永远不会被调用和我我不知道为什么,但我知道这个技巧,所以我 不知道为什么很长
  2. 我不能添加一个事件处理程序后,已经注册了监听器 我必须取消注册和重新注册事实上, 事件处理程序仅在注册期间被扫描。然后 再次,我从来不必这样做,所以我不会错过它。

尽管存在这些缺陷,我还是比喜欢让监听器的构造函数明确解释事件管理器,并且希望保持对此事件的这种调整。反正它的执行速度是一样的。

观点二:

在设计我们的事件管理器,我们要小心。很多时候,监听者会通过创建注册或注销摧毁监听器来响应事件。这事儿常常发生。如果我们不考虑它,那么我们的游戏可能会与RuntimeError:在迭代期间更改大小的字典。您提出的代码会迭代字典的副本,以防止爆炸;但它具有以下意义: - 由于事件而注册的监听器不会收到该事件。 - 由于事件而未注册的听众仍然会收到该事件 。 虽然我从未发现它是一个问题。

我为自己开发的游戏实现了自己的功能。我可以将您链接到两篇文章半我关于这个问题写道:

我的github账户的链接将带您直接到源相关部分的代码。如果你不能等待,那就是:https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py。在那里你会看到我的事件类的代码有点大,但是每一个继承的事件都用2行声明:基类事件类让你的生活变得简单。

所以,这一切都使用python的内省机制,并使用方法是可以放在字典中的任何其他对象的事实。我认为这是相当pythony :)。

+0

我从来没有想过使用函数名称作为确定应该通知什么事件的机制。 :)我更喜欢使用Python Zen,并且很明确,但它仍然是一个非常有趣的解决方案。 –

+0

够公平的!你可能仍然想看看我如何实现这些事件:它们是显式的,但可以为你节省大量的击键。当然,当事件处理程序是明确的时候,我写的其他事件管理器也是有效的:只有Listener.getHandlers方法发生变化。 – Niriel