Make a Python game in minutes with Gloss

Code

When Hudzilla isn't busy working on his free Mono tutorials using C#, he likes to hack on one of his pet Python projects: Gloss. It's hosted right here on TuxRadar and you may already have given it a try. If not, he wrote a short tutorial for PC Plus magazine a few months ago, and took the time to repurpose it for the web.

So, if you fancy learning the fastest way to create Python games, read on as Hudzilla talks you through an example Gloss project...

While Electronic Arts has done a good job of convincing the world that it takes 100 squibillion dollars to make a modern computer game, the indie scene continues to thrive in a world where people like to try out ideas just for fun, learn for learning's sake and are happy if a finished piece of software comes out at the end of the day.

If that sounds like you, hopefully you're already having a lot of fun programming your Linux box, Xbox, iPhone and any other device that stays still long enough. On the other hand, if you're fine with trying out ideas and happy with the idea of having a finished piece of software, but hit a bit of a mental speed bump at the idea of learning lots of new stuff to accomplish said feat, you're not alone: lots of people like the idea of having a bit of fun making their own games, but the Venn diagram of where they cross with people happy to spend their time learning to program is, well, non-existent.

Fortunately, if you have great ideas bubbling away in your brain, there are two things that will act as mental floss. The first is called Pygame, which is a set of libraries that make it easy for everyone to make games in the newbie-friendly Python programming language.

But Pygame itself is a bit limited because it does much of its drawing on the CPU, which makes it slow when drawing lots of things or performing complex operations such as rotation or scaling. And that's where our other useful tool comes in: Gloss is an OpenGL graphics system that sits on top of Pygame providing GPU-accelerated graphics, a simplified coding framework and a stack of coder-friendly functions that make it easy to perform common tasks.

In this tutorial, I'm going to walk you through what it takes to make a simple game with Pygame + Gloss. All the code is already written (and I think you'll find it very short!) so it's just a matter of explaining to you what it does and why. We used Linux (naturally), but both Pygame and Gloss should work fine on Windows too - make sure you have Pygame, Python OpenGL and Gloss installed, and if you're on Linux you'll also need the Numpy Python module.

Important note #1: the source code download for this project is essential to follow this explanation. I'll be referring to line numbers freely, so open it up in something like Gedit or Notepad so you can follow along.

Important note #2: the source code download for this project includes the Gloss library so it will run straight away, but it does not include the Gloss documentation, tutorial or example projects - you should get them from the Gloss homepage.

The skeleton key

The key to understanding how Gloss works lies in the file skeleton_game.py, which we've included in the source code for this project - this is basically a simple shell of a game that contains lots of empty functions. When you run it, a big empty window will appear, and that's about it. Just so that you can be sure everything is working properly, here's what that nice big window should look like:

The skeleton Python game for Gloss just brings up a 720p window with a light blue background, waiting for you to customise...

The skeleton Python game for Gloss just brings up a 720p window with a light blue background, waiting for you to customise...

Like I said, it's big and blue. To turn that into a real game, you just need to flesh out all those empty functions with something meaningful. In essence, Gloss games work like this:

  • When the game starts, load_content() is called so you can load all your sprites.
  • Thereafter, about once every 60th of a second, update() is called so you can update your game, move things around, etc.
  • After update() finishes, draw() is called so you can render your game to the screen.

Here's the Python code inside skeleton_game.py:

from gloss import *

class SkeletonGame(GlossGame):
	def preload_content(self):
		pass

	def draw_loading_screen(self):
		pass

	def load_content(self):
		pass

	def draw(self):
		pass

	def update(self):
		pass

game = SkeletonGame("Empty game skeleton")
game.run()

Those two preload_content() and draw_loading_screen() work just like the other ones, but are only really necessary if your game takes a long time to load, because they allow you to load and show some graphics before the main loading sequence kicks in.

So, to make a game, you really just need to fill in the blanks. It's not quite coding by numbers, but it's not far off. All these callbacks are handled automatically for you because your game needs to build upon the GlossGame class, which provides all sorts of functionality for you.

Enough with the theory, download the source code for the project, open up Sharpshooter.py, and let's take a look at the source code for our game...

How it works: Sharpshooter

In our game, targets will fall down from the top of the screen, and the player needs to click on them before they reach the bottom. As you can imagine, each target needs to know its own X/Y position and whether it has been hit or not. But to make the game a little more interesting, we're going to make the targets sway left and right as if they were being blown around in the breeze.

The easiest way to do that is with a bit of mathematics: we store a number in each target, then use that to calculate a sine-wave-like movement to offset the target's position. All that is wrapped up in the Target class on lines 3-9:

