Code Project: Build a Space Invaders clone
Programming is great. You get to create something new, stimulate your brain and have fun along the way - especially if you're programming games. So we're going to show you how to write your very own Space Invaders lookalike called PyInvaders - but don't panic if you're tired of dull programming theory: take that palm away from your forehead. Here we'll focus on doing Cool Stuff(tm), making a game work instead of warbling about algorithms, data structures and object oriented polymorphism encapsulation. Or whatever.
Consequently, to follow this guide it helps if you have some prior programming experience. We're not going to explain everything in depth; if you've dabbled in some code before, and know your arrays from your elbow, you won't have any problems. For those completely new to programming, you might find some of the terminology a bit bamboozling, but you don't have to understand it all. Just take in what you can, grab the source code from the DVD and start experimenting by making changes yourself. That's how all great programmers got started!
So, as mentioned, we'll be making a mini Space Invaders clone. Our choice of programming language is Python due to its simple syntax and code cleanliness - it's very easy to read. PyGame, a language binding that wraps the SDL multi-media library around Python, will provide the graphical plumbing for our program, saving us from the chore of manipulating images by hand. Most distros have Python pre-installed, and PyGame is available in nigh-on every repository, so get the tools, open up a text editor, and let's get cracking...
A Python primer
Before embarking on any programming project, it's essential to get comfortable with the language to be used, even if it's just the raw basics. Given that 99% of programming is about manipulating variables (data storage places), calling routines (standalone bits of code) and acting on the result (if a = b, then do c), we can summarise Python's workings very succinctly below. (If you're a a regular Python hacker, just skip over this bit.)
def Multiply(x, y): z = x * y return z a = 5 b = 10 print "a is", a, "and b is", b answer = Multiply(a, b) if answer > 10: print "Result is bigger than 10" else: print "Less or equal to 10"
This very short program demonstrates many features of Python in action. Save this code to a file called test.py in your home directory, then open a terminal and enter 'python test.py' to run it.
The first three lines create (define) a function called Multiply - that is, a chunk of code that isn't executed when we start the program, but a routine that we can call upon later. The x and y are two variables that need to be sent to the routine when we run it. You can then see that a new variable called z is created, and it's assigned the value of x multiplied by y. We then return the number in that variable back to the calling program.
After this function, execution of the program starts. We know this because there's no indentation - ie tabs or spaces before the code. Python makes heavy use of indentation to show where code belongs, whether it's part of a function or a loop etc. In this case, there's no indentation because it's not part of the preceding Multiply function, so execution begins here.
We create two variables called a and b, giving them the values 5 and 10 respectively. (A variable is a container for data - it can contain other numbers throughout the duration of the program.) We print out the contents of the variables, and then send them to the Multiply function that we created before. Remember the 'return' part of the Multiply function? Well, that sends back the multiplied result, so we store that result into a new 'answer' variable.
Finally, we check to see if the answer variable is bigger than 10; if so, we print a message. If it's smaller than (or equal to) 10, we print a different message. Try changing the numbers in this program and experimenting with the code to get to grips with Python - once you feel comfortable, you're ready for some game coding capers.
Text editors with syntax colouring, such as KWrite, make it easier to read your code.
The aliens arrive
Before we thrash out our code, though, we need to get some graphics in place. Text-mode Space Invaders would be cool for your geek ranking, but let's make decent use of our graphics cards. For PyInvaders, we need five images that you can create by hand with Gimp, or you can use the quick mock-ups we made ourselves. Here's what you'll need if you want to create them yourself:
- backdrop.bmp - A 640x480 pixel image to serve as the background in the game. It's best not to make it too bright or busy, as it'll just distract you from the sprites.
- hero.bmp - 32x32 pixels for the player craft; for those parts you want to be transparent, colour them black.
- baddie.bmp - Same as above, but for the evil invaders.
- heromissile.bmp and baddiemissile.bmp - Again, 32x32 using black for transparent bits, with the player's missile pointing upwards and enemy's pointing downwards.
Create a directory called PyInvaders in your home folder, then make a subdirectory called data containing the above files.
Let's also think about what we want to do with a Space Invaders-like game: if you've never seen it before, it essentially involves several rows of aliens moving back and forth at the top of the screen, firing missiles and occasionally moving down towards the player. You can fire missiles upwards to zap enemies - your goal is to destroy them before they destroy you.
The code in full
To keep things simple (and the code compact), we'll just have one row of aliens for now, and no score counter or bonuses. But these are things you can add later when you understand the code! We'll now go through the source in chunks to explain it; you can find it as a single file (pyinvaders.py) in the source code for this project. For now, read the following text to fathom out how it all works.
from pygame import * import random
These first two lines are very simple: they just tell Python that we want to use the PyGame module, so that we can load images and manage the screen easily, and let us generate random numbers later on.
class Sprite: def __init__(self, xpos, ypos, filename): self.x = xpos self.y = ypos self.bitmap = image.load(filename) self.bitmap.set_colorkey((0,0,0)) def set_position(self, xpos, ypos): self.x = xpos self.y = ypos def render(self): screen.blit(self.bitmap, (self.x, self.y))
Next comes this class. If you're familiar with object oriented programming, you'll already know how a class works, but if not, think of it as a type of box for storing data and commands. This code isn't executed at the start of the program - it just says "Here's a box of data and commands called Sprite, which you can use later". The class sets up variables for a sprite, most notably the x and y variables which will hold a sprite's position on the screen. The __init__ routine is run when we first create a new instance of the class (a new box based on this description), and loads the filename provided as the sprite image. Also, the set_colorkey line tells PyGame that we want black (0,0,0 in RGB) pixels to be transparent.
Head over to www.pygame.org/docs for a complete guide and reference to PyGame's functionality.
If you're new to object oriented programming, you might find all this a tad confusing. But again, just think of this as a type of box containing variables and routines (those labelled def), and we can create many instances (copies) of this box with different data contents. So, we'll create 10 instances of this Sprite class for the enemies, one for the player, and so forth.
def Intersect(s1_x, s1_y, s2_x, s2_y): if (s1_x > s2_x - 32) and (s1_x < s2_x + 32) and (s1_y > s2_y - 32) and (s1_y < s2_y + 32): return 1 else: return 0
Next up is this slightly intimidating bit of code. Because it's a function (denoted by def) it isn't executed when the program starts, but can be called upon later. All this does is check whether two sprites overlap - it takes the x and y pixel positions of one sprite (s1_x and s2_x variables), compares them against the positions of another (s2_x and s2_y), and returns 1 if they're overlapping. Et voila: simple collision detection! Note that this is hard-coded for sprites of 32x32 pixels, but you can change the numbers accordingly if you use bigger sprites later on.
init() screen = display.set_mode((640,480)) key.set_repeat(1, 1) display.set_caption('PyInvaders') backdrop = image.load('data/backdrop.bmp')
Next, we initialise PyGame, set up the screen mode, configure keyboard repeat to be rapid (for controlling our player), set the text in the titlebar, and load the background image.
enemies =  x = 0 for count in range(10): enemies.append(Sprite(50 * x + 50, 50, 'data/baddie.bmp')) x += 1 hero = Sprite(20, 400, 'data/hero.bmp') ourmissile = Sprite(0, 480, 'data/heromissile.bmp') enemymissile = Sprite(0, 480, 'data/baddiemissile.bmp')
Here we create a new list of objects called 'enemies'. (A list in Python is similar to an array in other languages, albeit much more flexible.) The list is empty to start with, so we add (append) 10 new Sprite class objects in a loop. Here you can see how we create instances of the class (or copies of the box) we created before, providing the initial x position, y position and filename. the '50 * x + 50' part looks a bit odd, but it means that the horizontal position of the new enemy sprites is staggered. So, the first sprite x position is 50, the next is 100, and so forth as we go through the loop.
The last three lines of this chunk are easy to grok - they load sprites for the player (hero), the player's missile and the enemy missile. Remember earlier that we set the screen mode to 640x480? Well, here we set the missile y (vertical) positions at 480, off the bottom of the screen, as we don't want to show them until they're fired.
quit = 0 enemyspeed = 3 while quit == 0: screen.blit(backdrop, (0, 0)) for count in range(len(enemies)): enemies[count].x += enemyspeed enemies[count].render() if enemies[len(enemies)-1].x > 590: enemyspeed = -3 for count in range(len(enemies)): enemies[count].y += 5 if enemies.x < 10: enemyspeed = 3 for count in range(len(enemies)): enemies[count].y += 5
Now the main game kicks in. The quit variable is a simple yes/no (1/0) variable that's used to determine if the game should end (ie whether the player has zapped all the baddies, or been hit by a missile). Then enemyspeed determines how fast the enemies move - you can play around with that later to make the game more challenging.
Following that, the game's main loop starts in the 'while' line. First, we draw the backdrop image at x and y position 0 and 0 respectively (the top-left of the screen). Then, we run through a loop counting up the enemies in our previously created list of objects. the len(enemies) tells us how many enemy objects are in the list - it will decrease from 10 as the player kills the baddies. In the loop we add the enemy's speed counter to each baddie sprite object, then call the render() function in the Sprite definition at the top of the file for each baddie.
The next two loops determine the direction and vertical position of the row of enemies. If the furthest right enemy, enemies[len(enemies)-1], has reached the right-hand side of the screen (590 pixels), then send the row heading off in the other direction by inverting the speed value. Also, move all of the enemies down by adding 5 their y (vertical) positions. The loop after this does the same, but when the enemies hit the left-hand side of the screen.
if ourmissile.y < 479 and ourmissile.y > 0: ourmissile.render() ourmissile.y += -5 if enemymissile.y >= 480 and len(enemies) > 0: enemymissile.x = enemies[random.randint(0, len(enemies) - 1)].x enemymissile.y = enemies.y
It's missile handling time. The first if construct draws the player's missile if it's on the screen (ie in play), subtracting 5 pixels from its y position each game loop to make it move up the screen. The second code chunk checks to see if the enemy missile isn't in play - if so, it creates a new one from a random choice of enemy. It does this by setting the missile's x position to one of the enemies: random.randint(0, len(enemies) - 1) chooses an enemy in the list of objects between 0 (the first enemy) up to the last enemy, according to the size of the list.
if Intersect(hero.x, hero.y, enemymissile.x, enemymissile.y): quit = 1 for count in range(0, len(enemies)): if Intersect(ourmissile.x, ourmissile.y, enemies[count].x, enemies[count].y): del enemies[count] break if len(enemies) == 0: quit = 1
Now for some collision detection. If the player's sprite (hero) has come into contact with the enemy's sprite, quit out of the game. In the next chunk, we count through the enemy list in a 'for' loop, seeing if any of them intersect with our missile. If they do, we delete the enemy object that has been touched from the list, and break out of the loop (so that it doesn't continue with non existing objects). When an enemy object is deleted, the list gets smaller. Finally, we check to see if the length of the enemies list is zero - that is, the player has killed all the enemies. If so, quit out!
for ourevent in event.get(): if ourevent.type == QUIT: quit = 1 if ourevent.type == KEYDOWN: if ourevent.key == K_RIGHT and hero.x < 590: hero.x += 5 if ourevent.key == K_LEFT and hero.x > 10: hero.x -= 5 if ourevent.key == K_SPACE: ourmissile.x = hero.x ourmissile.y = hero.y
Here's the keyboard handling bit. We get a list of SDL events (keyboard, mouse, window manager etc.) and worth through them. If we get a QUIT event, it means that the user has tried to close the window, so set the quit variable which will halt our program in the master 'while' loop.
However, if a KEYDOWN event has been received, we need to process it. This is very clear here: if the right cursor key is pressed and we're not flying off the screen, add 5 to the player's horizontal position. Then it's the same for the left cursor key, but inverted. We also check for the space key; if it's pressed, bring a new missile to live by placing it at the player's position.
enemymissile.render() enemymissile.y += 5 hero.render() display.update() time.delay(5)
And here's the final chunk of code. We render the enemy's missile and make it move down the screen, then render the player's sprite. Note display.update() - it's an essential part of PyGame programming. Whatever you do with the screen, it won't actually be displayed until you call that routine, hence why it's at the end of the 'while' main game loop. Lastly, we add a delay so that the game doesn't move too quickly -- try playing around with it.
And we're done
So there's the code! It's a lot to take in if you're new to Python and PyGame, but if you follow it carefully it should all make sense. Grab the complete file (pyinvaders.py) from here, then copy it into the PyInvaders folder you made before, which also contains the data directory for the images. Then open up a terminal and enter this to run the game:
cd PyInvaders python pyinvaders.py
Our finished game! Behold the expertly drawn aliens. Really, have you ever seen anything scarier?
Taking it to the next level
Once you're familiar with the code, why not try your hand at enhancing PyInvaders with new features? Here's some suggestions for things you can modify, along with their corresponding difficulty levels...
- EASY - Add a score counter. For this, all you have to do is add 'score = 0' near the top of the code to set up a new variable, and then increment it ('score += 1') whenever you successfully hit an enemy. Then, when the program exits, you can add 'print "You scored:", score' to display the number.
- MEDIUM - Check for missile-to-missile collisions. Currently, the game detects when missiles hit spacecraft, but not when one missile hits another. You can add a check into the middle of the code, alongside the current collision detection routines, and then reset both player and enemy missile positions if they're touching.
- HARD - Add another row of aliens. You'll have to duplicate a few things in the code here. First, you'll want to set up a secondary list of enemy sprites, with a y position offset larger than the existing list (eg 100). Then you'll have to add the collision checks again, and set up another enemy missile. It'll certainly make the game more taxing!
There are lots of other things you can do too. For instance, you could add sound effects and music to the game, or change sprites when they're hit by a missile. Boom! You could even add joystick or mouse support. See www.pygame.org/docs for a wealth of information - in particular, thumb through the Tutorials section and the Chimp game guide for help on using sound effects.