PyGTK/Smooth Animation with PyGTK

From OLPC
< PyGTK
Revision as of 20:41, 17 January 2008 by Wade (talk | contribs) (Idle Events)
Jump to: navigation, search

This page describes different ways to get smooth animation in PyGTK, with various pros and cons.

Battery life

It's important to understand that adding constant animation (say an animating background) will prevent the laptop from entering suspend mode and will drastically reduce battery life, making your application less popular in the real world.

Therefore, you should *only* use animation in activities that absolutely require it, and for the shortest amount of time possible.

PyGTK Events

The PyGTK event loop is the means by which PyGTK events like mouse clicks, key presses and notifications are dispatched.

The general process when a PyGTK application starts up is:

  • Create one or more GTK windows / widgets.
  • Connect events to functions.
  • Call gtk.main().

The gtk.main() function simply processes the event loop, dispatching events to the appropriate handlers, until the quit signal is received at which point it returns and the application usually exits. Note that in Sugar Activities, gtk.main() is called by the sugar-activity launcher script.

This system is good for GUI applications where you typically want the program to be idle until some external circumstance such as user input occurs. But for realtime applications, you generally want some piece of code executing all the time. Fortunately, there are several ways to achieve background processing in PyGTK.

Timer Events

Using gobject.timeout_add(ms, fn) you can cause PyGTK to call a specific function every N milliseconds. This is most people's first approach to smooth animation.

One problem with timer events is that the timer only starts counting again *after* the function returns. So, if your function takes 20ms to execute, it will be called 70ms after it it was first called. Therefore, the timing becomes inconsistent unless your function always takes exactly the same amount of time (and the cost of your function must be accounted for when choosing the timeout).

A second problem is that if the millisecond count not enough for all other events to be processed between timer events, you can starve the event queue in such a way that other events will never happen. This can cause the program to stutter, the GUI to fail to update, and numerous other problems.

Note that when the animation has finished, you can stop the timer event by returning False from your handler function.

Example

 class MyActivity(Activity)
   def __init__(self):
     # Add a 20fps (50ms) animation timer.
     gobject.timeout_add(50, self.timer_cb)
 
   def timer_cb(self):
     # Generate an expose event.
     self.queue_draw()

Idle Events

Using gobject.idle_add(fn) you can set up a function to be called whenever PyGTK is idle, that is when there are no other events to process.

This can be an improvement over timer events since it ensures that the event queue will not be starved, and other events like input and GUI updates will execute before your function is called.

The downside is that other events (like mouse movement) take priority over your idle event, or vice versa, which leads to animation stuttering when lots of external events happen, or else delays in processing external events while animation is running.

Like timer events, you can stop the idle event from being generated by returning False from your handler function.

Example

 class MyActivity(Activity)
   def __init__(self):
     gobject.idle_add(50, self.idle_cb)
 
   def idle_cb(self):
     # Generate an expose event.
     self.queue_draw()

Threads

Yet another option for animation is to create a secondary thread which repeatedly calls queue_draw or some other function to update graphics.

This works fairly well, but in my experience the event queue tends to fill up leading to jerky animation and an unresponsive application. Since the XO only has a single processor, multithreading adds additional overhead to the system, and you will have to deal with extra GTK and Python synchronization overhead as well.

Example

 # Do not forget this!  Without it, you will get random crashes.  It must be called before gtk.main().
 gtk.gdk.threads_init()
 
 class MyActivity(Activity)
   def __init__(self):
     thr = threading.Thread(function=self.mainloop)
     thr.start()
 
   def mainloop(self):
     while True:
       # Generate an expose event.
       self.queue_draw()
       # Allow the main thread to process for a while.
       time.sleep(10)

Taking over the Event loop

The last method, and the only one I have found that is reliable enough for games, is to take over the PyGTK event loop. This is what SDL does internally, and is the pattern by which most Windows games operate as well.

The gtk.main() function supports being called recursively, and it has a gtk.main_iteration() function which processes a single event and returns. The final piece of the puzzle is gtk.events_pending(), which returns True if any events are pending.

This method is the most reliable, since you know that you will get all the available time, but also that all GTK events will be processed as soon as possible.

Example

 class MyActivity(Activity)
   def __init__(self):
     # Start up the main loop just after application initialization.
     self.timeout_add(20, self.mainloop)
 
   def mainloop(self):
     while True:
       # Process all pending events.
       while gtk.events_pending():
         gtk.main_iteration(False)
       # Generate an expose event (could just draw here as well).
       self.queue_draw()

Expose events versus Immediate drawing

In the above examples, we have called self.queue_draw() to cause an event to be generated which will update the entire screen. In a real activity, you would just want to refresh whatever widget is animating.

However, another option is to just execute drawing commands (using cairo, gtk.Drawable, etc) in your main loop. There is no penalty to doing this, so you might want to consider it in game-like applications where rendering logic is mixed with update code.

Real world example

For an example of a game written entirely in PyGTK using these techniques in practice, see the ThreeDPong activity.