class Target(object):
	def __init__(self):
		self.x = 0
		self.y = 0
		self.sin_val = 0
		self.x_speed = 0
		self.hit_time = -1

If all that talk of sine waves sounds a bit hazy that means you probably slept during maths class at school. Relax - we'll cover it in more depth later.

Moving on, lines 12-24 make up the load_content() method, which is where all the basic initialisation for the game takes place. This method gets called before your game starts, so it needs to load any assets you want to start using straight away, like this:

class Sharpshooter(GlossGame):
	def load_content(self):
		self.sfc_background = Texture("sharpshooter.png")
		self.sfc_target = Texture("target.png")
		self.targets = []
		self.last_created_time = 0
		self.create_delay = 800
		self.target_speed = 350.0
		self.target_radius = self.sfc_target.half_width
		self.num_targets = 0
		self.num_hit = 0

		self.on_mouse_up = self.handle_mouse_clicks

This method starts off by loading the background and target textures, and all the game variables are set up with their starting values. The self.targets line sets up an empty array that will store all the targets that are currently in play; we'll be adding and removing items later.

The last_created_time and create_delay variables track when a target was last created and how much time the game should wait before creating another target respectively. target_radius, num_targets and num_hit are all there to track score: target_radius is used to figure out whether the mouse was clicked over a target, num_targets is incremented each target is created, and num_hit is incremented each time a target is shot.

Right at the end of the method the value self.on_mouse_up is set, and it's particularly important because it tells Gloss to call the handle_mouse_clicks() method whenever the player clicks their mouse. That method will do all the click tracking to decide whether a target was shot, and Gloss will automatically pass into it the position of the mouse click.

Lerping and smooth steps

The human eye is highly sensitive to movement, which in practical terms means game players would rather see game objects move around smoothly rather than just popping into existing. Rather than making you write animation code yourself, Gloss makes it easy to move things around using a technique called linear interpolation - arguably better known as tweening thanks to Adobe Flash.

Linear interpolation (often just called “lerping”) calculates points between two numbers by letting you specify a position between 0.0 and 1.0. For example, if you ask Gloss to calculate value between 0 and 1000, then position 0.5 would return 500 because it's halfway between 0 and 1000. So, to make something move across the screen, you just need to specify its start and end position, then say how far through its animation it should be.

Of course, the human eye is also highly sensitive to acceleration, which in practical terms means that if game players see things sliding around at a constant speed it looks a little dull. Again, Gloss comes to the rescue with its smooth step functionality, which in Flash terminology is called easing - rather than having an object move at a constant speed, smooth step will make it start slow, pick up speed, then slow down before reaching its destination.

This bit of theory gets put into action in the draw() method, which is nestled neatly from lines 25 to 34 and looks like this:

def draw(self):
	self.sfc_background.draw((0, 0))

	for target in self.targets:
		if target.hit_time != -1:
			diff = (Gloss.tick_count - target.hit_time) / 150.0
			if (diff > 1): diff = 1

			# this next line has been broken in two for easier the web
			self.sfc_target.draw((target.x, target.y), origin = None,
				color = Color(1.0, 1.0, 1.0, Gloss.smooth_step(1.0, 0, diff)), scale = 1.0 - diff)
		else:
			self.sfc_target.draw((target.x, target.y), origin = None, color = Color.WHITE)

That method starts off quite sedately, simply drawing the background at the co-ordinates 0,0, which is the top-left corner of the window. And then comes the main chunk of the method: a “for” loop that draws all the targets. By default, all targets have their hit_time value set to -1, meaning they haven't been shot, in which case the target gets drawn with the colour white.

If the target has been clicked, things get a little more complicated. Each target needs to disappear when it gets shot, so that code uses the "diff" variable to calculate how transparent each target should be. The way to do that is simple: when a target is shot, its hit_time gets set to Gloss.tick_count, which contains the number of milliseconds that have passed since the game began.

To calculate how faded out a shot target should be, we need to subtract the current time from the hit_time, then divide that by how fast the targets should fade away. In this code, we've chosen 150 milliseconds for the fade out time, and line 31 just makes sure that the “diff” variable is definitely no greater than 1.0 - remember, the smooth step function takes values between 0 and 1.

All the action takes place on the call to sfc_target.draw(), and really it's broken into two parts. First, the colour to draw the target is specified using Color(1.0, 1.0, 1.0, Gloss.smooth_step(1.0, 0, diff)), which tells Gloss to use 1.0, 1.0 and 1.0 for the red, green and blue values, but to use the smooth step function to calculate the alpha value so that it lies somewhere between 1.0 (full opaque) and 0.0 (fully transparent). The second part is “scale = 1.0 - diff”, which will cause Gloss to make the target increasingly smaller before it disappears entirely.

