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