17

I've been searching for some good tutorial about making simple sprite animation from few images in Python using Pygame. I still haven't found what I'm looking for.

My question is simple: how to make an animated sprite from few images (for an example: making few images of explosion with dimensions 20x20px to be as one but animated)

Any good ideas?

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
lbartolic
  • 1,165
  • 2
  • 12
  • 24
  • 2
    You want a *spritesheet*. You can either load from multiple images, or a single one with source `Rect`s set when you blit. Here's another example: http://www.pygame.org/wiki/Spritesheet?parent=CookBook – ninMonkey Dec 26 '12 at 21:11

4 Answers4

29

There are two types of animation: frame-dependent and time-dependent. Both work in similar fashion.


Before the main loop

  1. Load all images into a list.
  2. Create three variable:
    1. index, that keeps track on the current index of the image list.
    2. current_time or current_frame that keeps track on the current time or current frame since last the index switched.
    3. animation_time or animation_frames that define how many seconds or frames should pass before switching image.

During the main loop

  1. Increment current_time by the amount of seconds that has passed since we last incremented it, or increment current_frame by 1.
  2. Check if current_time >= animation_time or current_frame >= animation_frame. If true continue with 3-5.
  3. Reset the current_time = 0 or current_frame = 0.
  4. Increment the index, unless if it'll be equal or greater than the amount of images. In that case, reset index = 0.
  5. Change the sprite's image accordingly.

A full working example

import os
import pygame
pygame.init()

SIZE = WIDTH, HEIGHT = 720, 480
BACKGROUND_COLOR = pygame.Color('black')
FPS = 60

screen = pygame.display.set_mode(SIZE)
clock = pygame.time.Clock()


def load_images(path):
    """
    Loads all images in directory. The directory must only contain images.

    Args:
        path: The relative or absolute path to the directory to load images from.

    Returns:
        List of images.
    """
    images = []
    for file_name in os.listdir(path):
        image = pygame.image.load(path + os.sep + file_name).convert()
        images.append(image)
    return images


class AnimatedSprite(pygame.sprite.Sprite):

    def __init__(self, position, images):
        """
        Animated sprite object.

        Args:
            position: x, y coordinate on the screen to place the AnimatedSprite.
            images: Images to use in the animation.
        """
        super(AnimatedSprite, self).__init__()

        size = (32, 32)  # This should match the size of the images.

        self.rect = pygame.Rect(position, size)
        self.images = images
        self.images_right = images
        self.images_left = [pygame.transform.flip(image, True, False) for image in images]  # Flipping every image.
        self.index = 0
        self.image = images[self.index]  # 'image' is the current image of the animation.

        self.velocity = pygame.math.Vector2(0, 0)

        self.animation_time = 0.1
        self.current_time = 0

        self.animation_frames = 6
        self.current_frame = 0

    def update_time_dependent(self, dt):
        """
        Updates the image of Sprite approximately every 0.1 second.

        Args:
            dt: Time elapsed between each frame.
        """
        if self.velocity.x > 0:  # Use the right images if sprite is moving right.
            self.images = self.images_right
        elif self.velocity.x < 0:
            self.images = self.images_left

        self.current_time += dt
        if self.current_time >= self.animation_time:
            self.current_time = 0
            self.index = (self.index + 1) % len(self.images)
            self.image = self.images[self.index]

        self.rect.move_ip(*self.velocity)

    def update_frame_dependent(self):
        """
        Updates the image of Sprite every 6 frame (approximately every 0.1 second if frame rate is 60).
        """
        if self.velocity.x > 0:  # Use the right images if sprite is moving right.
            self.images = self.images_right
        elif self.velocity.x < 0:
            self.images = self.images_left

        self.current_frame += 1
        if self.current_frame >= self.animation_frames:
            self.current_frame = 0
            self.index = (self.index + 1) % len(self.images)
            self.image = self.images[self.index]

        self.rect.move_ip(*self.velocity)

    def update(self, dt):
        """This is the method that's being called when 'all_sprites.update(dt)' is called."""
        # Switch between the two update methods by commenting/uncommenting.
        self.update_time_dependent(dt)
        # self.update_frame_dependent()


def main():
    images = load_images(path='temp')  # Make sure to provide the relative or full path to the images directory.
    player = AnimatedSprite(position=(100, 100), images=images)
    all_sprites = pygame.sprite.Group(player)  # Creates a sprite group and adds 'player' to it.

    running = True
    while running:

        dt = clock.tick(FPS) / 1000  # Amount of seconds between each loop.

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    player.velocity.x = 4
                elif event.key == pygame.K_LEFT:
                    player.velocity.x = -4
                elif event.key == pygame.K_DOWN:
                    player.velocity.y = 4
                elif event.key == pygame.K_UP:
                    player.velocity.y = -4
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_RIGHT or event.key == pygame.K_LEFT:
                    player.velocity.x = 0
                elif event.key == pygame.K_DOWN or event.key == pygame.K_UP:
                    player.velocity.y = 0

        all_sprites.update(dt)  # Calls the 'update' method on all sprites in the list (currently just the player).

        screen.fill(BACKGROUND_COLOR)
        all_sprites.draw(screen)
        pygame.display.update()


