Code of the Day
IntermediateSprites and Levels

Animated sprites

Implement a frame-cycling Sprite subclass using solid-colour rectangles as placeholder frames, with animation speed controlled by a frame counter.

Game DevIntermediate10 min read
By the end of this lesson you will be able to:
  • Implement an AnimatedSprite class that cycles through a list of coloured surfaces
  • Control animation speed with a float frame counter
  • Switch animation states by resetting the frame index

This lesson builds the animation system described in the previous lesson, using solid-colour rectangles as stand-ins for real art. The logic is identical to what you would use with real sprite sheets — swap in the loaded surfaces when the art is ready.

Pyodide (the in-browser Python runner) does not support pygame's display system. Read through this code carefully in the browser, then run it locally: pip install pygame followed by python animated.py.

The code

import pygame
import sys

pygame.init()

SCREEN_W, SCREEN_H = 640, 480
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("Animated sprites demo")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont(None, 28)


# ── Helper — build placeholder frames as coloured surfaces ───────────────────

def make_frames(colors, width=48, height=48):
    """Return a list of solid-colour surfaces to use as animation frames."""
    frames = []
    for color in colors:
        surf = pygame.Surface((width, height))
        surf.fill(color)
        frames.append(surf)
    return frames


# ── AnimatedSprite ────────────────────────────────────────────────────────────

class AnimatedSprite(pygame.sprite.Sprite):
    """A sprite that cycles through a list of frames each update."""

    def __init__(self, animations, start_state, x, y, *groups):
        """
        animations  — dict mapping state name to list of Surface frames
        start_state — initial key into animations
        x, y        — initial top-left position
        """
        super().__init__(*groups)
        self.animations  = animations
        self.state       = start_state
        self.frame_index = 0.0
        self.anim_speed  = 0.15     # frames advanced per game-loop tick

        self.image = self.animations[self.state][0]
        self.rect  = self.image.get_rect(topleft=(x, y))

    def set_state(self, new_state):
        """Switch animation state and reset to the first frame."""
        if new_state != self.state:
            self.state       = new_state
            self.frame_index = 0.0

    def update(self, keys):
        # Advance the frame counter
        frames           = self.animations[self.state]
        self.frame_index = (self.frame_index + self.anim_speed) % len(frames)
        self.image       = frames[int(self.frame_index)]

        # Move with arrow keys and pick the right animation state
        moving = False
        if keys[pygame.K_LEFT]:
            self.rect.x -= 4
            moving = True
        if keys[pygame.K_RIGHT]:
            self.rect.x += 4
            moving = True
        if keys[pygame.K_UP]:
            self.rect.y -= 4
            moving = True
        if keys[pygame.K_DOWN]:
            self.rect.y += 4
            moving = True

        self.set_state("run" if moving else "idle")
        self.rect.clamp_ip(pygame.Rect(0, 0, SCREEN_W, SCREEN_H))


# ── Build animations with placeholder colours ─────────────────────────────────

# Idle: two frames cycling between slightly different greens
# Run:  four frames with progressively lighter greens (simulates stride)
animations = {
    "idle": make_frames([
        (80,  180, 90),
        (100, 210, 110),
    ]),
    "run": make_frames([
        (60,  160, 70),
        (100, 210, 110),
        (140, 230, 140),
        (100, 210, 110),
    ]),
}

all_sprites = pygame.sprite.Group()
player = AnimatedSprite(animations, "idle", 300, 216, all_sprites)


# ── Main loop ─────────────────────────────────────────────────────────────────

running = True
while running:
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    all_sprites.update(keys)

    screen.fill((20, 20, 35))
    all_sprites.draw(screen)

    info = font.render(
        f"State: {player.state}   Frame: {int(player.frame_index)}",
        True, (200, 200, 200))
    screen.blit(info, (10, 10))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

What to observe

Move the player with the arrow keys. The HUD shows the current animation state (idle or run) and the active frame index. When you release the keys, the state switches back to idle and the frame index resets to 0.

The colour cycling is subtle with only a 48x48 square, but it demonstrates the mechanics. Replace make_frames() with pygame.image.load() and sheet.subsurface() calls when real art is available — the rest of the code does not change.

Notice that set_state() only resets frame_index when the state actually changes. Calling it every frame without this guard would reset the animation every tick, freezing it on frame 0 forever.

Where to go next

Next: tile maps — representing levels as a 2D grid of tile IDs and rendering them efficiently.

Finished reading? Mark it complete to track your progress.

On this page