Chapter 7. Animation with Sprites: The Escape the Dragon Game

The previous chapter was a pretty good introduction to bitmap graphics programming. We learned that Pygame has a lot of good features for working with bitmaps. But, aside from the capabilities of the pygame.transform module, which includes scaling and rotation of bitmaps, there is no practical way to do animation with it. This is where the pygame.sprite module takes over, and that is the subject we’re learning about in this chapter.

In this chapter, you will learn to:

  • Manually animate a sprite with a special calculation

  • Use features in the pygame.sprite module

  • Make a game called Escape the Dragon!

Examining the Escape The Dragon Game

The sample game in this chapter will help you to understand sprite programming with Python and Pygame. The premise of the game is very simple: a dragon is chasing your character, so you must jump over flaming arrows coming toward you so that they hit the dragon and stop it from chasing you. The concept comes from one of the mini-games in the Facebook game Ninja Wars.

The Escape the Dragon Game features animated sprites.

Figure 7.1. The Escape the Dragon Game features animated sprites.

Using Pygame Sprites

The pygame.sprite module contains a class called Sprite that we can use as a starting point for our game sprites. I say starting point because pygame.sprite.Sprite is not a complete solution, it’s just a limited class that knows how to work with groups to update and draw itself. Even that is a bit of a stretch, given that we have to write the code to do these things. From the most objective point of view, a Pygame sprite contains an image (image) and a position (rect). We have to extend this with our own class to provide the features we want in a fully functional game sprite class.

Custom Animation

Animation with Pygame is a bit tricky, only because we have to know how pygame.sprite.Sprite works in order to write our own animation code. A Pygame sprite will be based around its image and rect properties, as already mentioned, so the trick is to wrap animation code around these two properties. When that is done, then the sprite group will automatically update the animation frame image and draw the specific frame (rather than the whole sprite sheet image). Let’s look at a sprite sheet image first to get an idea how this is going to work. Figure 7.2 shows just such an image.

The dragon sprite image has six frames of animation. Courtesy of Ari This was cutoff on my printout.

Figure 7.2. The dragon sprite image has six frames of animation. Courtesy of Ari This was cutoff on my printout.

Note

The dragon sprite image has six frames of animation. Courtesy of Ari This was cutoff on my printout.

The animated dragon sprite was drawn by Ari Feldman (www.flyingyogi.com). You can download his collection of free game sprites, called SpriteLib, from his website.

A sprite sheet image contains rows and columns of “tiles” or “frames,” each of which is one frame of the animation sequence. Figure 7.3 shows one frame highlighted in a sprite sheet with the rows and columns labeled for easier reference. Note that they are 0-based! This is important, as the calculations rely on counting the frame number starting at zero, not one.

Illustration of the rows and columns in a sprite sheet image.

Figure 7.3. Illustration of the rows and columns in a sprite sheet image.

The sprite sheet images shown here will be loaded and retained as the master image used for animation. While a sprite is being moved and drawn in a game, a call to the update() method will be made by the sprite group automatically, as is the call to draw(). We can write our own update() method, but draw() is not replaced, it’s passed on to the parent pygame.sprite.Sprite.draw() method. What we must do is make sure the image property of pygame.sprite.Sprite contains the image of the current frame of the animation, not the whole sprite sheet. Because of the way this works, the sprite sheet (master image) will be loaded as an independent class variable, not loaded directly into Sprite.image.

Loading a Sprite Sheet

When we load the master image, we must tell our sprite class how large one frame is—that is, the width and height of a single frame are passed as parameters when a new sprite is created. Usually the most sensible name for the method is load(), and typically it will have a filename parameter. In addition to frame width and height, we must also tell our sprite class how many columns there are in the sprite sheet. Take a look at the illustration in Figure 7.3 again for reference. Note that the highlighted frame is under column 3. This is really all we need to know, the number of columns, because the number of rows does not matter in the calculation to draw a single frame.

