Pyglet mouse events and rendering

The Problem

I added mouse event handlers to my Pyglet application recently. I found when I moved the mouse over the window, my rendering started to create “ghost” images.

I finally found the solution hidden in the docs.

The pyglet application event loop dispatches window events (such as for mouse and keyboard input) as they occur and dispatches the on_draw event to each window after every iteration through the loop.

So every mouse move (or any other event) triggers Pyglet to call “on_draw”.

But because I’m controlling my own render loop at 60Hz, I didn’t see a need to hook into this event. So I assume Pyglet is attempting to render the scene itself, and it’s failing miserably.

This is really bad design! What if I have lots of events that are seperate? Looks like Pyglet is going to hammer the “on_draw” event.

The Solution?

Well the basic solution is to ensure you’ve hooked into the “on_draw” event and just connect it to your render call. Don’t connect it to your game update logic or you will end up updating everything.

But…

This means your frame rate will vary depending on user input, and may even end up running balls out. Which could turn a simple game into a system hog. My basic application goes from 1% (of 2 cores on a dual core system), to 20% with the mouse moving.

It also means that you need to decouple the rendering from the system update, as each event you have scheduled will trigger an “on_draw” event. If you don’t decouple them, you will render each frame twice!

The Proper Solution?

We need to over-ride the “EventLoop.idle” method as mentioned in the docs.

The code without “on_draw” looks like this:

def idle( self ):
    """An alternate idle loop than Pyglet's default.

    By default, pyglet calls on_draw after EVERY batch of events
    which without hooking into, causes ghosting
    and if we do hook into it, it means we render after every event
    which is REALLY REALLY BAD
    http://www.pyglet.org/doc/programming_guide/the_application_event_loop.html
    """
    pyglet.clock.tick( poll = True )
    # don't call on_draw
    return pyglet.clock.get_sleep_time( sleep_idle = True )

def patch_idle_loop():
    """Replaces the default Pyglet idle look with the :py:func:`idle` function in this module.
    """
    # check that the event loop has been over-ridden
    if pyglet.app.EventLoop.idle != idle:
        # over-ride the default event loop
        pyglet.app.EventLoop.idle = idle

The “on_draw” function also calls pyglet.window.flip() for us, so now we need to do that after our rendering. So add the following (obviously you need to change the window variable depending on what you’ve called it):

# switch out the default idle function for ours
patch_idle_loop()

def render( self ):
    # do render here
    # flip the buffers
    self.window.flip()

And we’re done! No more superfluous “on_draw” events and my simple application no longer spikes the CPU usage when the mouse is moved!

Advertisements

3 Responses to “Pyglet mouse events and rendering”

  1. Hi! I am having the same problem. However, I am new to python and pyglet, and don’t know exactly how to connect your code to my program. This is what I have:
    import pyglet
    from pyglet.gl import *

    class EventLoop:
    def __init__( self ):
    # code goes here
    # over-ride the default event loop to remove on_draw
    pyglet.app.EventLoop.idle = self.idle
    def idle( self ):
    pyglet.clock.tick( poll = True )
    # don’t call on_draw
    return pyglet.clock.get_sleep_time( sleep_idle = True )
    pyglet.app.EventLoop = EventLoop()

    And it doesnt seem to work. Could you tell me what’s wrong?

    • I use this in my PyGLy project.

      However, I’ve implemented it outside of a class, unlike above.
      I find this easier as you can just use it whenever instead of needing to inherit from the class.

      The code can be found here:
      https://github.com/adamlwgriffiths/PyGLy/blob/master/pygly/monkey_patch.py

      Just do the following:
      1. Copy monkey_patch.py to your project
      2. Add the line “import monkey_patch”
      3. Add the line “monkey_patch.patch_idle_loop()”

      Replace the monkey_patch.idle function with whatever you like.
      Mine is just the standard Pyglet idle function without the ‘on_draw’ call at the end.

      Hope that solves your problem.

      Cheers,
      Adam

    • I’ve updated the example code in the article to make it clearer.

      The code demonstrated a class that you could inherit from and it would replace the event loop with itself.
      However, you had to instantiate the object and then keep it around somewhere.

      Ie

      class EventLoop:
      # code goes here
      # …

      # instantiate here and keep around
      eventloop = EventLoop()

      But this isn’t good. It’s not clear what the object is used for, and if I found code like that, I would delete it and then wonder why things didn’t work anymore =P.

      So I’ve replaced it with the function version found in my PyGLy project.

      Thanks for pointing out that my example wasn’t so obvious =).

      Cheers,
      Adam

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: