Timers and Animation: What Would Disney Do? - Teach Your Kids to Code: A Parent-Friendly Guide to Python Programming (2015)

Teach Your Kids to Code: A Parent-Friendly Guide to Python Programming (2015)

Chapter 8. Timers and Animation: What Would Disney Do?

One way that I learned to program in my teens was by programming short games and animations, and then changing the code to do something new. I was amazed that I could immediately see my code make graphics appear on the screen, and I think you’ll enjoy it as much as I did.

Games and animations have several things in common. First, they’re fun! Second, they both involve drawing graphics on the screen and changing those graphics over time to give the illusion of motion. We’ve been able to draw graphics from the beginning of this book, but the Turtle library is too slow to use for a lot of animation or moving objects. In this chapter, we’re going to install and work with a new module, Pygame, that lets us draw, animate, and even create arcade-style games using the skills you’ve picked up so far.

Getting All GUI with Pygame

A graphical user interface (GUI, sometimes pronounced “gooey”) includes all the buttons, icons, menus, and windows that you see on your computer screen; it’s how you interact with a computer. When you drag and drop a file or click an icon to open a program, you’re enjoying a GUI. In games, when you press keys, move your mouse, or click, the only reason you can expect anything to happen (like running, jumping, rotating your view, and so on) is because a programmer set up the GUI.

Like the Turtle library, Pygame is very visual, perfect for GUIs for games, animations, and more. It’s portable to just about every operating system, from Windows to Mac to Linux and beyond, so the games and programs you create in Pygame can run on pretty much any computer. Figure 8-1shows the Pygame website, where you’ll go to download Pygame.

Pygame is free, and so are the tutorials and sample games on its website.

Figure 8-1. Pygame is free, and so are the tutorials and sample games on its website.

To get started, install the pygame module by downloading the installer from the Downloads page at http://www.pygame.org/. For Windows, you’ll probably want to download pygame-1.9.1 .win32-py3.1.msi, but see Appendix B for help if you have any trouble. For Mac and Linux, the installation is more involved; see Appendix B or go to http://www.nostarch.com/teachkids/ for step-by-step instructions.

You can check that Pygame installed with no errors by entering the following into the Python shell:

>>> import pygame

If you get a regular >>> prompt in response, you know that Python was able to find the pygame module without error and the Pygame library is ready to use.

Drawing a Dot with Pygame

Once you have Pygame installed, you can run a short sample program to draw a dot on the screen, like the one in Figure 8-2.

The ShowDot.py program at work

Figure 8-2. The ShowDot.py program at work

Type the following in a new IDLE window or download it from http://www.nostarch.com/teachkids/:

ShowDot.py

import pygame

➊ pygame.init()

➋ screen = pygame.display.set_mode([800,600])

➌ keep_going = True

➍ GREEN = (0,255,0) # RGB color triplet for GREEN

radius = 50

➎ while keep_going:

➏ for event in pygame.event.get():

➐ if event.type == pygame.QUIT:

keep_going = False

➑ pygame.draw.circle(screen, GREEN, (100,100), radius)

➒ pygame.display.update()

➓ pygame.quit()

Let’s step through this program line by line. First, we import the pygame module to gain access to its features. At ➊, we initialize Pygame, or set it up for use. The command pygame.init() will need to be called every time you want to use Pygame, and it always comes after the import pygamecommand and before any other Pygame functions.

At ➋, pygame.display.set_mode([800,600]) creates a display window 800 pixels wide by 600 pixels tall. We store it in a variable called screen. In Pygame, windows and graphics are called surfaces, and the display surface screen is the main window where all of our other graphics will be drawn.

At ➌, you might recognize our looping variable, keep_going: we used this in our HighCard.py and FiveDice.py game loops in Chapter 6 as a Boolean flag to tell our program to keep playing. Here in our Pygame example, we use a game loop to continue drawing the graphics screen until the user closes the window.

At ➍, we set up two variables, GREEN and radius, for use in drawing our circle. The GREEN variable is set to the RGB triplet value (0,255,0), a bright green. (RGB, or Red Green Blue, is one of many ways to specify a color. To pick a color, you choose three numbers, each between 0 and 255. The first number determines how much red is in your color, the second number is the amount of green, and the third is blue. We picked 255 as our value for green and 0 for red and blue, so our RGB color is all green and no red or blue.) Our variable GREEN is a constant. We sometimes writeconstants — variables we don’t intend to change — in all caps. Since the color should stay the same throughout our program, we’ve used all caps for GREEN. We set the radius variable equal to 50 pixels, for a circle 100 pixels in diameter.

The while loop at ➎ is our game loop, and it will keep running the Pygame window until the user chooses to exit. The for loop at ➏ is where we handle all the interactive events that the user can trigger in our program. In this simple example, the only event we’re checking for is whether the user clicked the red X to close the window and exit the program ➐. If so, keep_going gets set to False and our game loop ends.

image with no caption