if __name__ == '__main__':
    main()

When to chose which

Time-dependent animation allows you to play the animation at the same speed, no matter how slow/fast the frame-rate is or slow/fast your computer is. This allows your program to freely change the framerate without affecting the animation and it'll also be consistent even if the computer cannot keep up with the framerate. If the program lags the animation will catch up to the state it should've been as if no lag had happened.

Although, it might happen that the animation cycle don't synch up with the framerate, making the animation cycle seem irregular. For example, say that we have the frames updating every 0.05 second and the animation switch image every 0.075 second, then the cycle would be:

  1. Frame 1; 0.00 seconds; image 1
  2. Frame 2; 0.05 seconds; image 1
  3. Frame 3; 0.10 seconds; image 2
  4. Frame 4; 0.15 seconds; image 1
  5. Frame 5; 0.20 seconds; image 1
  6. Frame 6; 0.25 seconds; image 2

And so on...

Frame-dependent can look smoother if your computer can handle the framerate consistently. If lag happens it'll pause in its current state and restart when the lag stops, which makes the lag more noticeable. This alternative is slightly easier to implement since you just need to increment current_frame with 1 on each call, instead of dealing with the delta time (dt) and passing it to every object.

Sprites

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

Result

enter image description here

Ted Klein Bergman
  • 9,146
  • 4
  • 29
  • 50
  • Good answer. Would be even better if it used the included `pygame.sprite.Sprite` class. – martineau Jun 19 '17 at 18:42
  • 1
    @martineau Yeah, I thought about it but wasn't sure because it would make the answer more difficult for beginners to understand as it would introduce inheritance and sprite groups. Might change my mind though and implement it later when I got the time. – Ted Klein Bergman Jun 20 '17 at 20:54
  • If nothing else, I think you should change the name of class so it's not identical to the one in pygame—which I think could be especially confusing to folks in the process of beginning to learn about it. – martineau Jun 20 '17 at 21:00
  • 1
    @martineau That's a good point. I changed the name and added in the sprite groups as well. It seems to be simple enough still. – Ted Klein Bergman Jun 20 '17 at 23:56
  • This didn't work for me because `clock.tick(FPS) / 1000` yields always 0 (integer). Instead, I used `dt = clock.tick(FPS) / 1000.0` to force floating point usage. –  Jan 15 '19 at 12:09
  • @user2194299 Yes, that is true for Python 2. It's not necessary for Python 3. – Ted Klein Bergman Apr 26 '19 at 05:57
22

You could try modifying your sprite so that it swaps out its image for a different one inside update. That way, when the sprite is rendered, it'll look animated.

Edit:

Here's a quick example I drew up:

import pygame
import sys

def load_image(name):
    image = pygame.image.load(name)
    return image

class TestSprite(pygame.sprite.Sprite):
    def __init__(self):
        super(TestSprite, self).__init__()
        self.images = []
        self.images.append(load_image('image1.png'))
        self.images.append(load_image('image2.png'))
        # assuming both images are 64x64 pixels

        self.index = 0
        self.image = self.images[self.index]
        self.rect = pygame.Rect(5, 5, 64, 64)

    def update(self):
        '''This method iterates through the elements inside self.images and 
        displays the next one each tick. For a slower animation, you may want to 
        consider using a timer of some sort so it updates slower.'''
        self.index += 1
        if self.index >= len(self.images):
            self.index = 0
        self.image = self.images[self.index]

def main():
    pygame.init()
    screen = pygame.display.set_mode((250, 250))

    my_sprite = TestSprite()
    my_group = pygame.sprite.Group(my_sprite)

    while True:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit(0)

        # Calling the 'my_group.update' function calls the 'update' function of all 
        # its member sprites. Calling the 'my_group.draw' function uses the 'image'
        # and 'rect' attributes of its member sprites to draw the sprite.
        my_group.update()
        my_group.draw(screen)
        pygame.display.flip()

if __name__ == '__main__':
    main()

It assumes that you have two images called image1.png and image2.png inside the same folder the code is in.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • hey... do you maybe know how to slow down the animation? problem is if i use pygame.time.Clock, whole game will be slowed down, not just animation. – lbartolic Dec 30 '12 at 05:29
  • @lucro93: What you could try doing is adding `self.counter = 0` inside `__init__`. Then, inside `update`, increment `self.counter`. If it equals some high number (perhaps 99), then reset it to zero and increment `self.index` by one. That way, the animation will update only once every 100 ticks. I'm sure there are better ways, but this way is pretty simple and will probably work for you. – Michael0x2a Dec 30 '12 at 05:45
  • Late comment, but the way I do it is use `self.image = self.images[math.floor(self.index)]` and then increment `self.index` by 0.08 or so. – gusg21 Aug 09 '17 at 13:17
