Writing a game in Python with Pygame. Part II

December 20th, 2008 at 12:34 pm

This is part II of the "Writing a game in Python with Pygame" tutorial.

Welcome back

In the first part of this tutorial we’ve created a simple simulation of "Creeps" – round creatures moving around the screen and bouncing off walls. Not much of a game there, but a good start nonetheless.

In this part, we are going to extend this simulation, making it much more game-like. It is not the final step, of course. The final product of this part is still going to be far from a real, interesting game, but many useful game programming concepts will be introduced, and the simulation will definitely have much more feeling of a game in it.

Here’s a teaser screenshot of the final product of this part:

http://eli.thegreenplace.net/wp-content/uploads/2008/12/creeps_screenshot_2.png

The code

The full code for this part can be downloaded from here. As before, it is highly recommended to download it, run it and have it open in the editor while reading this tutorial.

Goals for this part

In this part I’m going to cover:

  • A prettier background for the game
  • Responding to user events
  • A more complex internal state for the creeps
  • Simple animation
  • Rendering text

So let’s get started.

Background

In the first part we’ve just splashed a bucket of greenish swamp-like color onto the screen and called that a background. Since we want the game to be a bit more appealing, this won’t do any longer.

We’ll now tile a pretty background image onto the screen, and create a bounded "game field" for the creeps to roam in.

What is tiling, you ask? Tiling, in simple terms, is taking a small surface and repeating it in a pattern until a larger surface is covered. In our case, we’ll take this image:

http://eli.thegreenplace.net/wp-content/uploads/2008/12/brick_tile.png

And tile it in a simple repeating-row pattern. The code doing it is in the draw_background function:

def draw_background(screen, tile_img, field_rect):
    img_rect = tile_img.get_rect()

    nrows = int(screen.get_height() / img_rect.height) + 1
    ncols = int(screen.get_width() / img_rect.width) + 1

    for y in range(nrows):
        for x in range(ncols):
            img_rect.topleft = (x * img_rect.width,
                                y * img_rect.height)
            screen.blit(tile_img, img_rect)

    field_color = (109, 41, 1)
    draw_rimmed_box(screen, field_rect, field_color, 4, Color('black'))

The loop does the tiling. The last line of the function creates the playing field – a dark-brown filled rectangle to which the creeps will be restricted. The field is drawn using the utility function draw_rimmed_box – it’s very simple, and you can study it on your own [1].

In the game screenshot above you can also see another box with some text on the right. This is drawn separately, and we’ll get to it soon enough.

Hey, you’ve clicked me!

So far the only user event our game has responded to was closing closing the game window. Not much interaction there, so we’ll pump it up. Here’s the new event handler in our main loop:

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        exit_game()
    elif event.type == pygame.KEYDOWN:
        if event.key == pygame.K_SPACE:
            paused = not paused
    elif (  event.type == pygame.MOUSEBUTTONDOWN and
            pygame.mouse.get_pressed()[0]):
        for creep in creeps:
            creep.mouse_click_event(pygame.mouse.get_pos())

A couple of things were added. First, there’s a handler for the user’s pressing the space key on the keyboard. This flips the "paused" state of the game – try it now.

The second handler is only slightly more complex. When a left mouse button is clicked inside the application, each creep gets its mouse_click_event method called with the mouse click coordinates.

The idea is simple: creeps have health, and we can decrease their health by successfully clicking on them. A health bar is drawn above each creep showing its health as a proportion of red to green (click on some creeps to see their health decrease).

The implementation is also quite simple. Here’s the mouse click handler of a creep:

def mouse_click_event(self, pos):
    """ The mouse was clicked in pos.
    """
    if self._point_is_inside(vec2d(pos)):
        self._decrease_health(3)

You see that when the click was found to be inside the creep, its health is decreased. Let’s see how the click inside the creep is detected:

def _point_is_inside(self, point):
    """ Is the point (given as a vec2d) inside our creep's
        body?
    """
    img_point = point - vec2d(
        int(self.pos.x - self.image_w / 2),
        int(self.pos.y - self.image_h / 2))

    try:
        pix = self.image.get_at(img_point)
        return pix[3] > 0
    except IndexError:
        return False

This method detects if the click is inside the creep. More specifically, inside the solid area of the creep’s image. Clicking inside the creep’s bounding box but outside its body won’t result in True. Here’s how it’s done:

First, the click point is recomputed to be relatively to the creep’s image. If the point isn’t inside its image on the screen, there’s nothing to talk about. If it is inside, we still don’t know if it’s in the solid region. For this purpose, the pixel at the point of the click is examined. If the alpha constituent of the point is positive, this is part of the creep’s body. Otherwise, it’s just part of its bounding box but outside the body (see [2]).

Drawing the health bars is very simple, and you should be able to understand the code of the draw method (which replaces blitme from the code of part I) that implements this.

Simple animation

What happens when the creep’s health goes down to 0? I hope you’ve already experimented with the game and saw it, but if you didn’t, here’s a screenshot:

http://eli.thegreenplace.net/wp-content/uploads/2008/12/explode1.png

If you’ve played with the game, however, you’ve surely noticed that the explosion the creep undergoes is animated – it’s changing with time.

What is an animation? In its simplest form, it is a sequence of images that are drawn one after another in the same location, creating the appearance of movement. It’s not unlike our creeps moving on the screen (you can see the whole game as an animation, really), but for the sake of this part I’m specifically referring to a static animation that stays in the same place.

The animation is implemented in the module simpleanimation.py which you can find in the downloaded code package. You can experiment with it by running it standalone (the module uses Python’s if __name__ == "__main__" feature to allow stand-alone running).

The code should be very simple to understand, because there’s nothing much to it. The SimpleAnimation class receives a list of image objects and draws them to the screen with the given period and duration. Note how the explosion is simulated by taking the same image, rotating it by 90 degrees and using SimpleAnimation to change the two in rapid succession.

Back in creeps.py, our creep uses SimpleAnimation to show its own explosion after its health has reached 0:

def _explode(self):
    """ Starts the explosion animation that ends the Creep's
        life.
    """
    self.state = Creep.EXPLODING
    pos = ( self.pos.x - self.explosion_images[0].get_width() / 2,
            self.pos.y - self.explosion_images[0].get_height() / 2)
    self.explode_animation = SimpleAnimation(
        self.screen, pos, self.explosion_images,
        100, 300)

It’s very straightforward, really.

Creep state

The creeps of this part are much more complex than of part I. They have health which can decrease, and they can explode and disappear. To manage this complexity, we’re going to use a state machine [3].

What states can the creep be in? A normal state, when the creep is roaming around, an exploding state in which the creep is replaced by the explosion animation, and an inactive state, in which the creep no longer functions. These are coded as follows:

(ALIVE, EXPLODING, DEAD) = range(3)

See the code of update for state management – the creep is updated differently, depending on which state it’s in. The same is true for the draw method. It’s a good idea now to search for self.state throughout the code, taking note of where the state is modified, and where it is used.

Displaying textual information

When you run the game (or in the large screenshot at the top of this part), you’ll see a simple scoreboard in the top right corner of the screen. It counts the amount of active creeps on the screen, and will also display an exciting message when you’ve killed all the creeps.

This display is implemented in the function draw_messageboard – study its code now, it should be quite simple to understand in conjunction with the docs.

Sprites and sprite Groups

I hope you’ve noticed that in both parts of the tutorial, the Creep class derives from pygame.sprite.Sprite. Sprite is a utility class of Pygame that implements some useful common methods for managing the animated images that represent the actors of the game (known in game programming jargon as sprites).

In the first part I didn’t use any of its capabilities at all. Here, I’m using its capability of being collected into sprite Groups.

The list of creeps in the main function has now turned into a sprite group. The cool thing is that whenever a sprite is added to a group, it keeps track of which groups it’s in, so calling self.kill() in a sprite causes it to be removed from all the groups and thus from the game. The update method of Creep calls kill() when the explosion has ended. This way, the main loop doesn’t have to explicitly keep track of which sprites are active in the group – they do it themselves.

That said, I’m still not sure I’m going to use the full capabilities of Sprites. For me, they’re just a guideline, not a must. Perhaps I’ll find out later that my code can be structured better with them. Or perhaps I’ll see I don’t need them at all. We’ll see [4].

Wrapping up

All the goals stated in the beginning of this part were achieved, so that’s it, for now. We’ve turned the simplistic creeps simulation into something resembling a rudimentary game. True, it’s more likely to pave your way to severe RSI than cause you any serious fun, but it’s a simple game nonetheless. Not bad for just 450 lines of Python code!

In future parts of this tutorial, we’ll continue developing the code on our way to a real game, so stay tuned. Oh, and give the exercises a go, I guarantee you they will make your understanding of the material much deeper.

http://eli.thegreenplace.net/wp-content/uploads/hline.jpg

Exercises

  1. Try to pause the game (press SPACE), click a couple of times on a creep, and resume the game. Notice that the creep’s health decreased? Try to fix this, i.e. block mouse events during the pause.
  2. Can you see that when a creep is facing diagonally, his health bar is a bit farther from his body than when he’s facing up or to the side? Can you figure out why? (Hint: re-read the section of part I dealing with the size of the rotated image). Propose ways to fix this.
  3. Review the code of the _explode method. Note the complex computation of the explosion’s position. Why is it needed? Try to modify it (for example, removing the consideration of the explosion image’s size) and observe the difference.
  4. Add a running clock to the scoreboard. It should begin at 00:00 and advance by 1 each second. When the game ends, the clock should stop.
  5. Set the creeps’ speed to higher than the default and attempt to catch them. It’s quite challenging!