At ➑, we draw a green circle with a radius of 50 on the screen window at position (100,100): right 100 and down 100 pixels from the upper-left corner of the window (see What’s New in Pygame for more information on how Pygame’s coordinate system is different from Turtle’s). We’re using pygame.draw, a Pygame module for drawing shapes like circles, rectangles, and line segments. We pass four arguments to the pygame.draw.circle() function: the surface on which we want to draw the circle (screen), the color for our circle (GREEN), the coordinates of its center point, and the radius. The update() function at ➒ tells Pygame to refresh the screen with the drawing changes.

Finally, when the user exits the game loop, the pygame.quit() command at ➓ clears the pygame module (it undoes all the setup from ➊) and closes the screen window so that the program can exit normally.

You should see an image like the one in Figure 8-2 when you run ShowDot.py. Take some time to play around with this dot program — create a different RGB color triplet, draw the dot in a different location on the screen, or draw a second dot. You’ll begin to see the power and ease of drawing graphics with Pygame, and you’ll have fun along the way.

This first program contains the foundation that we’ll build on to create more complex graphics, animation, and, eventually, games.

What’s New in Pygame

Before we dive deeper into the exciting world of Pygame, it’s worth noting some important differences between Pygame and our old friend turtle graphics:

§ We have a new coordinate system, as shown in Figure 8-3. Back in turtle graphics, the origin was at the center of the screen, and y got larger as we went up the screen. Pygame uses a more common window-oriented coordinate system (we see this in many other GUI programming languages, including Java, C++, and more). The upper-left corner of a window in Pygame is the origin, (0, 0). The x-coordinate values still increase as you move to the right (but there are no negative x-coordinate values, as they would be off the screen to the left); y-coordinate values increase as you move down (and negative y-coordinate values would be off the top of the window).

Pygame uses a window-oriented coordinate system.

Figure 8-3. Pygame uses a window-oriented coordinate system.

§ The game loop is always used in Pygame. In our earlier programs, we used a loop only if we wanted to keep playing or go back and do something again, but Pygame requires the game loop to keep updating the screen and handling events (even if the only event we handle is closing the window).

§ We handle events in Pygame by calling pygame.event.get() to fetch a list of events that the user has performed. These events could be mouse clicks, key presses, or even window events like the user closing the window. We use a for loop to handle everything in this list of events frompygame.event.get(). In our turtle programs, we used callback functions to handle events. In Pygame, we can still create functions and call them in our event handler code, but we can process events just using if statements for those events that we care to listen for.

These differences make Pygame a new way of solving problems, and that’s what we’re always looking for! The more tools we have, the more problems we can solve.

The Parts of a Game

In this section, we’ll change our ShowDot.py program to display a smiley face image instead of a green circle, as shown in Figure 8-4.

ShowPic.py draws the image CrazySmile.bmp on the screen.

Figure 8-4. ShowPic.py draws the image CrazySmile.bmp on the screen.

As we build our ShowPic.py program, we’ll learn about the three main parts of a game or animation in Pygame. First, there’s the setup, where we import modules we need, create our screen, and initialize some important variables. Then comes the game loop, which handles events, draws graphics, and updates the display. This game loop is a while loop that keeps running as long as the user doesn’t quit the game. Finally, we need a way to end the program when the user quits the game.

Setting Up

First, download the smiley face image and save it in the same folder as your Python programs. Go to http://www.nostarch.com/teachkids/ to find the source code downloads and save the image CrazySmile.bmp to the folder where you’ve been saving your .py files. It doesn’t really matter where you keep your .py files; just make sure to save the BMP (short for bitmap, a common image file format) image file to the same location.

Next, let’s take care of the setup:

import pygame # Setup

pygame.init()

screen = pygame.display.set_mode([800,600])

keep_going = True

➊ pic = pygame.image.load("CrazySmile.bmp")

As always, we import the pygame module and then initialize using the pygame.init() function. Next, we set up our screen to be a new Pygame window 800×600 pixels in size. We create our Boolean flag keep_going to control our game loop and set it equal to True. Finally, we do something new: at ➊, we use pygame.image.load(), which loads an image from a file. We create a variable for our image file and load CrazySmile.bmp, which we’ll refer to as pic in our program.

Creating a Game Loop

At this point, we haven’t drawn anything, but we’ve set up Pygame and loaded an image. The game loop is where we’ll actually display the smiley face image on the screen. It’s also where we’ll handle events from the user. Let’s start by handling one important event: the user choosing to quit the game.

while keep_going: # Game loop

for event in pygame.event.get():

➊ if event.type == pygame.QUIT:

keep_going = False

Our game loop will keep running as long as keep_going is True. Inside the loop, we immediately check for events from the user. In advanced games, the user can trigger a lot of events at the same time, like pressing the down arrow on the keyboard while moving the mouse left and scrolling the mouse wheel.

In this simple program, the only event we’re listening for is whether the user clicked the close window button to quit the program. We check for this at ➊. If the user triggered the pygame.QUIT event by trying to close the window, we want to tell our game loop to exit. We do this by settingkeep_going to False.

