Sound, Animation and Program development: The Astrocrash Game

 

In this chapter, you’ll expand your multimedia programming skills to include sound and animation. You’ll also see how to write a large program in stages. Specifically you’ll learn to do the following:

 

  • Read the keyboard
  • Play sound files
  • Play music files
  • Create animations
  • Develop a program by writing progressively more complete versions of it

 

Introducing the Astrocrash Game

 

Here are the files we will be discussing in this chapter:

 

 

The project for this chapter, the Astrocrash game, is my version of the classic arcade game Asteroids. In Astrocrash, the player controls a ship in a moving field of deadly asteroids. The ship can rotate and thrust forward---most importantly, though, it can fire missiles at the asteroids to destroy them. But the player has some work cut out for him/her as large and medium-sized asteroids break apart into two smaller asteroids when destroyed. And just when the player manages to obliterate all of the asteroids, a new larger wave appears. The player’s score increases with every asteroid 9s)he destroys, but once the player’s ship collides with a floating space rock, the game is over. The pictures below show the game in action. The player controls a spaceship and blasts asteroids to increase his or her score. (Nebula image is in the public domain. Credit: NASA, The Hubble Heritage Team-AURA/STScl).

 

 

If an asteroid hits the player’s ship, the game is over:

 

 

Reading the Keyboard

 

You already know how to get keyboard input from the user as a string through the raw_input() function. But reading the keyboard for individual keystrokes is another matter. Fortunately, there’s a simple Screen method that lets you do just this.

 

Introducing the Read Key Program

 

The Read Key program displays the ship on the nebula background. The user can move the ship around on the background with a few, different keystrokes. When the user presses the W key, the ship moves up. When the user presses the S key, the ship moves down. When the user presses the A key, the ship moves left. When the user presses the D key, the ship moves right. The user can also press multiple keys simultaneously for a combined effect. For example, when the user presses the W and D keys simultaneously, the ship moves diagonally, up and to the right. The program is illustrated below; the ship moves around the screen based on key presses:

 

 

Here’s the code for this program:

 

 

Let’s now dissect the code.

 

Setting Up the Program

 

As I do with all programs that use the livewires package, I start by importing the modules I need and setting up some global constants for the screen dimensions:

 

# Read Key

# Demonstrates reading the keyboard

# A202/A598 Labs 9/8-15/2006

 

from livewires import games

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

Creating the Ship Class

 

Next, I write a class for the ship. I start with the constructor method, which accepts a screen, x- and y-coordinates, and an image. I initialize the sprite with these values.

 

class Ship(games.Sprite):

    """ A moving ship. """

    def __init__(self, screen, x, y, image):

        """ Initialize ship sprite. """

        self.init_sprite(screen = screen, x = x, y = y, image = image)

 

Testing for Keystrokes

 

Next, I define a moved() method. First, I get the current position of the ship and assign the coordinates to x and y. Next I check for various keystrokes and change the values associated with x and y accordingly. If the W key is pressed, I decrease the value of y by 1, moving the sprite up the screen by one pixel. If the S key is pressed, I increase the value of y by 1, moving the sprite down the screen. If the A key is pressed, I decrease the value of x by 1, moving the sprite left. If the D key is pressed, I increase the value of x by 1, moving the sprite right.

 

    def moved(self):

        """ Move ship based on keys pressed. """

        x, y = self.get_pos()

        if self.screen.is_pressed(games.K_w):

            y -= 1          

        if self.screen.is_pressed(games.K_s):

            y += 1

        if self.screen.is_pressed(games.K_a):

            x -=1

        if self.screen.is_pressed(games.K_d):

            x +=1

        self.move_to(x, y)

 

I use the Screen method is_pressed() to test for specific keystrokes. The method returns a value that can be treated as a condition. If the key being tested for is pressed, then the value returned by is_pressed() can be treated as True; if the key is not pressed, the value can be treated as False. I use the method in a series of structures to test if any of the four keys-W, S, A, or D-is being pressed.

 

The games module has a set of constants that represent keys that you can use as an argument in is_pressed(). In this method, I use games.K_w constant for the W key; games.K_s for the S key; games.K_a for the A key; and games.K_d for the D key. The naming of these constants is ptretty intuitive. Here’s a quick way to figure out the name of most key constants:

 

  • All keyboard constants begin with games.K_.
  • For alphabetic keys, add the key letter, in lowercase, to the end of the constant. For example, the constant for the A key is games.K_a.
  • For numeric keys, add the key number to the end of the constant. For example, the constant for the 1 key is games.K_1.
  • For other keys, you can often add their name, in all capital letters, to the end of the constant name. For example, the constant for the spacebar is games.K_SPACE.

 

For a complete list of keyboard constants, see the livewires documentation to be posted very soon.

 

The is_pressed() method has a couple of nice features. First, it allows you to detect if a key is pressed even if the user is pressing multiple keys. As a result, keystrokes can have a combined effect. For example, if the user holds down the W and D keys simultaneously in the Read Key program, the ship moves both up and to the right. Second, uppercase and lowercase keystrokes are interpreted as the same key. So in the Read Key program, it doesn’t matter if the user accidentally has Caps Lock on---if the user presses the W key, the ship will move up the screen.

 

Wrapping Up the Program

 

Finally, I write the familiar main part of the program. I create the screen, load the nebula background image, create a ship sprite in the middle of the window, and kick everything off by invoking my_screen’s mainloop() method:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

nebula_image = games.load_image("nebula.jpg", transparent = False)

my_screen.set_background(nebula_image)

 

ship_image = games.load_image("ship.bmp")

Ship(screen = my_screen,

     x = SCREEN_WIDTH / 2, y = SCREEN_HEIGHT / 2,

     image = ship_image)

 

my_screen.mainloop()

 

 

Rotating a Sprite

 

In the previous lab (the Pizza Panic lab), you learned how to move graphics objects around the screen, but livewires lets you rotate them as well. You can rotate any graphics object, including sprites, through two rotation methods. One method lets you rotate a graphics object by a certain number of degrees, while the other method lets you rotate the graphics object to an exact orientation.

 

Introducing the Rotate Sprite Program

 

The Rotate Sprite Program is an extension of the Read Key program. So, in addition to moving the ship, the user can rotate it. If the user presses the Right Arrow key, the ship rotates clockwise. If the user presses the Left Arrow key, the ship rotates counterclockwise. If the user presses the 1 key, the ship rotates to 0 degrees. If the user presses the 2 key, the ship rotates to 90 degrees. If the user presses the 3 key, the ship rotates to 180 degrees. If the user presses the 4 key, the ship rotates to 270 degrees. The figure below shows off the program; the ship can rotate clockwise, counterclockwise, or jump to a predetermined orientation (hard to see in this picture, but you can run the program):

 

 

 

Here’s the code for this program:

 

 

Now let’s start dissecting the program.

 

Rotating a Sprite by a Number of Degrees

 

