Code Project: Build a Space Invaders clone

Code

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.

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.

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[0].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[0].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?

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.

You should follow us on Identi.ca or Twitter


Your comments

Nice but ...

I think that one universal "best practice" is to avoid peppering the code with magic numbers.

So I think that code for beginners should emphasize this point.

I smell Algol/C/Java family programmers :-)

I used to, and still do occasionally, handle iterating over lists in the C style way of thinking instead of the Pythonic and more efficient way.

Python will handle iteration for you freeing you from setting ranges, count variables, len() calculations, and make your code even more readable and meaningful as a bonus! -- being the nice language it is it will still let us code old C or Java ways of thinking and not complain either but make the resulting code look a bit off to a seasoned Python programmer.

The basic idiom is this:

for item in listofitems:
do something with item

Item automatically is set to each element in the list is sequential manner and when the end of listoftems is reached the for loop exits.

Much cleaner to say

for enemy in enemies:
if intersect(ourmissle.x, ourmissle.y, enemy.x, enemy.y):
enemies.remove(enemy)
break

Any time you see len() in logic it's a trigger that you'll possibly be slowing down your logic reinventing a builtin way in Python:

if no enemies:
quit = 1

List comprehensions are powerful and normally easier to read and understand the logic for beginners -- not us who darted in a c like language who love isn't counters, indices, etc.

5 lines of initiallization code for the enemies list becomes a single line of python in the form of a list comprehension. Remember the more lines you type the more typing and logic based bugs enter your code and have to have your time spent debugging them out later. It is fine for a small program but good to start the habit early for when developing the next set of games.

Is the beginner a c programmer trying out Python or a complete beginner?

The list comprehension replacing 5 line of C style code would be:

enemies = [Sprite(50 * x + 50, 50, 'data/baddie.bmp) for x in range(10)]

Typo correction

Multiple lines = greater typing bugs :-)

Should read

if not enemies:
quit = 1

Another for count

There a another for count len() range C programming idiom in the loop after quit = 0 that I'll leave to the readers to catch, rewrite, and then test run the code on their computers.

Wonderful article...

Really, really enjoyed reading that article, thank you very much! It's been a while since I did some good old fun game programming :)

And also the comments.. coming from a C background, I'm still in the old way of thinking. Thanks PythonNutter for enlightening me, I may just have to try dabbling in Python again..

Sorry - could not stand the horrible key event routine.

I had to rewrite the key event routine as well as it does not handle the keyboard very well.

With the code in this article try holding down right, then press space while holding down right, then release space while holding down right... yuck

Here is the rewritten event code separating out key routines:

COMMENT OUT THIS CODE BLOCK
if len(enemies) == 0:
quit = 1

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

REPLACE WITH THIS CODE BLOCK
if not enemies:
quit = 1

for event_ in event.get():
if event_.type == QUIT:
quit = 1

key_pressed = key.get_pressed()

if key_pressed[K_RIGHT] and hero.x < 590:
hero.x += 5

if key_pressed[K_LEFT] and hero.x > 10:
hero.x -= 5

if key_pressed[K_SPACE]:
ourmissile.x = hero.x
ourmissile.y = hero.y

Of course this web sites comment engine blows up all the spacing. Horrible really.

For those new to Python. Search google for PEP 8. That is your style guide where you learn do not mix spaces and tabs in formatting python code. And 4 spaces per indent is normal although Guido and others who write lots of code prefer to use 2 spaces. This becomes more important when you have a lot of code that keeps running off the screen/window of your code editor program.

Now go back and hold down keys and press other keys and release while holding down the original key and see how much nicer it runs now :-)

Thanks, nice.

Very nice introduction to Python, just say that pygame can be installed searching it on Synaptic in Xubuntu 8.10.
What I don't like from python is tab and space indentation required.
The time.delay(5) method freeze(stop) the program so make my P-350 slower but in a fast CPU this will be near 200 FPS, in the pygame page I saw that they use this:
# Make sure game doesn't run at more than 60 frames per second
clock.tick(60)

Python and Tux radar ar the best!!

I loved building a space invaders clone with all the codes and things it was the best!!!

Please give me the code in turbo c form

please....................................................
please....................................................
please....................................................
please....................................................
please....................................................
please....................................................

send it to my e-mail bhong015@yahoo.com
please....................................................

problem

i had to re write the code a bit to get it to work but still wont please help
it wont upload the pictures

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"
import pygame
import random
image = pygame.image.load("data/pictures/baddie.png").convert()
image = pygame.image.load("data/pictures/hero.png").convert()
image = pygame.image.load("data/pictures/heromissile.png").convert()
image = pygame.image.load("data/pictures/baddiemissile.png").convert()
class Sprite:
def __init__(self, xpos, ypos, filename):
self.x = xpos
self.y = ypos
self.bitmap = image.load(filename).convert()
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))
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
init()
screen = display.set_mode((640,480))
key.set_repeat(1, 1)
display.set_caption('PyInvaders')
backdrop = image.load('backdrop.png')
enemies = []
x = 0
for count in range(10):
enemies.append(Sprite(50 * x + 50, 50, 'data/baddie.png'))
x += 1

hero = Sprite('data/hero.png'(20, 400,))
ourmissile = Sprite( 'data/heromissile.png'(0, 480))
enemymissile = Sprite( 'data/baddiemissile.png'(0, 480))

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[0].x < 10:
enemyspeed = 3
for count in range(len(enemies)):
enemies[count].y += 5

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[0].y

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

for ourevent in event.get():
if len(enemies) == 0:
quit = 1

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

if not enemies:
quit = 1

for event_ in event.get():
if event_.type == QUIT:
quit = 1

key_pressed = key.get_pressed()

if key_pressed[K_RIGHT] and hero.x < 590:
hero.x += 5

if key_pressed[K_LEFT] and hero.x > 10:
hero.x -= 5

if key_pressed[K_SPACE]:
ourmissile.x = hero.x
ourmissile.y = hero.y

enemymissile.render()
enemymissile.y += 5

hero.render()

display.update()
time.delay(5)

Traceback (most recent call last):
File "E:\dont go in my stuff\school stuff\software design and development\python games\space invaders\space inaders.py", line 18, in <module>
image = pygame.image.load("data/pictures/baddie.png").convert()
error: Couldn't open data/pictures/baddie.png

I would also like to add

I would also like to add that in later versions of Python (3.2 for instance), you will need to use brackets after print ("a is", a) and so on otherwise you will get a syntax error!

Deleting ourmissile

How would i go about deleting the hero missile after it comes into contact with an enemy sprite? As hard as I try, I can't figure it out.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

CAPTCHA
We can't accept links (unless you obfuscate them). You also need to negotiate the following CAPTCHA...

Username:   Password:
Create Account | About TuxRadar