Advanced Sprite Animation Using Pygame in Python2.7

Update 22 Aug 2020: Fixed the link to the source code.


Advanced Sprite Animation Demo

you can download the codes for this tutorial here.

it has been quite a long while since i last posted a tutorial. fortunately i was able to come up with another one despite the busy schedule. and to continue from what we’ve left here, i am now presenting a tutorial on how to animate sprites with the look and feel that is a lot closer to how we see them in our 2D games. this tutorial will deal with animating sprites that use multiple images for each action, with the said images extracted from a large single image containing views of the sprites from all possible angles.

to begin let us visit the code for the sprites that we are going to use. as what we have learned from our OOP lessons, it is ideal to keep functions that we are going to use across our classes in a base class. this will save us from repetition and make the implementation of changes for all sub-classes easier. so let us check out first the code for our base class contained in lib_sprites.py. normally i would name it class_spritebase.py, but since i’m planning it to contain all other classes and helpers to be used by sprites in the future, i used the lib_ prefix instead to let me know it’s a library.

it of course starts with the imports.

import pygame
from pygame import *

import sys
sys.path.append('utils')

import lib_utils
from lib_utils import StaticFunction

as you can see, aside from the pygame library, we also imported our own library which is lib_utils. if you have been visiting the other tutorials, you can perhaps recall that it is the library we created for helper classes. in this tutorial we are going to use it to create static function calls against our SpriteDirection class.

SPRITEDIRECTION CLASS

the SpriteDirection class is meant to contain all valid directions that we are going to accept. this is so other developers who might want to use our library won’t be guessing parameters to pass whenever setting a sprite’s direction. this class also comes with an IsDirection function to validate direction values.

class SpriteDirection:
    NORTH = '+Y'
    EAST = '+X'
    WEST = '-X'
    SOUTH = '-Y'

    def IsDirection(direction):
        if(direction == SpriteDirection.NORTH or \
           direction == SpriteDirection.EAST or \
           direction == SpriteDirection.WEST or \
           direction == SpriteDirection.SOUTH):
            return True

        return False

    IsDirection = StaticFunction(IsDirection)

SPRITEBASE CLASS

the SpriteBase class as mentioned earlier should serve as the base class for all our sprite classes. it inherits from the built-in object type and has a set of properties with their own getters and setters. for now, it only has two functions namely RenderImage and RenderImageAtLocation which we are going to use to display the sprite’s image on our screen.

to describe our properties in detail:

ImageSource

a Surface returned by pygame.image.load() given a path to a sprite’s source image.

HotSpots

list of Rect objects pointing to portions in the source image that contains images to be used by the sprite.

Direction

a SpriteDirection value specifying the direction to which the sprite faces.

Location

the location of the sprite in the screen

class SpriteBase(object):
    def __init__(self):
        self._imagesource = None
        self._hotspots = list()
        self._direction = SpriteDirection.SOUTH
        self._location = [0, 0]

    #Properties
    def ImageSource(self, imgsrc=None):
        if (imgsrc == None):
            return self._imagesource
        else:
            self._imagesource = imgsrc

    def HotSpots(self, rect=None):
        if(rect == None):
            return self._hotspots
        else:
            self._hotspots.append(rect)

    def Direction(self, direction=None):
        if(direction == None):
            return self._direction
        else:
            if(SpriteDirection.IsDirection(direction)):
                self._direction = direction

    def Location(self, location=None):
        if(location == None):
            return self._location
        else:
            self._location = location

    #Methods
    def RenderImage(self, screen, spotindex):
        screen.blit(self._imagesource, self._location, self._hotspots[spotindex])

    def RenderImageAtLocation(self, screen, location=None, spotindex=0):
        screen.blit(self._imagesource, location, self._hotspots[spotindex])

SHEEP CLASS

i’ve created a number of classes inheriting from the SpriteBase class as requested by my 2 year old nephew. he loved watching the animals move. but for this tutorial i will only discuss the Sheep class contained in class_sheep.py.