By adding the following code to the end of Ship’s moved() method, I allow the user to rotate the ship:

 

        # rotate the ship based on key presses

        if self.screen.is_pressed(games.K_LEFT):

            self.rotate_by(-1)        

        if self.screen.is_pressed(games.K_RIGHT):

            self.rotate_by(1)

 

I first check if the Right Arrow key is pressed. If it is, I invoke the Ship object’s rotate_by() method, which rotates the sprite by the number of degrees passed to the method. In this case, I pass 1, so the sprite rotates by 1 degree clockwise. Next, I check if the Left Arrow key is pressed. If it is, I rotate the sprite by -1 degree, rotating the sprite 1 degree counterclockwise. You can rotate a sprite by any number of degrees you like.

 

Rotating a Sprite to a Specific Orientation

 

You can also rotate a sprite directly to a certain orientation by invoking the sprite’s rotate_to() method. All you have to do is pass a number of degrees, and the sprite will rotate to that orientation. I add the following lines to illustrate the method:

 

        if self.screen.is_pressed(games.K_1):

            self.rotate_to(0)

        if self.screen.is_pressed(games.K_2):

            self.rotate_to(90)

        if self.screen.is_pressed(games.K_3):

            self.rotate_to(180)

        if self.screen.is_pressed(games.K_4):

            self.rotate_to(270)

 

So now, then the user presses the 1 key, the sprite rotates to 0 degrees (its starting orientation). When the users presses the 2 key, the sprite rotates to 90 degrees. When the user presses the 3 key, the sprite rotates to 180 degrees. And finally, when the user presses the 4 key, the sprite rotates to 270 degrees.

 

Creating an Animation

 

Moving and rotating sprites adds excitement to a game, but animation really makes a game come to life. Fortunately, the games module contains a class for animations, aptly named Animation.

 

Introducing the Explosion Program

 

The Explosion program creates an animation of an explosion in the middle of a graphics screen. The animation plays continuously so that you can get a good look at it. When you’re done appreciating the cool effect, you can end the program by closing the graphics window. The figure below shows a snapshot of the program.

 

 

Although it’s hard to tell from a still image, an explosion animates at the center of the graphics window.

 

Here’s the code:

 

 

Examining the Explosion Images

 

An animation is a sequence of images (also called frames) displayed in succession. I created a sequence of nine images that, when displayed in succession, resembles a fiery explosion. Here they are:

 

 

Shown in rapid succession, these nine frames of animation look like an explosion.

 

This would be a good place to remind ourselves of the IceBlock game, which has the following art:

 

 

The picture has been magnified on purpose, at 200% the original size, just to see better what’s inside.

 

Setting Up the Program

 

As always, the initial code imports the games module and defines constants for the graphics screen’s dimensions:

 

# Explosion

# Demonstrates creating an animation

# A202/A598 Labs 09/8-15/2006

 

from livewires import games

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

In  the main part of the program, I create a graphics screen with the following lines:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

nebula_image = games.load_image("nebula.jpg", transparent = 0)

my_screen.set_background(nebula_image)

 

Here’s the nebula image:

 

 

Here are the images for the ship:  and the bullets:  (or missiles).

 

The explosion images have been presented before.

 

Creating a List of Image Files

 

The constructor of the Animation class takes a list of image file names or a list of image objects for the sequence of images to display. So next, I create a list of image file names, which correspond to the images shown earlier:

 

explosion_files = ["explosion1.bmp",

                   "explosion2.bmp",

                   "explosion3.bmp",

                   "explosion4.bmp",

                   "explosion5.bmp",

                   "explosion6.bmp",

                   "explosion7.bmp",

                   "explosion8.bmp",

                   "explosion9.bmp"]

 

Creating an Animation Object

 

Finally, I create an Animation object in the following lines:

 

games.Animation(screen = my_screen,

                x = SCREEN_WIDTH/2, y = SCREEN_HEIGHT/2,

                images = explosion_files,

                n_repeats = 0, repeat_interval = 5)

 

The Animation class is derived from Sprite, so it inherits all of Sprite’s methods and attributes. To create an animation, you must supply a screen and x- and y-coordinates as arguments to define where the object will be located, just as you do for a new sprite. In the previous code, I supply coordinates so that the animation is created at the center of the screen.

 

An animation requires images, so you must supply a list of image file names or a list of image objkects for the images to be displayed. I supply a list of image file names, explosion_files.

 

Next, I supply the n_repeats parameter with the value 0. n_repeats represents how many times the animation (as a sequence of all of its images) is displayed. A value of 0 means that the animation will loop forever. The default value of n_repeats is 0.

 

Then, I pass to the repeat_interval parameter the value 5. repeat_interval represents the delay between successive animation images. A higher number means a longer delay between frames, resulting in a slower animation. A lower number represents a shorter delay, producing a faster animation.

 

Finally, I kick off the program by invoking my_screen’s mainloop() method:

 

my_screen.mainloop()

 

Working with Sound and Music

 

Sound and music add another sensory dimension to your programs. Loading, playing, looping, and stopping sound and music are easy to do with the games module. And while people might argue about the difference between sound and music, there’s no such argument when it comes to the games module, where there’s a clear distinction between the two.

 

Introducing the Sound and Music Program

 

The Sound and Music program allows the user to play, loop, and stop the sound effect of a missile firing and the theme music from the Astrocrash game. The user can even play both at the same time. The picture below shows the program running:

 

 

Here’s the code, in two stages:

 

This was the upper half, here’s the second half now:

 

Working with Sounds

 

You can create a sound object for use in a program by loading a WAV file. The WAV format is great for sound effects because it can be used to encode whatever you can record with a microphone.

 

Files Needed

 

There are four .wav files and a .mid file:

 

 

Loading a Sound

 

First, I set up the program as always by importing games:

 

# Sound and Music

# Demonstrates playing sound and music files

# A202/A598 Labs 9/8-15/2006

 

from livewires import games

 

Then, I load a WAV file by using the games function load_sound(). The function takes a string for the name of the sound file to be loaded. I load the file missile.wav and assign the resulting sound object to missile.

 

# load a sound file

missile = games.load_sound("missile.wav")

 

Note: you can load only WAV files with the load_sound() function.

 

Next, I load the music file:

 

#load the music file

games.load_music("theme.mid")

 

I will save the discussion of music until after I finish demonstrating sounds.

 

Playing a Sound

 

Next, I write the menu system that you’ve seen before in A201:

 

choice = None

while choice != "0":

 

    print \

    """

    Sound and Music

   

    0 - Quit

    1 - Play missile sound

    2 - Loop missile sound

    3 - Stop missile sound

    4 - Play theme music

    5 - Loop theme music

    6 - Stop theme music

    """

   

    choice = raw_input("Choice: ")

    print

 

    # exit

    if choice == "0":

        print "Good-bye."

 

If the user enters 0, the program says good-bye.

 

