|
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:
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:
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:
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: ")
# 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:
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:
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:
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 |