We still need to draw our picture to the screen and update the drawing window to make sure everything appears on the screen, so we’ll add these two final lines to our game loop:

screen.blit(pic, (100,100))

pygame.display.update()

The blit() method draws pic, the image that we’ve loaded from disk (our smiley face), onto our display surface, screen. We’ll use blit() when we want to copy pixels from one surface (like the image we loaded from disk) onto another (like the drawing window). Here, we need to useblit() because the pygame.image.load() function works differently than the pygame.draw.circle() function we used earlier to draw our green dot. All pygame.draw functions accept a surface as an argument, so by passing screen to pygame.draw.circle(), we were able to havepygame.draw.circle() draw to our display window. But pygame.image.load() doesn’t take a surface as an argument; instead, it automatically creates a new, separate surface for your image. The image won’t appear on the original drawing screen unless you use blit().

image with no caption

In this case, we’ve told blit() that we want to draw pic at the location (100,100), or right 100 pixels and down 100 pixels from the upper-left corner of the screen (in Pygame’s coordinate system, the origin is the upper-left corner; see Figure 8-3).

The final line of our game loop is the call to pygame.display.update(). This command tells Pygame to show the drawing window with all the changes that have been made during this pass through the loop. That includes our smiley face. When update() runs, the window will be updated to show all the changes to our screen surface.

So far, we’ve taken care of our setup code, and we have a game loop with an event handler that listens for the user hitting the close window button. If the user clicks the close window button, the program updates the display and exits the loop. Next, we’ll take care of ending the program.

Exiting the Program

The last section of our code will exit the program once the user has chosen to quit the game loop:

pygame.quit() # Exit

If you leave this line out of your programs, the display window will stay open even after the user tries to close it. Calling pygame.quit() closes the display window and frees up the memory that was storing our image, pic.

Putting It All Together

Put it all together, and you’ll see our CrazySmile.bmp image file — as long as you’ve saved the image in the same directory as your ShowPic.py program file. Here’s the full listing:

ShowPic.py

import pygame # Setup

pygame.init()

screen = pygame.display.set_mode([800,600])

keep_going = True

pic = pygame.image.load("CrazySmile.bmp")

while keep_going: # Game loop

for event in pygame.event.get():

if event.type == pygame.QUIT:

keep_going = False

screen.blit(pic, (100,100))

pygame.display.update()

pygame.quit() # Exit

When you click the close window button, the display window should close.

This code has all the basic components we’ll build on to make our programs even more interactive. In the rest of this chapter and in Chapter 9, we’ll add code to our game loop to respond to different events (for example, making images on the screen move when the user moves the mouse). Now let’s see how to create a program that draws an animated bouncing ball!

Timing It Just Right: Move and Bounce

We already have the skills needed to create animation, or the illusion of motion, by making one small change to our ShowPic.py app. Instead of showing the smiley face image at a fixed location every time through the game loop, what if we change that location slightly every frame? Byframe, I mean each pass through the game loop. The term comes from one way people make animations: they draw thousands of individual pictures, making each picture slightly different from the one before it. One picture is considered one frame. The animators then put all the pictures together on a strip of film and run the film through a projector. When the pictures are shown one after another very quickly, it looks like the characters in the pictures are moving.

image with no caption

With a computer, we can create the same effect by drawing a picture on the screen, clearing the screen, moving the picture slightly, and then drawing it again. The effect will look a bit like Figure 8-5.

In this first attempt at animation, our smiley image will streak off the screen.

Figure 8-5. In this first attempt at animation, our smiley image will streak off the screen.

We still call each drawing a frame, and the speed of our animation is how many frames per second (fps) we draw. A video game might run 60–120 frames per second, like high-definition television. Older, standard-definition TVs in the United States run at 30 fps, and many film projectors run at 24 fps (newer high-definition digital projectors can run at 60 fps or higher).

If you’ve ever made or seen a flip-book animation (in which you draw on the corners of pages in a notebook and then flip through them to create a mini-cartoon), you’ve seen that the illusion of motion can be created at many different frame rates. We’ll aim for a rate around 60 fps, fast enough to create smooth animations.

Moving a Smiley

We can create simple motion in our while loop by drawing the smiley face image at different locations over time. In other words, in our game loop, we just need to update the (x, y) location of the picture and then draw it at that new location each time through the loop.

We’ll add two variables to ShowPic.py: picx and picy, for the x- and y-coordinates of the image on the screen. We’ll add these at the end of the setup portion of our program and then save the new version of the program as SmileyMove.py (the final version is shown in SmileyMove.py).

import pygame # Setup

pygame.init()

➊ screen = pygame.display.set_mode([600,600])

keep_going = True

pic = pygame.image.load("CrazySmile.bmp")

➋ colorkey = pic.get_at((0,0))

➌ pic.set_colorkey(colorkey)

picx = 0

picy = 0

