Make a Python game in minutes with Gloss
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...
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 + 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 - 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, event.pos), (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.
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.