Let’s try writing a function that will get the job done of loading an image and setting a sprite’s properties. The function definition below requires a filename, width, height, and columns as parameters. These are the bare essentials for doing sprite animation. We’ll peruse a complete class listing after going over the theory behind these concepts, so don’t worry about typing in any of this code just yet.

    def load(self, filename, width, height, columns):
        self.master_image = pygame.image.load(filename).convert_alpha()
        self.frame_width = width
        self.frame_height = height
        self.rect = 0,0,width,height
        self.columns = columns

Changing the Frame

Normally, animation proceeds one frame at a time from first to last. A more advanced animation system will allow a sprite to animate forward, backward, and within any specified range of the animation set. We’ll keep it simple by just animating from first to last frame, then wrapping around to the first frame again. This is pretty easy to write in code:

    self.frame += 1
    if self.frame > self.last_frame:
        self.frame = self.first_frame

The trick is not so much changing the frame number, but making that happen at a certain time interval. Yes, we have to use timing code! It’s a bit of a challenge to wrap your mind around at first. At least, it was for me! But once you learn the basic Python code for getting the current time value in ticks, then the rest of the process is pretty easy to handle.

First, we need to create an object variable from pygame.time.Clock(). I have called my variable framerate:

framerate = pygame.time.Clock()

When this Clock() method is called, it starts an internal timer running from that point forward that we can use to get incremental time update values, with even the option to set the game running at a fixed framerate. Inside the main while loop in a game, then, call:

    framerate.tick(30)

The parameter, 30, can be set to any desired framerate. It does a pretty good job of keeping the game running at this speed, but 30 might be too slow for some games that would run better at 40 or 60 (the most common framerates used).

That’s the first step, just to get the game loop running at a consistent framerate. Next, we need a timing variable that works not at the speed of framerates, but at the millisecond level. The pygame.time module has a method called get_ticks() that will meet our needs for the purpose of timing sprite animation.

    ticks = pygame.time.get_ticks()

This ticks variable can be passed to our own sprite class’ update() method to give our sprites independent animation timing at any desired framerate. In the code below, note that unless the timing is correct, the animation frame does not change.

    def update(self, current_time, rate=0):
        if current_time > self.last_time + rate:
            self.frame += 1
            if self.frame > self.last_frame:
                self.frame = self.first_frame
            self.last_time = current_time

In addition to the animation frame update code shown here, we also will be copying the current frame image into Sprite.image (self.image, in this case), which is the subject of the next paragraph.

Drawing One Frame

Knowing that Sprite.draw() is called automatically by the sprite group, we will not be writing our own drawing code, only setting up the properties to make the draw happen the way we want it to. That is done in the update() method of our custom sprite class. Sprite.draw() expects that Sprite.image and Sprite.rect are set to valid values or else an error will occur (a common error is an invalid position when the rect is undefined).

To draw a single frame from a sprite sheet, we must calculate the X,Y position of the frame’s top-left corner, and then copy the frame image based on the frame’s width and height. The X position represents the column number. The Y position represents the row number. We calculate Y, or row, by dividing the frame number by the number of columns, and then multiplying that value by the frame height:

Y = (frame / columns) * height

To calculate the X, or column value, we divide frame by columns again, but this time we only care about the remainder, not the quotient (in mathematical terms, that is the answer to a division problem). We can get the remainder by using modulus rather than division, and then multiply the value by the frame width:

X = (frame % columns) * width

