Scenes, Audio, y Tilemaps

Curso de Desarrollo de Videojuegos con Pygame

Felipe A. Moreno-Vera

πŸ—ΊοΈ Tilemaps & Scrolling

ΒΏQuΓ© es un Tilemap?

Un tilemap es una grilla 2D donde cada celda contiene un nΓΊmero entero pequeΓ±o (tile ID) que se asocia a una imagen de tile. AsΓ­ es como casi todos los juegos 2D almacenan sus niveles β€” desde Super Mario Bros hasta los juegos indie modernos.

ΒΏQuΓ© es un Tilemap?

Un tilemap es una grilla 2D donde cada celda contiene un nΓΊmero entero pequeΓ±o (tile ID) que se asocia a una imagen de tile. AsΓ­ es como casi todos los juegos 2D almacenan sus niveles β€” desde Super Mario Bros hasta los juegos indie modernos.

Level data (2D list of ints):     Rendered result:
0 = air   1 = grass   2 = stone

 0  0  0  0  0  0  0  0            β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
 0  0  0  1  1  0  0  0            β–‘β–‘β–‘πŸŒΏπŸŒΏβ–‘β–‘β–‘
 0  0  0  0  0  0  0  0            β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
 1  1  1  2  2  1  1  1            🌿🌿🌿πŸͺ¨πŸͺ¨πŸŒΏπŸŒΏπŸŒΏ
 2  2  2  2  2  2  2  2            πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨

ΒΏQuΓ© es un Tilemap?

Un tilemap es una grilla 2D donde cada celda contiene un nΓΊmero entero pequeΓ±o (tile ID) que se asocia a una imagen de tile. AsΓ­ es como casi todos los juegos 2D almacenan sus niveles β€” desde Super Mario Bros hasta los juegos indie modernos.

Level data (2D list of ints):     Rendered result:
0 = air   1 = grass   2 = stone

 0  0  0  0  0  0  0  0            β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
 0  0  0  1  1  0  0  0            β–‘β–‘β–‘πŸŒΏπŸŒΏβ–‘β–‘β–‘
 0  0  0  0  0  0  0  0            β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
 1  1  1  2  2  1  1  1            🌿🌿🌿πŸͺ¨πŸͺ¨πŸŒΏπŸŒΏπŸŒΏ
 2  2  2  2  2  2  2  2            πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨πŸͺ¨

Beneficios de los tilemaps: - Los niveles son compactos β€” un mapa de 100Γ—50 son solo 5,000 enteros - FΓ‘ciles de serializar (guardar/cargar niveles como archivos de texto) - Renderizado eficiente β€” dibuja solo los tiles visibles (culling) - Assets reutilizables β€” 10 imΓ‘genes de tile pueden construir niveles infinitos

Clase TileMap

