Python Graphics: The Pizza Panic Game

 

The majority of the programs you’ve seen in A201 have focused on presenting text. But today, people expect rich, visual content from their programs, regardless of the application. So in this lab, you will learn how to use graphics with the help of a few multimedia modules designed for writing games in Python. Specifically, you’ll learn to do the following: 

 

  • create a graphics window,
  • display text in a graphics window,
  • create and manipulate sprites,
  • test for collisions between graphics objects,
  • handle mouse input,
  • control a computer opponent.

 

Introducing the Pizza Panic Game

 

The project for this chapter, the Pizza Panic game, involves a crazy chef, a deep-dish pan, and a bunch of flying pizzas. Here’s the scenario: After being pushed over the edge by one-too-many finicky diners, the chef at the local pizza parlor has taken to the rooftop and is madly flinging pizzas to their doom. Of course, the pizzas must be saved. Using the mouse, the player controls a pan that he or she maneuvers to catch the falling pizzas. The player’s score increases with every pizza caught. But once a pie hits the ground, the game is over. I’ve shown this is in class, now let’s just show the chef in action. The player must catch the falling pizzas:

 

 

Once a pizza gets by the player, the game is over.

 

 

Introducing the pygame and livewires Packages

 

pygame and livewires are sets of modules (called packages) that give Python programmers access to a wide range of multimedia classes. With these classes, you can create programs with graphics, sound effects, music and animation. You can even play back MPEG movies. The packages also allow input from a variety of devices, including mice, keyboards, joysticks, and trackballs. pygame is the secret weapon in your media arsenal. Written by Pete Shinners, the package allows you to write impressive, multimedia programs in Python. But because the package is so powerful, it can be a bit overwhelming for the new programmer. livewires was designed specifically to take advantage of the power of pygame, while reducing the complexity for the programmer. Written by a group of educators in the United Kingdom, livewires is a pygame wrapper (code that provides a simpler interface to other programming code). And even though we won’t directly access pygame, it will be there,  working hard behind the scenes. Software wrappers are an important tool in the world of professional programming and can dramatically cut down on the amount of development time for a project. Wrappers exist for different programming areas, including graphics, database management, and networking, to name just a few.

 

You need to install both pygame and livewires before you can run the programs presented in this lab and the next. Python CDs containing python 2.2, pygame 1.5 and the livewires wrapper will be distributed in class on Tue, Sept. 5. The combination on the CDs works just fine. In labs UITS has installed Python 2.4.3 with Pygame 1.7, and we are very very grateful for the patience with which they’ve listened to what we needed and finally managed to have a winning combination of software installed. The livewires modules from your CD is also installed in the UITS STC labs. So, your environment at home might differ from the one in the lab, but they’re equivalent. If you want to recreate the UITS STC lab environment at home, that’s also possible, and you can just follow my installation instructions, posted on the What’s New? Page. Over the next two labs, you’ll gain a good understanding of the livewires package. In addition, its documentation will be posted by Thursday night, so you can refer to it in lab this week. If you want to learn more about pygame, visit its web site at http://www.pygame.org. Although you’re welcome to visit the website of the LiveWires organization (their website is at http://www.livewires.org.uk) be aware that the livewires package used in this class is a modified version of the package that LiveWires created.

 

Creating a Graphics Window

 

Before you can display any graphics, you have to first create a graphics window. Once you’ve created the window, you have your blank canvas on which to display text and images.

 

Introducing the New Graphics Window Program

 

Creating a graphics window with the livewires package is a snap. The New Graphics Window program creates an empty graphics window in just a few lines of code.  The results of the program are shown below; my first graphics windows may not be much but it’s mine:

 

 

Here’s the code that produces this window:

 

 

As always, you can just double-click the Python icon for this game to get it started.

 

Importing the games module

 

The livewires package contains several important modules, including games, which contains a group of key classes for game programmimg. You can import a specific module of a package by using the from statement. To import a module, use from, followed by a package name, followed by import, followed by a module name (or a list of module names separated by commas). The first thing I do in the program is import the games module of the livewires package:

 

# New Graphics Window

# Demonstrates creating a graphics window

# A202/A598 Lab 09/08/2006

 

from livewires import games

 

As a result of this code, I can use games like any other module I import. To get an overview of what the games module is all about, let me list below the most commonly used games classes:

 

Commonly Used games Module Classes

Class

Description of Class Object

Screen

Provides a region on which graphics objects may exist, move, and interact.

Text

A graphics object for text displayed on a Screen object.

Message

A graphics object that is a special kind of Text object that disappears after a set period of time.

 

Sprite

A graphics object for images that can be displayed on a Screen object.

 

Defining Global Constants

 

Next, I define two global constants:

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

These represent the width and height of the new graphics window in pixels (a single point in a graphics area).

 

Creating a Screen Oject

 

Next, I start a main section and create the graphics screen:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

This causes a graphics window, 640 pixels wide by 480 pixels high, to spring into existence and be displayed. The new Screen object is assigned to my_screen. When you create a Screen object, you may pass values of width and height; otherwise, their respective default values of 640 and 480 are used. In this case, I could have written my_screen = games.Screen() and achieved the same results. It’s also important to note that you can have only one active Screen object at a time. If you try to create a second, you’ll raise an exception. Though not an exhaustive list, the table below describes some of the most useful Screen methods. All of these methods are important, but don’t bother trying to memorize them. The table is just meant to give you an overview of what you can do with a Screen object.

 

 

Useful Screen Methods

 

Method

Description

set_background(image)

Sets the background

mouse_pos()

Returns the position of the mouse pointer on a Screen object.

mouse_visible(on)

Sets the mouse pointer to visible or invisible; on can be True  or False.

mainloop([fps])

Starts a loop that draws all of the graphics objects associated with the Screen object. Takes an optional argument, fps, the number of frames per second to update the Screen 0bject. The default value is 50.

all_objects()

 

Resturns a list of all objects associated with the Screen object. 

