Understanding decorators like @self.window.event (accumulation vs replacement)

228 views
Skip to first unread message

Mindok

unread,
Oct 14, 2017, 3:05:30 PM10/14/17
to pyglet-users
Hi

I'm trying to understand the behaviour of decorators like @self.window.event.
Sometimes the decorators accumulate and sometimes they replace each other and
I really don't understand the logic. I know that decorators are a feature
of Python and not just Pyglet but since a lot of the Pyglet tutorials and example code I have
read encourages their use I think it would appropriate to ask this here.

Sometimes it seems like only the most recent decoration is applied, so if I write

window = pyglet.window.Window(width = 800, height = 640, caption = "Pyglet Experiment")

@window.event
def on_key_press(key, modifiers):
    print('A key was pressed') 
    if key == pyglet.window.key._1:
        print("Change Things")
        change_things(window)
    elif key == pyglet.window.key._2:
        print("Change Things 2")
        change_things2(window)
    print()

def change_things(window):
    @window.event
    def on_key_press(symbol, modifiers):
        print('Ole!')

def change_things2(window):
    @window.event
    def on_key_press(symbol, modifiers):
        print('Zamzam!')


And then press a key other than '1' or '2', the program prints
A key was pressed
(followed by a new line)
Then by pressing '1' or '2' I can accumulate lines like
'Ole' or 'Zamzam', which are presented in reverse order
(the 'A key was pressed' comes last).

However if I add this

change_things(window)

