Pyglet mouse events and rendering
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.
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.
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!