The following code handles the case where a user enters 1:

 

    # play missile sound

    elif choice == "1":

        missile.play()

        print "Playing missile sound."

 

To play the sound once, I invoke the sound object’s play() method. When a sound plays, it takes up one of the eight available sound channels. To play a sound, you need at least one open sound channel. Once all eight sound channels are in use, invoking a sound object’s play() method has no effect.

 

If a sound is already playing, you can invoke the sound object’s play() method again. As a result, the sound will start playing on another sound channel, if one is available.

 

Looping a Sound

 

You can loop a sound by passing to the object’s play() method the number of additional times you want the sound played. For example, if you pass 3 to play(), the corresponding sound will play four times (its initial playing plus an additional three times). You can loop a sound forever by passing -1 to play().

 

The following code handles the case when a user enters 2:

 

 

    # loop missile sound

    elif choice == "2":

        loop = int(raw_input("Loop how many extra times? (-1 = forever): "))

        missile.play(loop)

        print "Looping missile sound."

 

In this section of code, I get the number of additional times the user wants to hear the missile sound and then I pass that value to the sound object’s play() method.

 

Stopping a Sound

 

You stop a sound object from playing by invoking its stop() method. This stops that particular sound on all of the channels on which it’s playing. If you invoke the stop() method of a sound object that’s not currently playing, Python is forgiving and won’t complain with an error.

 

If the user enters 3, I stop the missile sound (if it’s playing):

 

    # stop missile sound

    elif choice == "3":

        missile.stop()

        print "Stopping missile sound."

 

Working with Music

 

In livewires, music is handled somewhat differently than sound. First, there is only one music channel, so only one file can be designated as the current music file at any given time. However, the music channel is more flexible than the sound channels. The music channel accepts many different types of sound files, including WAV, OGG, and MIDI. Finally, since there is only one music channel, you don’t create and work with an object like you do with sounds. Instead, you access the single music channel through a group of functions from the games module.

 

Loading Music

 

You saw the code for loading the music file in the section “Loading a Sound”. The code I used to load the music file, games.load_music(“theme.mid”), sets the current music to the MIDI file theme.mid. You load a music file with the games.load_music() function by passing the file name as a string. MIDI files are often used for music (rather than WAV or OGG files) because of their small size and their tendency to place lower system demands on the computer playing the music.

 

Playing Music

 

The following code handles the case where the user enters 4:

 

    # play theme music

    elif choice == "4":

        games.play_music()

        print "Playing theme music."

 

As a result, the computer plays the music file that I loaded, theme.id. You can play the current music file with games.play_music(). If you don’t pass any values to the function, the music plays once.

 

Looping Music

 

You can loop the music by passing to games.play_music() the number of  additional times you want the music played. For example, if you pass 3 to games.play_music(), the music will play four times (its initial playing plus an additional three times). You can loop a music file forever by passing -1 to the function.

 

The following code handles the case when a user enters a 5:

 

 

    # loop theme music

    elif choice == "5":

        loop = int(raw_input("Loop how many extra times? (-1 = forever): "))

        games.play_music(loop)

        print "Looping theme music."

 

In this section of code, I get the number of additional times the user wants to hear the theme music and then I pass that value to the games.play_music() function.

 

Stopping Music

 

If the user enters 6, the following code stops the music (if it’s playing):

 

    # stop theme music

    elif choice == "6":

        games.stop_music()

        print "Stopping theme music."

                

You can stop the current music from playing by calling the games.stop_music() function, which is what I do here. If you call the games.stop_music() function while there is no music playing, Python is forgiving and won’t complain with an error.

 

Wrapping Up the Program

 

Finally, I wrap up the program by handling an invalid choice and waiting for the user:

 

    # some unknown choice

    else:

        print "\nSorry, but", choice, "isn't a valid choice."

 

raw_input("\n\nPress the enter key to exit.")

 

Planning the Astrocrash Game

 

It’s time to return to the chapter project: the Astrocrash game. I plan to write progressively more complete versions of the game until it’s done, but I still feel I need to list a few details of the program, including: the game’s major features, a few necessary classes, and the multimedia assets the game requires.

 

Game Features

 

Although my game is based on a classic video game that I know well (and learned about the hard way, one quarter at a time), it’s still a good idea that I write out a list of features:

 

  • The ship should rotate and thurst forward based on keystrokes from the player
  • The ship should fire missiles based on a keystroke from the player
  • Asteroids should float at different velocities on the screen. Smaller asteroids should generally have higher velocities than larger ones.
  • The ship, any missiles, and any asteroids should “wrap around” the screen—if they move beyond a screen boundary, they should appear at the opposite boundary.
  • If a missile hits another object on the screen, it should destroy the other object and itself in a nice, fiery explosion.
  • If the ship hits another object on the screen, it should destroy the other object and itself in a nice, fiery explosion.
  • If the ship is destroyed, the game is over.
  • If a large asteroid is destroyed, two new, medium-sized asteroids should be produced. If a medium-sized asteroid is destroyed, two new, small asteroids should be produced. If a small asteroid is destroyed, no new asteroids should be produced.
  • Every time a player destroys an asteroid, his or her score should increase. Smaller asteroids should be worth more points than larger ones.
  • The player’s score should be displayed in the upper-right corner of the screen.
  • Once all of the asteroids have been destroyed, a new, larger wave of asteroids should be created.

 

I decide to leave out a few features of the original to keep the game simple.

 

Game Classes

 

Next, I make a list of the classes that I think I need:

 

  • Ship
  • Missile
  • Asteroid
  • Explosion

 

I know a few things about these classes already. Ship, Missile, and Asteroid will be derived from game.Sprite, while Explosion will be derived from games.Animation.

 

Game Assets

 

Since the game includes sound, music, sprites, and animation I know I need to create some multimedia files. Here’s a list I came up with:

 

  • An image file for the ship
  • An image file for the missiles
  • Three image files, one for each size of asteroid
  • A series of image files for an explosion
  • A sound file for the thrusting of the ship
  • A sound file for the firing of a missile
  • A sound file for the explosion of an object
  • A music file for the theme

 

Creating Asteroids

 

Since the game involves deadly asteroids, I thought I’d start with them. Although this seems like the best first step to me, it may not to another programmer—and that’s fine. You could certainly start with a different first step, such as getting the player’s ship on the screen. There’s no one right first step. The important thing to do is to define and complete “bite-sized” programs that build on each other, working your way toward the completed project.

 

 

 

The Astrocrash01 Program

 

First off, here’s the code for the program:

 

 

The Astrocrash01 program creates a graphics window, sets the nebula background, and spawns eight randomly located asteroids. The velocity of each asteroid is randomly calculated, but smaller asteroids have the potential to move faster than larger ones. The picture below shows the program in action

 

 

Setting Up the Program

 

The program starts like most others:

 

# Astrocrash01

# Get asteroids moving on the screen

# A202/A598 Labs 09/08-15/2006

 

import random

from livewires import games

 

# global constants

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

