Week 9 - Pygame and OLPCGames

Making Educational Games with OLPCGames

Creating fun and engaging activities can be a challenging process. One of the best ways to help keep a younger generation of learners engaged in their education is to make the learning process fun with educational games.

Making game activities with python can be a fun and quick process thanks to the Pygame, a set of modules designed for writing video games. Pygame comes with built in graphics and sound libraries so that you won't have to worry about constructing things from the ground up. This is invaluable and fits right in with the high-level paradigm of the Python language.

On the Sugar platform, Pygame is wrapped in a library called OLPCGames. OLPCGames adds even more functionality to the Pygame platform. OLPCGames wraps a lot of the underlying details of the Sugar API, which is prone to constant changes, leading to breakage in your activities on every new build. This leads you with a great platform with a lot of the underlying details already taken off your plate; you can concentrate solely on developing a fun and educational game.

Pygame/OLPCGames difference

There are some slight differences between Pygame and OLPCGames.

  • Most of these differences are related to OS and UI information that OLPCGames overrides to make sure the games look 'Sugar-esque'.
  • You won't be able to set the display mode or the default font, because these are essentially locked out.
  • Since the XO doesn't have a CD drive, the cdrom module provided in Pygame is not useable.
  • In some recent builds of OLPCGames, the keypad settings are slightly different, but this isn't noticeable in some builds.

Added functionality

OLPCGames adds a lot of functionality to Pygame. You'll have to look into the full set of OLPCGames libraries to see for yourself all of the added functionality, but here are some of the major points:

  • Mesh-networking functionality
  • Video and Camera functionality
  • SVG sprite modules

Installation

Your build might not come with OLPCGames packaged. You can download the wrapper manually or clone the repository - both are easy to do. Here are the steps:

wget http://dev.laptop.org/~mcfletch/OLPCGames/OLPCGames-1.6.zip
# unzip this file and place it along with other libraries, or...
git clone git://dev.laptop.org/projects/games-misc 
# make sure you clone this repository to your libraries

cd OLPCGames-1.4/skeleton
# 1.4 is the version number as of this writing, for you it might be different
chmod a+x buildskel.py
./buildskel.py activityname "Your Activity's Name"

If you'd like to test to make sure you've installed this correctly, restart Sugar and then head back to the command line.

python setup.py dev

Getting started with Pygame

Simple Graphics and game functionality

You'll want to start your game by importing all the necessary libraries into your project.

import pygame, olpcgames, sys, os
from pygame.locals import *

Remember you can use the dir and help functions on those modules if you're experiencing trouble.

Now that we've imported the modules, initialize pygame. This will initialize the video, sound, and a number of other pygame functionalities.

>>> pygame.init()
(6,0)

The tuple returned will let you know that 6 modules were imported and 0 failed on import. Now, lets set up the screen and the window. Usually we would start by setting the resolution. This is generally overridden by OLPCGames, but it's important to include this first line just in case.

window = pygame.display.set_mode((468, 60))
pygame.display.set_caption('Your Game Name')
screen = pygame.display.get_surface()

The screen object you now have represents the area in memory where all images are drawn. The surface object has a lot of key functions and variables for drawing on the screen. Check them out here

Lets start by drawing a picture on the screen. Get a picture in bmp format and place it in a new folder called 'data' in the same directory of your .py file. Now import the picture and draw it on screen.

your_picture_file = os.path.join('data', 'pic.bmp')
picsurface = pygame.image.load(your_picture_file)
screen.blit(picsurface, (0,0))

What's happening here is we're opening a new file, making a surface out of it, and then drawing it on the screen. The surface object's blit function is essentially the same as saying 'draw'. Specify the surface you want to draw, and a location as an (x,y) tuple. Half of what you'll be doing while making games is setting up interactions between screen events. Drawing screen events and keeping track of their objects' locations is this easy. You now know almost enough to create a very simple game.

Now lets add a way for the player to issue commands, in particular to quit the game. Notice what this function does: it looks for a quit event, and it prints the other events if the even isn't a quit event.