clear()

Destroys all objects associated with the Screen object.

quit()

Stops mainloop() and destroys the Screen object and all objects associated with it.

 

Invoking a Screen Object’s mainloop() Method

 

The final line in my first program is

 

my_screen.mainloop()

 

This kicks off the Screen object’s event loop, which updates the graphics window, redrawing every graphics object 50 times per second. Just as with a program that uses Tkinter to create a new window, you shouldn’t run a livewires program from IDLE. Instead, run the program directly, by double-clicking the program’s icon in Windows, for example.

 

Setting a Background Image

 

A blank screen is well and good, if your goal is to create the world’s most boring program.  Fortunately, the Screen class has a method to set a background image.

 

Introducing the Background Image Program

 

The Background Image program is just a modification of the New Graphics Window program. I add only two lines of code to create a graphics window with a background image. By taking advantage of Screen’s background-setting method the program produces the window shown below:

 

 

 

By using the set_background() method, a background image can be applied to a Screen object. Here’s the program that produces this output:

 

To create the Background Image program (listed above) I add two lines to the previous program, the New Graphics Window program, just before invoking mainloop().

 

Loading an Image

 

Before you can do anything with an image, like set it as a background of a graphics screen, you have to load the image into memory to create an image object. I load an image by adding the following line right after I create the graphics screen:

 

wall_image = games.load_image("wall.jpg", transparent = False)

 

This calls the the games load_image() function, loads the image stored in the file wall.jpg into memory, and assigns the resulting image object to wall_image.  Make sure that any file you want your Python program to access is associated with the correct path information. The simplest file management solution, and the one we use here, is to store image files in the same folder with the program that loads them. If you follow this method, you won’t need to worry about path information at all.  The load_image() function takes two arguments: a string for the file name of the image and True or False for transparent. I’ll go over what transparent means a bit later in this chapter. For now, just remember this rule: Always load a background image with transparent = False.  You’ll notice that I load a JPEG image for the background in this program. However, you’re not restricted to JPEGs when using the load_image() function. It works just as well with many other image file types, including: BMP, GIF, PNG, PCX, and TGA.

 

Setting the Background

 

In order to set an image object as the background of a Screen object, you need to invoke the Screen object’s set_background() method, so I add the following line right after I load the image:

 

my_screen.set_background(wall_image)

 

This sets the background of my_screen to the image object referenced by wall_image. You can use this method with any Screen object and image object. When the program encounters mainloop(), it keeps the graphics window open, with its new background image, for all to see.

 

Understanding the Graphics Coordinate System

 

So far, I’ve created several graphics screens, each time with a width of 640 and a height of 480, but I haven’t said much about them beyond that. So I want to take a closer look at the screen and its coordinate system. You can think of a graphics screen as a grid, of 640 columns across by 480 rows down. Each intersection of a column and a row is a location on the screen, a single point or pixel. When you talk about a specific point on the screen, you give two coordinates, an x, which represents the column, and a y, which represents the row. You start counting coordinates from the upper-left corner of the screen, so the upper-leftmost point is where the x-coordinate is 0 and the y-coordinate is 0, which you write as the pair (0,0).  As you move to the right, the x values increase. As you move down the screen, the y values increase. That makes the point in the lower-right corner (639, 479). The picture below gives a visual representation of the graphics screen coordinate system:

 

 

You can place graphics objects, like the image of a pizza or the red-colored text “Game Over,” on the screen using the coordinate system. The center of a graphics object is placed at the specified coordinates. You’ll see exactly how this works in the next chapter program.

 

Displaying Text

 

Whether you need to show off the numbers for a sales presentation or the number of aliens obliterated, there are times when you’ll want to display text on a graphics screen. The games module contains a class that allows you to do just that, aptly named Text.

 

Introducing the Big Score Program

 

Displaying text on a graphics window is just a matter of creating an object of the Text class. In the Big Score program, I add some text to the graphics window to display a score in the upper-right corner of the screen, just like in many classic arcade games. The impressively high score shown below is displayed after a Text object is instantiated:

 

 

This Big Score program builds on the Background Image program. I add just one line of code and modify another:

 

Importing the color Module

 

The livewires package contains another module, color, which has a set of constants that represent different colors. These colors can be applied to certain graphics objects, including any Text or Message object. For a complete list of predefined colors, see the livewires documentation to be posted soon. To choose from a group of possible colors, I import the color module by modifying the import line like so:

 

from livewires import games, color

 

Now, both the color and games modules are loaded from the livewires package.

 

Creating a Text Object

 

A Text object represents text on a Screen object. A Text object has attributes for x- and y-coordinates, a font size, a color, and some text too, of course.  In the Big Score program, I use a Text object to represent a game score. Just before I invoke the Screen object’s mainloop() method, I create a Text object through the following code:

 

games.Text( screen = my_screen,

            x = 500,

            y = 30,

            text = "Score: 1756521",

            size = 50,

            color = color.black

          )

 

The constructor method for a Text object requires the following values: a Screen object, x- and y-coordinates, a string, a font size, and a color as shown. I pass my_screen to screen since that’s the active graphics window. Next I pass 500 to x and 30 to y, placing the center of the object at the coordinates (500, 300). This puts the text in the upper-right corner of the graphics window. I pass the string “Score: 1756521” to text so that those characters will be displayed. Then I pass 50 to size so the font is nice and big, to match the score. Finally, I pass color the constant color.black from the color module to make the text, you guessed it, black. You may have noticed that I don’t assign the new Text object to a variable. That’s okay, since the Screen object, my_screen, is designed to keep track of all the graphics objects (just as a root window in a Tkinter program holds references to all of its widgets). Once my_screen’s mainloop() method is invoked, the graphics window is displayed along with the new Text object.

 

Displaying a Message

 

There may be times when you’ll want to display some text on the screen for only a brief period of time. You may want to show a message saying “All records have been updated” or “Attack Wave Seven Complete!” The games class Message is perfect for creating temporary messages just like these.

 