after all the code above, this line seems to override the earlier on_key_press,
so whenever I press a button (any button including '1' or '2' I get a single line of 'Ole'.

Clearly there is something about decorators that I just don't get.
Help would be appreciated, including links to general Python explanations not exclusively
related to Pyglet.


Benjamin Moran

unread,
Oct 15, 2017, 9:44:55 AM10/15/17
to pyglet-users
Hi Mindok,

I tried to explain this better in the recent documentation, but I agree that it's a bit confusing.
If you use the decorator, it lets you add one event handler to the stack. If you use it again, it should replace the previous event handler. If you have a need to add multiple handlers, you will need to use the push_handlers() method instead.
The order that the events are handled is by design.  You actual code for all of this is actually not all that difficult, so you might want to give it a look:
https://bitbucket.org/pyglet/pyglet/src/e45ff688add81422b079e9c01fc07fd22897bff4/pyglet/event.py?at=default&fileviewer=file-view-default

Mindok

unread,
Oct 16, 2017, 3:41:48 AM10/16/17
to pyglet-users

I forgot to include something. In my code I had

key_handler = key.KeyStateHandler()

def update(dt):
    window.push_handlers(key_handler)


when the accumulation happened. If I comment this out and add pass,
then key press gives 'A key was pressed' until you press '1' or '2', in
which case key press gives 'Ole!' or 'Zamzam!' no matter how many
times you press (replacement). Pushing the handlers was the key.

In my game code I have run into problems with this kind of inconsistent behaviour.
Some scenes need to check if a key is pressed continuously, so I used
the KeyStateHandler to do that. However I'm also moving between scenes
that define their own on_key_press. This causes some stuff to accumulate
between scenes. What is the recommended way of doing this?
Should I clear the stack whenever I change scenes?
If so do I use
remove_handlers()?


Greg Ewing

unread,
Oct 16, 2017, 3:56:23 AM10/16/17
to pyglet...@googlegroups.com
Mindok wrote:
> However I'm also moving between scenes
> that define their own on_key_press. This causes some stuff to accumulate
> between scenes. What is the recommended way of doing this?

Rather than swapping handlers around all the time, I think
I would just leave one handler installed that routes events
to the appropriate scene.

--
Greg

Daniel Gillet

unread,
Oct 16, 2017, 7:05:43 AM10/16/17
to pyglet-users
Hello,

I don't quite understand your description of the problem. Running your code on my machine, pressing a key prints A key was pressed followed by a blank line. If I press '1', I have the additional message Change Things. From now on, pressing any keys prints only Ole!, and nothing else.

This is the expected behaviour. Using the decorator will replace the current event handler on the top of the handlers stack. In your example there is only one level of event handlers.

When you want to have different event handlers, you normally use the push_handlers() method. When you're done, you use the pop_handlers() to remove it. While several event handlers are stacked, you can let a previous handler still run by returning EVENT_UNHANDLED from the newer event handler. Otherwise returning EVENT_HANDLED will stop the event propagation.

Here is an example using push_handlers and pop_handlers.

import pyglet
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED

window = pyglet.window.Window(width=200, height=200, 
                              caption="Event handlers demo")

@window.event
def on_key_press(key, modifiers):
    print('A key was pressed')  
    if key == pyglet.window.key._1:
        print("Stacking `ole` event handler")
        window.push_handlers(on_key_press=ole)
    elif key == pyglet.window.key._2:
        print("Stacking `zamzam` event handler")
        window.push_handlers(on_key_press=zamzam)
    print("-" * 60)

def ole(key, modifiers):
    print('Ole! This handler lets the event propagate. '
        'To remove the event, press 1')
    if key == pyglet.window.key._1:
        print('Removing `ole` as event handler')
        window.pop_handlers()
        # Here we need to stop propagation otherwise we are going to add
        # ole again in the `on_key_press` event handler
        return EVENT_HANDLED

    return EVENT_UNHANDLED

def zamzam(key, modifiers):
    print('Zamzam! This event handlers does not let the event propagate. '
        ' To remove the event, press 2')
    if key == pyglet.window.key._2:
        print('Removing `zamzam` as event handler')
        window.pop_handlers()

    return EVENT_HANDLED

pyglet.app.run()

Notice how `ole` allows you still to see the message from the previous event handler while `zamzam` does not.

I hope this helps.

Mindok

unread,
Oct 16, 2017, 9:42:25 AM10/16/17
to pyglet-users

Hi Daniel

As I wrote earlier I forgot to add
key_handler = key.KeyStateHandler()

def update(dt):
    window.push_handlers(key_handler)
which was in my code but I didn't write it down. This caused the accumulation (apparently?).
I have used this
key_handler to check if a button is currently pressed down,
and I was surprised that it affected the other stuff.
If in the above I comment out

window.push_handlers(key_handler)
and put pass,
'Ole' replaces the original message and no further changes happen.
Is there a good explanation for why messing with key_handler affects on_key_press stuff?

I think I may do what Greg suggested and try to define the handlers once so that they
trigger the appropriate functions in the game scenes. This seems rather safe,
unless there are performance issues or other unforeseen problems.

Thanks for the example though!

Daniel Gillet

unread,
Oct 16, 2017, 11:45:16 AM10/16/17
to pyglet-users
Hello Mindok,

Thanks for clarifying the situation. There is still one thing unclear for me with your new code. Where do you call your `update` function? I would imagine it's scheduled every frame?

If that's the case, it's a very bad idea to push the keystate again and again every frame. Let me try to show you what happens when you do that. Let's take this code as an example:

import pyglet
from pyglet.window import key
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED

window = pyglet.window.Window(width=200, height=200, 
                              caption="Event handlers demo")



@window.event
def on_key_press(key, modifiers):
    print('A key was pressed')  
    if key == pyglet.window.key._1:
        print("Change Things")
        change_things(window)
    elif key == pyglet.window.key._2:
        print("Change Things 2")
        change_things2(window)
    print("Event stack depth:", len(window._event_stack))
    print()

def change_things(window):
    @window.event
    def on_key_press(symbol, modifiers):
        print('Ole!')

def change_things2(window):
    @window.event
    def on_key_press(symbol, modifiers):
        print('Zamzam!')

def update(dt):
    window.push_handlers(key_handler)


key_handler = key.KeyStateHandler()
pyglet.clock.schedule_interval(update, 1.0/30.)
pyglet.app.run()

When the application starts, update is going to be called 30 times per second. When update is executed, the call to push_handlers will push a new frame of event handlers on the event stack and it will be populated with the on_key_press and on_key_release event handlers from KeyStateHandler.

The class KeyStatedHandler does not return anything from its on_key_press and on_key_release, which means that the event will propagate down the stack until you reach the first layer which is the one containing your function on_key_press.

When you press '1', you change the on_key_press event from the last event handler pushed. As there is a new one every frame, it's really a mess. :)

Here is an example where I have a default event handler on_key_press and I push also a KeyStateHandler. In the on_key_press, I display all the keys that are being held down. If you press '1', I pop the KeyStateHandler and I assign a new on_key_press to another function, which would be the function from a new scene.

import pyglet
from pyglet.window import key
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED

window = pyglet.window.Window(width=200, height=200, 
                              caption="Event handlers demo")


@window.event
def on_key_press(symbol, modifiers):
    print('Here are all the keys that are being pressed ',
          ' '.join(key.symbol_string(key_pressed) 
                   for key_pressed, state in key_handler.items() 
                   if state is True)
          )

    if symbol == pyglet.window.key._1:
        print("Going to scene 2.")
        print("Popping the key_handler.")
        window.pop_handlers()
        print("And changing the current event handler for on_key_press.")
        window.set_handlers(on_key_press=on_key_press_in_scene_2)

    print()
    return EVENT_HANDLED

def on_key_press_in_scene_2(symbol, modifiers):
    print('Ole!')
    return EVENT_HANDLED



print("Press and hold multiple keys to see them on the screen.")
print("Press '1' to change to a different scene where pressing keys "
    "diplay just one message.")
key_handler = key.KeyStateHandler()
window.push_handlers(key_handler)
pyglet.app.run()

This example does not use any classes. But in a more real example, a Scene would be a class that we push on the stack. Here is an example of that:

import pyglet
from pyglet.window import key
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED


class SceneManager:
    def __init__(self):
        self.current_scene = None

    def change_scene(self, scene):
        if self.current_scene is not None:
            self.current_scene.exit_scene()
            window.pop_handlers()
        self.current_scene = scene
        window.push_handlers(scene)
        scene.enter_scene()


class Scene:
    def enter_scene(self):
        pass

    def exit_scene(self):
        pass


class SceneOne(Scene):
    def __init__(self):
        self.key_handler = key.KeyStateHandler()
        
    def enter_scene(self):
        window.push_handlers(self.key_handler)

    def exit_scene(self):
        window.pop_handlers()

    def on_key_press(self, symbol, modifiers):
        print('Here are all the keys that are being pressed ',
              ' '.join(key.symbol_string(key_pressed) 
                       for key_pressed, state in self.key_handler.items() 
                       if state is True)
              )

        if symbol == pyglet.window.key._1:
            print("Going to scene 2.")
            scene_mgr.change_scene(SceneTwo())

        print()
        return EVENT_HANDLED


class SceneTwo(Scene):
    def on_key_press(self, symbol, modifiers):
        print('Ole!')
        return EVENT_HANDLED


window = pyglet.window.Window(width=200, height=200, 
                              caption="Event handlers demo")
scene_mgr = SceneManager()
scene_mgr.change_scene(SceneOne())

pyglet.app.run()

This example looks a lot like what Cocos2D does to handle scenes, but here with only the more basic stuffs.

Benjamin Moran

unread,
Oct 20, 2017, 12:20:17 AM10/20/17
to pyglet-users
My own Scene management classes look very similar. I suppose this is just a common sense design.
Reply all
Reply to author
Forward
0 new messages