Python duck-typing for handling MVC events in pygame

My friend and I play with pygame and push this tutorial to create games using pygame. We really liked how she left the game in the model-view-controller system with events as an intermediate, but the code heavily uses isinstance checks for the event system.

Example:

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

This leads to some extremely non-nutritional code. Does anyone have any suggestions on how this can be improved? Or an alternative methodology for implementing MVC?


This is some code that I wrote based on @ Mark-Hildreth's answer (how do I connect users?) Does anyone have any good suggestions? I am going to leave it open for one more day before deciding on a solution.

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

This is another build using examples from @Paul - impressively simple!

 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() 
+7
source share
3 answers

A cleaner way of handling events (and also much faster, but perhaps consuming a bit more memory) is to have multiple event handlers in your code. Something like that:

Desired 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 

Here the __init__ method takes an EventDispatcher as an argument. The EventDispatcher.add function now accepts the type of event and listener you are interested in.

This has advantages for efficiency, because the listener only ever receives challenges for the events of interest to us. This also leads to more general code inside the EventDispatcher itself:

EventDispatcher Implementation

 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 

But the problem is with this implementation. Inside NotifyThisClass you do the following:

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

The problem is self.on_keyboard_event : this is the related method that you passed to EventDispatcher . Related methods contain a reference to self ; this means that as long as the EventDispatcher has an associated method, self will not be deleted.

WeakBoundMethod

You will need to create a WeakBoundMethod class containing only a weak reference to self (I see that you already know about weak links), so that EventDispatcher does not prevent the removal of self .

An alternative would be to have the NotifyThisClass.remove_listeners function, which you call before deleting the object, but this is not a very clean solution, and I find it very error prone (easy to forget).

The WeakBoundMethod implementation will look something like this:

 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) 

Here's a more robust implementation I posted in CodeReview, and here is an example of how you will use the class:

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

When removing listeners from dispatcher / dispatcher, instead of using EventDispatcher useless to search for listeners until it finds the correct type of event, then search the list until it finds the correct listener, you may have something like this

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

Here, EventDispatcher.add returns a Connection object that knows where the dict lists in which it is located in the EventDispatcher . When the NotifyThisClass object NotifyThisClass deleted, that is, self._connections , which will call Connection.__del__ , which will remove the listener from the EventDispatcher .

This can make your code faster and easier to use, since you only need to explicitly add functions, they are automatically deleted, but you decide whether you want to do this. If you do, note that EventDispatcher.remove should no longer exist.

+12
source

I came across an SJ Brown tutorial on making games in the past. This is a great page, one of the best I've read. However, like you, I did not like the calls for isinstance, or the fact that all listeners receive all the events.

Firstly, isinstance is slower than checking that the two lines are equal, so I ended up storing the name in my events and checking the name, not the class. But still, the notification function with its battery, if there was an itch, because it seemed like a waste of time. Here we can do two optimizations:

  • Most listeners are only interested in a few types of events. For performance reasons, when a QuitEvent is submitted, only listeners interested in it should be notified. The event manager keeps track of which listener wants to listen to which event.
  • Then, to avoid passing multiple if statements in the same notification method, we will have one method for each type of event.

Example:

 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 

Since I want the developer to print as little as possible, I did the following:

When a listener is registered in the event manager, the event manager scans all the listener methods. When one method begins with 'on' (or any prefix you like), then it looks at the others ("QuitEvent") and associates this name with this method. Later, when the event manager calls his event list, he looks at the name of the event class: "QuitEvent". He knows this name and therefore can directly access all relevant event handlers. There is nothing for the developer to do except add the WhateverEvent methods to make them work.

It has some disadvantages:

  • If I make a typo in the name of the handler ("onRunPhysicsEvent" instead of "onPhysicsRanEvent, for example"), then my handler will never be called, and I will wonder why. But I know the trick, so I won’t be surprised why it takes so long.
  • I can not add an event handler after listening for registered. I have to register and re-register. Indeed, event handlers are checked only during registration. then again, I never had to do this, so I did not miss it.

Despite these shortcomings, I like it more than having a listener constructor clearly explains to the event manager that he wants to stay tuned to this, this, this, and this event. And still the same execution speed.

Second point:

When developing our event manager, we want to be careful. Very often, the listener will respond to the event, creating-registering or unregistering-destroying listeners. This happens all the time. If we don’t think about it, our game may break with RuntimeError: resized dictionary during iteration. The code you offer iterates over a copy of the dictionary, so you are protected from explosions; but it has consequences to be aware of: - Listeners registered due to an event will not receive this event. - Listeners who are unregistered due to the event will still receive the event. I never found this to be a problem.

I implemented this myself for the game I am developing. I can link you to two articles that I wrote about this:

Links to my github account will lead you directly to the source code of the corresponding parts. If you can't wait, here's what: https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py . There you will see that the code for my event class is a bit large, but each inherited event is declared in two lines: the base event class makes your life easier.

So, all this works using the python introspection mechanism and using the fact that methods are objects like any others that can be put into dictionaries. I think this is pretty pythony :).

+2
source

Give each event a method (possibly even using __call__ ) and pass the Controller object as an argument. The call method then calls the controller object. For example...

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

No matter what code you use to route events to your controllers, you will call the __call__ method with the correct controller.

+1
source

All Articles