it begins with a number of imports for pygame, the lib_sprites, datetime, and config as shown below. the datetime will be used for implementing the delay in displaying the series of images involved in one action while config refers to class we created that is supposed to contain the configurations for the game.

import pygame
from pygame import *

import datetime
from datetime import datetime, timedelta

import config
from config import AppConfig

import lib_sprites
from lib_sprites import SpriteBase, SpriteDirection

the constructor for the class will simply call the parent constructor, load the image to use, and specify the hotspots.

class Sheep(SpriteBase):
    def __init__(self):
        super(Sheep, self).__init__()
        self.ImageSource(pygame.image.load('images/Animal.png'))

        #looking north
        self.HotSpots(pygame.Rect(320, 96, 32, 32)) #standing
        self.HotSpots(pygame.Rect(288, 96, 32, 32)) #step right foot
        self.HotSpots(pygame.Rect(352, 96, 32, 32)) #step left foot

        #looking east
        self.HotSpots(pygame.Rect(320, 64, 32, 32)) #standing
        self.HotSpots(pygame.Rect(288, 64, 32, 32)) #step right foot
        self.HotSpots(pygame.Rect(352, 64, 32, 32)) #step left foot

        #looking west
        self.HotSpots(pygame.Rect(320, 32, 32, 32)) #standing
        self.HotSpots(pygame.Rect(352, 32, 32, 32)) #step right foot
        self.HotSpots(pygame.Rect(288, 32, 32, 32)) #step left foot

        #looking south
        self.HotSpots(pygame.Rect(320, 0, 32, 32)) #standing
        self.HotSpots(pygame.Rect(288, 0, 32, 32)) #step right foot
        self.HotSpots(pygame.Rect(352, 0, 32, 32)) #step left foot

the Turn() method will make the sprite turn to a different direction. note that screen.fill() is called before rendering the image. this is to cover the previously displayed image before rendering another. this can be replaced by a function to render the background when it’s already available.

    #makes a sprite turn to specified direction
    def Turn(self, screen, direction):
        self.Direction(direction)
        index = -1

        if(direction == SpriteDirection.NORTH):
            index = 0
        elif(direction == SpriteDirection.EAST):
            index = 3
        elif(direction == SpriteDirection.WEST):
            index = 6
        elif(direction == SpriteDirection.SOUTH):
            index = 9
        screen.fill(0)
        self.RenderImage(screen, index)
        pygame.display.update()

the Walk() method initiates variables for making the sprite walk to current direction. the direction is checked first and locations to display images are put into a list, the same as the indices of the images to display from the sprite’s hotspots. each location in the list of locations should correspond to one image in the sprite’s hotspots, therefore the length of locations array and index array should be equal.

putting a grid over the display screen, the length of either array could be taken as the number of steps that a sprite would take to move to the next cell in the grid. in our example we have a 32px x 32px cell and so a sprite’s ending location should be 32px from the current location. the more step we use, the smoother the action will be. however it could have a few issues like making the distance between two locations seem so far and too much redrawing of the surface than necessary. in our example, we’re just going to use 4 steps.

    #makes a sprite walk to current direction
    def Walk(self, screen):
        locations = list() #contains locations for each sprite image to render in succession
        indexarray = None #contains the indices of all sprite images to render
        if(self.Direction() == SpriteDirection.NORTH):
            locations.append([self.Location()[0], self.Location()[1]-8])
            locations.append([self.Location()[0], self.Location()[1]-16])
            locations.append([self.Location()[0], self.Location()[1]-24])
            locations.append([self.Location()[0], self.Location()[1]-32])
            indexarray = [1, 2, 1, 0]
        elif(self.Direction() == SpriteDirection.EAST):
            locations.append([self.Location()[0]+8, self.Location()[1]])
            locations.append([self.Location()[0]+16, self.Location()[1]])
            locations.append([self.Location()[0]+24, self.Location()[1]])
            locations.append([self.Location()[0]+32, self.Location()[1]])
            indexarray = [4, 5, 4, 3]
        elif(self.Direction() == SpriteDirection.WEST):
            locations.append([self.Location()[0]-8, self.Location()[1]])
            locations.append([self.Location()[0]-16, self.Location()[1]])
            locations.append([self.Location()[0]-24, self.Location()[1]])
            locations.append([self.Location()[0]-32, self.Location()[1]])
            indexarray = [7, 8, 7, 6]
        elif(self.Direction() == SpriteDirection.SOUTH):
            locations.append([self.Location()[0], self.Location()[1]+8])
            locations.append([self.Location()[0], self.Location()[1]+16])
            locations.append([self.Location()[0], self.Location()[1]+24])
            locations.append([self.Location()[0], self.Location()[1]+32])
            indexarray = [10, 11, 10, 9]

        self.__animate_walk__(screen, locations, indexarray)

