Scenes, Audio, and Tilemaps

Pygame Game Development Course

Felipe A. Moreno-Vera

Note

In these modules you will learn:

  • Scene management, game state machines, and transitions
  • Background music, sound effects, channels, and spatial audio
  • Tile maps, camera systems, scrolling, and parallax backgrounds

Scenes & Game States

The Problem: what is a “game state”?

A game is never in just one mode. It alternates between states:

┌─────────┐   ENTER    ┌──────────┐   PAUSE    ┌─────────┐
│  MENU   │ ─────────► │   GAME   │ ─────────► │  PAUSE  │
└─────────┘            └──────────┘            └─────────┘
                            │                       │ RESUME
                          DEAD                      ▼
                            │                  ┌──────────┐
                            └────────────────► │ GAME     │
                                               └──────────┘
                                                    │ 0 lives
                                                    ▼
                                               ┌──────────┐
                                               │ GAME OVER│
                                               └──────────┘

Without a proper state system, you end up with a jungle of if game_state == "menu" checks scattered everywhere. The Scene pattern solves this cleanly.

The Scene Base Class

Every screen in the game is a Scene. Each scene knows how to:

class Scene:
    def enter(self):        # Called once when scene becomes active
        pass

    def exit(self):         # Called once when scene is deactivated
        pass

    def handle_event(self, event):   # Process pygame events
        pass

    def update(self, dt):   # Update logic; dt = delta time in seconds
        pass

    def draw(self, surface): # Render to the screen
        pass

This interface forces you to keep each scene’s logic self-contained. A MenuScene never touches GameScene’s variables.

The Scene Manager

The Scene Manager owns a stack of scenes. The scene on top of the stack is the one that runs.

class SceneManager:
    def __init__(self):
        self._stack = []

    def push(self, scene):
        """Add a scene on top — previous scene stays in memory."""
        if self._stack:
            self._stack[-1].exit()
        self._stack.append(scene)
        scene.enter()

    def pop(self):
        """Remove the top scene — reveals the scene below."""
        self._stack.pop().exit()
        if self._stack:
            self._stack[-1].enter()

    def replace(self, scene):
        """Swap the current scene — no going back."""
        if self._stack:
            self._stack.pop().exit()
        self._stack.append(scene)
        scene.enter()

Push vs Replace vs Pop

Method Stack effect Use case
push(PauseScene) [Menu, Game, Pause] Overlay — game stays in memory
pop() [Menu, Game] Resume — return to previous scene
replace(GameOver) [Menu, GameOver] Full transition — no going back

Push vs Replace vs Pop

=== PUSH Menu ===
  ► MenuScene.enter()
  Stack: ['MenuScene']

=== PUSH Game ===
  ◄ MenuScene.exit()
  ► GameScene.enter()
  Stack: ['MenuScene', 'GameScene']

=== PUSH Pause (overlay) ===
  ◄ GameScene.exit()
  ► PauseScene.enter()
  Stack: ['MenuScene', 'GameScene', 'PauseScene']

=== POP (resume) ===
  ◄ PauseScene.exit()
  ► GameScene.enter()
  Stack: ['MenuScene', 'GameScene']

=== REPLACE with GameOver ===
  ◄ GameScene.exit()
  ► GameOverScene.enter()
  Stack: ['MenuScene', 'GameOverScene']

Sharing Data Between Scenes

Scenes are decoupled — they don’t hold references to each other. Use the manager’s shared dict to pass data:

# In GameScene — store the score when dying
self.manager.shared['score'] = self.score
self.manager.replace(GameOverScene(self.manager))

# In GameOverScene — read it back
self.score = self.manager.shared.get('score', 0)

Scene Transitions

A transition is a visual effect that plays while swapping scenes. The key insight: render both scenes to surfaces, then blend them.

class TransitionEngine:
    def start(self, surf_from, surf_to, mode="fade", duration=0.5):
        self.surf_from = surf_from.copy()
        self.surf_to   = surf_to.copy()
        self.progress  = 0.0
        self.active    = True

    def update(self, dt):
        self.progress += dt / self.duration
        if self.progress >= 1.0:
            self.active = False

    def draw(self, surface):
        p = self.progress * self.progress * (3 - 2 * self.progress)  # Ease

        if self.mode == "fade":
            if p < 0.5:
                alpha = int(255 * (1 - p * 2))
                self.surf_from.set_alpha(alpha)
                surface.blit(self.surf_from, (0, 0))
            else:
                alpha = int(255 * ((p - 0.5) * 2))
                self.surf_to.set_alpha(alpha)
                surface.blit(self.surf_to, (0, 0))

        elif self.mode == "slide_left":
            offset = int((1 - p) * WIDTH)
            surface.blit(self.surf_from, (-offset, 0))
            surface.blit(self.surf_to, (WIDTH - offset, 0))

Scene Transitions

t        linear   ease_in  ease_out  ease_inout
----------------------------------------------------
  0.0     0.00    0.00     0.00      0.00
  0.1     0.10    0.01     0.19      0.03
  0.2     0.20    0.04     0.36      0.10
  0.3     0.30    0.09     0.51      0.22
  0.4     0.40    0.16     0.64      0.35
  0.5     0.50    0.25     0.75      0.50
  0.6     0.60    0.36     0.84      0.65
  0.7     0.70    0.49     0.91      0.78
  0.8     0.80    0.64     0.96      0.90
  0.9     0.90    0.81     0.99      0.97
  1.0     1.00    1.00     1.00      1.00

Scene transitions — Summary

  • Scene base class enforces enter / exit / handle_event / update / draw
  • SceneManager owns a stack: push overlays, pop returns, replace swaps
  • Share data between scenes via manager.shared dict
  • Transitions pre-render both scenes to surfaces, then blend them
  • Easing functions (ease_inout) make transitions feel smooth