Introducing the You Won Program

 

The You Won program is a modified version of the Big Score program. In fact, I instantiate just one Message object right before invoking the Screen object’s mainloop() method to display the text “You won!” in big, red letters. The message is displayed for about five seconds, and then the program ends.  Here’s the actual program:

 

 

 

Here’s what the program produces (but only for a few seconds, then the program ends):

 

 

Creating a Message Object

 

Messages are created from the games class Message. A message is a special kind of Text object that destroys itself after a period of time. A message can also specify a method or a function to be executed after the object destroys itself. The constructor method for Message takes all the values you saw with Text, but adds two more: lifetime and after_death. lifetime takes an integer value that represents how long the message should be displayed, measured in mainloop() cycles. after_death can be passed a method or function to be executed after the Message object destroys itself. The default value for after_death is None, so a value isn’t required. I create this Message object right before I invoke my_screen’s mainloop() method with the following code:

 

games.Message( screen      = my_screen,

               x           = SCREEN_WIDTH/2,

               y           = SCREEN_HEIGHT/2,

               text        = "You won!",

               size        = 100,

               color       = color.red,

              lifetime    = 250,

               after_death = my_screen.quit

             )

 

This creates the message “You won!” in big, red letters at the center of the screen for about 5 seconds, after which the program ends. This code instantiates a new Message object with a lifetime attribute set to 250. This means that the object will live for about five seconds, because mainloop() runs at 50 frames per second. After the five seconds, my_screen.quit() is called, since that’s what I pass after_death. At that point, the Screen object and all of its associated objects are destroyed and the program ends.

 

Understanding the Games_Object Class

 

The games module has a foundational class called Games_Object upon which all classes that represent graphics objects are based. The classes Text, Message and Sprite are all (directly and indirectly) derived from Games_Object. This means that those three classes all inherit a long list of important methods for manipulating graphics objects and extracting information about an object’s location on a graphics screen. The table below lists some useful Games_Object methods that all three classes inherit:

 

Useful Games_Object Methods

Method

Description

get_pos()

Returns the object’s x-coordinate and y-coordinate.

get_xpos()

Returns the object’s x-coordinate.

get_ypos()

Returns the object’s y-coordinate.

get_left()

Returns the x-coordinate of the object’s left edge.

get_right()

Returns the x-coordinate of the object’s right edge.

get_top()

Returns the y-coordinate of the object’s top edge.

get_bottom()

Returns the y-coordinate of the object’s bottom edge.

get_velocity()

Returns the object’s x and y velocity components as a two-element tuple.

move_to(x,y)

Moves the object to the new coordinates (x, y).

set_left(x)

Moves the object horizontally so that its left edge is at the new coordinate x.

set_right(x)

Moves the object horizontally so that its right edge is at the new coordinate x.

set_top(y)

Moves the object vertically so that its top edge is at the new coordinate y.

set_bottom(y)

Moves the object vertically so that its bottom edge is at the new coordinate y.

set_velocity(dx, dy)

Sets the object’s x velocity to dx and its y velocity to dy.

overlapping_objects()

Returns a list of objects that overlap the object.

destroy()

Removes all of the associated Screen object’s references to an object.

 

In the course of writing a game, you will never directly instantiate an object of Games_Object. The class is meant only as a base for other classes. This type of class is called an abstract class. Again, there’s no need to try to remember all of the methods described in the table above. Just know that they’re available through all Text, Message and Sprite objects.

 

Displaying a Sprite

 

Background images and text can spruce up a plain graphics window. But even a stunning background is still just a static image. A graphics screen with only a background image is like an empty stage. What you need are some actors. Enter the sprite. A sprite is a special, graphics object with a graphics image that that can make programs really come alive. Sprites are used in games, entertainment software, presentations, and all over the Web. In fact, you’ve already seen examples of sprites in the Pizza Panic game. The crazy chef, pan, and pizzas are all sprites. While it would be cool to see a bunch of sprites flying around and crashing into each other, I start with the first step: displaying a single, nonmoving sprite. Sprites aren’t just for games. There are plenty of places in non-entertainment software where they’re used... or misused. In fact, you probably know the most infamous sprite in application software history, Clippy the Office Assistant, the animated paperclip meant to give helpful suggestions in Microsoft Office. However many people found Clippy obtrusive and irritating. One major online publication even ran an article entitled “Kill Clippy!” Well, Microsoft finally saw the light. And starting in Office XP, Clippy is no longer installed by default. A user must request him (and if a user requests him, he or she deserves him). So, while graphics can make a program more interesting, remember: Use your sprite powers for good instead of evil.

 

Introducing the Pizza Sprite Program

 

In the Pizza Sprite program, I create a graphics window and set a background image. This time, I use the background image from the Pizza Panic game. Then I create a new class based on Sprite and instantiate an object of this new class using the image of a pizza. Here’s the program:

 

 

And here’s the result:

 

 

The program really runs exactly as it looks above: a still image of a pizza will be placed in the middle and it will stay there.  Now let’s dissect the program.

 

Setting Up the Program

 

I start the program just as before, by importing the games module and setting global constants for the graphics screen’s height and width:

 

# Pizza Sprite

# Demonstrates creating a sprite

# A202/A598 Lab 09/08/2006

 

from livewires import games

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

Creating the Pizza Class

 

Next, I create a new class, Pizza, based on Sprite:

 

class Pizza(games.Sprite):

    """ A pizza sprite. """

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

        """ Initialize pizza object. """

        self.init_sprite(screen = screen,

                         x      = x,

                         y      = y,

                         image  = image)

 

The only thing I do in the constructor method is invoke the Pizza object’s init_sprite() method to initialize the sprite. You must invoke a sprite object’s init_sprite() method every time you create one. You must supply init_sprite() with the Screen object the sprite will be associated with, x- and y-coordinates, and an image object for the graphics image of the sprite.

 

