Chapter 9. Gaming with Pyglet

 

This chapter covers

  • Display images and text on the screen
  • Using event loops and timers
  • Game design, and making your game fun

 

In this chapter, you’ll be writing your own arcade game using a library called Pyglet. Pyglet bills itself as a “cross-platform windowing and multimedia library for Python,” but you’ll be using it for its real purpose—writing games!

If you’re familiar with various arcade games, yours will be sort of a cross between Spacewar!, Asteroids, and Space Invaders—it will have a spaceship, evil aliens to shoot, and a planet to run into. To make the game more interesting, you’ll give the planet some gravity, so it draws in the ship gradually.

But first, you’ll need to get Pyglet installed and working on your computer.

Installing Pyglet

The first thing you’ll need to do is to download and install Pyglet. A Windows installer and source code are available from www.pyglet.org/download.html, and Pyglet is available as a package for several Linux distributions. Pyglet installation is straightforward under Windows: download the installer program, and run it. Mac users can download a .dmg image with an installer on it, and there are packages for most Linux distributions. The next figure shows the Windows installer doing its thing.

 

Note

Pyglet uses OpenGL under the hood, so you’ll need an OpenGL-capable graphics card. This normally isn’t a problem, unless you’re running an old computer—most cards released in the past five years or so support OpenGL automatically.

 

If none of those options work for you, you can always download the source package and run python setup.py install, though you’ll also need to install AVbin separately if you take this route.

Figure 9.1. Installing Pyglet

Let’s start with a simple Pyglet program, breaking it down line by line:

import pyglet                
window = pyglet.window.Window(fullscreen=True)    
pyglet.app.run()                

All the submodules of Pyglet are stored within the pyglet module. You can access the window module, for example, with pyglet.window. This saves you importing several modules at the top of your program, and makes your code easier to read.

Pyglet’s Window object handles all the screen initialization and rendering. You’ll generally need one in every Pyglet application you write. You’re passing in fullscreen=True as an argument so the window takes up the whole screen.

Pyglet is a framework, so after you’ve set everything up, you need to call its main application loop.

If you type this program in and run it, you should see a screen similar to figure 9.2.

Figure 9.2. A black screen

That’s right—a big black screen. Not very impressive, but it’s your black screen: the blank canvas on which you’ll write your masterpiece. As an added bonus, you know Pyglet is working properly. To exit Pyglet, press the Escape key.

Next, we’ll figure out how to make that black screen more impressive.

First steps

Let’s get started! The first thing you’d like to do is display an image on the screen. Because you’re writing a space game, let’s make it a nice big planet. I’ve used an image of Mars that I downloaded from NASA’s website at www.nasa.gov/multimedia/imagegallery/, but feel free to create your own if you’re feeling artistic. The next listing will display your planet image on the screen.

Listing 9.1. Drawing on the screen

Before you display images, you need to tell Pyglet where to find them. To do that, you append the path to an images folder onto Pyglet’s resource path and ask it to reindex its resources. You’ll also need to create the folder manually and save your planet image in it.

Once you have your image source, all you need to do is call the pyglet.resource.image function, which will read the image from your resource directory .