Back to those sine waves

Lines 36-62 make up the update() method, which performs three functions: create new targets for the player to shoot, remove any targets that have gone off the screen or were shot more than 150ms ago, and to update the position of all the targets that remain.

Lines 37-48 are where all new targets are created, either one, two or three at a time depending on a random number:

def update(self):
	if (self.last_created_time + self.create_delay < Gloss.tick_count):
		action = Gloss.rand_float(0, 10)

		if (action < 5):
			self.create_target()
		elif (action < 7):
			self.create_target()
			self.create_target()
		elif (action < 8):
			self.create_target()
			self.create_target()
			self.create_target()

We'll be looking at the create_target() method later, but for now you should see that the first line in that method checks to make sure that enough time has been left since the last target was created by comparing self.last_created_time (the time the last target was created) added to self.create_delay (the time between creating each target) against Gloss.tick_count (the number of milliseconds that have passed since the game started).

Once new targets have been created if necessary, the next part of the code deals with removing targets that are off the screen or have been shot:

for target in reversed(self.targets):
	if (target.y > Gloss.screen_resolution[1] + self.target_radius):
		self.targets.remove(target)
		continue

	if (target.hit_time != -1 and target.hit_time + 150 < Gloss.tick_count):
		self.targets.remove(target)
		continue

Notice we loop over the "targets" array in reverse - that's because if you loop through an array forwards and remove items, it has a tendency to implode because everything shuffles down one place. By looping through the array backwards, we can delete old targets without worrying about the effect that will have on subsequent targets.

Those two checks remove targets for two different reasons: the first is used when targets have fallen off the screen, and the second checks whether a target has been shot (target.hit_time != -1) and whether that was a suitably long time ago for it to have finished fading out (target.hit_time + 150 < Gloss.tick_count); in either case, the target gets removed and the loop continues.

Then comes the slightly tricky part: updating the position of all the targets. Here's the code:

target.y += self.target_speed * Gloss.elapsed_seconds
target.x_speed += math.sin(target.sin_val) * Gloss.elapsed_seconds * 4
target.x += target.x_speed
target.sin_val += 1.5 * Gloss.elapsed_seconds

That code uses the Gloss.elapsed_seconds variable three times - this contains the number of seconds since the last game update, which is usually 0.016666667 - ie, 1/60th of a second. If you multiply any movement by Gloss.elapsed_seconds it has the effect of making that movement run at the same speed regardless of what computer your game is running on. If a computer is slower, the number of elapsed seconds will be higher, so the movement will be correspondingly faster to make up for it.

So, onto how it works. The first line modifies the Y position of the targets so they fall downwards. The next two handle the sway of targets to the left and right by using the math.sin() function to generate values between -1 and 1. This is then added to the x_speed value of the target to make it move around gently. So that the sine wave keeps moving up and down, the last line increases the target's sin_val so that the next call to math.sin() will generate a different value.

Creation and destruction

Line 64 kicks off the create_target() method, which is called whenever we want one new target to be produced - if more than one is required, create_target() is just called multiple times. Here's the code:

def create_target(self):
	target = Target()
	target.x = Gloss.rand_float(0, Gloss.screen_resolution[0] - 150)
	target.y = -128
	target.sin_val = Gloss.rand_float(0, 36000) / 100
	self.targets.append(target)

	self.last_created_time = Gloss.tick_count
	self.create_delay = self.create_delay - 1
	self.target_speed = self.target_speed + 1
	self.num_targets = self.num_targets + 1

This is the easiest function in the script, because all it does is create a new target, assign it a few basic values, then add it to the "targets" array so it can be updated and drawn properly. But at the end of the method come four important lines: line 71 resets last_created_time so that the game doesn't create another target for a little while, line 72 decrements create_delay so that targets are created ever-so-slightly faster, line 73 increments target_speed so that the game is slightly harder, and line 74 increments num_targets so that we can track how many targets have been created.

The real action starts on line 76, which is where the handle_mouse_clicks() method starts. Here it is:

def handle_mouse_clicks(self, event):
	for target in reversed(self.targets):
		if (target.hit_time != -1): continue
		distance = Point.distance((event.pos[0], event.pos[1]), (target.x, target.y))

		if (distance < self.target_radius):
			target.hit_time = Gloss.tick_count
			self.num_hit = self.num_hit + 1