NOTE

The lines atandare an optional fix for a minor issue. If the CrazySmile.bmp image looks like it has square black corners on your screen, you can include these two lines to make sure those corners look transparent.

Notice that we’ve also changed our window screen to 600×600 pixels to make the window square at ➊. The game loop will begin the same way it did in ShowPic.py, but we’ll add code to change the picx and picy variables by 1 pixel every time the loop runs:

while keep_going: # Game loop

for event in pygame.event.get():

if event.type == pygame.QUIT:

keep_going = False

picx += 1 # Move the picture

picy += 1

The += operator adds something to the variable on the left side (picx and picy), so with += 1, we’ve told the computer we want to change the x- and y-coordinates of the picture, (picx, picy), by 1 pixel every time through the loop.

Finally, we need to copy the image onto the screen at the new location, update the display, and tell our program what to do to exit:

screen.blit(pic, (picx, picy))

pygame.display.update()

pygame.quit() # Exit

If you run those lines, you’ll see our image take off! In fact, you’ll have to look fast because it will move right off the screen.

Look back at Figure 8-5 for a glimpse of the smiley image before it slides out of view.

This first version may leave streaks of pixels on the display even when the smiley image has left the drawing window. We can make the animation cleaner by clearing the screen between each frame. The streaking lines we’re seeing behind our smiley are the upper-left pixels of the smiley image; every time we move down and over each frame to draw a new version of our image and update the display, we’re leaving behind a few stray pixels from the last picture.

We can fix this by adding a screen.fill() command to our drawing loop. The screen.fill() command takes a color as an argument, so we need to tell it what color we’d like to use to fill the drawing screen. Let’s add a variable for BLACK (using all uppercase for BLACK to show that it’s a constant) and set it equal to black’s RGB color triplet, (0,0,0). We’ll fill the screen surface with black pixels, effectively clearing it off, before we draw each new, moved copy of our animated image.

Add this line to your setup right after picy = 0 to create the black background fill color:

BLACK = (0,0,0)

And add this line right before the screen.blit() that draws our pic image on the screen:

screen.fill(BLACK)

Our smiley face still speeds off the screen, but this time we’re not leaving a trail of pixels behind our moving image. By filling the screen with black pixels, we’ve created the effect of “erasing” the old image from the screen every frame, before we draw the new image at the new location. This creates the illusion of smoother animation. On a relatively fast computer, though, our smiley flies off the screen way too fast. To change this, we need a new tool: a timer or clock that can keep us at a steady, predictable rate of frames per second.

Animating a Smiley with the Clock Class

The final piece to make our SmileyMove.py app behave like an animation we might see in a game or movie is to limit the number of frames per second our program draws. Currently, we’re moving the smiley image only 1 pixel down and 1 pixel to the right each time through the game loop, but our computer can draw this simple scene so fast that it can produce hundreds of frames per second, causing our smiley to fly off the screen in an instant.

image with no caption

Smooth animation is possible with 30 to 60 frames of animation per second, so we don’t need the hundreds of frames zooming past us every second.

Pygame has a tool that can help us control the speed of our animation: the Clock class. A class is like a template that can be used to create objects of a certain type, with functions and values that help those objects behave in a certain way. Think of a class as being like a cookie cutter and objects as the cookies: when we want to create cookies of a certain shape, we build a cookie cutter that can be reused anytime we want another cookie of the same shape. In the same way that functions help us package reusable code together, classes allow us to package data and functions into a reusable template that we can use to create objects for future programs.

We can add an object of the Clock class to our program setup with this line:

timer = pygame.time.Clock()

This creates a variable called timer linked to a Clock object. This timer will allow us to gently pause each time through the game loop and wait just long enough to make sure we’re not drawing more than a certain number of frames per second.

Adding the following line to our game loop will keep the frame rate at 60 fps by telling our Clock named timer to “tick” just 60 times per second:

timer.tick(60)

The following code, SmileyMove.py, shows the whole app put together. It gives us a smooth, steady animated smiley face slowly gliding off the lower right of the screen.

SmileyMove.py

import pygame # Setup

pygame.init()

screen = pygame.display.set_mode([600,600])

keep_going = True

pic = pygame.image.load("CrazySmile.bmp")

colorkey = pic.get_at((0,0))

pic.set_colorkey(colorkey)

picx = 0

picy = 0

BLACK = (0,0,0)

timer = pygame.time.Clock() # Timer for animation

while keep_going: # Game loop

for event in pygame.event.get():

if event.type == pygame.QUIT:

keep_going = False

picx += 1 # Move the picture

picy += 1

screen.fill(BLACK) # Clear screen

screen.blit(pic, (picx,picy))

pygame.display.update()

timer.tick(60) # Limit to 60 frames per second

pygame.quit() # Exit

The remaining problem is that the smiley still goes all the way off the screen in a few seconds. That’s not very entertaining. Let’s change our program to keep the smiley face on the screen, bouncing from corner to corner.