By default, an image has an anchor at the lower-left corner; you’d prefer it in the center. I’ve created a function that will do that for you. Because you want the x and y coordinates to be integers, Python’s integer division operator (//) makes sure the result is an integer.

Pyglet is capable of drawing images directly to the screen, but a faster and cleaner way is to use a Sprite class . Sprites track their position and image and have their own optimized drawing routines, which make your program run faster. You’ll create one instance of your planet and stick it right in the middle of the screen. One thing to note is that you’re calling super(Planet, self) to get the parent class of your sprite—so you don’t have to worry about manually updating it.

 

Tip

Games are an area where a class-based design often makes a lot of sense, because there are usually a number of entities that behave similarly.

 

Once you’ve created the sprite, you need to tell Pyglet to draw it every frame. To do this, you create an on_draw event handler for the window (we’ll cover event handlers in more detail in the next section). You’ll do more later, but for now you clear the screen and draw the planet.

Figure 9.3. Your planet. Ideal for running into with your spaceship! (Image courtesy of NASA/JPL/Malin Space Science Systems)

You should see a nice big planet in the middle of your screen.

The planet will be a hazard for your spaceship, but first you need a spaceship. Let’s do that part next. In the process, we’ll introduce some important concepts when writing games or any event-based program.

Starship piloting 101

Your ship follows much of the same process as the planet, with one main exception: it will move around the screen in response to the player pressing keys. If you’ve ever played Asteroids, you’ll be familiar with the control method you’ll use. The up arrow will fire your engines, and left and right will turn your ship. If you want to slow down or go backward, you need to turn your ship around completely and fire your engines in the opposite direction.

The following listing shows the start of your Ship class. You’ll be adding features to it through the rest of this section. I’ve included this class in the same file as the planet, but feel free to create a new file and import it.

Listing 9.2. Ship class

First, you load the image for your ship , in the same way you did for the planet. Your Ship class looks similar to Planet, but you have some extra information : .dx and .dy are the ship’s speed in the x and y directions, and .rotation is how far left or right you’ve turned. You also put in .thrust and .rot_spd to determine how fast the ship should accelerate and turn. The higher these numbers are, the faster the ship will go.

Now you can create an instance of your ship . You feed in the ship’s speed here as dx and dy, but it won’t have any effect until you start updating the ship’s position in the next section.

Once you have your ship, you can add ship.draw() to the on_draw event handler, and your ship will appear on the screen .

Now you can see where your ship will start, and what it looks like.

Figure 9.4. Your spaceship

So far it’s no different than the planet you’re drawing, but now that you’ve set up your sprite, you can start making it do things.

Making things happen

In most games, you have control over some aspect—such as the main character—and can give input to tell them what to do next. Push the left arrow and move left; push the right arrow and move right. In this section, you’ll see how games accomplish this.

Pyglet uses an event-based programming model, and it’s how most interactive programs like games and graphical user interfaces are written. Rather than checking or waiting for input at certain sections of your program, you instead register functions to be called when something interesting happens. Pyglet refers to these functions as event handlers. If you’re used to a standard imperative design (“do this, then this...”), an event-based structure can seem odd, but it’s a much cleaner way of writing some sorts of programs. The next listing introduces two event handlers—one for when keys are pressed, and another for when they’re released.

Listing 9.3. Handling events

To respond to keys, Pyglet defines two event handlers, on_key_press and on_key_release . They’re defined in much the same way the on_draw function is, but they have two arguments: the key that is pressed, and any extra keys that are held down, such as Shift or Ctrl.

The symbol argument is an integer, but Pyglet defines a large number of keys you can use without having to worry about how to represent non-printable keys, like left arrow or the Esc key. To use them, import key from pyglet.window.

If arrow keys are pressed, you need to make some change to the game’s state. In this case, they correspond directly to the ship, so you’ll make a change to the ship’s state, and let the ship handle the changes during its update method .

 

Tip

Events are a powerful technique that make your programs simpler and easier to write. The alternative is to write one big loop that checks everything in your game. It has to run as quickly as possible, or your game will be slow and unplayable.

 

Once you’ve done that, pressing the arrow keys will trigger an on_key_press event and update your ship’s status—but you won’t see anything happen on the screen. That’s because you haven’t told the ship how to respond to changes in its status. For that, you’ll need to write an update method to change the rotation of the ship according to its status.

Listing 9.4. Updating the ship

When the ship is first created, it won’t be turning left or right, or firing its engines. You set the ship’s state here so your update function won’t throw an exception later.

By convention, most Pyglet classes will have an update method that gets called on each “tick” of the game engine . This is where your sprites change their position, create new objects in the game, and update their internal state. An update method takes one argument, dt, which tells you how much time has passed since the last time update was called.

You’re starting out simply, so you’re rotating the ship left and right for now . If you’re turning, then you update the .rotation attribute (a Pyglet built-in that rotates the sprite) by multiplying your rotation speed by dt.

Later, you’ll have other objects with update methods, so it’s a good idea to collect all of the method calls in one place .

Figure 9.5. Turning the ship

Finally, you set Pyglet’s built-in scheduler to call your main update method 60 times per second . This is the maximum speed at which Pyglet will run your game. If it’s slower, then you’ll get different values for dt, but your game will still run.

Now your feedback loop is finished, and you can see the results of all your hard work. If you run the program, you should be able to rotate your ship left and right by pushing the left and right arrow keys.

The next step is to make your ship move. To do that properly, though, you’ll need to learn a bit about how to specify directions and distances.

Back to school: Newton’s first law (and vectors)

In order to make your ship move consistently, you’ll need to apply a little bit of theory. You may remember some of this from school, from math or physics courses. If not, don’t worry—we’ll be taking things one step at a time. The first thing to know is that x represents values that go left to right, and y represents values that go up and down, as illustrated here.

Newton’s First Law

If you think back to your physics classes, you might remember Newton’s first law. Briefly, it states, “A body in motion will continue that motion unless acted on by an external force.” What this means is that your ship should move in a straight line unless you fire the engines. You already have a velocity—that’s the .dx and .dy attributes of your Ship class.

Figure 9.6. x and y coordinates. x represents values that go left to right, and y represents values that go up and down.

Vectors

The second thing you need is a way to convert the angle of the ship and its acceleration into values you can add to the ship’s x and y velocities. Whenever your ship’s engines are firing, you’ll need to break up its angle like this to work out the effect on your velocity in the x and y directions. The direction in the next figure means that when the ship’s engines fire, you’ll need to add 2 to your x velocity and 3 to your y velocity.

You’ll need a few math modules to do this in Python, but the principle isn’t any different from figure 9.7: figure out the x and y parts of the acceleration, and add those to your x and y velocities. During each update, add your velocity to your position.

Figure 9.7. The ship’s angle can have x and y parts.

Listing 9.5. Moving the ship

All the trigonometric functions you need are stored in Python’s math module, so you start by importing it .

Thinking ahead a little, you’ll also want to be able to handle the case where the ship moves off the edge of the screen. You’ll take the easy option and wrap the game up and down and left to right . wrap is a function that does that—given the value and the amount you’d like it to be constrained to.

Next, you break your angle into x and y parts . Note that these might be negative if the angle points left or down. Also, Pyglet and the .math module use different representations of angles (degrees versus radians), so you need a function to convert from Pyglet’s version into something the math module can use. You also need to flip your rotation around to get the right values in the y direction.

Figure 9.8. Now you can drive your spaceship around. Brrm! Brrm!

Once you have your two components, the rest is relatively straightforward. You multiply each part by the ship’s acceleration and how long it’s been since your last update, and add each 1 to your velocity .

The last step is to update your position on the screen . You also check to make sure you can’t go over the edge of the screen by wrapping your x and y positions based on the height and width of the window.

Finally, it looks a bit odd for your ship to be flying around without any visual feedback, so I created an extra image with some flames shooting out of the back. You swap it over whenever the ship’s engines are on .

Now you can drive your ship around the screen, accelerate, and turn around to decelerate. Wheee! It’s fun for a while, but ultimately there’s not much to do, and the mechanics are easy to understand. What you’d like is to have something more complex, so you have more opportunities for different sorts of interaction with the game.

Gravity

You’ll add to the game by making the planet have gravity, so it pulls on the ship. If the ship collides with the planet, then BOOM! No more ship! Fending off aliens while trying to keep clear of the planet should add enough difficulty to keep the player occupied and entertained.

Calculating gravity

How exactly do you go about adding that functionality? Well, the obvious place to put it is within the Planet class. It makes sense because it’s the planet that’s affecting the ship, and if you want anything else to be pulled by the planet’s gravity, it won’t be too hard to add. Essentially, you’re adding another force to the ship, just as you did when firing its engines.

Figure 9.9 shows you what the problem looks like. The long line is the vector from your ship to the planet. You’d like to find that, convert it into a force vector, and then split that vector into an x and y so you can easily add it to your ship’s velocity. Let’s deal with the easy bit first: splitting the force vector.

Figure 9.9. Gravity applies a force to your ship.

Listing 9.6. Planet updates

First you need to find out how much gravitational force the planet will put on the ship. We’ll gloss over this part for now; all you need to know is that, in a minute, you’ll create a method that will tell you the magnitude and direction of the force . Other than this, it’s the same as when you updated the ship when its engines were firing.

Don’t forget to include the update method in the main update function.

Now you have a nice, well-defined problem to solve: find the distance and angle to the ship. This is the opposite problem to the one you solved earlier. Back then, you had an angle and distance and wanted the x and y parts; now you have the x and y parts and want to know the angle and distance.

Listing 9.7. Figuring out gravity

First, set your planet’s mass . The heavier your planet is, the more it will pull on your ship. This is one of the elements of your game you can tweak to make it easier or harder.

The core of your method is to find out how far away the ship is, and in which direction . Based on that, you can calculate everything else you need.

Next, you find out the distance to the target (the ship) in the x and y directions . The distances might end up being negative if the ship is to the left or below—that’s normal.

Now you can find the first part of what you need, which is the distance to the ship . This is determined using the Pythagorean theorem: square the two smaller sides, and take the square root.

The angle is a little trickier . With a horizontal and vertical distance, you can use math.acos or math.asin to find the angle, but you need to take the complete 360-degree range into account. math.acos is only valid for the first half of the circle, so you need to reflect the angle by subtracting it from 2*math.pi if it’s in the wrong half. Figure 9.10 shows this in a little more detail: the two angles are different, even though the x distance and the direct distance are the same.

Figure 9.10. Two different angles, same x position and distance

Once you have the distance and angle, you can return those vectors . I’ve chosen (distance, angle) as the way a vector is represented, to avoid accidental confusion later.

 

Note

If all this math seems a little complicated, don’t worry too much. You have easy-to-use methods for calculating vectors and forces that you can reuse in your next game.

 

Now that you know the distance and direction to the ship, calculating the force due to gravity is easy . It’s proportional to the mass of the planet and diminishes with the square of the distance. The closer the ship, the more force you apply to it. Figure 9.11 shows a time lapse of the ship moving.

Figure 9.11. Your ship in orbit around the planet

When you run the program, you should see the ship being affected by gravity! Rather than moving in a straight line, it will have a force applied to it by the planet and will move in a graceful curve. If you’re careful, you can even put your ship into an orbit around the planet.

Watch out for that planet!

For collision detection, we’re sticking with circles around the ship, planet, and alien. Circles like those in figure 9.12 make the code simpler and more straightforward; but in trading accuracy for simplicity, you might notice a few collisions that should have been near misses. It’s possible to get pixel-perfect accuracy with Pyglet by comparing the overlap of the images themselves, but that’s outside the scope of this chapter.

Figure 9.12. The planet’s and ship’s collision circles

In practice, though, you don’t need to draw circles—you can compare the distance between the ship and the planet, and then compare that to the radius of the planet and the ship.

Listing 9.8. Crashing into the planet

You’ll need a few attributes on your objects . One is to tell the game whether the ship is alive or not, and the others are the radius of the planet and the ship. To make life easier, you’ll calculate the radius of the ship and the planet from the size of their images. If you change the image later, you won’t need to update the object’s radius.

With circles to detect collisions, all you need to do is compare the distance between the ship and the planet, and the sum of their radiuses . If the distance is shorter, then the circles intersect, and you have a collision.

Once your spaceship has crashed , you mark the ship as dead and reset the player’s position.

The ship’s .reset() method puts the ship back at the start and sets its velocity to something reasonable. You’re also setting a “life timer” that determines the time until the ship restarts, giving the player a few seconds to think about what went wrong. You can use this to set the ship’s position at the start so you don’t need to feed in a position when you create your class.

To delay the ship’s return, you check the life_timer attribute you set in the .reset() method . If you’re dead and the timer is greater than zero, then you still have some time left. If it’s less than 0, then you can mark the ship as alive and reset its position once more (because gravity still affects it), and you’re back to normal.

The last thing you need to do is make sure the ship isn’t drawn when it’s dead . A simple if statement takes care of that.

Now that your game is starting to take shape, you can see the general form a game takes. It has a certain state, effectively a simulation of a number of things like planets and ships, and you can have an effect on that simulation in certain ways. With some thought, a bit of luck, and some experimentation, your simulation will have aspects that are fun.

Next up, let’s add some excitement to your game.

Guns, guns, guns!

What’s a space game without aliens to shoot? Even space trading games have guns of some sort, so if you don’t have any, you’ll look a bit odd. They’re easy to add, given the work you’ve already done on angles and timers: set the bullet travelling at high speed at the same angle as the ship, and update it in a similar way. You’ll also want to keep track of whether the bullet has run into anything, and after a certain amount of time, remove it from the game.

Listing 9.9. Shooting

An easier way to manage key presses is to use Pyglet’s KeyStateHandler class . This class keeps track of which keys have been pressed and makes them available with a dictionary syntax, so you don’t need extra event handlers and state on your Ship class. If you push the left arrow key, then self[key.LEFT] will be set to True. The only tricky part to remember is that the ship instance is now a key handler, so you need to do a window .push_handlers(ship) so Pyglet knows to pass it events.

If you only let the player fire when the spacebar is pressed, they’ll get a bullet per frame, or 60 shots per second! Even if your computer is fast enough to handle hundreds of bullets onscreen, it makes the game a bit easy: it means you can fill the screen with bullets until there’s nowhere for the alien to hide. You’ll limit the number of shots by setting a timer whenever the ship fires a bullet . Every update, you’ll subtract dt from the timer, until it’s 0 and the player is ready to fire again.

Firing is straightforward—you create an instance of the Bullet class going in the right direction . You give the bullets a speed of 500, with the ship’s velocity added in (otherwise you get weird effects when you’re travelling fast or shooting while travelling sideways). You store the bullet instance in ship.bullets, because if you don’t have a reference to them somewhere, Python’s garbage collector will remove them, and you’ll wonder why your bullets aren’t appearing on the screen.

is the class you use whenever you fire a bullet. Bullet updates are easy, because they’re not affected by gravity and move in a straight line.

Figure 9.13. Your ship firing—ready to take on the alien armada

You don’t want bullets hanging around forever, so they have their own timer. Once they’ve been around for 5 seconds, you delete them from ship.bullets and let Python handle the rest . You also check for collisions with the planet, the same way you did for the ship.

Because there are potentially so many bullets, it makes sense to use Pyglet’s Batch class, which makes sprite rendering much faster if you have lots of sprites to draw. To use the bullets batch, you pass it in when creating the bullet sprite and then call bullets.draw() to draw all the bullets at once . You should see something like the next figure.

Now you can fly around the galaxy, doing good deeds and destroying alien scum. Hang on—you don’t have any alien scum to shoot yet. Let’s fix that next.

Evil aliens

What good are bullets without aliens to try them out on? In this section, you’ll add an alien spaceship whose sole purpose in life is to destroy the evil Earthling intruder. To make life easier, you’ll assume the alien has advanced technology that isn’t influenced by gravity, and that they can enter and leave the planet’s atmosphere at will. You’re going to be a little lazy and not worry about all those vectors and collisions with the planet—only whether the alien has hit the player. The next listing gives you all the code you need to put an alien in your game.

Listing 9.10. A random alien

One thing I’ve done in this section of code is pull the vector functions out of the class and made them more standalone . I’ve left them as is here, but, ultimately, you’ll probably want to put them into their own module or find a vector library you can reuse.

The Alien class ends up being similar to the Ship class, except for its update method , so there shouldn’t be any major surprises in this part. You have all the same concepts—speed, acceleration, wrapping the x and y position, death, and respawning after a countdown.

Your alien has a simple AI—every so often, it accelerates in a random direction . The frequency of the acceleration and the parameters set in __init__ give the alien enough changes in direction that shooting it can be a bit of a challenge.

Finally, I noticed while play-testing that it’s possible for the alien to accelerate to ridiculous speeds , which makes it hard to shoot. To stop it from doing that, you check that the x and y speeds are within the alien’s maximum speeds and reduce them if they aren’t.

 

Note

The alien is one thing in this game that you definitely want to experiment with. The rough rule of thumb is that an alien should be easy enough for the player to shoot some of the time, but hard enough to be a challenge. Without the right balance, your game won’t be fun.

 

The last thing you need to do is make the alien interact with the other objects you have onscreen: it should be killed by bullets and should kill the player when it runs into the ship. You’d also like some sort of reward system for the player, so you’ll add a score. Every time the player does something wrong, like crash into the planet or the alien, you’ll subtract 100 points. If the player shoots the alien, then you’ll add 100 points.

Listing 9.11. Making the alien interact

You’d like the alien to be an extra hazard for the player to avoid, so you check the distance between the ship and the alien —the same way you do for the ship and the planet. It’s up to you whether you want the alien to disappear when it collides with the player.

A score is how you let players know that they’ve done something right according to the rules of the game, so you give them 100 points for shooting the alien and subtract 100 points if they run into the alien or the planet.

The bullets should have an effect on the alien . So, for each bullet, you check the range to the alien. If it’s within the alien’s radius, then you’ve shot the alien! Resetting the alien works in pretty much the same way it does for the player—only you draw the alien if it’s alive and have a short delay between the alien dying and its reappearance.

Players need to be able to see their score on the screen , so you add a Label class in the bottom of the screen, 10 pixels from the lower-left corner. Setting the color looks a little odd, because you might be expecting three numbers for red, green, and blue. The fourth is the alpha value—255 is opaque and 0 is completely transparent—which is useful for fading text in and out.

You should see something like the next figure, complete with alien scum!

Now you have a full-blown space-alien-shooting-get-as-many-points-as-you-can-but-don’t-run-into-the-planet game (I’m sure you can think of a catchier title). You can send the game to your friends and even compete with each other.

Figure 9.14. Die, alien scum!

Where to from here?

You can make a number of improvements or changes to the game, either to refine what’s already there, expand on the game play, or turn the game into something completely different. Here are some ideas.

Extending the game play

There are a number of other elements that would normally be in a game like this. A good idea might be to pick your favorite space-shooting game and see how many of its features you can add. You might want to make the alien shoot back, tweak its AI to make it nastier, or add more aliens. Adding extra, harder levels with each wave of aliens and limited lives for the player would be another feature. Sound effects are also good for setting an atmosphere.

Altering the game play

Another option is to extend the game in a completely different direction—after all, perhaps you’re not a big fan of space-alien destruction. If you add a second player, and make your shots affected by gravity, you’ll have something pretty close to Spacewar!, the original space shooting game, written for the PDP-1 back in 1962.

If shooting at stuff isn’t really your thing, you could add extra planets and turn the game into a space-trading game or a 3D version of Lunar Lander. Limited fuel and lighter or heavier gravity on different planets would add to the challenge of the game, in addition to trading well.

Pyglet comes with several examples you can use to add text input and other features.

Refactoring

Now that you understand the program, there are some areas where the code could be improved. For example, there’s quite a bit of duplication in terms of the objects and how their positions are updated—making them derive from a subclass could make your code clearer and easier to extend.

You could also use an external vector class within your objects, so you don’t have to look at (or debug) all that geometry code. It helps to know what’s behind the vector library before you start.

Unit tests would be a big help in ensuring that your program is functioning properly when you make these changes. It’s difficult to test visual and game play aspects, but you can still check that collisions are detected properly by manually placing ship, bullet, and alien objects and checking whether they overlap. Other game data, such as forces and velocities, can be tested in the same way.

Get feedback

Another thing to bear in mind is that you’re writing a game. You can write the most beautiful code, with all sorts of features, but all that will be for nothing if your game isn’t fun to play. One good way to design and develop games is to create a minimal version that includes the elements you think will be fun and test it out on a few people, tweaking the various parts as necessary.

Summary

In this chapter, you learned how to write your own arcade game. You used Pyglet graphics classes to display images onscreen and move them around. To make your objects move realistically, you used some geometry and physics modeling to update their positions onscreen.

You added several types of objects and learned how to make them interact with each other—your ship could run into a planet and fire bullets; then, finally, you added an alien that could run into the ship and be shot by bullets.

Along the way, you learned about other game elements, such as collision detection and scheduling actions to take place over time.

Finally, we covered some aspects of game design: your game needs to be fun and include some familiar elements to attract people. It’s important to get feedback from others: what’s fun for you might not be fun for anybody else.

In the next chapter, you’ll learn more about Django and how you can make the web applications you write available on the internet for other people to use.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.161.132