The zombie apocalypse has arrived. You can run, but you can’t escape. All you can do is run around until you get tired. It’s a sad situation, but it’s the first step in programming a realistic game.
Interactive gameplay Keyboard events Inheritance Collision detection Custom events
Make sure you are in your virtual environment for this course with Pygame installed. Create the file ~/cmsi1010/lab15/ktah.py. Prime it with the skeleton code for a game that we will call K'tah. It is going to be a shadow of the famous K'tah, which you can find on GitHub.
import pygame pygame.init() WIDTH, HEIGHT = 1024, 768 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("K'tah") clock = pygame.time.Clock() def draw_scene(): screen.fill((0, 100, 0)) pygame.display.flip() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() raise SystemExit clock.tick(60) draw_scene()
This is the basic shell of any Pygame application which uses animation. We saw this in the previous lab, but things don’t always stick until you’ve done them a few times, so take the time to review each line in the code to make sure you follow.
Now let’s get a “game” going. We’re going to have a player that is chased by four zombies. Before we set up the chasing part, let’s get the player and the zombies defined and on the screen.
Here’s an idea that is so powerful in computer science, computational thinking, and data modeling: there are often things (in our case players and zombies), that are at once kind of the same and kind of different. They have some things in common but not everything. The parts that are the same are: they have a position and size and color and that they can move and be drawn. Whenever we see such a situation we see if we can get a handle the common attributes and make an abstraction for them. In our case, players and zombies are both kinds of agents. So let’s define a type for agents (that will be circles for now):
@dataclass
class Agent:
x: int
y: int
radius: int
speed: int
color: tuple
def draw(self):
pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius)
Remember to write from dataclasses import dataclass at the top of your file.
Next, we will define two kinds of agents: players and zombies. Players will have defaults for everything: positioned at the center of the canvas, have a radius of 20 pixels, move at 5 pixels per frame, and appear light blue. (We saw default attributes in the last lab.) Players will also have a superpower: they can teleport to another position. Add this dataclass:
@dataclass class Player(Agent): x: int = WIDTH // 2 y: int = HEIGHT // 2 radius: int = 20 speed: int = 5 color: tuple = (200, 200, 255) def teleport(self, pos): self.x, self.y = pos
Zombies will not have defaults for x, y, radius, or speed. Those attributes will have to be set on creation, but we will allow defaults for the other three attributes:
@dataclass class Zombie(Agent): speed: int = 2 radius: int = 20 color: tuple = (80, 255, 0)
Vocabulary Time! Agent is the supertype and Player and Zombie are its subtypes. This means every player IS-A agent and every zombie IS-A agent. You add supertypes to a type when you declare it—look at the definitions of Player and Zombie above and make sure you follow the syntax. When you make a type a subtype of an existing type, all the supertype’s attributes and methods are inherited by the subtype. It’s pretty convenient, and it makes sense too: since a player is an agent and a zombie is an agent, everything an agent can do, a player and zombie can too—by definition!
Agenthas a supertype itself, even though we didn’t specify it directly. Find out what it is and why it is so special.
Now we can create a player and some zombies. We’ll put the zombies near the corners of the display, and set them up with different speeds, but none will be greater than the player’s speed, since we want to give the player a fighting chance:
player = Player() zombies = [ Zombie(x=20, y=20, speed=1.8), Zombie(x=WIDTH-20, y=20), Zombie(x=20, y=HEIGHT-20, speed=2.5), Zombie(x=WIDTH-20, y=HEIGHT-20, speed=0.9)]
These lines of code only create five objects, but to display the objects, we have to call their draw methods inside our game loop. Update draw_scene to:
def draw_scene(): screen.fill((0, 100, 0)) player.draw() for zombie in zombies: zombie.draw() clock.tick(60) pygame.display.flip()
Run the program now and make sure you see the five circles.
player.draw() and zombie.draw() even though neither class Player nor Zombie defined draw methods. Do you see now why we use the term inheritance in the context of supertype-subtype relationships?
In our game, the player moves toward the mouse and the zombies move toward the player. But wait! Can we generalize this? Both players and agent move toward...something! So let’s create a move_towards method in the common superclass. Add this code to Agent:
def move_towards(self, target): dx = target[0] - self.x dy = target[1] - self.y distance = math.hypot(dx, dy) if distance > 3.0: # Allow three pixels of leeway to avoid jittering self.x += (dx / distance) * self.speed self.y += (dy / distance) * self.speed
OMG MATH!Don’t be scared. We’ll explain in class.
Now just add this code at the very beginning of draw_scene:
player.move_towards(pygame.mouse.get_pos()) for zombie in zombies: zombie.move_towards((player.x, player.y))
Run the program again. You should see the player circle move toward the mouse and the zombie circles move toward the player circle.
pygame.mouse.get_pos() come from? It turns out get_pos is one of many things defined within the module pygame.mouse. Browse the Pygame mouse module documentation to find out what else you can do with the mouse.
We’ve made progress, but we don’t have a game yet. If the zombies crash into the player, nothing happens. Let’s fix that.
We need to detect when the player and a zombie collide. We can do this by checking the distance between the player and each zombie. If the distance is less than the sum of their radii, then they have collided. Collision can in theory be computed for any agent, so let’s add a method to the Agent class:
def is_collided_with(self, other): distance = math.hypot(self.x - other.x, self.y - other.y) return distance < (self.radius + other.radius)
Breaking this downWe crafted this method to be both readable and flexible. We call it as
a1.is_collided_with(a2), which isTrueif and only if agenta1has collided witha2, namely that their circles overlap.How this code actually determines “overlap” will be discussed in class.
Now we are really close to implementing the important part of the game, which is to make sure a player does not get caught by (collides with) any of the zombies. Continuing with our powerful ideas of implementing small bits of functionality with great function (or method) names, we realize that “getting caught” is something that applies to a player, so let’s add this method to the player class:
def is_caught_by_any_of(self, zombies): for zombie in zombies: if self.is_collided_with(zombie): return True return False
Why was that method so long?When learning how to program, it’s common to see lots of for-loops and if-statements. The code above is direct and readable. With very little exposure to, and training in, Python, it just makes sense. But yes, there are more concise ways to do these kinds of things. The entire body of this function can be the one line:
return any(self.is_collided_with(zombie) for zombie in zombies)Before you use this kind of code in your projects, make sure you understand what it does.
Back in draw_scene, let’s make it so the animation “stops” when there’s a collision. By stopping we simply mean there’s no more movement. Add this to the beginning of draw_scene:
if player.is_caught_by_any_of(zombies): return
Now run the program and let yourself get caught. Everything just stops. Perhaps that is good enough? but no, we have much more to learn.
The first enhancement to the game is a relatively easy one. If you click anywhere in the window the player immediately teleports to the click position.
How to think about it: Pygame sends us events. We know about the MOUSEBUTTONDOWN event from earlier labs. We just have to handle it by teleporting the player to the current mouse position. We already defined the teleport method, so we just need to call it in the event loop. We add this code (after the full handler for the QUIT event):
if event.type == pygame.MOUSEBUTTONDOWN: player.teleport(event.pos)
Pressing the f key will “freeze” the zombies for 5 seconds. This allows the player a short amount of time to move farther away.
How to think about it: We need a variable frozen, initially false. If true, the zombies should not move. When the user press the f-key, detected through the Pygame event KEYDOWN with event.key == K_f, we set frozen to true only if is not already true! If we do set it, we will create a once-only-timer that will expire in five seconds, and send a custom event which we’ll call UNFREEZE. Handling this event will set frozen back to False (enabling the zombies to automatically move again).
In code:
frozen = False UNFREEZE = pygame.USEREVENT + 1
MOUSEBUTTONDOWN handler:
elif event.type == pygame.KEYDOWN: if event.key == pygame.K_f: if not frozen: frozen = True pygame.time.set_timer(UNFREEZE, 5000, loops=1) elif event.type == UNFREEZE: frozen = False
draw_scene method, only move the zombies if frozen is False:
if not frozen: for zombie in zombies: zombie.move_towards((player.x, player.y))
Try it!
Custom EventsWe can create our own events in Pygame. We just have to give them event identifiers starting at
USEREVENT+1. You can find details in the Pygame event documentation page.
Pressing the s key will set a scarecrow at the current mouse position, causing the zombies, for the next 5 seconds, to not chase the player but to move toward the scarecrow. This also gives the player a short breather to get away.
How to think about it: Create a variable called scarecrow set to the position of the scarecrow if present or None if not. When the user presses the s-key, we set the scarecrow to the current mouse position and start a timer that will remove it after 5 seconds. In the draw_scene method, if there is a scarecrow, we draw it and move the zombies toward it instead of toward the player.
scarecrow = None REMOVE_SCARECROW = pygame.USEREVENT + 2
In the event loop, in the KEYDOWN handler, slip in handling for the s-key under the f-key:
elif event.key == pygame.K_s: if scarecrow is None: scarecrow = (player.x, player.y) pygame.time.set_timer(REMOVE_SCARECROW, 5000, loops=1)
UNFREEZE handler:
elif event.type == REMOVE_SCARECROW: scarecrow = None
draw_scene method, adjust the movement of the zombies:
for zombie in zombies: if not frozen: target = scarecrow or (player.x, player.y) zombie.move_towards(target)
if scarecrow is not None: pygame.draw.circle(screen, (255, 200, 0), scarecrow, 20)
This has been a long lab, so we’ll do just one more thing: write the words “GAME OVER” in red text when the player is caught. To do this, (1) add the import line import pygame.freetype to your import section, (2) under your assignment to the clock variable, create a font object: font = pygame.freetype.SysFont('sans', 100), and change the top of the draw_scene function to display the text:
if player.is_caught_by_any_of(zombies): font.render_to(screen, (20, 20), "GAME OVER", (255, 0, 0)) pygame.display.flip() return
Finally, on to the challenges.
Now it’s your turn. There are so many ways to build on the lovely little game stub we just made together. Here are just a few ideas. Use your creativity to come up with additional extensions.
render_to as in the main lab. You’ll definitely want a smaller font than the one used to message that the game is over!Feel free to bring in new ideas of your own.
Now that we’ve officially studied inheritance, Chapter 18 of Think Python, 2nd edition has a short treatment. You can also read about inheritance in the official Python tutorial.
Inheritance, which comes from creating hierarchies, or ontologies, of classes can be sometimes very useful, but be careful not to overuse it. Please read Clay Shirky’s remarkable essay Ontology is Overrated.
The “game” in this lab barely meets the requirements of being a game. There is so much more to the field of game design! You might like to read The Art of Game Design by Jesse Schell to learn more.
You may also be interested in some K'Tah-style projects made by students in the past, when this course used the much more game-friendly and game-accessible JavaScript language. Out of the nearly 100 past submissions, here are four fun ones:
We’ve covered:
math.hypotHere are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
KEYDOWN event.key that indicates which key was pressed.pygame.mouse.get_pos().math.hypot(x2 - x1, y2 - y1).pygame.USEREVENT constant and add an integer to it to create a unique event identifier. For example, STOP = pygame.USEREVENT + 1.pygame.time.set_timer(STOP, 10000).pygame.time.set_timer(STOP, 10000, loops=1).pygame.freetype module.font.render_to(screen, (100, 100), "Hello world", (255, 0, 0)).