Scenes, Audio y Tilemaps

Curso de Desarrollo de Juegos con Pygame

Felipe A. Moreno-Vera

Nota

En estos módulos aprenderás:

  • Scene management, game state machines y transitions
  • Música de fondo, sound effects, channels y spatial audio
  • Tile maps, sistemas de camera, scrolling y fondos con parallax

Scenes & Game States

El problema: ¿qué es un “game state”?

Un juego nunca está en un solo modo. Alterna entre states:

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

Sin un state system adecuado, terminas con una jungla de checks if game_state == "menu" esparcidos por todas partes. El Scene pattern resuelve esto de forma limpia.

The Scene Base Class

Cada pantalla del juego es una Scene. Cada scene sabe cómo:

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

Esta interface obliga a mantener la lógica de cada scene self-contained. Una MenuScene nunca toca las variables de GameScene.

The Scene Manager

El Scene Manager posee un stack de scenes. La scene que está en la cima del stack es la que se ejecuta.

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 Efecto en el stack Caso de uso
push(PauseScene) [Menu, Game, Pause] Overlay — el juego permanece en memoria
pop() [Menu, Game] Resume — vuelve a la scene anterior
replace(GameOver) [Menu, GameOver] Transición completa — sin vuelta atrás

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']

Compartir datos entre scenes

Las scenes están decoupled — no mantienen referencias entre sí. Usa el shared dict del manager para pasar datos:

# 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

Una transition es un efecto visual que se reproduce mientras se cambian las scenes. La idea clave: renderizar ambas scenes a surfaces, y luego hacer blend entre ellas.

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 — Resumen

  • La base class Scene obliga a implementar enter / exit / handle_event / update / draw
  • SceneManager posee una stack: push superpone, pop regresa, replace reemplaza
  • Comparte datos entre scenes mediante el manager.shared dict
  • Las transitions pre-renderizan ambas scenes a surfaces, y luego hacen blend entre ellas
  • Las easing functions (ease_inout) hacen que las transitions se sientan smooth