2011-08-30 53 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