Previously we told Gloss to use this method whenever the player clicks their mouse, and what it sends us is the “event” variable, containing information such as which button was clicked and where. What this method does is loop over over the "targets" array, and, if a target hasn't been clicked already, checks whether the mouse clicked the target.

And now comes a little more mathematics. More specifically, Pythagoras's theorem, which tells us that the sum of A*A + B*B = C*C, meaning that if we we to calculate whether the mouse was clicked on a target we need to use the X and Y distance from the target's centre to calculate the hypotenuse of a triangle, which is really the distance between the mouse and the centre of the circle. If the distance between that click and the centre of the circle is less than the radius of the circle, then the target has been hit.

All that maths is wrapped up in Gloss's Point.distance() method, which takes two co-ordinate sets as its parameters and returns the distance between them. All we have to do is pass in the X and Y co-ordinates of the mouse click (stored in event.pos 0 and 1) plus the X and Y co-ordinates of the target, then check whether that value is lower than the target_radius variable we set way back in the beginning.

Now, experienced maths heads may already have spotted one performance flaw here: the more common way of phrasing Pythagoras's theorem involves a square root operation, which is notoriously slow, especially on lower-powered devices. If you want to make this whole thing run substantially faster, change line 19 so that self.target_radius is set to self.sfc_target.half_width * self.sfc_target.half_width, then change line 79 so that it uses distance_squared() rather than distance(). Behind the scenes, distance() actually calls distance_squared(), then calculates its square root, so if you can skip the square root operation it saves a lot of CPU time.

Once we know that a target has been clicked, all we have to do is set its hit_time value to the current tick count, then increment num_hit so we can keep track of the player's score.

The finished product: the Python PyGame game made following this simple tutorial.

The finished product: the Python PyGame game made following this simple tutorial.

What next?

That pretty much wraps up the game - hopefully you can now see how every line works, how easy it is to make quick little 2D games, and how Python, PyGame and Gloss come together to take away much of the boring grunt work of programming.

There's still more to do, if you fancy pushing your coding skills a little further. Right now, for example, we're tracking scores, but not showing them - you can do that either with a Gloss SpriteFont object or by setting the window title. Gloss also makes it very easy to create particle systems for explosion effects and the like, so you can easily tart up the target hits a little more.

If you're looking for more code examples, you'll find them in the Gloss source code - it comes with comprehensive documentation, a tutorial covering all the key features, and various example scripts showing off key concepts.

You should follow us on Identi.ca or Twitter


Your comments

Super awesome

This series is just awesome. That's why tuxradar.com is my favourite site. Keep it up!

Hangs for me

When I run "python sharpshooter.py" it hangs and the python process takes up 90-100% CPU. Same thing when I run "python skeleton_game.py" I've traced it to pygame.init(). As soon as pdb steps in to pygame.init() it hangs.

Thats funny and interesting

I'd basically built the same sort of framework for myself in Java when I was messing around with Java game development. I think I'll dust off that code and play with it again after reading this.

Problems

from: can't read /var/mail/gloss
: command not foundline 2:
./sharpshooter.py: line 3: syntax error near unexpected token `('
./sharpshooter.py: line 3: `class Target(object):'

I assume I'm missing some python framework. Regardless Why would a graphics/game framework need /var/mail access?

Article updates

Thanks to the people who took the time to write in with some, er, colourful comments on the article. We've updated the text and the code download, but we would ask that in the future people refrain from getting too angry here - if you want to start a whitespace war concerning tabs vs spaces, please do it on your own websites.

@David Grant: all the skeleton game does is initialise OpenGL and show an empty window. If that doesn't work, your OpenGL configuration is faulty.

Re: Problems. Gloss doesn't need /var/mail at all. Feel free to look inside Gloss.py (it's open source - hurray!) and you'll see that for yourself. It's possible you're running an old version of Python, or maybe you don't have Python OpenGL installed.

You are doing it wrong...

Really, Python has augmented assignment, so instead of

self.create_delay = self.create_delay - 1

You can write

self.create_delay -= 1

The same thing with +=, *= and so on (Python has twelve of them). Also, snake gods blessingly gave us slices: event.pos[:1] instead of (event.pos[0], event.pos[1])

err, event.pos[:2] :)

err, event.pos[:2] :)

Your knowledge of Python sucks, again...

First of all, to justify the 'coloricity' of the comments from people who do know Python. We are not getting angry, we are just getting a little frustrated as you public your material to teach people something, and when you do this - you should care teaching them right. So, no offence, but you should master the skills better yourself, before teaching others.