Setting Up the Screen

 

Next, I set up the graphics screen, just as before:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

wall_image = games.load_image("wall.jpg", transparent = False)

my_screen.set_background(wall_image)

 

First, I create a Screen object. Then I load the image of the brick wall and set it as the background.

 

Loading an Image for a Sprite

 

In order to create a sprite, you first need to load an image into memory to create an image object, like so:

 

pizza_image = games.load_image("pizza.bmp")

 

However, you’ll notice one small difference from the way I load a background image. This time, when loading an image from a sprite, I did not include a value for transparent. The default value is True, so the image is loaded with transparency on. When an image is loaded with transparency on, it’s displayed on a graphics screen so that the background image shows through its transparent parts. This is great for irregular sprites that aren’t perfect rectangles and sprites with “holes” in them, like, say, a Swiss cheese sprite. The parts of an image that are transparent are defined by their color. If an image is loaded with transparency on, then the color of the point at the upper-left corner of the image, its (0, 0) coordinate, is set as its transparent color. What this means is that all parts of the image that are this transparent color will allow the background image of the screen to show through. The picture below shows such a sprite on a solid, white background, ready to take advantage of transparency.

 

 

Note that the sprite has been magnified 4 times (400%). If I load this sprite with transparency on, every part that is pure white (the color taken from the pixel at the sprite’s (0, 0) coordinate) will be transparent when the sprite is displayed on a graphics window. The background image will show through these transparent parts. Here’s a program that uses the image shown above to create two sprites, one transparent and one opaque.

 

 

And here’s the result (magnified and cropped a bit) of running this program:

 

 

 

On the left the image is loaded with transparency on. On the right, the same image is loaded with transparency off. As a general rule, you’ll want to create your sprite image files on a solid color that is not used in any other part of the image. This transparent color must of course appear at the upper-left corner of the image, its (0, 0) coordinate. Then, when the image is loaded with transparency on, it will allow the background to show through in all the right places. Make sure your sprite image doesn’t also contain the color you’re using for transparency. Otherwise, those parts of the sprite will become transparent too, making your sprite look like it has small holes or tears in it as the background image of the graphics window shows through.

 

Creating a Sprite

 

Next, I create a pizza sprite:

 

Pizza( screen = my_screen,

       x = SCREEN_WIDTH/2,

       y = SCREEN_HEIGHT/2,

       image = pizza_image

     )

 

A new instance of the Pizza class is created, with x- and y- coordinates that put the sprite right in the middle of the screen. That’s all it takes.

 

Wrapping Up

 

Finally, I end the program by invoking mainloop():

 

my_screen.mainloop()

 

In addition to keeping the graphics window open, mainloop() now draws the sprite onto the background. You don’t need to be an artist to create graphics for your games. As you see in this chapter, I make up for my utter lack of artistic ability with a modern piece of technology: my digital camera. If you have access to a digital camera, you can create some great images for your projects. In fact that’s how I created all of the graphics for the Pizza Panic game. The brick wall is the back of a friend’s house. For the pizza, I ordered delivery one night. And the chef is my brave, brave friend Dave. While this is a great technique, an important thing to remember is that if you take a picture of a person or object, you don’t necessarily own the image. Obviously some things are trademarked or copyrighted. But using a digital camera is a great way to capture generic images while infusing your programs with a unique, photorealistic style.

 

Moving Sprites

 

Moving images are the essence of most games, and most forms of entertainment for that matter. With sprites, going from stationary to moving is easy. Sprite objects have additional attributes and methods that allow them to move around a graphics screen.

 

Introducing the Falling Pizza Program

 

According to the latest research, pizza doesn’t float, it falls. So I wrote the Falling Pizza program. This new program is a modification of the Pizza Sprite program. In this program, the pizza falls down the screen. All I need to do is change a few lines of code to get the pizza to move. That’s the power of sprites. The picture below illustrates the program. The pizza falls down the screen in the direction of the arrow:

 

 

 

Here’s the code of the program:

 

 

 

Modifying the Pizza Class

 

First, I modify the Pizza class from the Pizza Sprite program:

 

class Pizza(games.Sprite):

    """ A falling pizza. """

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

        """ Initialize pizza object. """

        self.init_sprite( screen = screen,

                          x      = x,

                          y      = y,

                          image  = image,

                          dx     = dx,

                          dy     = dy

                        )

 

The class doesn’t look all that different. When I invoke the init_sprite() method for a newly created Pizza object, I pass optional values to the parameters dx and dy. Every object based on Games_Object has dx and dy attributes that represent the object’s velocity along the x and y axes respectively. (“d,” by the way stands for “delta,” which means a change.) So, dx is the change in the object’s x attribute and dy is the change in the object’s y value each time the Screen object is updated by mainloop(). A positive value for dx moves the object to the right, while a negative value moves it to the left. A positive value for dy moves the object down, while a negative value moves it up.  Back in the Pizza Sprite program, I didn’t pass any values to the init_sprite() method for the dx or dy. Although the sprite in that program did have these dx and dy attributes, they were both assigned their default value of 0 by the init_sprite() method.

 

Passing Values to dx and dy

 

Next, I modify the code that creates a new Pizza object in the main part of the program by providing additional values for dx and dy to the construtor method:

 

Pizza( screen = my_screen,

       x      = SCREEN_WIDTH/2,

       y      = SCREEN_HEIGHT/2,

       image  = pizza_image,

       dx     = 0,

       dy     = 1

     )

 

I want the pizza to fall down the screen so I pass 1 to dy. Since I don’t want any vertical movement, I pass 0 to dx. As a result, every time the graphics window is updated by mainloop(), the Pizza object’s y value is increased by 1, moving it down the screen: it’s falling.  

 

Dealing with Screen Boundaries

 

