Code of the Day
IntermediateSprites and Levels

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.

Game DevIntermediate6 min read
By the end of this lesson you will be able to:
  • Explain frame-based animation as a cycle through a list of surfaces
  • Understand sprite sheets and how to extract sub-surfaces from them
  • Manage animation speed using a frame counter rather than wall-clock time

A static sprite conveys position but nothing else. A cycling sprite conveys motion, state changes, and personality. The difference between a character that teleports across the screen and one that visibly runs is a handful of frames cycling at the right speed.

Frame-based animation in pygame is straightforward: you hold a list of surfaces and replace self.image with the next one every N game loop iterations.

How frame cycling works

Frame list:  [surface_0, surface_1, surface_2, surface_3]
                 ^
            index = 0

After 8 ticks → index = 1
After 16 ticks → index = 2
After 24 ticks → index = 3
After 32 ticks → index = 0  (wraps)

The key variables are:

frames — the list of surfaces making up the animation. For a four-frame walk cycle you have four surfaces.

frame_index — an integer (often a float for fractional speeds) indicating which frame is currently shown.

animation_speed — how many frames to advance per game loop iteration. At 60 fps, an animation_speed of 0.1 cycles through a four-frame animation in 40 frames (about 0.67 seconds). An animation_speed of 0.25 completes the cycle in 16 frames (0.27 seconds).

Using a float accumulator and flooring it for the index gives smooth speed control without needing a separate tick counter.

Sprite sheets

Most game art is distributed as a sprite sheet: a single image that contains all frames in a grid. Loading one file is faster than loading dozens of small files, and keeping related art together simplifies asset management.

To extract frames, you slice sub-surfaces by position:

sheet = pygame.image.load("player.png").convert_alpha()

FRAME_W, FRAME_H = 32, 48      # size of one frame
frames = []
for col in range(4):           # four frames in a horizontal strip
    rect = pygame.Rect(col * FRAME_W, 0, FRAME_W, FRAME_H)
    frames.append(sheet.subsurface(rect))

Surface.subsurface(rect) returns a new Surface that shares memory with the parent — no pixels are copied. Slicing many frames from one sheet is cheap.

Multiple animation states

Most characters have more than one animation: idle, run, jump, fall, die. The cleanest approach is a dictionary mapping state names to frame lists:

self.animations = {
    "idle": load_strip("player_idle.png", 2, FRAME_W, FRAME_H),
    "run":  load_strip("player_run.png",  6, FRAME_W, FRAME_H),
    "jump": load_strip("player_jump.png", 4, FRAME_W, FRAME_H),
}
self.state       = "idle"
self.frame_index = 0.0

When the player's state changes, reset frame_index to 0.0 so the new animation starts from the beginning rather than mid-cycle.

If you do not have real art yet, use solid-colour rectangles as placeholder frames — different colours for different frames so you can see the cycle working. The next lesson does exactly this, so you can validate the animation logic before integrating real assets.

Frame timing: ticks vs delta time

Advancing the frame counter by a fixed amount per tick works well when the frame rate is locked. For variable frame rates, multiply animation_speed by the elapsed time in seconds:

dt               = clock.tick(60) / 1000.0    # seconds since last frame
self.frame_index = (self.frame_index + animation_speed * dt * 60) % len(frames)

This keeps animation speed proportional to real time rather than frame count, so animations do not run faster on a 144 Hz monitor.

Where to go next

Next: animated sprites — implement the frame-cycling pattern with placeholder coloured rectangles inside a pygame.sprite.Sprite subclass.

Finished reading? Mark it complete to track your progress.

On this page