Rate this page del.icio.us  Digg slashdot StumbleUpon

Building the XO: Porting a PyGTK game to Sugar, part one

by John (J5) Palmieri

Welcome to this tutorial series on porting a PyGTK game to the OLPC’s Sugar environment. While we will be concentrating on a game called Block Party, the lessons taught here can be used as a guide to create or port any number of applications. Games are just more fun to learn with. On top of learning a bit about the Sugar environment, one will also learn about graphics and input handling in PyGTK as well as a few object oriented concepts. All code in this tutorial should run as standalone PyGTK apps as well as inside of the Sugar environment.

What is Sugar?

Sugar is the desktop environment which runs on the laptops being developed at OLPC. It provides a number of interfaces which can be used by applications to integrate into the Sugar environment. These include interfaces for registering with Sugar and using the mesh networking capabilities of the laptop, among other features. When an application is integrated into Sugar it is said to be a Sugar activity.

What is Block Party?

Block Party is a Tetris clone written in PyGTK by Vadim Gerasimov and modified by John (J5) Palmieri for this tutorial. Vadim is one of the original authors of Tetris which he helped create in 1985 when he was 16 years old. It has been ported to many platforms and clones of it with various modifications are too numerous to count. Because of this and its relative simplicity it is the perfect program to start playing around with on the OLPC laptop.

What you will need for this tutorial?

You will need a computer with the latest version of the Sugar environment running on it. This can either be from sugar-jhbuild, which was covered in a previous Red Hat Magazine article, or any build after or including build 303 running in an emulated environment like qemu, from the LiveCD or on the laptop itself. The builds have vim and pico editors installed in them but you may want to use an editor you are more familiar with and copy over the files to test them.

You will also need to download the source files for the lessons. There is a separate package for each lesson.

It is also assumed the reader knows a bit about the concepts of gtk+ programming and in particular gtk+ programming in python.

Lesson 1: An Overview of the Block Party Source Code

The first step to porting an application is understanding the application you are porting. To get the source code please download and untar the first lesson’s source bundle which can be found at http://dev.laptop.org/~j5/BlockParty_tutorial/block_party_lesson_1.tar.gz. You should end up with a directory structure looking like this:


block_party_lesson_1/
    BlockParty.py

To run the program simple change into the lesson directory and run BlockParty.py using python.


$ cd block_party_lesson_1
$ python BlockParty.py

The anatomy of a game

Games for the most part follow a similar structure consisting of a couple of high level elements. There is the main loop and timing element which makes sure events are triggered according to a relatively strict time line. Along with that goes the logic phase which determines positioning and interactions between elements in the game world. There is also input which needs to be processed and can affect game state. Finally there is the output which typically happens on the computer monitor.

Mainloops and Timing