If you watch the Falling Pizza program run for any length of time, you may notice that once the pizza hits the ground, it keeps going. In fact, it keeps falling, appearing to go below the graphics window and out of sight. Whenever you set a sprite in motion, you need to create a mechanism to deal with the graphics window’s boundaries. That is, you need to tell your class what to do when one of its objects reaches the edge of the screen. You have a few choices. A moving sprite could simply stop when it reaches the edge of the screen. It could die in, say, a fiery explosion. It could bounce, like a giant rubber ball. It could even wrap around the screen so that just as it disappears off one edge, it reappears on the opposite. What seems to make most sense for a falling pizza? Bouncing, of course.

 

The Bouncing Pizza Program

 

When I say that a sprite “bounces” off the edges of the graphics window, I mean that when it reaches a screen boundary, it should reverse the velocity component that was moving it towards that boundary. So, if the bouncing pizza sprite reaches the top or bottom screen edge, it should reverse its dy attribute. When it reaches the sides of the screen, it should reverse its dx. The picture below should illustrate the Bouncing Pizza program:

 

 

Though you can’t tell from the screen shot, the pizza bounces around, following the path of the arrow. To create the Bouncing Pizza program, I modify the Falling Pizza program by adding one method to the Pizza class. Here’s the code that produces this behaviour.

 

Writing the moved() Method

 

I need to add just a single method to the Pizza class to turn a falling pizza into a bouncing one. Every time the graphics window is update by a Screen object’s mainloop() method, a Games_Object’s moved() method is automatically invoked. So, by creating a moved() method in Pizza, I get the perfect place to put code to handle screen boundary checking.

 

def moved(self):

    """ Reverse a velocity component if edge of screen reached. """

    dx, dy = self.get_velocity()

    if self.get_right() > SCREEN_WIDTH or self.get_left() < 0:

        self.set_velocity(-dx, dy)

    if self.get_bottom() > SCREEN_HEIGHT or self.get_top() < 0:

            self.set_velocity(dx, -dy)

 

First, I retrieve the current velocity of the Pizza object by invoking its get_velocity() method, which returns the values of an object’s dx and dy attributes. Next, I check to see if the sprite is about to go beyond the screen limits in any direction. If so, I reverse the responsible velocity by invoking the object’s set_velocity() method, which takes two arguments and sets an object’s dx and dy attributes. If the x-coordinate of the sprite’s right edge is greater than the screen width, then the pizza is about to go off the right edge into oblivion. If the x-coordinate of the sprite’s left edge is less than 0, then the pizza is headed off the screen to the left. In either case, I simply reverse dx, the pizza’s horizontal velocity. If the y-coordinate of the sprite’s bottom edge is greater than the screen height, then the pizza is about to fall through the bottom of the screen. If the y-coordinate of the sprite’s top edge is less than 0, then the pizza is about to float through the top of the screen. In either case, I reverse dy, the pizza’s vertical velocity.

 

Creating the Bouncing Pizza Object

 

I don’t need to do anything else to the program for this bouncing behavior to work. But to make things more interesting, I give the sprite some velocity in the x direction, so that it will bounce all over the screen. I pass a dx value of 1 when I create the sprite, like so:

 

Pizza( screen = my_screen,

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

       image = pizza_image,

       dx = 1, dy = 1

     )

 

And that’s it. I now have a bouncing pizza!

 

Handling Mouse Input

 

Although you’ve seen a lot of what the livewires package has to offer, you haven’t seen the main ingredient of interactivity: user input. One of the most common ways to get input from a user is through the mouse. livewires offers a simple Screen method to do just that.

 

Introducing the Moving Pan Program

 

The Screen class has a method that makes reading the mouse position on the graphics screen a piece of cake. With this method, I create the Moving Pan program that allows a user to drag a pan sprite across the screen as he or she moves the mouse. The results of the program are displayed in the picture below:

 

 

 

The pan sprite follows the mouse around the graphics screen (although you have to run the program to experience it, the picture above is just a still picture, and nothing more). Here’s the program that generates this behaviour:

 

Setting Up the Program

 

The following code should look familiar:

 

# Moving Pan

# Demonstrates mouse input

# A201/A598 Lab 09/08/2006

 

from livewires import games

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

As before, I import games and establish global constants for the screen’s width and height.

 

Creating the Pan Class

 

Next, I create Pan for the pan sprite:

 

class Pan(games.Sprite):

    """" A pan. Controlled by the mouse. """

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

        """ Initialize pan object. """

        self.init_sprite(screen = screen,

                         x      = x,

                         y      = y,

                         image  = image)

    def moved(self):

        """ Move pan to mouse position. """

        x, y = self.screen.mouse_pos()

        self.move_to(x,y)

 

Notice that I omit dx and dy when I invoke the object’s init_sprite() method. Since the pan sprite won’t have any sort of velocity, I’ll let the two attributes each get their default value of 0. In the moved() method, I invoke the Screen object’s mouse_pos() method. The method returns the x- and y- coordinates of the mouse pointer on the graphics screen, which I assign to x and y. Then, I invoke the Pan’s object’s move_to() method with x and y as arguments, which moves the pan to the location of the mouse pointer.

 

Writing the Main Program

 

The rest of the program is the familiar main section:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

wall_image = games.load_image("wall.jpg", transparent = False)

my_screen.set_background(wall_image)

 

pan_image = games.load_image("pan.bmp")

Pan(screen = my_screen,

    x      = SCREEN_WIDTH/2,

    y      = SCREEN_HEIGHT/2,

    image  = pan_image)

 

my_screen.mouse_visible(False)

 

my_screen.mainloop()

 

Setting up the screen and loading the brick wall background is exactly as before. Next, I load a pan image and create the Pan object. Then I invoke the Screen method mouse_visible() and set the mouse pointer to invisible. As always, I kick everything off by invoking the Screen object’s mainloop().

 

Detecting Collisions

 

In most games when two things collide, there’s a clear result. It can be as simple as a 2-D character running into a boundary that won’t let him pass, or as spectacular as a 3-D scene where an asteroid tears through the hull of a massive mother ship. Either way, there’s a need to detect when objects collide.

 