Bouncing a Smiley Off a Wall

We’ve added motion from one frame to the next by changing the position of the image we were drawing on each pass through our game loop. We saw how to regulate the speed of that animation by adding a Clock object and telling it how many times per second to tick(). In this section, we’ll see how to keep our smiley on the screen. The effect will look a bit like Figure 8-6, with the smiley appearing to bounce back and forth between two corners of the drawing window.

Our goal is to keep the smiley “bouncing” between the corners of the screen.

Figure 8-6. Our goal is to keep the smiley “bouncing” between the corners of the screen.

The reason our image ran off the screen before is that we didn’t set boundaries, or limits, for our animation. Everything we draw on the screen is virtual — meaning it doesn’t exist in the real world — so things don’t really bump into one another. If we want the virtual objects on our screen to interact, we have to create those interactions with programming logic.

Hitting the Wall

When I say that we want the smiley face to “bounce” off the edge of the screen, what I mean is that when the smiley comes to the edge of the screen, we want to change the direction it’s moving so that it looks like it bounces off the solid edge of the screen. To do this, we need to test whether the (picx,picy) location of the smiley has reached the imaginary boundary at the edge of the screen. We call this logic collision detection because we’re trying to detect, or notice, when a collision occurs, like the smiley face image “hitting” the edge of the drawing window.

We know that we can test for conditions using an if statement, so we could see if our image is touching, or colliding with, the right side of the screen by checking whether picx is greater than some value.

Let’s figure out what that value might be. We know our screen is 600 pixels wide because we created our screen with pygame.display.set_mode([600,600]). We could use 600 as our boundary, but the smiley face would still go off the edge of the screen because the coordinate pair (picx,picy)is the location of the top-left pixel of our smiley face image.

To find our logical boundary — that is, the virtual line that picx has to reach for our smiley face to look like it has hit the right edge of the screen window — we need to know how wide our picture is. Because we know picx is the top-left corner of the image and it continues to the right, we can just add the width of our picture to picx, and when that sum equals 600, we’ll know that the right edge of the image is touching the right edge of the window.

One way to find the width of our image is by looking at the properties of the file. In Windows, right-click the CrazySmile.bmp file, select the Properties menu item, and then click the Details tab. On a Mac, click the CrazySmile.bmp file to select it, press -I to get the file info window, and then click More Info. You’ll see the width and height of the picture, as shown in Figure 8-7.

To determine our virtual boundaries so our smiley face can bounce off them, we need to know the width of our image file.

Figure 8-7. To determine our virtual boundaries so our smiley face can bounce off them, we need to know the width of our image file.

Our CrazySmile.bmp file measures 100 pixels across (and 100 pixels down). So if our screen is currently 600 pixels wide and the pic image needs 100 pixels to display the full image, our picx has to stay left of 500 pixels in the x-direction. Figure 8-8 shows these measurements.

Calculating a bounce against the right side of the window

Figure 8-8. Calculating a bounce against the right side of the window

But what if we change our image file or want to handle images of different widths and heights? Fortunately, Pygame has a convenient function in the pygame.image class that our picture variable pic uses. The function pic.get_width() returns the width in pixels of the image stored in thepygame.image variable pic. We can use this function instead of hardcoding our program to handle only an image that measures 100 pixels wide. Similarly, pic.get_height() gives us the height in pixels of the image stored in pic.

We can test whether the image pic is going off the right side of the screen with a statement like this:

if picx + pic.get_width() > 600:

In other words, if the starting x-coordinate of the picture, plus the picture’s width, is greater than the width of the screen, we’ll know we’ve gone off the right edge of the screen, and we can change the image’s direction of motion.

image with no caption

Changing Direction

“Bouncing” off the edge of the screen means going in the opposite direction after hitting that edge. The direction our image is moving is controlled by the updates to picx and picy. In our old SmileyMove.py, we just added 1 pixel to picx and picy every time through the while loop with these lines:

picx += 1

picy += 1

However, these lines kept our image moving right and down 1 pixel every time; there was no “bounce,” or changing direction, because we never changed the number added to picx and picy. Those two lines mean we’re guaranteed to move right and down at a speed of 1 pixel per frame, every frame, even after the smiley has left the screen.

image with no caption

Instead, we can change the constant value 1 to a variable that will represent the speed, or number of pixels the image should move each frame. Speed is the amount of movement in a period of time. For example, a car that moves a lot in a short time is moving at a high speed. A snail that barely moves in the same period of time is moving at a low speed. We can define a variable called speed in the setup portion of our program for the amount of movement in pixels that we want for each frame:

speed = 5

Then, all we have to do in our game loop is change picx and picy by this new speed amount (instead of the constant amount 1) every time through the loop:

picx += speed

picy += speed

One pixel per frame seemed a bit too slow at 60 frames per second in SmileyMove.py, so I’ve increased the speed to 5 to make it move faster. But we’re still not bouncing off the right edge of the screen; we just move off the screen quickly again, because the speed variable doesn’t change when we hit the edge of the screen.