4

You should have all your sprite animations on one big "canvas", so for 3 20x20 explosion sprite frames you will have 60x20 image. Now you can get right frames by loading an area of the image.

Inside your sprite class, most likely in update method you should have something like this (hardcoded for simplicity, I prefer to have separate class to be responsible for picking the right animation frame). self.f = 0 on __init__.

def update(self):
    images = [[0, 0], [20, 0], [40, 0]]
    self.f += 1 if self.f < len(images) else 0
    self.image = your_function_to_get_image_by_coordinates(images[i])
Ruslan Osipov
  • 5,655
  • 4
  • 29
  • 44
3

For an animated Sprite a list of images (pygame.Surface objects) must be generated. A different picture of the list is displayed in each frame, just like in the pictures of a movie. This gives the appearance of an animated object.
One way to get a list of images is to load an animated GIF (Graphics Interchange Format). Unfortunately, PyGame doesn't offer a function to load the frames of an animated GIF. However, there are several Stack Overflow answers that address this issue:

One way is to use the popular Pillow library (pip install Pillow). The following function loads the frames of an animated GIF and generates a list of pygame.Surface objects:

from PIL import Image, ImageSequence
def loadGIF(filename):
    pilImage = Image.open(filename)
    frames = []
    for frame in ImageSequence.Iterator(pilImage):
        frame = frame.convert('RGBA')
        pygameImage = pygame.image.fromstring(
            frame.tobytes(), frame.size, frame.mode).convert_alpha()
        frames.append(pygameImage)
    return frames

Create a pygame.sprite.Sprite class that maintains a list of images. Implement an update method that selects a different image in each frame.
Pass the list of images to the class constructor. Add an index attribute that indicates the index of the current image in the list. Increase the index in the Update method. Reset the index if it is greater than or equal to the length of the image list (or use the modulo (%) operator). Get the current image from the list by subscription:

class AnimatedSpriteObject(pygame.sprite.Sprite):
    def __init__(self, x, bottom, images):
        pygame.sprite.Sprite.__init__(self)
        self.images = images
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom = (x, bottom))
        self.image_index = 0
    def update(self):
        self.image_index += 1
        if self.image_index >= len(self.images):
            self.image_index = 0
        self.image = self.images[self.image_index]

See also Load animated GIF and Sprite

Example GIF (from Animated Gifs, Animated Image):

Minimal example: repl.it/@Rabbid76/PyGame-SpriteAnimation

import pygame
from PIL import Image, ImageSequence

def loadGIF(filename):
    pilImage = Image.open(filename)
    frames = []
    for frame in ImageSequence.Iterator(pilImage):
        frame = frame.convert('RGBA')
        pygameImage = pygame.image.fromstring(
            frame.tobytes(), frame.size, frame.mode).convert_alpha()
        frames.append(pygameImage)
    return frames
 
class AnimatedSpriteObject(pygame.sprite.Sprite):
    def __init__(self, x, bottom, images):
        pygame.sprite.Sprite.__init__(self)
        self.images = images
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom = (x, bottom))
        self.image_index = 0
    def update(self):
        self.image_index += 1
        self.image = self.images[self.image_index % len(self.images)]
        self.rect.x -= 5
        if self.rect.right < 0:
            self.rect.left = pygame.display.get_surface().get_width()

pygame.init()
window = pygame.display.set_mode((300, 200))
clock = pygame.time.Clock()
ground = window.get_height() * 3 // 4

gifFrameList = loadGIF('stone_age.gif')
animated_sprite = AnimatedSpriteObject(window.get_width() // 2, ground, gifFrameList)    
all_sprites = pygame.sprite.Group(animated_sprite)

run = True
while run:
    clock.tick(20)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    all_sprites.update()

    window.fill((127, 192, 255), (0, 0, window.get_width(), ground))
    window.fill((255, 127, 64), (0, ground, window.get_width(), window.get_height() - ground))
    all_sprites.draw(window)
    pygame.display.flip()

pygame.quit()
exit()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • For me the gif looked super messed up after the first frame ([link to gif](https://en.wikipedia.org/wiki/File:Earth_rotation.gif)). Did not find the cause, seems to be a bug in Pillow, but converting it with `gifsicle in.gif --colors 256 out.gif` solved the problem, aside from that the colors are now limited to 256. You can play with dithering (-f), optimization (-O3), and --color-method to try and get it better. Without `--colors 256` it'll use local colormaps and that's what seems to mess things up. ... maybe I shoulda gone with jpgs/pngs in a folder after all (`convert in.gif %d.png` iirc) – Luc Apr 03 '22 at 02:47