def input(events): 
   for event in events: 
      if event.type == QUIT: 
         sys.exit(0) 
      else: 
         print event

Now lets start creating the core game function, the update loop. Notice how this code uses the function you just wrote.

while True:
     input(pygame.event.get())

This code is an infinite loop, so it'll keep going until the player or game issues a QUIT event. The pygame.event.get() function is how the player will communicate with the game.

You now know enough to create your own simple game! Put all of this code together in a file and run it from the command line.

Try a few of these exercises before you move on:

  • Move your picture around on the screen.
  • Find the size of a surface that you've created.
  • Move or scale the picture when you press a key.

Adding more to your game.

Lets start adding some more functionality to your game.

Remember from the first section how we added the import statement from pygame.locals import *. The pygame.locals package contains some of the most commonly used functions and objects. Adding this line is optional but very helpful.

We should start by adding some important functionality to your already existing code.

def load_image(name, colorkey=None):
    fullname = os.path.join('data', name)
    try:
        image = pygame.image.load(fullname)
    except pygame.error, message:
        print Cannot load image:, name
        raise SystemExit, message
    image = image.convert()
    if colorkey is not None:
        if colorkey is -1:
            colorkey = image.get_at((0,0))
        image.set_colorkey(colorkey, RLEACCEL)
    return image, image.get_rect()

myimage = load_image("pic.bmp")

This code block introduces some new concepts. Lets go through them.

First, notice the error handling that you might need to be including. Adding these error checking might be a pain while you're coding, but will save you a lot of headaches when you're in the testing phase. It'll also increase the portability of your code.

Notice that we also gave an optional parameter, the colorkey. Your image might contain transparent features. The colorkey will draw over those transparent features with the color you've specified. This is very useful in debugging, as colorkeys can be used to show the borders of an object. This is helpful in watching how collisions work on screen.

The image.convert() function is important for formatting purposes. This will take your image and convert it to match the color format and depth of the display. This will solve some scaling issues and will make the image blend in better, preventing some ugliness in your game.

Colors in Pygame generally come in two formats, RGB and RGBA. Both are passed as tuples: (255,255,255) or (255,255,255,X) with X being the alpha value.

Now lets write some code for loading sounds.

def load_sound(name):
    class NoneSound:
        def play(self): pass
    if not pygame.mixer:
        return NoneSound()
    fullname = os.path.join('data', name)
    try:
        sound = pygame.mixer.Sound(fullname)
    except pygame.error, message:
        print Cannot load sound:, wav
        raise SystemExit, message
    return sound

First we're going to check to see if the pygame.mixer class was loaded correctly. If it wasn't, we're going to return an empty class so that our game doesn't crash every time we want to load a sound. It's important to have this check somewhere in your code; this can be after your import statements at the top, if you prefer. This can be an issue sometimes on the XO platform if your dependencies haven't been configured correctly.

Pygame accepts a number of different formats when loading sounds. You can pass in mp3, ogg, or wav formatted sound files and they'll be accepted. This is one of the considerations you'll have to make when creating a game. Take in mind that you'll have a limited amount of storage and memory space on the XO, and that games tend to be large in size because they're heavy on multimedia. You'll want to cut back on as much of those sizes as possible, especially since the XO's speakers can't take advantage of better quality sound files.

Lets make a class for a new game object.

class Paddle(pygame.sprite.Sprite):
    """Moves a paddle around on the screen"""

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect = load_image('paddle.bmp')

    def update(self):
        """Moves the paddle around based on the mouse position"""
        pos = pygame.mouse.get_pos()
        self.rect.midtop = pos

This class represents a paddle for a tennis or pong type game. This is a very important time to be using your docstrings, especially if you're working on a team.

We start by subclassing the pygame.sprite.Sprite class, the default pygame Sprite class. Every game object that will appear on screen and interact with other game objects should extend this class. In your init method, make sure to call the superclass's init method as well.

Notice how we are also creating an update method to override the superclass's method. Every Sprite object has an update() method that tells Pygame how to handle the Sprite on each frame. Make sure to call this for each appropriate object in your update loop.