http://eli.thegreenplace.net/wp-content/uploads/hline.jpg
[1] Note also the usage of the Color module to specify color. This is a useful PyGame module, and I’ve changed almost all the hard-coded color references in the code to named colors.
[2] The images of creeps are stored in PNG files that support alpha transparency. If you examine the image with an image editor that supports transparency (Paint.NET or GIMP, for example), you’ll see that the area outside the creep’s round body is transparent. This is used to blend the creep in a pleasing way into its background, and can also be employed to detect what’s inside and what’s outside of the creeps’s body.
[3] I assume you know what a state machine is, and if you don’t, Wikipedia and Google are your friends.
[4] To learn more about Pygame sprites, read this tutorial.

Related posts:

  1. Writing a game in Python with Pygame. Part I
  2. Writing a game in Python with Pygame. Part IV
  3. Writing a game in Python with Pygame. Part III
  4. Book review: “Beginning game development with Python and Pygame” by Will McGugan
  5. Finding out the mouse click position on a canvas with Javascript

11 Responses to “Writing a game in Python with Pygame. Part II”

  1. André RobergeNo Gravatar Says:

    Nice series!

    A small nit: you don’t need the parentheses when assigning the states. You can simply write

    ALIVE, EXPLODING, DEAD = range(3)

    or you can skip range altogether and write

    ALIVE, EXPLODING, DEAD = 0, 1, 2

    which may be less confusing to Python beginners since range() returns a list (before Python 3.0) and we have tuple being seemingly made equal to a list object.

  2. AaronNo Gravatar Says:

    @Andre: Just a note of clarification for those learning python:

    the expression “(x, y, z) = 1, 2, 3″ is not a tuple being assigned to a tuple… it’s a tuple (on the right hand side) being assigned in a multiple assignment statement.

    Python evaluates variables immediately, so in an _expression_, “x,y,z” will immediately evaluate the present value of x, and the other variables, and store the results in a tuple… once stored in the tuple, the tuple no longer refers to the variable x in any way, so it can’t, for instance, assign something back to x.

    Feel free to try this out at the interactive prompt:
    >>> x,y,z = 1,2,3
    >>> x
    1
    >>> tup = x,y,z
    >>> tup
    (1, 2, 3)
    >>> tup = 7,8,9
    >>> x
    1
    >>> *tup = 7,8,9
    File "<stdin>", line 1
    *tup = 7,8,9
    ^
    SyntaxError: invalid syntax
    >>>

  3. ZachNo Gravatar Says:

    great tutorial… a week ago i decided i wanted to learn this sort of thing, a quick google search and i find this great guide :-)

    keep up the good work!

  4. jeff (Game Talk)No Gravatar Says:

    Thank you very much great video game.

  5. Trainer DanNo Gravatar Says:

    Eli

    This is great. So detailed and yet very clearly explained. I’ve said it before and I’ll say it again. You should be a trainer. The way you write shows you have a natural empathy with learners. That’s a gift not everyone has.

    Ben

  6. ElephantIncNo Gravatar Says:

    When ever I run this I get the error: AttributeError: ‘str’ object has no attribute ‘get_rect’
    this is referring to to the line: img_rect = tile_img.get_rect()
    Im using python 2.5.2 and the latest version of pygame on mac OSX 10.4.11
    What could the problem be? (it occurs in both your code and mine)

  7. JohnnyNo Gravatar Says:

    These tutorials are fantastic. However, when I try to run the creeps example from this week, Python Launcher quits, and after spitting a bunch of code at me, I read:

    UnboundLocalError: local variable ‘fonts’ referenced before assignment

    Anybody know how to fix this? Thanks!

  8. elibenNo Gravatar Says:

    @Johnny, what version of Python and Pygame are you using? These tutorials were developed with Python 2.5.2 and Pygame 1.8.1 on Windows

  9. JohnnyNo Gravatar Says:

    Hello Eli!
    I am on a MAC running 10.6.5, using Python 2.5.4 and pygame 1.9.1. I was able to fix this problem by changing this line:

    my_font = pygame.font.SysFont(‘arial’, 20)

    to this:

    my_font = pygame.font.Font(None, 20)

    However, I’ve moved on to other tutorials and realized the new creeps game has a different setup for the font coding. By changing the lines in the widget.py file to pygame.font.Font(None, 20) I fixed the problem once more, though trying any particular style gets me this –

    my_font = pygame.font.Font(‘Arial’, 20)
    IOError: unable to read font filename

    Any help would be appreciated! Also any advice for exercise 1 of Part 2 of this tutorial? I can get the mouse events blocked once I hit pause, but not back on again once I unpause!

    Thank you so much, and keep writing tutorials – these are great!

  10. elibenNo Gravatar Says:

    Johnny,

    Thanks for these tips. I don’t have a Mac machine to test it on, but I hope your comment will be useful for other people running into the same problem.

    As for exercise 1: strange. Can you successfully block/unblock mouse events, regardless of the pausing of the game? “Blocking” in this context may also mean just ignoring them once you’re in certain state of the game.

  11. JasonNo Gravatar Says:

    Cheat: Pause the game, and click on the creeps repeatedly. :-)