We can solve that final problem by adding our collision detection logic — that is, our test to see if we’ve hit the imaginary boundary at the left or right edges of the screen:

if picx <= 0 or picx + pic.get_width() >= 600:

speed = -speed

First, we’re checking both the left and right boundaries of the screen by seeing if picx is trying to draw at a negative x-coordinate value (off the left of the screen where x < 0) or if picx + pic.get_ width() totals more than the 600-pixel width of the screen (meaning the picture’s starting x-coordinate plus its width have gone off the right edge of the screen). If either of these happens, we know we’ve gone too far and we need to change the direction we’re going in.

Notice the trick we’re using if either of those boundary tests evaluates to True. By setting speed = -speed, we’re changing the direction of the movement in our while loop by multiplying speed by –1, or by making it the negative of itself. Think of it this way: if we keep looping with speedequal to 5 until our picx plus the image’s width hits the right edge of the screen at 600 pixels (picx + pic.get_width() >= 600), setting speed = -speed will change speed from 5 to -5 (negative five). Then, whenever our picx and picy change in the next pass through the loop, we’ll add -5 to our location. This is the same as subtracting 5 from picx and picy, or moving left and up on our screen. If this works, our smiley face will now bounce off the lower-right corner of the screen and start traveling backward, back up to (0, 0) at the upper-left corner of the screen.

But that’s not all! Because our if statement is also checking for the left screen boundary (picx <= 0), when our smiley face looks like it has hit the left side of the screen, it will change speed to -speed again. If speed is -5, this will change it to -(-5), or +5. So if our negative speed variable was causing us to move to the left and up 5 pixels every frame, once we hit picx <= 0 at the left edge of the screen, speed = -speed will turn speed back to positive 5, and the smiley image will start moving to the right and down again, in the positive x- and y-directions.

Putting It All Together

Try version 1.0 of our app, SmileyBounce1.py, to see the smiley face bounce from the upper-left corner of the window to the lower-right corner and back again, never leaving the drawing screen.

SmileyBounce1.py

import pygame # Setup

pygame.init()

screen = pygame.display.set_mode([600,600])

keep_going = True

pic = pygame.image.load("CrazySmile.bmp")

colorkey = pic.get_at((0,0))

pic.set_colorkey(colorkey)

picx = 0

picy = 0

BLACK = (0,0,0)

timer = pygame.time.Clock()

speed = 5

while keep_going: # Game loop

for event in pygame.event.get():

if event.type == pygame.QUIT:

keep_going = False

picx += speed

picy += speed

if picx <= 0 or picx + pic.get_width() >= 600:

speed = -speed

screen.fill(BLACK)

screen.blit(pic, (picx,picy))

pygame.display.update()

timer.tick(60)

pygame.quit() # Exit

With this first version of the program, we have created what looks like a smoothly animated smiley face bouncing back and forth between two corners of a square drawing window. We are able to achieve this effect precisely because the window is a perfect square, 600×600 pixels in size, and because we always change our picx and picy values by the same amount (speed) — our smiley face travels only on the diagonal line where x = y. By keeping our image on this simple path, we only have to check whether picx goes past the boundary values at the left and right edges of the screen.

What if we want to bounce off all four edges (top, bottom, left, and right) of the screen, in a window that isn’t a perfect square — say, 800×600? We’ll need to add some logic to check our picy variable to see if it passes an upper or lower boundary (the top or bottom of the screen), and we’ll need to keep track of horizontal and vertical speed separately. We’ll do that next.

Bouncing a Smiley Off Four Walls

In SmileyBounce1.py, we kept the horizontal (left-right) and vertical (up-down) motion locked so that whenever the image was moving right, it was also moving down, and when it was moving left, it was also moving up. This worked well for our square window because the width and height of the screen were the same. Let’s build on that example to create a bouncing animation that rebounds realistically off all four sides of the drawing window. We’ll make the window 800×600 pixels in size with screen = pygame.display.set_mode([800,600]) to make the animation more interesting.

Horizontal and Vertical Speed

First, let’s separate the horizontal and vertical components of the speed. In other words, let’s create one speed variable, speedx, for the horizontal speed (how fast the image is moving to the right or left), and another speed variable, speedy, for the vertical speed (how fast the image is moving down or up). We can accomplish this by changing the speed = 5 entry in the setup section of our app to initialize a speedx and speedy as follows:

speedx = 5

speedy = 5

We can then modify our image position updates in the game loop:

picx += speedx

picy += speedy

We change picx (the horizontal or x-position) by speedx (the horizontal speed) and picy (the vertical or y-position) by speedy (the vertical speed).

Hitting Four Walls

The last part to figure out is the boundary collision detection for each of the four edges of the screen (top and bottom in addition to right and left). First, let’s modify the left and right boundaries to match the new screen size (800 pixels wide) and to use the new horizontal speed speedx:

if picx <= 0 or picx + pic.get_width() >= 800:

