PyGTK/Smooth Animation with PyGTK

From OLPC
Jump to navigation Jump to search
  english | español HowTo [ID# 257709]  +/-  

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 queue is the means by which PyGTK events like mouse clicks, key presses and notifications are dispatched. The event queue is a list of events that need to be executed. Events can be generated by user input, or by other events such as an expose event generated by a window being resized.

The typical PyGTK application sequence looks like this:

  • Create GTK windows and widgets.
  • Connect events to functions.
  • Call gtk.main().

The gtk.main() function simply processes the event queue, dispatching events to the connected functions, until the gtk.main_quit() is called, at which point it returns and the application usually exits. Note that in Sugar Activities, gtk.main() is called for you by the sugar-activity launcher script.

This event-driven system is good for GUI applications which typically want to be idle until some external event (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 this kind of regular 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 the timer is set to 30ms and your function takes 20ms to execute, it will be called 50ms after 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()
     return True

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

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 is how PyGame works, 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(target=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.