2011-08-30 21 views
7

Một người bạn và tôi đã chơi đùa với một số đồ chơi và đi qua số this tutorial for building games bằng cách sử dụng pygame. Chúng tôi thực sự thích cách nó phá vỡ trò chơi thành một hệ thống mô hình xem-bộ điều khiển với các sự kiện như một sự thay đổi giữa, nhưng mã làm cho việc sử dụng nặng sử dụng số isinstance kiểm tra cho hệ thống sự kiện.Cách gõ vịt bằng Python để xử lý sự kiện MVC trong pygame

Ví dụ:

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

Điều này dẫn đến một số mã cực kỳ unpythonic. Có ai có bất cứ đề nghị về cách này có thể được cải thiện? Hoặc một phương pháp thay thế để thực hiện MVC?


Đây là mã tôi viết dựa trên câu trả lời @ Mark-Hildreth (làm cách nào để liên kết người dùng?) Có ai khác có đề xuất hay không? Tôi sẽ để cái này mở thêm một ngày nữa trước khi chọn một giải pháp.

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() 

Đây là một bản dựng khác sử dụng các ví dụ từ @Paul - đơn giản một cách ấn tượng!

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

Btw, thông số 'tên' của bạn trong' Sự kiện .__ init__' là không cần thiết. Tên của lớp đã được Python lưu trữ. In 'QuitEvent .__ name__' để xem. :) Ngoài ra, nếu bạn có một cá thể đối tượng, bạn có thể làm 'obj .__ class __.__ name__' để lấy chuỗi tên lớp của nó. –

+0

Tất cả điều đó có vẻ ổn. Nhưng đừng quên loại bỏ người nghe từ người quản lý sự kiện trước (hoặc khi) đối tượng RunController của bạn bị phá hủy! Ngoài ra, tôi không thấy bất kỳ vấn đề gì. Tôi vẫn nghĩ bạn nên tạo WeakBoundMethod bên trong RunController__init__ thay vì bên trong Eventmanager.add. EventManager nên bất khả tri đối với loại người nghe mà nó nhận được. –

+0

Re: * 'thực hiện điều này thực sự làm bất cứ điều gì?' * - Không, nhưng tôi muốn nêu rõ trong các hàm '__init__' thuộc tính của lớp đó. Dòng đó làm cho nó rõ ràng rằng self._listeners là một dict có các đối tượng như các khóa và danh sách dưới dạng các giá trị. –

Trả lời

12

Một cách xử lý sự kiện sạch hơn (và cũng nhanh hơn rất nhiều, nhưng có thể tiêu tốn nhiều bộ nhớ hơn một chút) là có nhiều chức năng xử lý sự kiện trong mã của bạn.Một cái gì đó dọc theo những dòng:

Các mong muốn Interface

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 

Ở đây, phương pháp __init__ nhận được một EventDispatcher như một cuộc tranh cãi. Chức năng EventDispatcher.add hiện có loại sự kiện bạn quan tâm và người nghe.

này có lợi ích cho hiệu quả kể từ khi người nghe chỉ bao giờ được gọi cho các sự kiện đó là quan tâm đến Nó cũng có kết quả trong mã chung chung hơn bên trong EventDispatcher bản thân:.

EventDispatcher Thực hiện

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 

Nhưng có vấn đề với việc triển khai này. Bên NotifyThisClass bạn làm điều này:

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

Vấn đề là với self.on_keyboard_event: nó là một phương pháp ràng buộc mà bạn truyền cho EventDispatcher. Các phương thức liên kết giữ tham chiếu đến self; điều này có nghĩa là miễn là EventDispatcher có phương thức bị ràng buộc, self sẽ không bị xóa.

WeakBoundMethod

Bạn sẽ cần phải tạo ra một lớp WeakBoundMethod chứa chỉ là một tài liệu tham khảo yếu để self (tôi thấy bạn đã biết về tài liệu tham khảo yếu) để các EventDispatcher không ngăn cản việc xoá self.

Một cách khác là có chức năng NotifyThisClass.remove_listeners mà bạn gọi trước khi xóa đối tượng, nhưng đó không thực sự là giải pháp sạch nhất và tôi thấy rất dễ bị lỗi (dễ quên).