THE_SCREEN = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

The Asteroid Class

 

The Asteroid class is used for creating is used for creating moving asteroids. The first thing I do in the class is load three images—one for each size of asteroid—and asign them to class variables:

 

class Asteroid(games.Sprite):

    """ An asteroid which floats across the screen. """

    image_big = games.load_image("asteroid_big.bmp")

    image_med = games.load_image("asteroid_med.bmp")

    image_small = games.load_image("asteroid_small.bmp")

 

Next, I tackle the constructor method:

 

    def __init__(self, screen, x, y, size):

        """ Intitialize asteroid sprite. """     

        if size == 1:

            image = Asteroid.image_small

        elif size == 2:

            image = Asteroid.image_med

        elif size == 3:

            image = Asteroid.image_big

        else:

            print "Asteroid size must be 1, 2, or 3."

            sys.exit()

 

        # set velocity based on asteroid size

        dx = random.choice([2, -2]) * random.random() / size

        dy = random.choice([2, -2]) * random.random() / size

 

        self.init_sprite(screen = screen, x = x, y = y,

                         dx = dx, dy = dy, image = image)

        self.size = size

 

The method’s screen, x and y parameter values determine where the new asteroid will start life. The value of the parameter size represents the size of the asteroid and can be 1 for small, 2 for medium, or 3 for large. Based on size, the appropriate image is used for the sprite. If  size isn’t passed either a 1, 2, or 3, the program displays an error message and exits.

 

Next, the constructor generates random values for the new object’s velocity components based partly on its size attribute. Smaller asteroids have the potential to move faster than larger ones. Finally, the constructor initializes the sprite and sets the object’s size attribute.

 

The moved() method keeps an asteroid in play by wrapping it around the screen:

 

    def moved(self):

        """ Wrap the asteroid around screen. """   

        if self.get_top() > SCREEN_HEIGHT:

            self.set_bottom(0)

 

        if self.get_bottom() < 0:

            self.set_top(SCREEN_HEIGHT)

 

        if self.get_left() > SCREEN_WIDTH:

            self.set_right(0)

 

        if self.get_right() < 0:

            self.set_left(SCREEN_WIDTH)

 

The Main Section

 

Finally, the main section of code sets the nebula background and creates eight randomly sized asteroids at random screen locations:

 

# main

my_screen = THE_SCREEN

nebula_image = games.load_image("nebula.jpg")

my_screen.set_background(nebula_image)

 

# create 8 asteroids

for i in range(8):

    x = random.randrange(SCREEN_WIDTH)

    y = random.randrange(SCREEN_HEIGHT)

    size = random.randrange (1, 4)

    Asteroid(screen = my_screen, x = x, y = y, size = size)

   

my_screen.mainloop ()

 

Rotating the Ship

 

For my next task, I introduce the player’s ship. My modest goal is to allow a user to rotate the ship with the arrow keys. I plan to attack the other ship functions later.

 

The Astrocrash02 Program

 

Here’s the code for this program:

 

 

The Astrocrash02 program extends  Astrocrash01. In the new version, I create a ship at the center of the screen that the user can rotate. If the user presses the Right Arrow key, the ship rotates clockwise. If the user presses the Left Arrow key, the ship rotates counterclockwise. Here’s the program in action:

 

 

The player’s ship is now part of the action.

 

The Ship Class

 

The main thing I have to do is to write a Ship class for the player’s ship:

 

 

class Ship(games.Sprite):

    """ The player's ship. """

    image = games.load_image("ship.bmp")

    ROTATION_STEP = 3     

 

    def __init__(self, screen, x, y):

        """ Initialize ship sprite. """

        self.init_sprite(screen = screen, x = x, y = y, image = Ship.image)

 

    def moved(self):

        """ Rotate the ship based on key presses. """

        # rotate based on left and right arrow keys

        if self.screen.is_pressed(games.K_LEFT):

            self.rotate_by(-Ship.ROTATION_STEP)

     

        if self.screen.is_pressed(games.K_RIGHT):

            self.rotate_by(Ship.ROTATION_STEP)

 

This class is taken almost directly from the Rotate Sprite program. In fact, there are only two, small differences worth noting. First, I load the image of the ship and assign the resulting image object to the class variable image. Second, I use the class constant ROTATION_STEP for the number of degrees by which the ship rotates when the user presses the Left or Right Arrow keys.  

 

Instantiating a Ship Object

 

The last thing I do is instantiate a Ship object. I create a new ship in the middle of the screen in the main part of the program:

 

# main

my_screen = THE_SCREEN

nebula_image = games.load_image("nebula.jpg")

my_screen.set_background(nebula_image)

 

Moving the Ship

 

In the next version of the program I get the ship moving. The player can press the Up Arrow key to engage the ships engines. This applies thrust to the ship in the direction the ship is facing. Since there’s no friction in this simple game, the ship keeps moving based on all of the thrust the player applies to it.

 

The Astrocrash03 Program

 

The ship can now move around the screen.

 

 

Here’s the code for the program:

 

 

When the player engages the the ship’s engines, the Astrocrash03 program changes the velocity of the ship based on its angle (and produces an appropriate sound effect too). The picture above illustrates the functionality of the program.

 

Importing the math Module

 

The first thing I do is import a new module at the top of the program:

 

import math

 

The math module contains a bunch of mathematical functions and constants. But don’t let that scare you. I use only a few in this program.

 

Adding Ship Class Variables

 

I create a class constant, VELOCITY_STEP, for altering the ship’s velocity:

 

    VELOCITY_STEP = .03

 

A higher number would make the ship accelerate faster, a lower number would make the ship accelerate more slowly.

 

I also add a new class variable, sound, for the thrusting sound of the ship:

 

    sound = games.load_sound("thrust.wav")

 

Updating Ship’s moved() Method

 

Next, I add code to the end Ship’s moved() method to get the ship moving. I check to see if the player is pressing the Up Arrow key. If so, I play the thrusting sound:

 

        # apply thrust based on up arrow key

        if self.screen.is_pressed(games.K_UP):

            Ship.sound.play()

 

Now, when the player presses the Up Arrow key, I need to alter the ship’s velocity components (the Ship object’s dx and dy attributes) based on the angle of the ship. For example, if the ship’s angle is 0 degrees (it’s facing straight up), then I need to decrease the object dy’s attribute. Conversely, if the ship’s angle is 90 degrees (it’s facing to the right), then I need to increase the object’s dx attribute. And if the ship is at 45 degrees (it’s facing diagonally up and to the right), then I need to decrease the object’s dy attribute and increase its dx attribute equally. Of course, every angle requires its own adjustments. So, how can I figure out how much to change each velocity component based on the angle of the ship? Well, the answer is trigonometry. Wait, don’t slam this book shut and run as fast as your legs can carry you, screaming incoherently. As promised, I use only two mathematical functions in a few lines of code to figure this out.

 