Introducing the Slippery Pizza Program

 

The Slippery Pizza program is an extension of the Moving Pan program. In the Slippery Pizza program, the user controls a pan with the mouse, just like in the Moving Pan program. But this time, there’s a pizza sprite on the screen. The user can move the pan toward the pizza, but as soon as he or she reaches it, the slippery pizza moves to a new, random screen location. The two pictures below try to give you an idea of what the program works like. First, the player almost reaches the pizza:

 

 

But as soon as the player truly reaches it, the slippery pizza gets away (by quantum leap).

 

 

Here’s the program that implements this behavior:

 

This is the first program that appears to be more than a screenful of code. Note that it does not in any way mean that programs are getting out of hand.

 

Setting Up the Program

 

The initial code is taken from the Moving Pan program, with one minor addition:

 

# Slippery Pizza Program

# Demonstrates testing for sprite collisions

# A202/A598 Lab 09/08/2006

 

import random

from livewires import games

 

SCREEN_WIDTH = 640

SCREEN_HEIGHT = 480

 

The one new thing I do is to import our old friend the random module. This allows me to generate a new, random location for the pizza sprite after the collision.

 

Creating the Pan Class

 

I create a new Pan class by adding some code for the collision detection:

 

class Pan(games.Sprite):

    """ A pan. Controlled the by mouse. """

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

        self.init_sprite(screen = screen,

                         x      = x,

                         y      = y,

                         image  = image)

    def moved(self):

        """ Move to mouse position. """

        x, y = self.screen.mouse_pos()

        self.move_to(x,y)

        self.check_collide()

 

    def check_collide(self):

        """ Check for collision with pizza. """

        if self.overlapping_objects():

            pizza = self.overlapping_objects()[0]

            pizza.handle_collide()

 

In the last line of moved(), I invoke the Pan method check_collide(). The check_collide() method invokes the Pan object’s overlapping_objects() method, which returns a list of all the objects that overlap with it. If there are any objects in the list, the first element of the list (which will be the Pizza object, since it’s the only other object on the graphics screen) is assigned to pizza. Finally, the Pizza object’s handle_collide() method is invoked so the object can react to the collision.

 

Creating the Pizza Class

 

Next, I create a new Pizza class:

 

class Pizza(games.Sprite):

    """" A slippery pizza. """

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

        self.init_sprite( screen = screen,

                          x      = x,

                          y      = y,

                          image  = image)

    def handle_collide(self):

        """ Move to a random screen location. """

        x = random.randrange(SCREEN_WIDTH)

        y = random.randrange(SCREEN_HEIGHT)

        self.move_to(x,y)

 

The Pizza class constructor method is the same as in the Pizza Sprite program. However, I add the handle_collide() method, which generates random screen coordinates, (x, y), and moves the Pizza object to this new location. This method is invoked by the Pan object when it collides with the Pizza object.

 

Writing the Main Program

 

Here’s the main section of the program:

 

# main

my_screen = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

wall_image = games.load_image("wall.jpg", transparent = False)

my_screen.set_background(wall_image)

 

x = random.randrange(SCREEN_WIDTH)

y = random.randrange(SCREEN_HEIGHT)

pizza_image = games.load_image("pizza.bmp")

Pizza(screen = my_screen, x = x, y = y, image = pizza_image)

 

pan_image = games.load_image("pan.bmp")

Pan(screen = my_screen,

    x      = SCREEN_WIDTH/2,

    y      = SCREEN_HEIGHT/2,

    image  = pan_image)

 

my_screen.mouse_visible(False)

 

my_screen.mainloop()

 

First, I initialize the Screen object and load a background, like always. Next, I create two objects, a Pizza object and a Pan object. I generate a random set of screen coordinates for the Pizza object and place the Pan object in the middle of the screen. Then, I set the mouse pointer to invisible. Finally, I kick everything off with my_screen.mainloop().

 

Back to the Pizza Panic Game

 

Now that you’ve gotten a taste of what the livewires multimedia package can do, it’s time to create the Pizza Panic game introduced at the beginning of the chapter. Much of the program for the game can be taken directly from the example programs. However, I’ll also introduce a few new concepts as I put the game together. Here’s how the code starts:

 

 

Here’s the second part of it (there will be four, in all):

 

Here’s the third part (showing the third class used, Chef):

 

And the last, illustrating the main program:

 

 

 

We can now start walking the code.

 

Setting Up the Program

 

As in all of the programs in this chapter, I begin by importing the modules and setting some global constants:

 

# Pizza Panic

# Player must catch falling pizzas before they hit the ground

# A202/A598 Lab 09/08/2006

 

import random

from livewires import games, color

 

SCREEN_WIDTH = 640 

SCREEN_HEIGHT = 480

THE_SCREEN = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)

 

To do any graphics work, I need to import games, while color gives me access to the set of predefined colors to use in creating screen text. I import random so that the crazy chef seems more life-like when he makes his choices. Next, I create global constants for the width and height of the graphics screen. Then I do something new. I create the graphics screen and assign it to the global constant THE_SCREEN. I do this because I need the screen object in existence before I can load an image object, which I do in several class definitions in this program, as you will soon see.

 

The Pan Class

 

The Pan class is a blueprint for the pan sprite that the player controls through the mouse. However, the pan will only move left and right. I’ll go through the class one section at a time.

 

Loading the Pan Image

 

I do something a little different in the beginning of this class: I load a sprite image and assign it to a class variable, image. I do this because Pizza Panic has several classes, and loading an image in its corresponding class definition is clearer than loading all of the images in the program’s main() function:

 

class Pan(games.Sprite):

    """

    A pan controlled by player to catch falling pizzas.

    """

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

 

The __init__() Method

 