These formulas can be written using Python code to update the Sprite.image used to draw a single frame. To get the frame image out of the sprite sheet, Surface.blit() could be used, but there’s a far easier way. Using the X and Y position values along with the frame width and height, we can just create a Rect and pass it to a different, and rather interesting method called subsurface(). This does not actually copy or blit the image at all, it just sets up a pointer to the existing master image! So, in effect, we’re going to be doing lightning-fast updates of the frame image because no pixels have to be copied at all!

    frame_x = (self.frame % self.columns) * self.frame_width
    frame_y = (self.frame // self.columns) * self.frame_height
    rect = ( frame_x, frame_y, self.frame_width, self.frame_height )
    self.image = self.master_image.subsurface(rect)

Note

Drawing One Frame

Always be on the lookout for awesome coding tricks like using Surface.subsurface() rather than drawing copies of every frame into an array or collection! By doing it this way, the code is greatly simplified, and there’s no performance hit!

Sprite Groups

Pygame uses sprite groups to manage updating and drawing sprites, as a means to handle a large number of entities usually found in a typical game. This is a good idea, as it saves us the trouble of doing it manually. It’s odd, though, that the Pygame creators thought to include an iterated sprite entity manager but did not include even rudimentary animation support. No matter, we’ll use what is provided and add our own code as needed!

A sprite group is a simple entity container that will call a sprite class’ update() method with whatever parameters it supports, and then draw all sprites contained in the container. A sprite group is created with pygame.sprite.Group() like so:

group = pygame.sprite.Group()
group.add(sprite)

where the sprite parameter is a sprite object that has already been created. After creating a group, any number of sprites can be added to the group container so that they can be managed more easily, and this also cuts down on global variable use. When we’re ready to update and draw the sprites in our game, we do this entirely with the group rather than the individual sprites:

    group.update(ticks)
    group.draw(screen)

The real power here is not containing all game sprites inside one group and using it to manage them, but creating several groups for each type of game sprite! This allows custom behaviors to be applied to specific types of sprites managed by their own group container objects. Another great advantage to using groups is that the updating and drawing code need not change when game objects are added or removed—the same update() and draw() methods are called and the group updates all of its attached sprite objects.

Note

Sprite Groups

Be careful not to accidentally overwrite the base Sprite.rect property with a basic tuple. That’s an easy mistake to make! Always set Sprite.rect to a new Rect(), like Rect(0,0,100,100), and not just an undefined tuple like (0,0,100,100). Python allows you to do that, and it can create the most bizarre error messages when things are expecting Sprite.rect to be a Rect, but it’s been replaced with a tuple! This is confusing because some rectangle code still works with the tuple-ized version!

MySprite Class

We can put all of this code into a reusable class which I’ll just call MySprite for lack of a better name. This class directly inherits (that is, extends) pygame.sprite.Sprite, and works directly with pygame.sprite.Group, for automated update and drawing. There are quite a few properties in this enhanced sprite class called MySprite, properties dealing with the animation, the master image, and so forth, that are not already in the base sprite class. But also, our new MySprite class is not overly complex, not filled with complex methods or properties, so think of this class as just a starting point for your own future sprite programming work. This is intentionally simple, a skeleton class for working with animated sprites. There are also three properties in the MySprite class: X, Y, and position. These are meant to help with setting the position of the sprite. Without these properties, we have to modify the rect which is kind of a pain when you just want to change the X or Y value.

class MySprite(pygame.sprite.Sprite):
    def __init__(self, target):
        pygame.sprite.Sprite.__init__(self) #extend the base Sprite class
        self.master_image = None
        self.frame = 0
        self.old_frame = -1
        self.frame_width = 1
        self.frame_height = 1
        self.first_frame = 0
        self.last_frame = 0
        self.columns = 1
        self.last_time = 0

    #X property
    def _getx(self): return self.rect.x
    def _setx(self,value): self.rect.x = value
    X = property(_getx,_setx)

    #Y property
    def _gety(self): return self.rect.y
    def _sety(self,value): self.rect.y = value
    Y = property(_gety,_sety)

    #position property
    def _getpos(self): return self.rect.topleft
    def _setpos(self,pos): self.rect.topleft = pos
    position = property(_getpos,_setpos)

    def load(self, filename, width, height, columns):
        self.master_image = pygame.image.load(filename).convert_alpha()
        self.frame_width = width
        self.frame_height = height
        self.rect = Rect(0,0,width,height)
        self.columns = columns
        #try to auto-calculate total frames
        rect = self.master_image.get_rect()
        self.last_frame = (rect.width // width) * (rect.height // height) - 1

    def update(self, current_time, rate=30):
        #update animation frame number
        if current_time > self.last_time + rate:
            self.frame += 1
            if self.frame > self.last_frame:
                self.frame = self.first_frame
            self.last_time = current_time

        #build current frame only if it changed
        if self.frame != self.old_frame:
            frame_x = (self.frame % self.columns) * self.frame_width
            frame_y = (self.frame // self.columns) * self.frame_height
            rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height)
            self.image = self.master_image.subsurface(rect)
            self.old_frame = self.frame

    def __str__(self):
        return str(self.frame) + "," + str(self.first_frame) + 
               "," + str(self.last_frame) + "," + str(self.frame_width) + 
               "," + str(self.frame_height) + "," + str(self.columns) + 
               "," + str(self.rect)

Sprite Animation to the Test

Figure 7.4 shows the output of the Sprite Animation Demo program, and the code listing follows. The MySprite class was just listed above, so it’s not repeated here; just be sure to include it in the program’s source code before trying to run the program.

The Sprite Animation Demo program.

Figure 7.4. The Sprite Animation Demo program.

import pygame
from pygame.locals import *

# remember to include MySprite here

#print_text function
def print_text(font, x, y, text, color=(255,255,255)):
    imgText = font.render(text, True, color)
    screen.blit(imgText, (x,y))

#initialize pygame
pygame.init()
screen = pygame.display.set_mode((800,600),0,32)
pygame.display.set_caption("Sprite Animation Demo")
font = pygame.font.Font(None, 18)
framerate = pygame.time.Clock()

#create the dragon sprite
dragon = MySprite(screen)
dragon.load("dragon.png", 260, 150, 3)
group = pygame.sprite.Group()
group.add(dragon)

#main loop
while True:
    framerate.tick(30)
    ticks = pygame.time.get_ticks()

    for event in pygame.event.get():
        if event.type == pygame.QUIT: sys.exit()
    key = pygame.key.get_pressed()
    if key[pygame.K_ESCAPE]: sys.exit()

    screen.fill((0,0,100))
    group.update(ticks)
    group.draw(screen)
    print_text(font, 0, 0, "Sprite: " + str(dragon))
    pygame.display.update()

The Escape the Dragon Game

Now we’ll use the MySprite class and the new sprite animation code just studied to create a simple game to demonstrate how to use the new class. Figure 7.5 shows one of the two ways to finish the game, and this does not bode well for the caveman character!

This caveman did not escape the dragon!

Figure 7.5. This caveman did not escape the dragon!

Jumping

The gameplay is simple—jump over the flaming arrows so they will hit the dragon, and escape! The Space key is used to jump over the arrows. The way this works is very simple in concept, but a bit challenging to understand at first. A Y velocity value is set to a negative number like -8.0. While the player is in “jumping mode,” so to speak, the velocity is increased by a small amount every frame. In this game, we want the player’s caveman sprite to jump up quickly but also fall back down quickly to make the game challenging. So, the modifier is a value of 0.5 (added to the velocity every frame). You can see the player jumping over a flaming arrow by flipping back to Figure 7.1 again. The end result is as follows:

-8.0 + 0.5 = -7.5

-7.5 + 0.5 = -7.0

-7.0 + 0.5 = -6.5

-6.5 + 0.5 = -6.0

-6.0 + 0.5 = -5.5

and so on, until we reach:

-0.5 + 0.5 = 0.0

At this point, the sprite will have peaked at the top of the jump and will begin moving back down again toward the ground:

0.0 + 0.5 = 0.5

0.5 + 0.5 + 1.0

1.0 + 0.5 = 1.5

and so on until the sprite reaches the starting Y position, at which point the jump cycle ends and the velocity is no longer used. When the player presses Space to jump again, that velocity value is restarted at -8.0 again. You can experiment with different heights by adjusting this and the incremental value to tweak the gameplay. Figure 7.6 shows what happens when the dragon is hit by enough arrows that it is pushed off the screen. That’s how to win, by jumping over the arrows so they hit the dragon!

The dragon was pushed off the screen by the flaming arrows.

Figure 7.6. The dragon was pushed off the screen by the flaming arrows.

Colliding

We haven’t covered sprite collisions yet, and won’t get into it in detail until the next chapter, so a quick perusal is in order now. There are several functions we can use to detect when two sprites collide with each other, using either the so-called “bounding rectangle” technique or “bounding circle.” Bounding rectangle collision detection works by comparing the rectangles of two sprites to see if they overlap. That is the technique used in this game to determine when the arrow has hit the player or the dragon, or when the dragon has “eaten” the player. Here is one example, comparing the arrow with the dragon sprite to see if there’s a collision:

pygame.sprite.collide_rect(arrow, dragon)

As long as we have inherited from pygame.sprite.Sprite for our own sprite class, then the rect property will be available, as that is what pygame.sprite.collide_rest() uses to see if the two sprites have hit each other.

Source Code

Here is the source code for The Escape the Dragon Game. I hope you enjoy it.

import sys, time, random, math, pygame
from pygame.locals import *

# insert MySprite class definition here

def print_text(font, x, y, text, color=(255,255,255)):
    imgText = font.render(text, True, color)
    screen.blit(imgText, (x,y))

def reset_arrow():
    y = random.randint(250,350)
    arrow.position = 800,y

#main program begins
pygame.init()
screen = pygame.display.set_mode((800,600))
pygame.display.set_caption("Escape The Dragon Game")
font = pygame.font.Font(None, 18)
framerate = pygame.time.Clock()

#load bitmaps
bg = pygame.image.load("background.png").convert_alpha()

#create a sprite group
group = pygame.sprite.Group()

#create the dragon sprite
dragon = MySprite(screen)
dragon.load("dragon.png", 260, 150, 3)
dragon.position = 100, 230
group.add(dragon)

#create the player sprite
player = MySprite(screen)
player.load("caveman.png", 50, 64, 8)
player.first_frame = 1
player.last_frame = 7
player.position = 400, 303
group.add(player)

#create the arrow sprite
arrow = MySprite(screen)
arrow.load("flame.png", 40, 16, 1)
arrow.position = 800,320
group.add(arrow)

arrow_vel = 8.0
game_over = False
you_win = False
player_jumping = False
jump_vel = 0.0
player_start_y = player.Y

#repeating loop
while True:
    framerate.tick(30)
    ticks = pygame.time.get_ticks()

    for event in pygame.event.get():
        if event.type == QUIT: sys.exit()
    keys = pygame.key.get_pressed()
    if keys[K_ESCAPE]: sys.exit()
    elif keys[K_SPACE]:
        if not player_jumping:
            player_jumping = True
            jump_vel = -8.0

    #update the arrow
    if not game_over:
        arrow.X -= arrow_vel
        if arrow.X < -40: reset_arrow()

    #did arrow hit player?
    if pygame.sprite.collide_rect(arrow, player):
        reset_arrow()
        player.X -= 10

    #did arrow hit dragon?
    if pygame.sprite.collide_rect(arrow, dragon):
        reset_arrow()
        dragon.X -= 10

    #did dragon eat the player?
    if pygame.sprite.collide_rect(player, dragon):
        game_over = True

    #did the dragon get defeated?
    if dragon.X < -100:
        you_win = True
        game_over = True

    #is the player jumping?
    if player_jumping:
        player.Y += jump_vel
        jump_vel += 0.5
        if player.Y > player_start_y:
            player_jumping = False
            player.Y = player_start_y
            jump_vel = 0.0

    #draw the background
    screen.blit(bg, (0,0))

    #update sprites
    if not game_over:
        group.update(ticks, 50)

    #draw sprites
    group.draw(screen)

    print_text(font, 350, 560, "Press SPACE to jump!")

    if game_over:
        print_text(font, 360, 100, "G A M E   O V E R")
        if you_win:
            print_text(font, 330, 130, "YOU BEAT THE DRAGON!")
        else:
            print_text(font, 330, 130, "THE DRAGON GOT YOU!")

    pygame.display.update()

Summary

Whew, sprite programming requires a lot of code just to get started, doesn’t it? I feel like we just invented our own Python module just to get animation up and running. The good news is we now have a great new class called MySprite that can be modified and enhanced for any purpose from here on out. I’m sure we’ll add new features to it in future chapters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.56.29