To start the process, I get the angle of the ship, converted to radians:

 

 

            # get velocity component changes based on ship's angle

            angle = self.get_angle() * math.pi / 180  # convert to radians

 

A radian is just a measure of rotation, like a degree. Python’s math module expects angles in radians  (while livewires works with degrees) so that’s why I need to make the conversion. In the calculation, I use the math module constant pi, which represents the number pi.

 

Now that I’ve got the ship’s angle in radians, I can figure out how much to change each velocity component using the math module’s sin() and cos() functions, which calculate an angle’s sine and cosine. The following lines calculate how much the object’s dx and dy attribute values should change based on the ship’s angle and VELOCITY_STEP:

 

            add_dx = Ship.VELOCITY_STEP * math.sin(angle)

            add_dy = -Ship.VELOCITY_STEP * math.cos(angle)

         

Next, I calculate the object’s new dx and dy values using add_dx and add_dy:

 

            # add current velocity and velocity change to get new velocity

            dx, dy = self.get_velocity()

            new_dx = dx + add_dx

            new_dy = dy + add_dy

 

Then, I set the object’s velocity with these new values:

 

            # set new velocity

            self.set_velocity(new_dx, new_dy)

 

All that’s left to do is handle the screen boundaries. I use the same strategy as I did with the asteroids: the ship should wrap around the screen. In fact, I copy and paste the code from Asteroid’s moved() method to the end of Ship’s moved() method:

 

        # wrap the ship around screen   

        if self.get_top() > SCREEN_HEIGHT:

            self.set_bottom(0)

 

        if self.get_bottom() < 0:

            self.set_top(SCREEN_HEIGHT)

 

        if self.get_left() > SCREEN_WIDTH:

            self.set_right(0)

 

        if self.get_right() < 0:

            self.set_left(SCREEN_WIDTH)

 

Although this works, copying and pasting large portions of code is usually a sign of poor design. I’ll revisit this code later and find a more elegant solution. Repeated chunks of code bloat programs and make them harder to maintain. When you see repeated code, it’s often time for a new function or class. Think about how you might consolidate the code into one place and call or invoke it from the parts of your program where the repeated code currently lives.

 

Firing Missiles

 

Next, I enable the ship to fire missiles. When the player presses the spacebar, a missile fires from the ship’s cannon and flies off in the direction the ship faces. The missile should destroy anything it hits, but to keep things simple, I save the fun of destruction for another version of the program.

 

The Astrocrash04 Program

 

 

The Astrocrash04 program allows the player to fire missiles by pressing the spacebar. But there’s a problem. If the player holds down the spacebar, a stream of missiles pours out of the ship, at a rate of about 50 per second. I need to limit the missile fire rate, but I leave that issue for the nect version of the game. The picture below shows off the Astrocrash04 program, warts and all:

 

 

Updating Ship’s moved() Method

 

I update Ship’s moved()  method by adding code so that a ship can fire missiles. If the player presses the spacebar, I create a new missile:

 

        # fire missile if spacebar pressed and enough time has elapsed    

        if self.screen.is_pressed(games.K_SPACE):

            Missile(self.screen,

                    self.get_xpos(),

                    self.get_ypos(),

                    self.get_angle())

 

Of course, in order to instantiate a new object from the line Missile(self.screen, self.get_xpos(), self.get_ypos(), self.get_angle()), I need to write a little something... like a Missile class.

 

The Missile Class

 

I write the Missile class for the missiles that the ship fires. I start by creating class variables and class constants:

 

class Missile(games.Sprite):

    """ A missile launched by the player's ship. """

    image = games.load_image("missile.bmp")

    sound = games.load_sound("missile.wav")

    BUFFER = 40

    VELOCITY_FACTOR = 7

    LIFETIME = 40

 

image is for the image of a missile—a solid, red circle. sound is for the sound effect of a missile launching. BUFFER represents the distance from the ship that a new missile is created (so that the missile isn’t created on top of the ship). VELOCITY_FACTOR affects how fast the missle travels. And LIFETIME represents how long the missile exists before it disappears (so that a missile won’t float around the screen forever).

 

I start the class constructor with the following lines:

 

    def __init__(self, screen, ship_x, ship_y, ship_angle):

        """ Initialize missile sprite. """

 

It may surprise you that the constructor for a missile requires values for the ship’s x- and y-coordinates and the ship’s angle, which are accepted into the ship_x, ship_y, and ship_angle parameters. The method needs these values so that it can determine two things: exactly where the missile first appears and the velocity components of the missile. Where the missile is created depends upon where the ship is located. And how the missile travels depends upon the angle of the ship.

 

Next, I play the missile-firing effect:

 

        Missile.sound.play()

 

Then, I perform some calculations to figure out the new missile’s location:

 

        # convert to radians

        angle = ship_angle * math.pi / 180 

 

        # calculate missile's starting position

        buffer_x = Missile.BUFFER * math.sin(angle)

        buffer_y = -Missile.BUFFER * math.cos(angle)

        x = ship_x + buffer_x

        y = ship_y + buffer_y

 

I get the angle of the ship, converted to radians. Then, I calculate the missile’s starting x- and y-coordinates, based on the angle of the ship and the Missile class constant BUFFER. The resulting x and y values place the missile right in front of the ship’s cannon.

 

Next, I calculate the missile’s velocity components. I use the same type of calculations as I did in the Ship class:

 

        # calculate missile's velocity components

        dx = Missile.VELOCITY_FACTOR * math.sin(angle)

        dy = -Missile.VELOCITY_FACTOR * math.cos(angle)

 

Finally, I initialize the new sprite. I also make sure to give the Missile object a lifetime attribute so that the object won’t be around forever.

 

        # create the missile

        self.init_sprite(screen = screen, x = x, y = y,

                         dx = dx, dy = dy, image = Missile.image)

        self.lifetime = Missile.LIFETIME

 

Then, I write a moved() method for the class. Here’s the first part:

 

    def moved(self):

        """ Move the missile. """

        # if lifetime is up, destroy the missile

        self.lifetime -= 1

        if not self.lifetime:

            self.destroy()

 

This code just counts down the life of the missile. lifetime is decremented. When it reaches 0, the Missile object destroys itself. In the second part of moved(), I include the familiar code to wrap the missile around the screen:

 

        # wrap the missile around screen   

        if self.get_top() > SCREEN_HEIGHT:

            self.set_bottom(0)

 

        if self.get_bottom() < 0:

            self.set_top(SCREEN_HEIGHT)

 

        if self.get_left() > SCREEN_WIDTH:

            self.set_right(0)

 

        if self.get_right() < 0:

            self.set_left(SCREEN_WIDTH)

 

I see that the preceding code is repeated three different times in my program. I’ll definitely be consolidating it later.

 

Controlling the Missile Fire Rate

 

As you saw in the last program, the ship can fire about 50 missiles per second. Even for a player who wants to win, this is a bit much. So, I go about putting a limit on the missile fire rate.

 

The Astrocrash05 Program

 

