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.
- 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.0When 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.
Sprite groups in practice
Subclass pygame.sprite.Sprite for Player and Enemy, use Groups for update and draw, and detect collisions with groupcollide().
Animated sprites
Implement a frame-cycling Sprite subclass using solid-colour rectangles as placeholder frames, with animation speed controlled by a frame counter.