Now, let's get to the point. First of all - there is no need to start a war between tabs and spaces, as there is PEP8, an officially adopted document which strongly recommends the way of code layout.

Some of your code, btw, still violates those, and I don't mean the tabs/spaces issue here:
if (diff > 1): diff = 1
First of all - they should not be on the same line. Second of all - parentheses () are completly redundant here.

Okay, you fixed the increment/decrement issue, but you did it inefficient:
self.create_delay = self.create_delay - 1
self.target_speed = self.target_speed + 1
self.num_targets = self.num_targets + 1

This should be written using the augmented assigment operators:
self.create_delay -= 1
self.target_speed += 1
self.num_targets += 1

The overall look and feel of those mistakes tells that you have some C-like background. So, as I already told - you should probably learn Python better, as you teach people (potentally new to programming, or it's specific area) bad things/style/habits.

That's all folks

It's very kind of you to write in with suggestions, and you are of course welcome to write your Python as PEP8-shiny as you want to. Please do. Enjoy yourself. However, we just don't care about it as much as you seem to, and I'm afraid it's unlikely we ever will.

When putting this up, we did type -= and +=, then realised we're just throwing extra garbage at people. At least with the lines of code as used it's really clear to people even if we "did it inefficient".

As for tabs vs spaces, it's really great that you prefer spaces. Please go and write all the tutorials you want and use spaces galore. If you want, send us the link and we'll even point folks towards your tutorials for you. In fact, we're happy to give you, Anonymous Red Penguin, permission to take all the code for this project, style it up in the most beautiful Python code imaginable, then upload it somewhere for everyone to have.

sound problem

@TuxRadar: actually gloss.py does pygame.mixer.pre_init(44100, 16, 2, 4096) before calling pygame.init() and that is what is causing the problem because once I comment out the mixer line, the init() lines works fine. It's like it's blocking to get access to the sound card or something. I'll google around for why that might be happening. BTW, I have no other sound issues, all apps work great, using ALSA or pulse.

pulseaudio and sdl_mixer

Quick update, it looks like pulseaudio and sdl_mixer don't play well together... lots of google hits.

Fixed

Putting os.environ['SDL_AUDIODRIVER'] = 'esd' anywhere fixes the problem. I put it in gloss.py right after it sets os.environ['SDL_VIDEO_CENTERED'] = '1'

@David Grant

Thanks for spotting that problem - that sounds like quite a serious issue for SDL games! Which distro are you using? It might be worth reporting a bug to them, because everything should really play nicely.

Get over it

You know something guys, while I understand that the Python code that is supplied here by the author of the article may not be the very best it still serves a very useful purpose in that it teaches you about game design (albeit at a very high level). I really don't appreciate people nitpicking over little things like parenthesis or syntax. Get over yourself! Let the people here bring interesting articles even if it has a few very minor flaws.

Get over it - AGREED

This is a great article and I don't care if it's not super shiny uber perfect Python PEP whatever crud because IT WORKS and I can use it, have fun, play with Python etc. My thanks to the author -- I definitely will give this a try!

Sound issue is with Ubuntu

I'm using the latest Ubuntu (Jaunty, 9.04)

I like Python

Is this the Spanish inquisition ?
Part of Python is " One right way to do it."

:D
:D

ben 10

thank you

Not getting the blue screen.

@subject.

I am using Fedora 12. I installed OpenGL, downloaded the gloss code and executed the skeleton_game.py
But, I get nothing. :( Am I missing something? please help.

Workaround needed on Ubuntu 10.04 too...

Great library - thanks.

And I can confirm that it hangs when you run the examples on Ubuntu 10.04 unless you use the workaround from David Grant - os.environ['SDL_AUDIODRIVER'] = 'esd' - thanks for sharing.

Ubuntu 9.10

I had to add the sdl audiodriver line to get Gloss to work in 9.10 also. I set it to 'pulseaudio' though.

I'm trying to make a drag

I'm trying to make a drag and drop interface with gloss, but I keep on getting the next error message when I assign the on_mouse_down event

File "gloss.py", line 187, in run
self.on_mouse_motion(event)
TypeError: 'NoneType' object is not callable

It works with mouse_up, but I need both (down and up) to drag and drop items. Any ideas?

bug fixed! 186c185 < if

bug fixed!

186c185
< if self.on_mouse_motion is not None:
---
> if self.on_mouse_down is not None:

Amazing tutorial and Thanks

Thanks bro!. I wanted some introduction to some game engine for Ubuntu to develop my carom board game(There is none available. Reply if you find one!). Your post was so help full.!!!! Thanks again!!!

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