9

我和一个朋友一直在玩 pygame,偶然发现了这个使用 pygame构建游戏的教程。我们真的很喜欢它如何将游戏分解成一个模型-视图-控制器系统,其中事件作为中间人,但是代码大量使用了isinstance对事件系统的检查。

例子:

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

这会导致一些极其不合 Python 的代码。有人对如何改进有任何建议吗?或者实现 MVC 的替代方法?


这是我根据@Mark-Hildreth 回答编写的一些代码(我如何链接用户?)还有其他人有什么好的建议吗?在选择解决方案之前,我将把它打开一天左右。

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

4 回答 4

14

处理事件的一种更简洁的方法(也更快,但可能会消耗更多内存)是在代码中包含多个事件处理函数。这些方面的东西:

所需的接口

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仅包含对弱引用的类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)

这是我在 CodeReview 上发布的更强大的实现,下面是您如何使用该类的示例:

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不应该再存在了。

于 2011-09-03T15:56:13.123 回答
2

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

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

  1. 大多数听众只对几种类型的事件感兴趣。出于性能原因,当发布 QuitEvent 时,只应通知对其感兴趣的侦听器。事件管理器跟踪哪个侦听器想要侦听哪个事件。
  2. 然后,为了避免在单个notify方法中使用大量if语句,我们将为每种类型的事件设置一个方法。

例子:

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. 如果我在处理程序的名称中打错字(例如“onRunPhysicsEvent”而不是“onPhysicsRanEvent”),那么我的处理程序将永远不会被调用,我会想知道为什么。但我知道诀窍,所以我不知道为什么很长。
  2. 注册侦听器后,我无法添加事件处理程序。我必须注销并重新注册。实际上,事件处理程序仅在注册期间被扫描。话又说回来,无论如何我都不必这样做,所以我不会错过它。

尽管有这些缺点,但我更喜欢它,而不是让侦听器的构造函数明确地向事件管理器解释它希望继续关注 this、this、this 和 this 事件。无论如何,它的执行速度是一样的。

第二点:

在设计我们的事件管理器时,我们要小心。很多时候,监听器会通过创建-注册或注销-销毁监听器来响应事件。这事儿常常发生。如果我们不考虑它,那么我们的游戏可能会因RuntimeError: dictionary changed size during iteration而中断。您提出的代码会遍历字典的副本,因此可以防止爆炸;但它有需要注意的后果: - 由于事件而注册的侦听器将不会收到该事件。- 由于某个事件而未注册的侦听器仍将收到该事件。我从来没有发现它是一个问题。

我自己为我正在开发的游戏实现了这一点。我可以把你链接到我写的关于这个主题的两篇半文章:

我的 github 帐户的链接将直接带您访问相关部分的源代码。如果你等不及了,那就是:https ://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py 。在那里,您会看到我的事件类的代码有点大,但是每个继承的事件都在 2 行中声明:基 Event 类使您的生活变得轻松。

因此,这一切都使用 python 的自省机制,并使用方法是可以放入字典中的任何其他对象的事实。我认为这很pythony :)。

于 2011-09-03T14:55:29.783 回答
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__使用正确的控制器调用该方法。

于 2011-08-30T20:37:13.603 回答
0

我偶然发现了同样的问题(差不多十年后!),这是我一直在使用的一个实现,以便 EventManager 只通知一部分侦听器。
它基于defaultdict_listenersEventManager 的属性defaultdictWeakKeyDictionary().
事件都是继承自一个空的抽象Event类,所以监听者可以只关注他们想监听的一些事件类。
这是一个极简的代码来了解其背后的想法:

from collections import defaultdict
from weakref import WeakKeyDictionary

class Event:
    def __init__(self):
       pass

class KeyboardEvent(Event): # for instance, a keyboard event class with the key pressed
    def __init__(self, key):
        self._key = key

class EventManager:
    def __init__(self):
        self._listeners = defaultdict(lambda: WeakKeyDictionary())

    def register_listener(self, event_types, listener):
        for event_type in event_types:
            self._listeners[event_type][listener] = 1

    def unregister_listener(self, listener):
        for event_type in self._listeners:
            self._listeners[event_type].pop(listener, None)

    def post_event(self, event):
        for listener in self._listeners[event.__class__]:
            listener.notify(event)

注册时,侦听器告诉事件管理器它想要通知的事件类型。
发布事件时,事件管理器只会通知注册为该类型事件通知的侦听器。
当然,与@Paul Manta 提出的非常通用(且非常优雅)的解决方案相比,这段代码的范围要小得多,但就我而言,它有助于删除一些重复调用isinstance和其他检查,同时保持事情像我一样简单可以。
这样做的一个缺点是所有类型的事件都必须是某个类的对象,但在 OO python 中,这应该是要走的路。

于 2019-12-13T15:47:49.317 回答