PyGame

At this year’s linux.conf.au I decided it was high time I learnt how to program simple graphics. The use case I had in mind in particular was simulating/visualising resource transportation in a grid based real-time strategy game like Widelands, but really I’ve been a bit annoyed that I haven’t been able to do basic graphics on Linux at a similar level to what I used to be able to do with AMOS Basic on the Amiga or QuickBASIC on the PC back in the early ’90s. I used to be able to write a clone of QBasic‘s “nibbles” in a couple of hours, with just “print” and “locate” and a bunch of logic; doing the same today would mean figuring out all the stuff required to setup ncurses, and well, augh.

Or doing it as a webapp, of course, and that tends to involve even more setup.

So with Carl and Keith in attendance I figured Cairo would be a good bet, but the documentation that’s out there for python-cairo didn’t seem very helpful as far as making it trivially easy (probably in part because cairo’s kinda multi-targetable and kinda powerful). Carl’s suggestion was python-gtk, which uses cairo as it’s drawing engine; I didn’t actually try that though — gtk is a bit more than I want to have to think about, in theory anyway. I thought about trying out nickle’s support for cairo, but it still seemed to involve a bit more setup work than I really wanted, and, ultimately, Python’s my play language of choice, and it seemed a bit overwrought switching languages just for pretty pictures.

Ultimately I went with PyGame, which seemed to have the right emphasis on “simple”, “works with python”, and “popular enough I’d heard of it before”. It’s possibly a bit kludgy in some respects — but it’s also pretty easy. I was a bit surprised that it doesn’t seem to actually use Cairo for its drawing, instead relying on SDL which seems to hit X natively. At some point, that might annoy me enough to revisit the topic, but then again, by that point it’ll probably be using Cairo one way or another anyway, the way things are going.

My first program was to switch from an example bouncing ball in a window to a bunch of lines whose end points bounce around a window. The code:

#!/usr/bin/env python
import pygame

size = (640,480)
colour = (40, 50, 0)
bgcolour = (255,255,255)

n_lines = 13

screen = pygame.display.set_mode(size)
pygame.display.set_caption("not a game")
running = 1

class BouncePoint:
    def __init__(self, start, velocity, colour, bbox):
        self.coord = tuple(start)
        self.speed = list(velocity)
        self.colour = colour
        self.bbox = bbox

    def update(self, time):
        self.coord = tuple( a+b*time
                 for a,b in zip( self.coord, self.speed ) )
        for d in [0,1]:
            if self.coord[d] < 0:
                self.speed[d] = abs(self.speed[d])
            if self.coord[d] > self.bbox[d]:
                self.speed[d] = -abs(self.speed[d])

pts = []
for i in range(n_lines):
    pts.append( (BouncePoint( (i*10,i*10), (30*i+45,50*i+12), colour, size),
                 BouncePoint( (i*50,i*50), (i*65+23,i*13+125), colour, size ))
    )
    colour = (colour[1], colour[2], colour[0])

clock = pygame.time.Clock()
clock.tick()

while running:
    event = pygame.event.poll()
    if event.type == pygame.QUIT:
        running = 0
    time = clock.tick()/1000.0
    for i in range(n_lines):
        pts[i][0].update(time)
        pts[i][1].update(time)

    screen.fill(bgcolour)
    for i in range(n_lines):
        pygame.draw.line(screen, pts[i][0].colour, pts[i][0].coord, pts[i][1].coord, i+1)
    pygame.display.flip()

Works adequately — I mean, it pegs the CPU, redraws the whole window every refresh, and flickers the cursor when you move it across the window; but it displays what I wanted to see, and doesn’t have too much excess code. Really, there’s just a call to “pygame.display.setmode” to create a window, “pygame.event.poll” to watch for events like the window close button, and “pygame.display.flip” to let me write my code to redraw the entire screen from scratch without the display actually flickering due to partially complete updates being visible.

There’s also a bit of boilerplate around the class declarations for the lines; I forget whether that’s there because the example I copied from did it that way, or because what I wanted to end up with would need separate objects for most of the drawable items, but either way it seemed like a reasonable approach. Even if it is a major departure from what you might see in QuickBASIC in ’93.

I suspect I’m abusing modern CPU speeds and such to get away with that code — like having a personal bodybuilder on hand just to unscrew a stuck jar or something; but equally it runs fine on my netbook, and I can always add a “time.sleep(0.1)” in here and there if I only actually want, say, ten frames per second.

(As far as I can pick, the right way to throttle graphics apps is by frames per second; and adding a sleep in there lets you do that directly — and if you make it sleep for “max(0, secs_per_frame – elapsed_time)”, you get it throttled on quick enough CPUs, and pegged out when your hardware isn’t as quick as your hopes. Maybe there’s better criteria out there, but that seems much closer to perfect than I’d expected, given how easy I’m insisting this be)

So yeah, PyGame: recommended. AAA+++ module, will do more braindead hacks with it.

Leave a Reply