the __animate_walk__() is a private method that takes care of the actual animation. this method simply loops through the specified locations and hotspot indices by the Walk() method and display the images on screen with certain delay enough for each image to be viewable to the user. a check was also included to make the sprite stay within the bounds of the display screen.

def __animate_walk__(self, screen, locations, indexarray):
        if(len(locations) == 0 or len(indexarray) == 0):
            return
        for i in range(len(locations)):
            loc = locations[i]
            if(loc[0] < 0 or loc[1] < 0 or loc[0] > AppConfig.DEFAULT_SCREENSIZE[0]-32 or loc[1] > AppConfig.DEFAULT_SCREENSIZE[1]-32):
                return
            self.Location(loc)
            screen.fill(0)
            self.RenderImage(screen, indexarray[i])
            pygame.display.update()
            #implements delay
            t1 = datetime.now() + timedelta(seconds=0.125)
            while(datetime.now() < t1):
                pass
            self.Location(loc)

THE GAME

the game.py file is where the game should be implemented. like the others, it begins with importing a number of libraries.

import pygame
from pygame import *

import datetime
from datetime import timedelta

import config
from config import AppConfig

import sys
sys.path.append('entities')

import lib_sprites
from lib_sprites import SpriteDirection

import class_sheep
from class_sheep import Sheep

import class_cow
from class_cow import Cow

import class_rooster
from class_rooster import Rooster

import class_butterfly
from class_butterfly import Butterfly

following imports is the initialization. notice that pygame.key.set_repeat() is used. this is to make the sprite walk as long as the the key is held down as opposed to making it walk from one cell to another for each key press.

pygame.init()
pygame.key.set_repeat(1, 1)

screen = pygame.display.set_mode(AppConfig.DEFAULT_SCREENSIZE)
display.set_caption('Advanced Animation Demo')

going = True

what follows is the initialization for the sprite and the main loop which is pretty self-explanatory. after rendering the sprite image in its initial position, we’re going to wait for a user triggered event and respond accordingly. we’ll close the application if we encounter a QUIT. if we encounter a button press in any of the four directions we either turn if it is different from the current one, or walk.

character = Sheep()
character.RenderImage(screen, 9)
pygame.display.update()

while going:
    for e in event.get():
        if e.type == QUIT: #checks if close button was clicked
            going = False
        elif e.type == KEYDOWN:
            keystate = pygame.key.get_pressed()
            if(keystate[K_UP]):
                if(character.Direction() == SpriteDirection.NORTH):
                    character.Walk(screen)
                else:
                    character.Turn(screen, SpriteDirection.NORTH)
            elif(keystate[K_RIGHT]):
                if(character.Direction() == SpriteDirection.EAST):
                    character.Walk(screen)
                else:
                    character.Turn(screen, SpriteDirection.EAST)
            elif(keystate[K_LEFT]):
                if(character.Direction() == SpriteDirection.WEST):
                    character.Walk(screen)
                else:
                    character.Turn(screen, SpriteDirection.WEST)
            elif(keystate[K_DOWN]):
                if(character.Direction() == SpriteDirection.SOUTH):
                    character.Walk(screen)
                else:
                    character.Turn(screen, SpriteDirection.SOUTH)

pygame.quit()

and that’s it. you can download the codes for this tutorial here. i hope you enjoyed working with this tutorial. ’til next time. -aB

One thought on “Advanced Sprite Animation Using Pygame in Python2.7

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s