The Astrocrash05 program limits the missile fire rate by creating a countdown that forces a delay between missile firings. Once the countdown ends, the player can fire another missile (but not until then).  Here’s a picture of this program in action:

 

 

Now the ship fires missiles at a more reasonable rate.

 

Here’s the code:

 

Adding a Ship Class Constant

 

My first step in forcing a delay between missile firings is to add a class constant to Ship:

 

    MISSILE_DELAY = 25

 

MISSILE_DELAY represents the total delay the player must wait between missile firings.

 

Updating Ship’s Constructor Method

 

I add a line to the Ship class constructor so that a newly created ship object gets a new attribute named missile_wait:

 

        self.missile_wait = 0

 

I use missile_wait to count down the delay until the player can fire the next missile. When missile_wait is 0, the player can fire a new missile at will. If missile_wait is greater than 0, it means that the player has fired a missile recently and I’m still counting down before he or she can fire another. After the player fires a missile, I reset missile_wait to MISSILE_DELAY and the countdown begins again.

 

Updating Ship’s moved() Method

 

I add some code to Ship’s moved() method that decrements an object’s missile_wait attribute (if it’s not already 0):

 

        # if waiting until the ship can fire next, decrease wait

        if self.missile_wait:

            self.missile_wait -= 1

 

Then I change the missile firing code from the last version of the game to the following lines:

 

        # fire missile if spacebar pressed and enough time has elapsed    

        if self.screen.is_pressed(games.K_SPACE) and not self.missile_wait:

            Missile(self.screen,

                    self.get_xpos(),

                    self.get_ypos(),

                    self.get_angle())

            self.missile_wait = Ship.MISSILE_DELAY

 

Now, when the player presses the spacebar, the countdown must be complete before the ship will fire a new missile. And once a new missile is fired, I reset missile_wait to MISSILE_DELAY to begin the countdown again.

 

Handling Collisions

 

So far, the player can move the ship around the field of asteroids and even fire missiles, but none of the objects interact. I change all of that in the next version of the game. When a missile collides with any other object, it destroys that other object and itself. Asteroids will be passive in this system, since I don’t want overlapping asteroids to destroy each other.

 

The Astrocrash06 Program

 

The Astrocrash06 program achieves all of this collision detection with the Sprite overlapping_objects() method. Also, I have to handle the destruction of asteroids in a special way. Remember that when a large asteroid is destroyed, two medium-sized asteroids are created. When a medium-sized asteroid is destroyed, two small asteroids are created. When a small asteroid is destroyed, nothing is created. Because all of the asteroids are generated at random locations, it’s possible for one to be created on top of the player’s ship, destroying the ship just as the program begins. I can live with this inconvenience for now, but I’ll have to solve this issue in the final game. Here’s the program in action (ship was destroyed):

 

 

The ship’s missiles now destroy asteroids. But be careful, as asteroids destroy the ship.

 

Here’s the code:

 

 

Updating Missile’s moved() Method

 

I add the following code to the end of Missile’s moved() method:

 

        # check if missile overlaps any other object

        for game_object in self.overlapping_objects():

            game_object.die()

            self.die()

 

If a missile overlaps any other object, the other object and the missile are both destroyed.

 

Adding Missile’s die() Method

 

Missile, like any class in this version of the game, needs a die() method. The method is about as simple as it gets:

 

    def die(self):

        """ Destroy the missile. """

        self.destroy()

 

When a Missile object’s die() method is invoked, the object destroys itself.

 

Updating Ship’s moved() Method

 

I add the following code to the end of the Ship’s moved() method:

 

        # check if ship overlaps any other object

        for game_object in self.overlapping_objects():

            game_object.die()

            self.die()

 

If the ship overlaps any other object, the other object and the ship are both destroyed. Notice that this method is exactly the same as Missile’s moved() method. Again, when you see duplicate code, you should think about how to consolidate it. In the next version of the game, I’ll get rid of this and other redundant code.

 

Adding Ship’s die() Method

 

This method is the same as Missile’s die() method:

 

    def die(self):

        """ End the game. """

        self.destroy()

 

When a Ship object’s die() method is invoked, the object destroys itself.

 

Adding Asteroid’s die() Method

 

Asteroid’s die() method is more involved:

 

    def die(self):

        """ Destroy asteroid. """

        # if asteroid isn't small, replace with two smaller asteroids

        new_size = self.size - 1

        if new_size:

            for i in range(2):

                Asteroid(screen = self.screen,

                         x = self.get_xpos(), y = self.get_ypos(),

                         size = new_size)

        self.destroy()

 

The wrinkle I add is that the method has the potential to create two new Asteroid objects. The method calculates the value of new_size, the size of the potential new asteroids. If new_size is either 1 or 2, then two new asteroids are created at the current asteroid’s location. Whether or not new asteroids are created, the current asteroid destroys itself and the method ends.

 

Adding Explosions

 

In the last version of the game

 

The Astrocrash07 Program

 

In the Astrocrash07 program, I write a new class for animated explosions based on games.Animation. But I also do some work behind the scenes, consolidating redundant code. Even though the player won’t appreciate these additional changes, they’re important nonetheless. Here’s the program in action; all oif the destruction in the game is now accompanied by fiery explosions:

 

 

Here’s the code for this (next to last) program:

 

 

The Wrapper Class

 

I start with the behind-the-scenes work. I create a new class, Wrapper, based on games.Sprite. Wrapper has a moved() method that automatically wraps an object around the screen:

 

class Wrapper(games.Sprite):

    """ A sprite that wraps around the screen. """

    def moved(self):

        """ Wrap sprite around screen. """   

        if self.get_top() > SCREEN_HEIGHT:

            self.set_bottom(0)

 

        if self.get_bottom() < 0:

            self.set_top(SCREEN_HEIGHT)

 

        if self.get_left() > SCREEN_WIDTH:

            self.set_right(0)

 

        if self.get_right() < 0:

            self.set_left(SCREEN_WIDTH)

 

You’ve seen this code several times already. It wraps a sprite around the screen. Now, if I base the other classes in the game on Wrapper, its moved() method will keep instances of those other classes on the screen—and the code only has to exist in one place.

 

I finish the class up with a die() method that destroys the object:

 

    def die(self):

        """ Destroy self. """

        self.destroy()

 

The Collider Class

 

I take on more redundant code. I notice that both Ship and Missile share the same collision handling instructions. So, I create a new class, Collider, based on the Wrapper, with a moved() method that handles collisions:

 

class Collider(Wrapper):

    """ A Wrapper that can collide with any other Wrapper. """

    def moved(self):

        """ Destroy self and overlapping object if object is Wrapper. """

        Wrapper.moved(self)

        for game_object in self.overlapping_objects():

            if isinstance(game_object, Wrapper):

                game_object.die()

                self.die()               

 

