Scene management
Implement a Scene base class and a SceneManager that swaps between title, game, and game-over screens cleanly.
- Implement a Scene base class with update() and draw() methods
- Build a SceneManager that holds the current scene and swaps on request
- Wire title → game → game-over transitions using set_scene()
The previous lesson described the state machine as a design principle. This
lesson turns it into code. The pattern is straightforward: a Scene base class
defines the interface every state must honour, and a SceneManager holds the
currently active scene and exposes a set_scene() method for transitions. The
main loop knows nothing about which scene is active — it just calls
manager.current.update(events) and manager.current.draw(screen).
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 scenes.py.
The Scene base class and SceneManager
import pygame
import sys
# ── Scene base class ─────────────────────────────────────────────────────────
class Scene:
"""All game states inherit from this."""
def __init__(self, manager):
self.manager = manager # back-reference so scenes can switch
def update(self, events):
"""Called once per frame with the current event list."""
pass
def draw(self, screen):
"""Called once per frame to render to the display surface."""
pass
# ── SceneManager ──────────────────────────────────────────────────────────────
class SceneManager:
def __init__(self, initial_scene):
self.current = initial_scene
def set_scene(self, scene):
"""Swap to a new scene immediately."""
self.current = scene
# ── Concrete scenes ───────────────────────────────────────────────────────────
class TitleScene(Scene):
def __init__(self, manager):
super().__init__(manager)
self.font = pygame.font.SysFont(None, 64)
def update(self, events):
for event in events:
if event.type == pygame.KEYDOWN:
# Any key starts the game
self.manager.set_scene(GameScene(self.manager))
def draw(self, screen):
screen.fill((20, 20, 40))
title = self.font.render("MY GAME", True, (255, 255, 255))
sub = pygame.font.SysFont(None, 32).render(
"Press any key to start", True, (180, 180, 180))
screen.blit(title, title.get_rect(center=(320, 200)))
screen.blit(sub, sub.get_rect(center=(320, 270)))
class GameScene(Scene):
def __init__(self, manager):
super().__init__(manager)
self.font = pygame.font.SysFont(None, 32)
self.px = 300
self.py = 220
self.health = 3
self.score = 0
def update(self, events):
for event in events:
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
# Simulated death for demonstration
self.manager.set_scene(
GameOverScene(self.manager, self.score))
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]: self.px -= 4
if keys[pygame.K_RIGHT]: self.px += 4
if keys[pygame.K_UP]: self.py -= 4
if keys[pygame.K_DOWN]: self.py += 4
def draw(self, screen):
screen.fill((30, 30, 50))
pygame.draw.rect(screen, (100, 210, 120),
pygame.Rect(self.px, self.py, 40, 40))
hud = self.font.render(
f"Score: {self.score} ESC = game over", True, (255, 255, 255))
screen.blit(hud, (10, 10))
class GameOverScene(Scene):
def __init__(self, manager, score):
super().__init__(manager)
self.score = score
self.font = pygame.font.SysFont(None, 56)
def update(self, events):
for event in events:
if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
# R restarts from the title
self.manager.set_scene(TitleScene(self.manager))
def draw(self, screen):
screen.fill((50, 10, 10))
msg = self.font.render("GAME OVER", True, (255, 80, 80))
score = pygame.font.SysFont(None, 36).render(
f"Score: {self.score} R to restart", True, (200, 200, 200))
screen.blit(msg, msg.get_rect(center=(320, 200)))
screen.blit(score, score.get_rect(center=(320, 270)))
# ── Main loop ─────────────────────────────────────────────────────────────────
def main():
pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Scene management demo")
clock = pygame.time.Clock()
# Build manager and wire the first scene
manager = SceneManager(None)
manager.set_scene(TitleScene(manager))
running = True
while running:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
manager.current.update(events)
manager.current.draw(screen)
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()How the pieces connect
Scene.__init__(manager) stores a reference to the SceneManager. This is
how a scene triggers a transition: instead of setting a global variable, it
calls self.manager.set_scene(SomeOtherScene(self.manager)). The new scene is
constructed with the same manager, so it can trigger further transitions later.
update(events) receives the already-collected event list from the main
loop rather than calling pygame.event.get() itself. This avoids draining the
queue twice and makes scenes easier to test in isolation.
SceneManager.set_scene() swaps immediately. The current frame's draw()
call will use the new scene, but the new scene's update() runs next frame.
That one-frame delay is imperceptible.
Per-scene state lives on the scene instance. GameScene owns px, py,
health, and score. When GameOverScene is constructed with self.score,
it captures the final value. When the player restarts and a fresh TitleScene
→ GameScene chain is constructed, all state is reset automatically — no
manual clearing required.
Notice the main loop never changes. To add a settings screen, a shop screen,
or a cutscene, you write a new Scene subclass and call set_scene() from
wherever the transition should happen. The loop stays untouched.
Where to go next
Next: entity design — thinking about how to structure the objects that live inside a scene as your games grow.
Game state machines
A state machine names each mode of play and enforces explicit transitions — turning a tangle of if/elif chains into a structure you can reason about.
Entity design
Understand why flat data plus logic works for small games, where deep inheritance breaks down, and how composition scales to complex behaviour.