Lets take a second and examine Pygame's rect class. This class is very useful for creating game objects that have arbitrary borders. Since we can use pictures with transparencies, we can draw an object's borders instead of specifying a coordinate system for each object. The picture will then be placed on top of a rectangle. So long as you don't want your collisions to look beautiful, this is a simple and easy way of making sprites.

The rect class has a lot of features - moving them around is very easy. You can also use a rect to check if something falls within a certain area, resize or reshape the rectangle with a command, as well as other things. Check out the documentation for more.

rectangle1 = pygame.Rect(1, 2, 3, 4)

This makes a rectangle at point (1,2) with a width of 3 and height of 4.

Now it's time to make the ball class for our game. It's a little more code, but its really not doing much more and should be easy to follow.

class Ball(pygame.sprite.Sprite):
    """makes a ball object and moves it around on screen"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) #call Sprite intializer
        self.image, self.rect = load_image('ball.bmp')
        screen = pygame.display.get_surface()
        self.area = screen.get_rect()
        self.rect.topleft = 10, 10
        self.spin = 1

    def update(self):
        "move or spin, depending on the ball's state"
        if self.spin:
            self._spin()
        else:
            self._move()

    def _move(self):
        "move the ball across the screen, and bounce at the ends"
        newposition = self.rect.move((self.move, 0))
        if self.rect.left < self.area.left or \
            self.rect.right > self.area.right:
            self.move = -self.move
            newposition = self.rect.move((self.move, 0))
            self.image = pygame.transform.flip(self.image, 1, 0)
        self.rect = newposition

    def _spin(self):
        "spin the ball image"
        center = self.rect.center
        self.spin = self.spin + 12
        if self.spin >= 360:
            self.spin = 0
            self.image = self.original
        else:
            rotate = pygame.transform.rotate
            self.image = rotate(self.original, self.spin)
        self.rect = self.image.get_rect()
        self.rect.center = center

    def hit(self):
        "this will cause the ball to start spinning"
        if not self.spin:
            self.spin = 1
            self.original = self.image

This code is fairly straightforward. The ball will move until it's hit by the paddle, which will spin the ball clockwise. Each time the ball hits the paddle, it'll rotate a little until it does a full circle.

This code takes advantage of two methods, _spin() and _move(), in it's update loop. The reason we've put underscores in front of the method is so that we don't accidentally overwrite methods or variables that might have already been written. Be careful when writing your own code not to overwrite methods that have already been written for you. If you're not sure if you're overwriting something, put an underscore before it. In python, this is the de facto way of saying that something is private. An underscore in front of a variable or method means "mine, don't touch".

In this section, take note of wealth of code that has already been written from you. Make sure to check the documentation before you do something that might be a commonly needed function. You'll very rarely have to do any kind of complicated transformations, scalings, or movements because Pygame has taken care of this for you. This is very valuable - you'll never have to write any complicated math or graphics stuff.

Lets put all our code together and see what a full pygame app looks like. This code will have all of our classes and also our initialization features.

import os, sys
import pygame
from pygame.locals import *

if not pygame.font: print 'Warning, fonts disabled'
if not pygame.mixer: print 'Warning, sound disabled'

class Helper():

    def load_image(name, colorkey=None):
        fullname = os.path.join('data', name)
        try:
            image = pygame.image.load(fullname)
        except pygame.error, message:
            print Cannot load image:, name
            raise SystemExit, message
        image = image.convert()
        if colorkey is not None:
            if colorkey is -1:
                colorkey = image.get_at((0,0))
            image.set_colorkey(colorkey, RLEACCEL)
        return image, image.get_rect()

    def load_sound(name):
        class NoneSound:
            def play(self): pass
        if not pygame.mixer:
            return NoneSound()
        fullname = os.path.join('data', name)
        try:
            sound = pygame.mixer.Sound(fullname)
        except pygame.error, message:
            print Cannot load sound:, wav
            raise SystemExit, message
        return sound

class Paddle(pygame.sprite.Sprite):
    """Moves a paddle around on the screen"""

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image, self.rect = Helper.load_image('paddle.bmp')

    def update(self):
        """Moves the paddle around based on the mouse position"""
        pos = pygame.mouse.get_pos()
        self.rect.midtop = pos

class Ball(pygame.sprite.Sprite):
    """makes a ball object and moves it around on screen"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) #call Sprite intializer
        self.image, self.rect = load_image('ball.bmp')
        screen = pygame.display.get_surface()
        self.area = screen.get_rect()
        self.rect.topleft = 10, 10
        self.spin = 1

    def update(self):
        "move or spin, depending on the ball's state"
        if self.spin:
            self._spin()
        else:
            self._move()

    def _move(self):
        "move the ball across the screen, and bounce at the ends"
        newposition = self.rect.move((self.move, 0))
        if self.rect.left < self.area.left or \
            self.rect.right > self.area.right:
            self.move = -self.move
            newposition = self.rect.move((self.move, 0))
            self.image = pygame.transform.flip(self.image, 1, 0)
        self.rect = newposition

    def _spin(self):
        "spin the ball image"
        center = self.rect.center
        self.spin = self.spin + 12
        if self.spin >= 360:
            self.spin = 0
            self.image = self.original
        else:
            rotate = pygame.transform.rotate
            self.image = rotate(self.original, self.spin)
        self.rect = self.image.get_rect()
        self.rect.center = center

    def hit(self):
        "this will cause the ball to start spinning"
        if not self.spin:
            self.spin = 1
            self.original = self.image

