PyGame is an API that is also for 2d surfaces.
It is best for applications where there is a lot of updating of animation without user input (especially as it uses blitting).
It has a set of baseclasses that make it easier to manage and change teh state of objects.
It also has collision detection routines, which is very useful in game programming.
So, net/net if you are doing something that is a game, or game-like, you're likely to want to use pyagem.
pygame has fairly good reference documentation here: http://pygame.org/docs/ref/index.html
There are also lots of tutorials available on the web. However, it's important to note that I use pygame a bit differently than they do in the typical tutorials.
Tutorials typically have you create a pygame window to display you game in, and then create a loop with a pygame clock object.
I don't do it this way anymore. Now I prefer to embed a pygame surface into a Gtk app. This has some benefits to me:
- I can use menus for the GUI for things like starting games, pausing etc...
- I can use dialog boxes for things like hight scores or colleting information from users
If you try to do these things from within a pygame loop, the gtk.main lool clashes with your pygame loop, and everything is just really hard to use.
So, for the approach I take, I have three samples that you can look at at your leisure: 1. sample_game code: http://bazaar.launchpad.net/~rick-rickspencer3/+junk/pygame-pygtk-example/view/head:/game.py blog posting: http://theravingrick.blogspot.com/2011/08/using-pygame-in-pygtk-app.html
This is the simplest code that I could make to demonstrate how to embed pygame and handle input.
2. jumper: http://bazaar.launchpad.net/~rick-rickspencer3/+junk/jumper/view/head:/jumper/JumperWindow.py This is only slightly more complex. It show how animate a sprite by changing the image, and show collision detection and playing a sound.
3. smashies: http://bazaar.launchpad.net/~rick-rickspencer3/+junk/smashies/files/head:/smashies/ This is a full blown game which I have almost completed. I'm considering selling it in the software center when I am done. This one handles all the complexity of lives, scores, pausing, etc...
For this tutorial, we'll focus on jumper since it has an animated Sprite.
The overall approach is simple'
- set up a drawing area in Gtk Window
- add pygame sprites to it
- handle keyboard input from the gtk window
- periodically call an update function to:
- update the data for the sprites
- update the view
- detect collisions and respond to them
make the background work
A game typically needs a background image. I put a background image and the other images and sounds in the data/media directory. Once you get the background image painting, it means you've got the main part of the game set up. So, we'll go through this part with patience.
I put all the code in JumperWindow, so it's easy to see in one place.
You need to import 2 modules: import pygame import os import gobject
You'll see why you need these each in turn.
First we want to create a a pygame.Image object to hold the background. Once we have that, we can use pygame functions to paint it.
Since Jumper is a Quickly app, it I can use "get_media_file" to load it.
So I make the background in these 2 lines of code in the finish_initializing function:
- bg_image = get_media_file("background.png") self.background = pygame.image.load(bg_image)
Before I use it, I have to set up the pygame environment though. I do this by adding a gtk.DrawingArea to the gtk.Window, and telling the os module to use the windows xid as a drawing surface.
You can't just do that in the finish_initializing function, though. This is because drawingarea1 may not actually have an xid yet. This is easy to handle by connecting to the drawing area's "realize" signal. At that point, it will have an xid, and you can set up the environment.
So, connect to the signal in finish initalizing:
and then write the self.realized function:
- def realized(self, widget, data=None):
- os.putenv('SDL_WINDOWID', str(self.ui.drawingarea1.window.xid)) pygame.init() pygame.display.set_mode((300, 300), 0, 0) self.screen = pygame.display.get_surface()
This function intializes pygame, and also create a pygame.Screen object that you need for drawing.
So now that the drawing area is set up as a pygame surface, we need to actually draw to it.
Actually, we'll want to periodically update the drawing so that it appears animated. So we want to update it over and over again.
So after setting up pygame in tghe realized function, add a gobject timeout to recurringly call a function to update the game:
- gobject.timeout_add(200, self.update_game)
the funciton update_game will be called every 200 millliseconds. For a real game, you might want to make it update more often.
So, now we need write the udpate_game function. Eventually it will do a lot more, but for now, it will just tell the game to draw. So we need to write the draw_game function as well.
- def update_game(self):
- self.draw_game() return True
- self.screen.blit(self.background, [0,0]) pygame.display.flip()
Note that update_game returns True. This is important, because if it returns anything else, gobject will stop calling it.
Looking at draw_game a little more, the first line tells the Screen object to "blit" the background. This means to only update the parts that have changed. This keeps the game from flickering on slower systems. We also pass in x/y coordinates to tell it to update the whole background.
This doesn't paint to the screen yet, though. It just prepares it in memory. You can call blit a whole bunch of times for different sprites, but until you call pygame.display.flip() they won't actually be painted to the screen. In this way, the screen only gets update once, and the animation is smooth.
Now if you run the game, you should see the background painted.
At this point you have a drawing surface set up, and you are drawing to it in a loop.
Animating a Sprite
Now let's add an animated sprite.
I put 2 png's in the data/media director. One called "guy1.png" and called "guy2.png". We will animate the game by swapping these images back and forth every time the game paints.
WARNING: I am doing something very wrong! Jumper loads the images as needed from disk. In a real game, this is a bad idea. This is a bad idea because that takes IO time, which can slow the game down.
It's better to load all the images and sounds at once when the game loads. See smashies for how I do that in the init.py file.
I mentioned before that pygame has some useful base classes. One of those base classes is called "Sprite" which is a really old game programming term.
When adding an object to your game, it's best to derive from sprite. It's easier to manage the data for a sprite that way, and also there are useful pygame functions that expect a Sprite object.
So, first create the sprite class and an initialization function: class Guy(pygame.sprite.Sprite):
pygame.sprite.Sprite.init(self) self.animation_stage = 1 self.x = 35 self.y = 180 self.direction = 0
Next, we'll write a function called "update". You'll see in a bit why it's important to call it "update". For this function, check which animation stage to use, and then use that image:
- def update(self):
- img = get_media_file("""guy%i.png""" % self.animation_stage) self.image = pygame.image.load(img)
Next, you need to set the "rect" for the Sprite. The rect will be used in any collision detection functions you might use:
- self.rect = self.image.get_rect() self.rect.x = self.x self.rect.y = self.y
Finally, update the animation stage.
- self.animation_stage += 1
if self.animation_stage > 2:
- self.animation_stage = 1
Now you just need to a "Guy" to your background.
First, create a guy in the finish_initializing function.
- self.guy = Guy()
Since the game will evenutally have a lot of sprites, it's easiest to manage sprites as a group. There is a pygame class for this called a SpriteGroup, which you create by called RenderUpdates. So, crate a SpriteGroup and the guy to it:
self.sprites =pygame.sprite.RenderUpdates() self.sprites.add(self.guy)
Remember when we created the update_game function? Now you can see how useful the SpriteGroup is. You can call "update" on the sprite group, and it will in turn call update on every sprite in it. So add that call to the update_game function:
Now, you also need to tell the Guy to draw. That's easy too with the SpriteGroup. Add this line to draw_game function:
Now when you run the game, each tick the guy will swap images, and it will look like it's moving.
Note that I handle keyboard and mouse input very differently than they describe in most pygame tutorials
Responding to keyboard input is really easy, because you can just use gtk events.
I have found that you need to attach to the key events for the window, not the drawing area.
So, to make the guy jump when the user clicks the space bar, I make a key_press_event signal handler, that calls "jump()" on the guy:
- def key_pressed(self, widget, event, data=None):
- if event.keyval == 32:
- if event.keyval == 32:
You can look at the jump and update functions in the Guy class to see how a jump was implemented.
So, that's the essence of creating an animated sprite, which gets you a lot of the way toward making a game.
We don't have time to delve into everything, but I did want to touch on collisions.
Assuming that you've added another sprite called self.apple that tries to hit the guy, you can use one of the many pygame collision detection functions in every call to update_game to see if the apple hit the guy:
- if pygame.sprite.collide_rect(self.guy,self.apple):
If you set the rect for your Sprite subclass, functions like this work well, and are easy.
I also mentioned sounds. Pygame has a really rich set of sound functions. The easiest thing to demo is playing a sound from a file, like this:
- sound_path = get_media_file("beep_1.wav") sound = pygame.mixer.Sound(sound_path) sound.play()