speedx = -speedx

Notice that our left-edge-boundary case remains the same at picx <= 0, because 0 is still the left boundary value when picx is at the left of the screen. This time, though, our right-edge-boundary case has changed to picx + pic.get_width() >= 800, because our screen is now 800 pixels wide, and our image still starts at picx and then draws its full width to the right. So when picx + pic.get_width() equals 800, our smiley face looks like it is touching the right side of the drawing window.

We slightly changed the action that our left and right boundaries trigger, from speed = -speed to speedx = -speedx. We now have two components of our speed, and speedx will control the left and right directions and speeds (negative values of speedx will move the smiley face left; positive values will move it right). So when the smiley hits the right edge of the screen, we turn speedx negative to make the image go back toward the left, and when it hits the left edge of the screen, we turn speedx back to a positive value to rebound the image to the right.

image with no caption

Let’s do the same thing with picy:

if picy <= 0 or picy + pic.get_height() >= 600:

speedy = -speedy

To test whether our smiley has hit the top edge of the screen, we use picy <= 0, which is similar to picx <= 0 for the left edge. To figure out whether our smiley has hit the bottom edge of the screen, we need to know both the height of the drawing window (600 pixels) and the height of the image (pic.get_height()), and we need to see if the top of our image, picy, plus the image’s height, pic.get_height(), totals more than the height of our screen, 600 pixels.

If picy goes outside these top and bottom boundaries, we need to change the direction of the vertical speed (speedy = -speedy). This makes the smiley face look like it’s bouncing off the bottom edge of the window and heading back up, or bouncing off the top and heading back down.

Putting It All Together

When we put the whole program together in SmileyBounce2.py, we get a convincing bouncing ball that is able to rebound off all four edges of the screen for as long as we run the app.

SmileyBounce2.py

import pygame # Setup

pygame.init()

screen = pygame.display.set_mode([800,600])

keep_going = True

pic = pygame.image.load("CrazySmile.bmp")

colorkey = pic.get_at((0,0))

pic.set_colorkey(colorkey)

picx = 0

picy = 0

BLACK = (0,0,0)

timer = pygame.time.Clock()

speedx = 5

speedy = 5

while keep_going: # Game loop

for event in pygame.event.get():

if event.type == pygame.QUIT:

keep_going = False

picx += speedx

picy += speedy

if picx <= 0 or picx + pic.get_width() >= 800:

speedx = -speedx

if picy <= 0 or picy + pic.get_height() >= 600:

speedy = -speedy

screen.fill(BLACK)

screen.blit(pic, (picx, picy))

pygame.display.update()

timer.tick(60)

pygame.quit() # Exit

The rebounds look realistic. If the smiley is coming toward the bottom edge at a 45-degree angle down and to the right, it bounces off at a 45-degree angle up and to the right. You can experiment with different values of speedx and speedy (say, 3 and 5, or 7 and 4) to see the angles change for every bounce.

Just for fun, you can comment out the line screen.fill(BLACK) in SmileyBounce2.py to see the path traveled by our smiley face as it bounces off each edge of the screen. When you comment out a line, you turn it into a comment by putting a hash mark at the beginning, as follows:

# screen.fill(BLACK)

This tells the program to ignore the instruction on that line. Now the screen is not erased after each smiley face is drawn, and you’ll see a pattern created by the trail your animation is leaving behind, like in Figure 8-9. Because each new smiley is drawn over the previous one, the result looks like cool, retro 3-D screensaver artwork as it draws.

Our collision-detection logic has allowed us to create the illusion of a solid smiley face bouncing off all four edges of a solid drawing screen. This is an improvement over our original version, which let the smiley slide off into oblivion. When we create games that allow the user to interact with pieces on the screen, and that allow those pieces to look as if they’re interacting with one another — like in Tetris, for example — we’re using the same kind of collision detection and boundary checking that we built here.

If we comment out the line that clears our screen after each frame, our smiley face leaves a bouncing trail behind in a cool pattern.

Figure 8-9. If we comment out the line that clears our screen after each frame, our smiley face leaves a bouncing trail behind in a cool pattern.

What You Learned

In this chapter, you learned how to create the illusion of motion, what we call animation, by drawing images in different locations on the screen over time. We saw how the Pygame module can make programming a game or animation much quicker, since it has hundreds of functions that can make almost everything in a game app easier, from drawing images to creating timer-based animation — even checking for collisions. We installed Pygame on our computer so we could use its features to create fun apps of our own.

You learned about the structure of a game or app that we might build in Pygame, with a setup section; a game loop that handles events, updates and draws graphics, and then updates the display; and finally an exit section.

We started our Pygame programming by drawing a simple green dot on the screen at a chosen location, but we quickly moved on to drawing a picture from an image file on disk, saved in the same folder as our program, to our display screen. You learned that Pygame has a different coordinate system from the Turtle library, with the origin (0, 0) in the upper-left corner of the screen and positive y-coordinate values as we move down.