if __name__=='__main__':

    pygame.init()  #initialize
    screen = pygame.display.set_mode((468, 60))  #the display size
    pygame.display.set_caption('My XO Game Activity') # the activity name
    pygame.mouse.set_visible(0) #hide the mouse

    background = pygame.Surface(screen.get_size()) 
    background = background.convert()
    background.fill((250, 250, 250))

    paddle_sound = Helper.load_sound('whiff.wav')
    ball_sound = Helper.load_sound('punch.wav')
    paddle = Paddle()
    ball = Ball()
    allsprites = pygame.sprite.RenderPlain((paddle, ball))
    clock = pygame.time.Clock()

    while True:
        clock.tick(60)
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit(1)
            elif event.type == KEYDOWN and event.key == K_ESCAPE:
                sys.exit(1)
            elif event.type == MOUSEBUTTONDOWN:
                if ball.hit(paddle):
                    ball_sound.play() #punch
                else:
                    miss_sound.play() #miss

            allsprites.update()
            screen.blit(background, (0, 0))
            allsprites.draw(screen)

By including if name == 'main':, we're telling this code only to initialize only if we're running this code as a program. This will also let us import our classes in other code we write. These code blocks are especially useful for quick testing purposes when you're writing libraries.

Porting Pygame Games to Sugar

Because of the open source nature of the OLPC and Pygame projects, you might run in to Pygame code that you'll want to port to Sugar. This process can be a bit tricky. Start by following these steps to port your game. You might have to do some of your own troubleshooting and research, but these steps are generally a good starting point.

1. Put your game on a USB flash device and plug it into your XO, then open the Journal activity and drag your files from the USB stick to the XO. Alternatively, if you're using a git or subversion repository, you can start the terminal app and pull everything from your repository.

2. Try running your game from the Terminal activity. Type in python yourgamefile.py. If it runs, great. If not, start by making sure that it runs on Pygame outside of Sugar

Extras

We've tried to cover as much of the basics of OLPCGames/Pygame programming as possible. The two libraries are huge - you might want to include more functionality in your game than we've been able to cover. Here are some additional tutorials to get you fully up to speed on game programming.

Troubleshooting

Always make sure you're using the most recent version of OLPCGames to avoid configuration and dependency errors. Also, make sure to check your log files whenever you experience a crash, or something out of the ordinary. The log viewer activity will keep logs for every activity, including the one you have in development. This is a great way for debugging your programs.

If you experience any problems that you can't solve, or if you think you've discovered a bug in the OLPCGames platform, head to the OLPCGames mailing list or check out the discussion on the #pygame, #sugar, or #olpc-content channels over IRC on freenode ( irc.freenode.net ).

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License