Animated sprites
Implement a frame-cycling Sprite subclass using solid-colour rectangles as placeholder frames, with animation speed controlled by a frame counter.
- 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.
Sprite animation
Bring sprites to life by cycling through a sequence of surfaces, controlling playback speed with a frame counter, and loading frames from a sprite sheet.
Tile maps
Represent levels as a 2D grid of tile IDs, render them by iterating the grid, and separate the collision layer from the visual layer.