Next, I write the constructor, which creates a new Pan object with the given coordinates. I define an attribute, score_value, for the player’s score, which I set to 0. I also define another attribute, score_text, for a Text object that displays the score on the graphics screen:

 

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

        """ Initialize pan object.

            Create a Text object for player's score. """

        self.init_sprite(screen = screen,

                         x      = x,

                         y      = y,

                         image  = Pan.image)

        self.score_value = 0

        self.score_text = games.Text(screen = self.screen,

                                     x      = 550,

                                     y      = 20,

                                     text   = "Score: 0",

                                     size   = 25,

                                     color  = color.black)

 

The moved() Method

 

This method moves the player’s pan:

 

    def moved(self):

        """ Move pan to mouse x position. """

        x, y = self.screen.mouse_pos()

        self.move_to(x, self.get_ypos())

        if self.get_left() < 0:

            self.set_left(0)

        if self.get_right() > SCREEN_WIDTH:

            self.set_right(SCREEN_WIDTH)

        self.check_for_catch()

 

The method gets the coordinates of the mouse, then moves the player’s pan to the mouse’s x-coordinate and the pan’s current y-coordinate. By using the pan’s current y-coordinate, the pan stays locked at the same height. As a result, the pan can move left and right but not up and down. Next, I use the object’s get_left() method to check if the left edge of the pan is less than 0, meaning that it’s beyond the left edge of the graphics window. If so, I set the left edge to 0 with the object set_left() method. This way, the pan will never be drawn beyond the left edge of the graphics window. Then, I use the object’s get_right() method to check if the right edge of the pan is greater than SCREEN_WIDTH, meaning that it is beyond the right edge of the graphics window. If so, I set the right edge to SCREEN_WIDTH with the object’s set_right() method. This way, the pan will never be drawn beyond the right edge of the graphics window. Finally, I invoke the object’s check_for_catch() method.

 

The check_for_catch() Method

 

This method checks if the player has caught one of the falling pizzas:

 

    def check_for_catch(self):

        """ Check if pan catches a pizza. """

        for pizza in self.overlapping_objects():

            self.handle_caught()

            pizza.handle_caught()

 

The method goes through the list of objects that overlap the player’s pan. For each object that overlaps the Pan object, the method invokes the Pan object’s own handle_caught() method and then invokes the overlapping object’s handle_caught() method.

 

The handle_caught() Method

 

This method is called whenever the player’s pan and a falling pizza collide:

 

    def handle_caught(self):

        """ Increase and display score. """

        self.score_value += 10

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

 

This method increases the Pan object’s score_value attribute score by 10 each time a pizza is caught. In order to reflect the change in the player’s score on the graphics screen, the Text object for the score must be updated. So next, this method invokes the set_text() method of the score_text attribute of the Pan object. set_text() assigns a new string to the object’s text attribute to reflect the player’s new score.

 

The Pizza Class

 

This class is for the falling pizzas that the player must catch:

 

class Pizza(games.Sprite):

    """

    A pizza which falls to the ground.

    """

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

    START_Y = 90  # start any pizza at chef's chest-level

    speed = 1

 

I define three class variables: image for the pizza image, START_Y, a constant for all pizzas’ starting y-coordinate, and speed, a class variable for all pizzas’ falling speed. START_Y is set to 90 so that any newly created pizza will appear at the chef’s chest level on the graphics screen. I set speed to 1 so that the pizzas fall at a fairly slow speed. I use all three class variables in the Pizza constructor method, as you’ll soon see. I didn’t make speed a constant because I thought I might want to change the speed at which the pizzas fall as the game progresses in a future version of the program (or you might want to, if you accept the chapter challenges).

 

The __init__() Method

 

This method initializes a new Pizza object:

 

    def __init__(self, screen, x):

        """ Initialize a pizza object. """

        self.init_sprite(screen = screen,

                         x      = x,

                         y      = Pizza.START_Y,

                         dx     = 0,

                         dy     = Pizza.speed,

                         image  = Pizza.image)

 

When the constructor method of a newly created Pizza object is invoked, the object’s init_sprite() method is invoked to initialize the sprite.

 

The moved() Method

 

This method handles screen boundary checking:

 

    def moved(self):

        """ Check if a pizza's bottom edge

            has reached screen bottom. """

        if self.get_bottom() > SCREEN_HEIGHT:

            self.game_over()

 

All this method does is check if a pizza has reached the bottom of the screen. If it has, the method invokes the object’s game_over() method.

 

The handle_caught() Method

 

Remember, this method is invoked by the player’s Pan object when the Pizza object collides with it:

 

    def handle_caught(self):

        """ Destroy self if caught. """

        self.destroy()

 

When a pizza collides with a pan, the pizza is considered “caught” and simply ceases to exist. So, the Pizza object invokes its own destroy() method and the pizza literally disappears.

 

The game_over() Method

 

This method is invoked by moved() when a pizza reaches the bottom of the screen. The method ends the game.

 

    def game_over(self):

        """ End the game. """

        # destroy all game objects

        # except the Text object (player's score)

        for game_object in self.screen.all_objects():

            if not isinstance(game_object, games.Text):

                game_object.destroy()     

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

 

When this method is invoked, the player’s pan, the crazy chef, and all of the pizzas disappear from the screen. Then, the message “Game Over” is displayed in big, red letters. About five seconds later, the program ends. The for loop moves through all of the objects on the screen and destroys each one, except the Text object, which represents the player’s score. The method checks if each object is a Text object with the isinstance() Python function, which takes an object and a class as arguments. isinstance() is True if the object is an instance of the class, and is False otherwise. Next, the game_over() method creates a Message object that declares that the game is over. Since the lifetime attribute is 250 and mainloop() is running at 50 cycles per second, the message stays on the screen for about five seconds. At that point, the method specified in the after_death attribute of the Message object is invoked. The specified method is the Screen object’s quit() method, so the graphics window disappears and the program ends.

 

The Chef Class

 

The Chef class is used to create the crazy chef who throws the pizzas off the restaurant rooftop. The class has a constructor method, a moved() method, and a drop_pizza() method, which, you guessed it, allows the chef to drop a new pizza:

 