The first thing I do in Collider’s moved() method is invoke Wrapper’s moved() method  to keep the object on the screen. But notice that I invoke Wrapper’s moved() method directly, with Wrapper.moved(self) and not with the super() function. I do this because the livewires classes are old-style, so I can’t use super().

 

I also make an additon to the collision detection loop. For each overlapping object, I first check to see if the overlapping object is a Wrapper object. I do this because some objects on the screen won’t be Wrapper objects and I want to ignore them, as far as collisions go. For example, the player’s score shouldn’t count when it comes to collisions. If a missile hits the player’s score, the missile should ignore the score (as ooposed to causing the score to explode and disappear). And remember that all Collider objects are also Wrapper objects, since Collider is based on Wrapper.

 

I next write a die() method for the class, since all Collider objects will do the same thing when they die—create an explosion and destroy themselves:

 

    def die(self):

        """ Destroy self and leave explosion behind. """

        Explosion(self.screen, self.get_xpos(), self.get_ypos())

        self.destroy()

 

In this method, I create an Explosion object. Explosion is a new class whose objects are explosion animations. You’ll see the class in its full glory soon.

 

Updating the Asteroid Class

 

In updating the Asteroid class, I change its class header so that the class is based on Wrapper:

 

class Asteroid(Wrapper):

 

Asteroid nopw inherits moved() from Wrapper, so I cut Asteroid’s own moved() method. The redundant code is starting to disappear!

 

The only other things I do in this classis change the last line of Asteroid’s die() method. I replace the current self.die() with the line

 

        Wrapper.die(self)

 

Now, if I ever change Wrapper’s die() method, Asteroid will automatically reap the benefits.

 

Updating the Ship Class

 

In updating the Ship class, I change its class header so that the class is based on Collider:

 

class Ship(Collider):

 

At the end of Ship’s moved() method, I add the line:

 

        Collider.moved(self)

 

I can now cut several more pieces of redundant code. Since Collider’s moved() method handles collisions, I cut the collision detection code from Ship’s moved() method. Since Collider’s moved() method invokes Wrapper’s moved() method, I cut the screen wrapping code from Ship’s moved() method too. I also cut Ship’s die() method and let the class inherit Collider’s version.

 

Updating the Missile Class

 

In updating the Missile class, I change its class header so that the class is based on Collider:

 

class Missile(Collider):

 

At the end of the Missile’s moved() method, I add the line:

 

        Collider.moved(self)

 

Just like with the Ship class, I can now cut redundant code from Missile. Since Collider’s moved() method handles collisions, I cut the collision detection code from Missile’s moved() method. Since Collider’s moved() method invokes Wrapper’s moved() method. I cut the screen wrapping code from Missile’s moved() method too. I also cut Missile’s die() method and let the class inherit Collider’s version. To help you understand the code changes I describe, feel free to check out the complete code that we posted above, since we won’t reproduce it here any more.

 

The Explosion Class

 

Since I want to create animated explosions, I write an Explosion class based on games.animation. I define the class variable sound, for the sound effect of an explosion. Next, I define a class variable, explosion_images, for a list of image objects of the nine frames of the explosion animation you saw earlier. I load the images from the nine files, explosion1.bmp through explosion9.bmp, using a loop:

 

class Explosion(games.Animation):

    """ Explosion animation. """

    sound = games.load_sound("explosion.wav")

    images = []

    for i in range(1, 10):

        file_name = "explosion" + str(i) + ".bmp"

        image = games.load_image(file_name)

        images.append(image)

 

In the Explosion constructor, I accept values into the screen, x, and y parameters, which represent the screen and coordinates for the explosion. I invoke the games.Animation constructor to create a new animation, and then play the explosion sound effect.

 

    def __init__(self, screen, x, y):

        games.Animation.__init__(self, screen = screen, x = x, y = y,

                                 images = Explosion.images,

                                 n_repeats = 1, repeat_interval = 4)

        Explosion.sound.play()

 

When I invoke the games.Animation constructor, I pass screen, x, and y to their corresponding parameters. I pass to images the list of image objects, Explosion.images. Next, I pass to n_repeats the value of 1 so that the animation plays just once. Finally, I pass to repeat_interval the value of 4 so that the speed of the animation looks right. Remember, you can pass to the games.Animation constructor either a list of file names or a list of image objects for the frames of animation.

 

Adding Levels, Scorekeeping, and Theme Music

 

The game needs just a few more things to feel complete. For my final pass, I add levels—meaning that when a player destroys all of the asteroids on the screen, a  new, more plentiful batch appears. I also add scorekeeping functionality and tense theme music to round out the game experience.

 

The Astrocrash08 Program

 

In addition to levels, scorekeeping, and theme music, I add some code thast may be less obvious to the player, but is still important to complete the program. The picture below shows off my final version of the game; the final touches let the game continue as long as the player’s Astrocrash skills allow:

 

 

Here’s the code:

 

 

 

Adding an Asteroid Class Variable

 

I make a few changes in the Asteroid class, all related to adding levels to the game. In order to change levels, the program needs to know when all of the asteroids on the current level are destroyed. So, I keep track of the total number of asteroids with a new class variable, total, which I define at the beginning of the class:

 

    total = 0

 

Updating Asteroid’s Constructor Method

 

In the constructor, I add a line to increment Asteroid.total:

 

        Asteroid.total += 1

 

Updating Asteroid’s die() Method

 

I make a few additions to Asteroid’s die() method. First, I decrement Asteroid.total:

 

        Asteroid.total -= 1

 

Next, I invoke Game’s static method increase_score(), which increases the player’s score based on the size of the asteroid (smaller asteroids are worth more than larger ones):

 

        Game.increase_score(30 / self.size)

 

Game is a new class that contains a few class variables and static methods for gamewide information and functionality. I explain this new class later in the chapter.

 

Toward the end of Asteroid’s die() method, I test Asteroid.total to see if all the asteroids have been destroyed. If so, I invoke Game’s static method next_level(), which creates a new group of asteroids.

 

        # if all asteroids are gone, create next level

        if not Asteroid.total:

            Game.next_level(self.screen)

 

Adding a Ship Class Variable

 

I make several additions to the Ship class. I create a class constant, VELOCITY_MAX, which I use to limit the maximum velocity of the player’s ship:

 

    VELOCITY_MAX = 3

 

Updating Ship’s moved() Method

 

In Ship’s moved() method, I cap the individual velocity components of a Ship object, dx and dy, using the class c0nstant MAX_VELOCITY:

 

            # cap velocity in each direction

            if new_dx < -Ship.VELOCITY_MAX:

                new_dx = -Ship.VELOCITY_MAX

            if new_dx > Ship.VELOCITY_MAX:

                new_dx = Ship.VELOCITY_MAX

            if new_dy < -Ship.VELOCITY_MAX:

                new_dy = -Ship.VELOCITY_MAX

            if new_dy > Ship.VELOCITY_MAX:

                new_dy = Ship.VELOCITY_MAX

 

I cap the ship’s speed to avoid several potential problems, including the ship running into its own modules.

 