You learned how to create animation by drawing objects on the screen, clearing the screen, and then drawing the objects in a slightly different location. We saw that the pygame.time.Clock() object could make our animations steadier by limiting the number of times our animation draws each second, which is called the frames per second, or fps.

We built our own collision detection to check for objects “hitting” the edge of the screen, and then we added the logic to make objects look like they’re bouncing back by changing the direction of their speed or velocity variables (by multiplying them by –1).

Programming the cool apps in this chapter has given us the skills to do the following:

§ Install and use the pygame module in our own Python programs.

§ Explain the structure of a Pygame app, including the setup, game loop, and exit.

§ Build a game loop that handles events, updates and draws graphics, and updates the display.

§ Draw shapes to the screen using pygame.draw functions.

§ Load images from disk with pygame.image.load().

§ Draw images and objects to the screen with the blit() function.

§ Create animations by drawing objects to the screen repeatedly in different locations.

§ Make animations smooth, clean, and predictable using a pygame.time.Clock() timer’s tick() function to limit the number of frames per second in our animations.

§ Check for collision detection by building the if logic to check for boundary cases, like a graphic hitting the edge of the screen.

§ Control the horizontal and vertical speeds of moving objects on the screen by changing the amount of movement in the x- and y-directions from one frame to the next.

PROGRAMMING CHALLENGES

Here are three challenge problems to extend the skills you developed in this chapter. For sample answers, go to http://www.nostarch.com/teachkids/.

#1: A COLOR-CHANGING DOT

Let’s explore RGB color triplets further. We worked with some RGB colors in this chapter; remember, green was (0,255,0), black was (0,0,0), and so on. At http://colorschemer.com/online/, enter different red, green, and blue values from 0 to 255 to see the colors you can create by combining different amounts of red, green, and blue light from your screen’s pixels. Start by choosing your own color triplet to use in the ShowDot.py program. Then modify the program to draw the dot larger or smaller and at different locations on the screen. Finally, try creating a random RGB color triplet using random.randint(0,255) for each of the three color components (remember to import random at the top of your program) so that the dot changes colors every time it draws on the screen. The effect will be a color-changing dot. Call your new creation DiscoDot.py.

#2: 100 RANDOM DOTS

As a second challenge, let’s replace the single dot with 100 dots in random colors, sizes, and locations. To do this, let’s set up three arrays capable of storing 100 values each for the colors, locations, and sizes:

# Colors, locations, sizes arrays for 100 random dots

colors = [0]*100

locations = [0]*100

sizes = [0]*100

Then, fill those three arrays with random color triplets, location pairs, and size/radius values for 100 random dots:

import random

# Store random values in colors, locations, sizes

for n in range(100):

colors[n] = (random.randint(0,255),random.randint(0,255),

random.randint(0,255))

locations[n] = (random.randint(0,800),

random.randint(0,600))

sizes[n] = random.randint(10, 100)

Finally, instead of drawing one dot in our while loop, add a for loop to draw the 100 random dots by using the colors, locations, and sizes arrays:

for n in range(100):

pygame.draw.circle(screen, colors[n], locations[n],

sizes[n])

Call your new creation RandomDots.py. The final app should look something like Figure 8-10 when complete.

An advanced version of our dot program, RandomDots.py, gives us 100 dots of random color, location, and size.

Figure 8-10. An advanced version of our dot program, RandomDots.py, gives us 100 dots of random color, location, and size.

#3: RAINING DOTS

Finally, let’s take RandomDots.py one step further by programming the dots to “rain” off the bottom and right sides of the screen and reappear along the top and left. You’ve learned in this chapter that we create animation by changing the location of an object over time. We have the location of each dot in an array called locations, so if we change each dot’s x- and y-coordinates, we can animate our dots. Change the for loop from RandomDots.py to calculate a new x- and y-coordinates for each dot based on the previous value, like this:

for n in range(100):

pygame.draw.circle(screen, colors[n], locations[n],

sizes[n])

new_x = locations[n][0] + 1

new_y = locations[n][1] + 1

locations[n] = (new_x, new_y)

This change calculates new x- and y-coordinates (new_x and new_y) for each dot every pass through the game loop, but it lets the dots fall off the right and bottom edges of the screen. Let’s fix this by checking whether each dot’s new_x or new_y is beyond the right or bottom edges of the screen and, if so, move the dot back up or back to the left before we store the new location:

if new_x > 800:

new_x -= 800

if new_y > 600:

new_y -= 600

locations[n] = (new_x, new_y)

The combined effect of these changes will be a steady flow of random dots “raining” down and to the right, disappearing off the bottom right of the screen and popping back up on the top or left edge. Four frames in this sequence are shown in Figure 8-11; you can follow groups of dots as they move down and to the right across the three images.

Save your new app as RainingDots.py.

Four frames showing 100 random dots as they move right and down across the screen

Figure 8-11. Four frames showing 100 random dots as they move right and down across the screen