Most games have a tight custom mainloop which they use to squeeze every last bit of performance out of their platform. Games can do this because for the most part they are dedicated applications which interact little with the environment outside their own codebase. Gtk+ games and in particular Sugar games are a bit different and will often access code not specifically written for the game itself (for instance Sugar's networking facilities which will be discussed in a later lesson). Because of this we need to use a more generic mainloop and do some tricks to get as tight a timing as possible to grantee our game runs smoothly. For this we use the ever popular glib mainloop.

The glib mainloop allows programs to attach sources to it in order to execute application code on specific events, whether it be networking events, keyboard interrupts or timers. Of particular interest to us right now is the timer sources. Timer sources are added using the gobject.timeout_add method.


import gobject
 
main_loop = gobject.MainLoop()
 
# call the timeout_cb function every second
timeout_id = gobject.timeout_add(1000, timeout_cb)
main_loop.run()

This can be seen in the BlockParty.py source at line 480 in the init() method.


gobject.timeout_add(20, timer)

Here we call the method timer() every 1/50th of a second. It is interesting to play with the timeout value to find the optimal timeout period to call. Calling more times per second doesn't necessarily speed up the game and can in fact slow it down as well as drain power from devices which run on battery. This is because you are running more instructions per second, mostly just checking to see if it is time to trigger an event. Most of the game's time is spent waiting around for events to happen; letting the gtk+ mainloop determine this for you means more efficient use of resources. However, the gtk+ mainloop is not guaranteed to call the timer() function at exactly every 1/50th of a second; it only tries to make its best effort to do so and may be preempted by higher priority sources. Let's look at the timer() function to see how we can tighten up our timing to make our game run smoothly.


# time_step = 100
 
def timer():
    global game_mode
    global next_tick, time_step
    while game_mode == PLAY and time.time() >= next_tick:
        next_tick += time_step
        tick()
    if game_mode != PLAY:
        next_tick = time.time()+100
    return True

What's happening here? We are getting the current time from the system clock and checking if it is 100 milliseconds after the last 'tick'. If we are in PLAY mode and it's time for a new tick, we update the next tick to happen 100 milliseconds from now and call the tick() function where we perform all of our game logic. Returning True at the end of the timer function tells the mainloop to call this function again at the prescribed timeout period. One always wants to set timeouts to be more granular than the actual frame rate needed for the game. This is why we call timer() every 1/50th of a second when we only need to update every 1/10th of a second. I leave it up to the reader to figure out how many frames per second that is. Hint: Block Party doesn't need to run all that fast to be playable.

Input

Again in most game mainloops one of the steps in the loop is to check for input. Luckily with the glib mainloop we just get notified when interesting input happens. This is separate from the timeout source, so we don't have to wait for the timeout to be called or the tick to be run to process an input event. This allows input to feel responsive even if we only update the game logic every 1/10th of a second.

For Block Party we are mainly interested in the keyboard keys for input, but other games may use the mouse, or even a web cam if the developer possesses some ingenuity. To get key press events we attach to the key_press_event and key_release_event signals on the main application window in the init() function in BlockParty.py at line 457.


window.connect("key_press_event", keypress_cb)
window.connect("key_release_event", keyrelease_cb)

In reality we only care about the key_press_event so the keypress_cb() function is where we do all of our work.


def keypress_cb(widget, event):
    key_action(gtk.gdk.keyval_name(event.keyval))
    return True

Notice that the keypress_cb takes a widget and event object. The widget in this case is the main GtkWindow we connected the signal handler to. The event itself contains the key we pressed. Here we get the key value from the event, translate it to a string and pass it into key_action. It should be noted that translating the key value to its string representation is a step used to make the code more readable when debugging or looking at other parts of the code. It does require more processing power to compare the strings and should perhaps be refactored in the future to use just the integer key values.

The key_action() function does most of the heavy lifting, so let's take a look there now.


left_key = ['Left', 'KP_Left']
right_key = ['Right', 'KP_Right']
drop_key = ['space', 'KP_Down']
rotate_key = ['Up', 'KP_Up']
exit_key = ['Escape']
sound_toggle_key = ['s', 'S']
enter_key = ['Return']
 
def key_action(key):
    global figure,px,py,tickcnt
    global glass_update
    global game_mode
    global soundon
    global level
    global next_tick, time_step
    if key in exit_key: quit_game()
    if key in sound_toggle_key: soundon = not soundon
    if game_mode == SELECT_LEVEL:
        if key in left_key:
            set_level(level-1)
            glass_update = True
        else:
            if key in right_key:
                set_level(level+1)
                glass_update = True
            else: # if key in enter_key:
                complete_update = True
                next_tick = time.time()+time_step
                game_mode = PLAY
        update_picture()
        return
    if game_mode == IDLE:
        return
    if game_mode == GAME_OVER:
        init_game()
        return
    changed = False
    if key in left_key:
        px-=1
        if not figure_fits(): px+=1
        else: changed=True
    if key in right_key:
        px+=1
        if not figure_fits(): px-=1
        else: changed=True
    if key in drop_key:
        changed = drop_figure()
    if key in rotate_key:
        changed = rotate_figure_ccw(True)
    if changed:
        glass_update = True
        update_picture()

If you look closely at the key definitions, you see that we define a list for each key. This makes use of the python 'in' operator which allows us to check against multiple key values with one statement. For instance the statement 'Left' in left_key will return True where as 'Right' in left_key will return False.

Really, key_action() is a small state machine that responds to user input based on what state it is in. For instance, in the SELECT_LEVEL state the right and left arrow keys will cause the level to go up and down, but in the PLAY state (it is not listed in an if statement because it is the default state) it updates the px variable which is what the drawing code uses to figure out what column to draw the falling block in (py defines which row).

If the input has changed the positioning of a block, the display now needs to be updated outside of the timing loop. If we were to wait until the tick decided to draw input would seem sluggish to the user. Go ahead and comment out the update_picture() call to see what I mean. Later on we will talk about the gdk graphics layer we use to draw to the screen and in a later lesson we will show how to refactor the drawing code to use the more modern cairo drawing layer.

While we are on the subject it should be noted that the way drawing is done in lesson 1's code is not completely correct since it actually draws to the graphics layer outside the expose event. All drawing should be done in the expose event to avoid bugs and work with features like double buffering. The reasons will become clearer in the Cairo lesson.

Logic

A game is no fun without logic. This is where the rules of the game are enforced and what makes the game fun, challenging and engaging. If you thought the graphics were the cool part, that is just the end result of the game logic. No amount of amazing graphics can make up for lack in game logic. This is why with Block Party's decidedly dull graphics it is still a fun game to play. Later on we may spice them up a bit but that is just icing on the cake.

While some of the game logic resides in the input handlers the bulk of the logic happens from the timing loop when tick() is called every 1/10th of a second. Every tick the current block is dropped a row and then checked against the game board to see if it created a line or ran into obstacles. If so the score is updated, a new block is dropped from the top and the next block is computed. If not the current block waits for another tick or input to do it all over again. Let's examine some of the code.


def tick():
    global figure, px, py, tickcnt, bh
    global figure_score
    global complete_update, glass_update
    global level, linecount
    global game_mode
    glass_update = True
    py-=1
    if figure_score > 0: figure_score -= 1
    if not figure_fits():
        py+=1
        put_figure()
        make_sound('heart.wav')
        new_figure()
        if not figure_fits():
            i = random.randint(0, 2)
            if i is 0: make_sound('ouch.wav')
            if i is 1: make_sound('wah.au'),
            if i is 2: make_sound('lost.wav')
            print 'GAME OVER: score ' + str(score)
            game_mode = GAME_OVER
            complete_update = True
            update_picture()
            return
    chk_glass()
    new_level = int(linecount/5)
    if new_level > level: set_level(new_level)
    tickcnt += 1
    update_picture()

In the tick() function one of the first things we do is drop the block by 1 row.


py-=py

We then check to see if the figure is off the screen in the figure_fits() function. If so the game is over and we set the game into the GAME_OVER state.


game_mode = GAME_OVER

If the figure still fits on the game board we then call the chk_glass() function. In Block Party the glass is how we refer to the game board where the blocks are dropped. The chk_glass() function goes over the game grid and compares each piece of the block to already filled in parts of the grid. If there is a collision the glass is checked for full lines and those lines are removed. Since the game logic from this point can get quite complex it is left as an exercise to the reader to follow the code and see how this is all done.

Drawing

At the end of the logic sequence the board is redrawn via the update_picture() function. This is also called at the end of input processing. Block Party uses the Gdk graphics layer to draw into an X window and the Pango API a layer above Gdk for drawing text. If you look in the init() function you cane see code similar to this.


window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.connect("expose_event", expose_cb)
area = window.window
gc = area.new_gc()

What we are doing is creating a toplevel window, connecting it to the expose event which will tell the window when something has caused it to need a redraw. We then get the GdkWindow and create a GdkGC. The GdkGC is used to tell the Gdk graphics layer how to draw its primitives. Later we will use it to set the colors of the object we wish to draw.


def update_picture():
    global complete_update, glass_update
    
    if complete_update:
    draw_background()
        draw_score()
    if complete_update or glass_update:
        draw_glass()
        draw_next()
        if game_mode is GAME_OVER: draw_game_end_poster()
        if game_mode is SELECT_LEVEL: draw_select_level_poster()
    complete_update = False
    glass_update = False

Looking at the update_picture() function we see a couple of draw function which are used to draw the various parts of the game board. We will be looking at the draw_score() and draw_next() functions to show how both text and graphics are handled in BlockParty.



def draw_score():
    global gc, area
    global linecount, score, level, scorefont
    global scorex, scorey
    global color_score
    displaystr = 'Score: ' + str(score)
    displaystr += '\nLevel: ' + str(level)
    displaystr += '\nLines: ' + str(linecount)
    pl = window.create_pango_layout(displaystr)
    pl.set_font_description(scorefont)
    gc.set_foreground(color_score)
    area.draw_layout(gc, scorex, scorey, pl)

In draw_score we create a string called displaystr of the text we wish to draw. We then create a pango layout and pass the string to it. A font description is then set which was created earlier in the init() function with these commands.


scorefont = pango.FontDescription('Sans')
scorefont.set_size(window_w*16*pango.SCALE/1024)

After setting the font we set the foreground color and use the draw_layout method on the GdkWindow to display the pango layout.

That is all there is to it. Now let's look at the draw_next() function.



def draw_next():
    global gc, area, window, next_figure
    global bw, bh, bwpx, bhpx, xshift, yshift
    gc.set_foreground(color_score)
    draw_centered_string('NEXT', xnext+bwpx*2.5, ynext)
    gc.set_foreground(colors[0])
    area.draw_rectangle(gc, True, xnext, ynext+50, bwpx*5, bhpx*5)
    gc.set_foreground(color_score)
    for i in range(4):
        for j in range(4):
            if next_figure[i][j] is not 0:
                gc.set_foreground(colors[next_figure[i][j]])
                area.draw_rectangle(gc, True, xnext+j*bwpx+bwpx/2, ynext+50+(3-i)*bhpx+bhpx/2, bwpx, bhpx)

The draw_next() function draws the box which displays the next block which will fall after the current block has been placed. Here we set the foreground color and use our draw_center_string() function to basically do the same thing we did in draw_score(). After we are done drawing the text label 'NEXT', we set the foreground color to the first value of the colors array which is black. The colors array is setup earlier in the code and contains a set of predefined colors we use throughout the game. Next we draw a black rectangle with the draw_rectangle() method. This will become the box where we will draw the block in.

Each block in Block Party is defined in a 4 by 4 array. A square block may look something like this:

0000
0110
0110
0000

Or a T block may look something like this:

0100
0110
0100
0000

The 0s represent no colors and the 1s actually represent a color value in the array. When the code loops over this array and sees a color value, a rectangle is drawn at the offset with the color specified by the color value. An interesting challenge for the reader would be to replace the color value with a picture, and change the drawing code to draw the picture instead of just plain colored squares.

The rest of the drawing code does similar tasks of drawing text and rectangles. The draw_glass() function is the most interesting of the drawing code, as it needs to keep track of the game grid and erase previous draws or falling blocks would simply paint the screen by leaving trails of itself every time it moved. We will get rid of some of this complexity when we move to cairo and will end up having to repaint the entire screen on expose events. That might seem like it would inefficient but with effective use of dirty regions it will actually clean up the code quite a bit without loss of performance.

Conclusion

Now that we have a decent understanding of how Block Party works, we can start to delve deeper and evolve the codebase. The next lesson will show how to take the code we have been working with and, with little modification, turn it into a Sugar activity bundle.

5 responses to “Building the XO: Porting a PyGTK game to Sugar, part one”

  1. Justin says:

    Hi,
    I am wondering how to run a python script located on a local machine from an emulator (qemu) that is running Sugar, particularly when the python script imports gtk.
    Much thanks.
    –justin

  2. John (J5) Palmieri says:

    Simple answer, you can’t. You need to copy the files over the “network”. You may also be able to mount a USB disk from qemu but I am not at all familiar with it. Other virtualization tools like VMWare may have easier ways of accessing the host system.

  3. Supreet Sethi says:

    Whats a safe lower limit on gobject.timeout_add so that it does not block every other event.

  4. John (J5) Palmieri says:

    Timeouts are pretty low priority sources so they theoretically won’t block every other event.

  5. Tim Wintle says:

    Justin:
    I believe that sugar runs an sshd on some local port. You can ssh into that and scp the files over.