Sprites, Collisions and Animations

Pygame Game Development Course

Felipe A. Moreno-Vera

🎬 Animations

How does 2D animation work?

Animation works exactly like a flipbook: a sequence of images (frames) that change at a certain speed to create the illusion of movement.

Frame 0    Frame 1    Frame 2    Frame 3
  🧍         🚶         🧍         🚶
  ↑          ↑          ↑          ↑
 100ms      100ms      100ms      100ms   → repeats

Important

Animation speed ≠ game FPS. At 60 FPS, you can animate at 10 FPS (change frame every 6 game frames).

Spritesheet

A spritesheet is a single image containing all frames for all animations. It is used to:

  • Reduce the number of individual files
  • Load GPU memory more efficiently
  • Simplify animation management
spritesheet.png:
┌──────────────────────────────────────────────────┐
│ [idle_0][idle_1][idle_2][idle_3]                 │  ← row 0: idle
│ [walk_0][walk_1][walk_2][walk_3][walk_4][walk_5] │  ← row 1: walk
│ [jump_0][jump_1][jump_2]                         │  ← row 2: jump
│ [attack_0][attack_1][attack_2][attack_3]         │  ← row 3: attack
└──────────────────────────────────────────────────┘

Spritesheet

A spritesheet is a single image containing all frames for all animations. It is used to:

  • Reduce the number of individual files
  • Load GPU memory more efficiently
  • Simplify animation management
spritesheet.png:
┌──────────────────────────────────────────────────┐
│ [idle_0][idle_1][idle_2][idle_3]                 │  ← row 0: idle
│ [walk_0][walk_1][walk_2][walk_3][walk_4][walk_5] │  ← row 1: walk
│ [jump_0][jump_1][jump_2]                         │  ← row 2: jump
│ [attack_0][attack_1][attack_2][attack_3]         │  ← row 3: attack
└──────────────────────────────────────────────────┘

To extract a frame use Surface.subsurface(rect):

sheet = pygame.image.load("spritesheet.png").convert_alpha()
SIZE = 64  # Frame size

# Extract frame at column=2, row=1
frame = sheet.subsurface(pygame.Rect(2 * SIZE, 1 * SIZE, SIZE, SIZE))

Animation Timer

The timer controls when to advance to the next frame. There are two approaches:

Animation Timer

The timer controls when to advance to the next frame. There are two approaches:

Approach 1 — Frame counter (simpler)

self.current_frame = 0
self.counter       = 0
self.anim_speed    = 8  # Change frame every 8 game frames

def update(self):
    self.counter += 1
    if self.counter >= self.anim_speed:
        self.counter = 0
        self.current_frame = (self.current_frame + 1) % len(self.frames)
    self.image = self.frames[self.current_frame]

Animation Timer

The timer controls when to advance to the next frame. There are two approaches:

Approach 1 — Frame counter (simpler)

self.current_frame = 0
self.counter       = 0
self.anim_speed    = 8  # Change frame every 8 game frames

def update(self):
    self.counter += 1
    if self.counter >= self.anim_speed:
        self.counter = 0
        self.current_frame = (self.current_frame + 1) % len(self.frames)
    self.image = self.frames[self.current_frame]
self.current_frame  = 0
self.last_update    = pygame.time.get_ticks()  # current ms
self.frame_duration = 100  # ms per frame (100ms = 10 anim FPS)

def update(self):
    now = pygame.time.get_ticks()
    if now - self.last_update >= self.frame_duration:
        self.last_update = now
        self.current_frame = (self.current_frame + 1) % len(self.frames)
    self.image = self.frames[self.current_frame]

Animation Timer

The timer controls when to advance to the next frame.

Tip

Approach 2 is preferred because if the game FPS drop, the animation still plays at the same real-time speed on screen.

Animation State Machine

A character has multiple animations depending on its state. The state machine decides which one to play.

                 ┌─────────────────────┐
                 │       IDLE          │
                 │  (standing still)   │
                 └─────┬───────┬───────┘
                       │       │
               move    │       │ airborne
               key     ↓       ↓
               ┌───────────┐ ┌───────────┐
               │   WALK    │ │   JUMP    │
               │ (walking) │ │ (jumping) │
               └─────┬─────┘ └─────┬─────┘
                     │ release     │ land
                     └──────┬──────┘
                            ↓
                          IDLE

Animation State Machine – code

class Player(pygame.sprite.Sprite):
    def __init__(self):
        ...
        self.state = 'idle'   # Current state
        self.animations = {
            'idle':   [frame_idle_0, frame_idle_1, frame_idle_2],
            'walk':   [frame_walk_0, frame_walk_1, frame_walk_2, frame_walk_3],
            'jump':   [frame_jump_0, frame_jump_1],
        }

    def change_state(self, new_state):
        if self.state != new_state:        # Only if it actually changed
            self.state = new_state
            self.current_frame = 0         # Reset animation on state change

    def update(self):
        # Determine state
        keys = pygame.key.get_pressed()
        if self.airborne:
            self.change_state('jump')
        elif keys[pygame.K_LEFT] or keys[pygame.K_RIGHT]:
            self.change_state('walk')
        else:
            self.change_state('idle')

        # Animate
        frames = self.animations[self.state]
        self.image = frames[self.current_frame % len(frames)]

State Machine Simulation

State machine simulation:
=============================================
  idle[1/2] → idle[2/2] → idle[0/2]
  walk[1/3] → walk[2/3] → walk[3/3] → walk[0/3] → walk[1/3]
  jump[1/1] → jump[0/1] → jump[1/1] → jump[0/1]
  idle[1/2] → idle[2/2]
  attack[1/3] → attack[2/3] → attack[3/3] → attack[0/3] → attack[1/3]
  idle[1/2] → idle[2/2] → idle[0/2]

State transitions:
  idle → walk
  walk → jump
  jump → idle
  idle → attack
  attack → idle

Animations — Summary

  • Spritesheet = single image holding all frames
  • ms timer = FPS-independent (preferred approach)
  • State machine = determines which animation to play
  • Reset current_frame = 0 whenever the state changes