Pygame中MVC事件处理的Python鸭子类型

9 投票
4 回答
4538 浏览
提问于 2025-04-17 00:46

我和一个朋友最近在玩pygame,发现了一个关于如何用pygame制作游戏的教程。我们特别喜欢这个教程把游戏分成了模型-视图-控制器(MVC)系统,并且用事件作为中介,但代码中大量使用了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 个回答

1

给每个事件定义一个方法(可能还可以用到 __call__),并把控制器对象作为参数传进去。这个“调用”方法应该去调用控制器对象。比如说...

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 Brown关于制作游戏的教程,真是个好页面,是我读过的最棒的之一。不过,就像你一样,我不太喜欢使用isinstance这个函数,或者说所有的监听器都接收所有事件的做法。

首先,isinstance比直接比较两个字符串是否相等要慢,所以我最后选择在事件中存储一个名字,然后用这个名字来测试,而不是用类名。不过,notify函数里面一堆的if让我觉得很烦,因为这感觉像是在浪费时间。我们可以在这里做两项优化:

  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. 我不能在监听器注册后再添加事件处理器。我必须先注销再重新注册。实际上,事件处理器只在注册时被扫描。再说了,我从来没有需要这样做过,所以也不觉得缺少。

尽管有这些缺点,我还是觉得这种方式比让监听器的构造函数明确告诉事件管理器它想要监听哪些事件要好得多。而且执行速度也是一样的。

第二点:

在设计我们的事件管理器时,我们需要小心。很多时候,监听器会通过创建、注册或注销、销毁监听器来响应事件。这种情况经常发生。如果我们不考虑这一点,游戏可能会因为RuntimeError: dictionary changed size during iteration而崩溃。你提到的代码是对字典的一个副本进行迭代,所以你可以避免这种问题;但这也有一些需要注意的后果: - 因为某个事件而注册的监听器将不会收到该事件。 - 因为某个事件而注销的监听器仍然会收到该事件。 不过我从来没有觉得这是个问题。

我自己在开发的游戏中实现了这个功能。我可以给你链接到我写的两篇半文章:

我github上的链接会直接带你到相关部分的源代码。如果你等不及了,这里有个链接:https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py。在里面你会看到我的事件类的代码有点大,但每个继承的事件只需两行就能声明:基础的Event类让你的生活变得简单。

所以,这一切都是利用Python的反射机制,以及方法也是像其他对象一样可以放在字典里的事实。我觉得这很符合Python的风格 :).

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

连接 对象(可选)

在从管理器/调度器中移除监听器时,不必让 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 就不应该再存在了。

撰写回答