Việc thực hiện WeakBoundMethod sẽ giống như thế này:

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) 

Dưới đây là a more robust implementation tôi được đăng trên CodeReview, và đây là một ví dụ về cách bạn sẽ sử dụng lớp:

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 Objects (Tùy chọn)

Khi xóa người nghe khỏi người quản lý/điều phối viên, thay vì thực hiện EventDispatcher không cần thiết tìm kiếm thông qua các thính giả cho đến khi nó tìm thấy loại sự kiện phải, sau đó tìm kiếm thông qua danh sách cho đến khi nó tìm thấy người nghe phải, bạn có thể có một cái gì đó như thế này:

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)) 
     ] 

đây EventDispatcher.add trả về một đối tượng Connection rằng biết nơi ở EventDispatcher 's dict của danh sách nó cư trú. Khi đối tượng NotifyThisClass bị xóa, do đó, self._connections, sẽ gọi Connection.__del__, sẽ xóa người nghe khỏi EventDispatcher.

Điều này có thể làm cho mã của bạn sử dụng nhanh hơn và dễ dàng hơn vì bạn chỉ cần thêm các chức năng một cách rõ ràng, chúng sẽ tự động bị xóa, nhưng bạn quyết định xem bạn có muốn thực hiện điều này hay không.Nếu bạn làm điều đó, lưu ý rằng EventDispatcher.remove không nên tồn tại nữa.

+0

@pual, cảm ơn câu trả lời đặc biệt. Tôi đã đăng một đoạn hoàn chỉnh vào câu trả lời. Bạn nghĩ sao? Tôi đã cố giữ mã đơn giản. – Petriborg

1

Cung cấp cho mỗi sự kiện một phương thức (thậm chí có thể sử dụng __call__) và chuyển đối tượng Controller làm đối số. Phương thức "gọi" sau đó sẽ gọi đối tượng điều khiển. Ví dụ ...

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): 
     ... 

Dù mã bạn đang sử dụng để định tuyến các sự kiện của bạn để điều khiển của bạn sẽ gọi phương thức __call__ với bộ điều khiển chính xác.

2

Tôi tình cờ gặp hướng dẫn về cách tạo trò chơi của SJ Brown trong quá khứ. Đó là một trang tuyệt vời, một trong những trang hay nhất mà tôi đã đọc. Tuy nhiên, giống như bạn, tôi không thích các cuộc gọi để khôi phục, hoặc thực tế là tất cả các thính giả nhận được tất cả các sự kiện.

Đầu tiên, việc khôi phục chậm hơn việc kiểm tra xem hai chuỗi có bằng nhau không, vì vậy tôi đã lưu trữ tên trên các sự kiện của mình và kiểm tra tên thay vì lớp. Nhưng vẫn còn, chức năng thông báo với pin của nó là nếu đã làm tôi ngứa ngáy bởi vì nó cảm thấy như một sự lãng phí thời gian. Chúng tôi có thể thực hiện hai tối ưu hóa tại đây:

  1. Hầu hết người nghe chỉ quan tâm đến một vài loại sự kiện. Vì lý do hiệu suất, khi QuitEvent được đăng, chỉ những người nghe quan tâm đến nó mới được thông báo. Người quản lý sự kiện theo dõi người nghe nào muốn nghe sự kiện nào.
  2. Sau đó, để tránh phải thực hiện một tấn câu lệnh nếu trong một đơn thông báo phương thức, chúng tôi sẽ có một phương thức cho mỗi loại sự kiện.

Ví dụ:

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 

Bởi vì tôi muốn các nhà phát triển để gõ càng ít càng tốt, tôi đã thực hiện những điều sau đây:

Khi một người nghe được đăng ký với một người quản lý sự kiện, quản lý sự kiện quét tất cả các phương pháp của người nghe. Khi một phương thức bắt đầu bằng 'on' (hoặc bất kỳ tiền tố nào bạn thích), thì nó xem xét phần còn lại ("QuitEvent") và liên kết tên này với phương thức này.Sau đó, khi người quản lý sự kiện bơm danh sách sự kiện của nó, nó nhìn vào tên lớp sự kiện: "QuitEvent". Nó biết tên đó, và do đó có thể trực tiếp gọi tất cả các trình xử lý sự kiện tương ứng trực tiếp. Nhà phát triển không có gì để làm ngoài việc thêm các phương thức onWhateverEvent để họ làm việc.

Nó có một số nhược điểm:

  1. Nếu tôi thực hiện một lỗi đánh máy trong tên của bộ xử lý ("onRunPhysicsEvent" thay vì "onPhysicsRanEvent" ví dụ ") sau đó xử lý của tôi sẽ không bao giờ được gọi và tôi Nhưng tôi biết điều này nên tôi không biết tại sao rất dài
  2. Tôi không thể thêm một trình xử lý sự kiện sau khi người nghe đã đăng ký Tôi phải hủy đăng ký và đăng ký lại. Bộ xử lý sự kiện chỉ được quét trong khi đăng ký. Sau đó, một lần nữa, tôi không bao giờ phải làm điều đó anyway vì vậy tôi không bỏ lỡ nó.

Mặc dù những hạn chế này tôi thích nó nhiều hơn là việc người xây dựng của người nghe giải thích rõ ràng người quản lý sự kiện rằng họ muốn theo dõi điều này, sự kiện này và sự kiện này. Và đó là cùng một tốc độ thực hiện anyway.

Điểm thứ hai:

Khi thiết kế trình quản lý sự kiện của chúng tôi, chúng tôi muốn cẩn thận. Rất thường xuyên, người nghe sẽ phản hồi sự kiện bằng cách tạo người đăng ký hoặc hủy đăng ký người nghe. Việc này xảy ra mọi lúc. Nếu chúng ta không nghĩ về nó thì trò chơi của chúng ta có thể vi phạm với RuntimeError: kích thước thay đổi từ điển trong khi lặp lại. Mã mà bạn đề xuất lặp lại trên một bản sao của từ điển để bạn được bảo vệ chống lại các vụ nổ; nhưng có những hậu quả cần lưu ý: - Người nghe đã đăng ký vì sự kiện sẽ không nhận được sự kiện đó. - Người nghe chưa đăng ký vì sự kiện sẽ vẫn nhận được sự kiện . Tôi chưa bao giờ thấy nó là một vấn đề.

Tôi đã thực hiện bản thân mình cho trò chơi mà tôi đang phát triển. Tôi có thể liên kết bạn đến hai bài báo và một nửa tôi đã viết về đề tài này:

Các liên kết đến tài khoản github của tôi sẽ mang lại cho bạn trực tiếp đến nguồn mã của các bộ phận liên quan. Nếu bạn không thể chờ đợi, đây là điều: https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py. Trong đó bạn sẽ thấy rằng mã cho lớp sự kiện của tôi là một chút lớn, nhưng mọi sự kiện được thừa kế đều được khai báo trong 2 dòng: lớp Event cơ sở làm cho cuộc sống của bạn trở nên dễ dàng.

Vì vậy, tất cả đều hoạt động bằng cách sử dụng cơ chế nội suy của python và sử dụng thực tế là các phương thức là các đối tượng giống như bất kỳ đối tượng nào khác có thể được đưa vào từ điển. Tôi nghĩ rằng nó khá pythony :).

+0

Tôi chưa bao giờ nghĩ đến việc sử dụng tên hàm làm cơ chế xác định sự kiện nào cần được thông báo. :) Tôi thích đi với Zen Zen và rõ ràng, nhưng đó là một giải pháp rất thú vị, tuy nhiên. –

+0

Đủ công bằng! Bạn vẫn có thể muốn xem cách tôi triển khai Sự kiện: chúng rõ ràng nhưng có thể giúp bạn tiết kiệm rất nhiều tổ hợp phím. Và tất nhiên, phần còn lại của trình quản lý sự kiện tôi đã viết cũng hoạt động khi trình xử lý sự kiện rõ ràng: chỉ có phương thức Listener.getHandlers thay đổi. – Niriel

Các vấn đề liên quan