class Chef(games.Sprite):

    """

    A chef which moves left and right, dropping pizzas.

    """

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

    Y = 55                    # put the chef right on the top of the brick wall

 

I define two class variables. image is for the chef image and Y is for the starting y-coordinate of the Chef object. I set Y to 55, which will put the image of the chef right at the rooftop.

 

The __init__() Method

 

This method creates a new chef:

 

    def __init__ (self, screen, x, speed, odds_change):

        """ Initialize the Chef object. """

        self.init_sprite(screen = screen,

                         x = x, y = Chef.Y,

                         dx = speed, dy = 0, image = Chef.image)

        self.odds_change = odds_change

        self.time_til_drop = 0

 

First, I invoke the newly created Chef object’s init_sprite() method to initialize the sprite. I pass the class constant Y for the y-coordinate. dx is passed speed, which determines the chef’s horizontal velocity as he moves along the rooftop. The method also creates two attributes, odds_change and time_til_drop. odds_change is an integer that represents the odds that the chef will change his direction. For example, if odds_change is 250, then there’s a 1 in 250 chance that every time the chef moves, he’ll reverse direction. You’ll see how this works in the moved() metod of the class. time_til_drop is an integer that represents the amount of time, in mainloop() cycles until the next time the chef drops his next pizza. I set it to 0 initially, meaning that when a Chef object springs to life, it should immediately drop a pizza. You’ll see how time_til_drop works in the drop_pizza() method. Lastly, since I’ve used OOP to build Pizza Panic, it becomes a trivial task to have multiple chefs in the same game. With one additional line of code to instantiate another Chef object, I can have two crazy, hat-wearing men tossing pizzas down at the player’s pan. Though I’ll be using only one chef in this version of the game, this knowledge might come in handy (say, for a chapter challenge). 

 

The moved() Method

 

This method defines the rules for how the chef decides to slide back and forth along the rooftop:

 

    def moved(self):

        """ Determine if direction needs to be reversed. """

        if self.get_left() < 0 or self.get_right() > SCREEN_WIDTH:

            self.reverse()

        else:

            same_direction = random.randrange(self.odds_change)

            if not same_direction:

                self.reverse()

        self.drop_pizza()

 

A chef slides along the rooftop in one direction until he either reaches the edge of the screen or “decides,” at random, to switch directions. The beginning of this method checks to see if the chef has moved beyond the left or right edge of the graphics window. If he has, then the reverse() method is invoked. Otherwise, the chef has a 1 in odds_change chance of changing direction. Regardless of whether or not the chef changes direction, the last thing the method does is invoke the Chef object’s drop_pizza() method.

 

The reverse() Method

 

This method is invoked by moved() and reverses the chef’s direction:

 

    def reverse(self):

        """ Reverse direction. """

        dx, dy = self.get_velocity()

        self.set_velocity((-dx, dy))

 

This method is quite simple. It reverses the horizontal velocity of the chef, changing his direction.

 

The drop_pizza() Method

 

This method is invoked every time moved() is invoked, but that doesn’t mean a new pizza is dropped each time:

 

    def drop_pizza(self):

        """ Decrease countdown or

            drop pizza and reset countdown. """

        if self.time_til_drop:

            self.time_til_drop -= 1

        else:

            # set so buffer will be 15 pixels,

            # regardless of pizza speed

            self.time_til_drop = int(65 / Pizza.speed)

            Pizza(self.screen, self.get_xpos())

 

time_til_drop represents a countdown for our chef. If time_til_drop is not 0, then 1 is subtracted from it. Otherwise, time_til_drop is reset and a new Pizza object is created. The value of time_til_drop is determined by the height of the pizza sprite image and the speed at which the pizzas are falling. Since the pizza image is 50 pixels high, the formula provides a nice 15 pixel-sized gap between each pie, independent of the falling speed.

 

The main() Function

 

The main() function creates a graphics screen, creates graphics objects and then kicks off the Screen object’s mainloop() to run the show:

 

def main():     

    my_screen = THE_SCREEN

    my_screen.mouse_visible(False)

    wall_image = games.load_image("wall.jpg", transparent = False)

    my_screen.set_background(wall_image)

 

    Chef(screen      = my_screen,

         x           = SCREEN_WIDTH/2,

         speed       = 1,

         odds_change = 250)

 

    Pan(screen = my_screen, x = SCREEN_WIDTH/2, y = 435)

 

    my_screen.mainloop()

 

First, I assign the graphics screen to my_screen and set the mouse pointer to invisible. Then, I set the brick wall as the background. Next, I create a chef with a speed of 1 and 1 in 250 chance of changing directions each move. Then, I create the player’s pan with a y-coordinate of 435, putting it at the bottom of the screen. Finally, I invoke my_screen’s mainloop() and the game begins.

 

Summary

 

In this chapter, you saw how to use the livewires multimedia package to add graphics to your programs. You learned how to create a new graphics window and how to set a new background image for it. You saw how to display text on a graphics window. You learned about the sprite, a special graphics object with an image. Specifically,  you saw how to place and move a sprite on a graphics screen. You also saw how to test for collisions between graphics objects. You learned how to get input from the mouse. Finally, you saw how to put everything together in a fast-paced video game, complete with a computer-controlled oponent.

 

Challenges

 

  1. Improve the Pizza Panic game by increasing its difficulty as the game progresses. Think of different ways to accomplish this. You could increase the speed of the pizzas and the speed of the chef. You could raise the player’s pan to a higher position on the screen. You could even increase the number of crazy chefs flinging pizzas.
  2. Create a simple, one-player game of pong, where a player controls a paddle, and the ball bounces off three walls. If the ball gets by the player’s paddle, the game is over.
  3. Write a game where the player controls a character that must avoid falling debris. The player controls the character with the mouse, and objects fall from the sky.

 

The Lab Assignment

 

To be posted here shortly.