Adding Ship’s die() Method

 

When the player’s ship is destroyed the game is over. I add a die() method to Ship that invokes Collider’s die() method and the new Ship game_over() method:

 

    def die(self):

        """ Destroy ship and end the game. """

        self.game_over()

        Collider.die(self)

 

Adding Ship’s game_over() Method

 

I add a new game_over() method, which displays the message “Game Over” in the middle of the screen in big, red letters for about five seconds. After that, the game ends and the graphics screen closes.

 

    def game_over(self):

        """ End the game. """

        # show 'Game Over' for 250 mainloop cycles

        # (at 50 fps that's 5 seconds)

        games.Message(screen = self.screen,

                      x = SCREEN_WIDTH/2, y = SCREEN_HEIGHT/2,

                      text = "Game Over", size = 90, color = color.red,

                      lifetime = 250, after_death = self.screen.quit)

 

The Game Class

 

The Game class is a new class that handles certain game-wide functionality such as creating the player’s ship, creating the object for the player’s score, increasing the player’s score, and creating new levels of asteroids.

 

The first thing I do in the class is define two class variables:

 

class Game(object):

    """ The game itself. """

    level = 1

    sound = games.load_sound("level.wav")

 

level is the current game level. sound is for the sound effect that indicates the player has reached a new level.

 

Next, I write the static method create_ship(), which creates a Ship object on the screen passed to screen at the coordinates passed to x and y:

 

    def create_ship(screen, x, y):

        Game.ship = Ship(screen = screen, x = x, y = y)

 

    create_ship = staticmethod(create_ship)

 

Next, I define the static method create_score(), which creates two Game class variables, score_value and score_text. score_value represents the value of the player’s score. score_text is a Text object that is displayed on the graphics screen to show the player’s score.

 

    def create_score(screen):

        Game.score_value = 0

        Game.score_text = games.Text(screen = screen, x = 550, y = 20,

                                text = "Score: " + str(Game.score_value),

                                size = 25, color = color.white)

       

    create_score = staticmethod(create_score)

 

Hopefully, the player’s score won’t remain zero for too long. So, I define another static method, increase_score(), which updates the player’s score:

 

I add to Game.score_value the number of points passed to the method. Then, I update Game.score_text so that the new score is displayed.

 

    create_score = staticmethod(create_score)

 

    def increase_score(points):

        """ Increase a player's score. """

        Game.score_value += points

        Game.score_text.set_text("Score: " + str(Game.score_value))

 

    increase_score = staticmethod(increase_score)

 

The last method I define in the class is the static method next_level(), which creates the next level full of asteroids. To begin the method, I define a constant, BUFFER, which represents the amount of space I need to preserve around the player’s ship when I create new asteroids. By setting the constant to 200, I’m saying that any new asteroid must be created at least 200 pixels away from the player’s ship. I do this so that a new asteroid isn’t created right on top of the player’s ship.

 

    def next_level(screen):

        """ Create the next level. """

        # amount of space around ship to preserve when creating asteroids

        BUFFER = 200

 

Next, I play the sound for advancing to the next level. I don’t play the sound for the first level, which is the start of the game:

 

        # play new level sound (except at first level)

        if Game.level > 1:

            Game.sound.play()

 

Next, I create the asteroids for the new level. Each level starts with the number of asteroids equal to the level number. So, the first level starts with only one asteroid, the second with two, and so on.

 

        # create new asteroids

        for i in range(Game.level):

            # pick random x and y at least BUFFER distance

            # from ship's x and y

            while True:

                x = random.randrange(SCREEN_WIDTH)

                y = random.randrange(SCREEN_HEIGHT)

                x_dist = abs(Game.ship.get_xpos() - x)

                y_dist = abs(Game.ship.get_ypos() - y)

                if x_dist + y_dist > BUFFER:

                    break          

 

            # create the asteroid

            Asteroid(screen = screen, x = x, y = y, size = 3)

 

The real work of the method is to generate random x- and y-coordinates that put a new asteroid at least BUFFER pixels from the player’s ship. I create an intentional infinite loop with while True. In the loop, I generate random x- and y-coordinates for the next asteroid. Then, I calculate the distance between the randomly generated x-coordinate and the x-coordinate of the player’s ship by using the built-in abs() method, which returns the absolute value of a number. I do the same for the y-coordinate. Then I add the two distances together. If the result is greater than BUFFER, I’ve found a safe location for a new asteroid and break out of the loop. Otherwise, the loop continues and I try with another randomply generated point on the graphics screen. Once I generate a new point that’s far enough away from the ship, I create a large asteroid at that location.

 

Finally, I display the level number the player has reached and increment the game level:

 

        # display level number

        games.Message(screen = screen,

                      x = SCREEN_WIDTH / 2, y = 50,

                      text = "Level " + str(Game.level),

                      size = 40, color = color.yellow, lifetime = 150)

           

        Game.level += 1

 

    next_level = staticmethod(next_level)

 

The main() Function

 

I replace the main part of the program with a new function, main(). I begin the function by loading the nebula background image onto the graphics screen:

 

# main

def main():

    my_screen = THE_SCREEN

    nebula_image = games.load_image("nebula.jpg")

    my_screen.set_background(nebula_image)

 

Then, I load and loop the game’s theme music:

 

    games.load_music("theme.mid")

    games.play_music(-1)

 

By passing -1 to games.play_music(), I ensure the theme will loop forever, giving the game an never-ending soundtrack.

 

Then, I cut all of the code that created the asteroids and the player’s ship. I replace it with the code that invokes Game’s static methods to create the player’s ship, thge player’s score, and the first level of asteroids:

 

    Game.create_ship(screen = my_screen,

                     x = SCREEN_WIDTH / 2,

                     y = SCREEN_HEIGHT / 2)

    Game.create_score(screen = my_screen)

    Game.next_level(screen = my_screen)

 

Starting the Program

 

Last, but not least, I kick off the whole program by calling main():

 

# start program

main()

 

Summary

 

In this chapter, you extended your knowledge of multimedia programming to include sound, music and animation. You learned how to load, play and stop both sound and music files. And you saw how to create animations. You also learned a technique for creating large programs by writing increasingly more complete, working versions of the final product. You saw how to tackle one new objective at a time, building your way to the full program. Finally, you saw all of this new information and these new techniques put to use in the creation of a fas-paced, action game with sound effects, animation, and its own musical score.

 

Challenges

 

  1. Improve the Astrocrash game by creating a new kind of deadly space debris. Give this new type of debris some quality that differentiates it from the asteroids. For example, maybe the debris requires two missile strikes to be destroyed (and change color after the first strike).
  2. Write a version of the Simon Says game where a player has to repeat an ever-growing, random sequence of colors and sounds, using the keyboard.
  3. Write your own version of another classic video game such as Space Invaders or Pac-Man.
  4. Create your own programming challenge, but most importantly, never stop challenging yourself to learn.