class TileMap:
    def __init__(self, level_data, tile_size):
        self.data      = level_data
        self.tile_size = tile_size
        self.rows      = len(data)
        self.cols      = len(data[0])

    def get_tile(self, col, row):
        if 0 <= row < self.rows and 0 <= col < self.cols:
            return self.data[row][col]
        return -1   # Out of bounds

    def set_tile(self, col, row, tile_id):
        if 0 <= row < self.rows and 0 <= col < self.cols:
            self.data[row][col] = tile_id

    def world_to_tile(self, world_x, world_y):
        return int(world_x // self.tile_size), int(world_y // self.tile_size)

    def draw(self, surface, camera_x, camera_y):
        ts = self.tile_size

        # CULLING: only draw tiles visible in the viewport
        col_start = max(0, int(camera_x // ts))
        col_end   = min(self.cols, int((camera_x + SCREEN_W) // ts) + 1)
        row_start = max(0, int(camera_y // ts))
        row_end   = min(self.rows, int((camera_y + SCREEN_H) // ts) + 1)

        for row in range(row_start, row_end):
            for col in range(col_start, col_end):
                tile_id  = self.data[row][col]
                if tile_id == 0: continue
                screen_x = col * ts - camera_x
                screen_y = row * ts - camera_y
                surface.blit(tile_images[tile_id], (screen_x, screen_y))
Culling savings β€” tiles omitidos vs dibujados por frame
============================================================
  TamaΓ±o de mapa   Total    Visible    Omitidos   Ahorrado
  --------------------------------------------------------
  PequeΓ±o (20Γ—15)  300      300        0          0%
  Mediano (50Γ—30)  1500     408        1092       73%
  Grande (100Γ—50)  5000     408        4592       92%
  Enorme (200Γ—80)  16000    408        15592      97%

La Camera

La Camera representa el viewport hacia el mundo. Todo se dibuja en world space y se convierte a screen space para el rendering.

class Camera:
    def __init__(self, view_w, view_h):
        self.x    = 0.0   # Camera top-left in world space
        self.y    = 0.0
        self.zoom = 1.0

    def world_to_screen(self, wx, wy):
        """What screen pixel does this world coordinate map to?"""
        sx = (wx - self.x) * self.zoom
        sy = (wy - self.y) * self.zoom
        return int(sx), int(sy)

    def screen_to_world(self, sx, sy):
        """What world coordinate is under this screen pixel?"""
        wx = sx / self.zoom + self.x
        wy = sy / self.zoom + self.y
        return wx, wy

    def follow(self, target_x, target_y, dt, world_w, world_h):
        """Smooth follow with lerp."""
        desired_x = target_x - self.view_w / (2 * self.zoom)
        desired_y = target_y - self.view_h / (2 * self.zoom)
        self.x += (desired_x - self.x) * 6.0 * dt  # Lerp
        # Clamp to world bounds
        self.x = max(0, min(self.x, world_w - self.view_w / self.zoom))
        self.y = max(0, min(self.y, world_h - self.view_h / self.zoom))

Camera Dead Zone

Una dead zone es una regiΓ³n alrededor del centro de la pantalla donde la Camera no se mueve. El jugador puede moverse libremente dentro de la dead zone; la Camera solo se mueve cuando el jugador llega a su borde.

Screen:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                  β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚      β”‚  DEAD ZONE   β”‚            β”‚
β”‚      β”‚   Player ●   β”‚ ← camera   β”‚
β”‚      β”‚  moves here  β”‚   doesn't  β”‚
β”‚      β”‚  freely      β”‚   move     β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Camera Shake

def shake(self, strength=8.0, duration=0.4):
    self._shake_strength = strength
    self._shake_timer    = duration

def update_shake(self, dt):
    if self._shake_timer > 0:
        self._shake_timer -= dt
        pct = self._shake_timer / 0.4
        self._shake_offset = (
            random.uniform(-1, 1) * self._shake_strength * pct,
            random.uniform(-1, 1) * self._shake_strength * pct,
        )
    else:
        self._shake_offset = (0, 0)

# Apply offset in world_to_screen():
sx = (wx - self.x) * self.zoom + self._shake_offset[0]

Parallax Scrolling

Parallax hace que el mundo se sienta profundo al hacer scroll de las diferentes capas de fondo a distintas velocidades.

Layer           Speed (% of camera)   Perceived depth
─────           ───────────────────   ───────────────
Stars              5%                   Very far away
Distant mountains  15%                  Far
Near mountains     25%                  Medium
Clouds            30–50%                Near background
class ParallaxLayer:
    """
    factor = 0.0 β†’ stationary (sky)
    factor = 1.0 β†’ moves with the world (foreground)
    """
    def __init__(self, factor):
        self.factor = factor

    def draw(self, surface, camera_x):
        offset_x = int(camera_x * self.factor)
        for element in self.elements:
            screen_x = element.x - offset_x
            # Seamless horizontal wrap
            screen_x = screen_x % (WORLD_W + SCREEN_W) - SCREEN_W
            # Draw at screen_x...

Parallax Scrolling

Camera se desplazΓ³ 500px hacia la derecha

  Capa                   Factor   Desplazamiento IlusiΓ³n
  --------------------------------------------------------
  Estrellas (lejos)      0.05     25           EstΓ‘tico
  MontaΓ±as distantes     0.15     75           Muy lejos
  MontaΓ±as cercanas      0.25     125          Lejos
  Nubes lejanas          0.30     150          Lejos
  Nubes cercanas         0.50     250          Medio
  Árboles de fondo       0.70     350          Igual que el mundo
  Objetos del mundo      1.00     500          Igual que el mundo

Tilemaps & Camera β€” Resumen

  • Tilemap = lista 2D de ints β†’ compacto, serializable, eficiente
  • Culling: dibuja solo los tiles dentro del viewport β€” crΓ­tico para mapas grandes
  • La Camera convierte coordenadas world β†”οΈŽ screen para cada objeto
  • Smooth follow: lerp hacia la posiciΓ³n deseada de la Camera en cada frame
  • Dead zone: el jugador se mueve libremente en una regiΓ³n central antes de que la Camera lo siga
  • Parallax: las capas de fondo hacen scroll segΓΊn camera_x * factor; menor = mΓ‘s lejos