Procedural Intermedio 12 min de lectura

Cellular Automata: Conway, cuevas, fuego con humedad y agua

Agrega complejidad para volver mas organico tu juego. Conway, cuevas roguelike, propagación de fuego con humedad y agua que cae como Terraria.

Publicado: · Por Juanjo "Banyo" López

0. Introducción

Un cellular automaton (CA) es un grid donde cada celda se actualiza según el estado de sus vecinos, con una regla idéntica para todas. La sorpresa: con reglas triviales, emergen comportamientos complejos — gliders en Conway, cuevas naturales, fuego que se siente realista, agua que cae con física simple.

En juegos, los CA son la base de:

AplicaciónReglas
Conway’s LifeSoporte vital: 2-3 vecinos sobreviven; 3 vecinos = nace. Emerge de simulación.
Cuevas roguelikeSmoothing iterativo: ≥4 vecinos pared → es pared; resto, libre.
Fuego con humedadVecinos quemándose × (1 - humedad propia) = probabilidad de prender.
Agua tipo TerrariaSi abajo libre → cae; si bloqueado → spread lateral.

Las cuatro caben en menos de 20 líneas de código cada una. Lo que las hace útiles es elegir la regla adecuada al efecto que buscas.

1. Conway’s Game of Life — el original

Conway's Game of Life: las reglas más famosas de cellular automata. Una celda viva con 2-3 vecinos sobrevive; una muerta con exactamente 3 vecinos nace. Click para pintar; presets abajo.

Figura 1 — Tres patrones plantados: glider (esquina), blinker, toad. Click+drag para pintar; presets para spawnear figuras conocidas. La regla es trivial pero la dinámica es Turing-completa.

1.1 Las reglas (B3/S23)

Para cada celda, contar vecinos vivos (vecindad de Moore: 8 alrededor):

si la celda está VIVA:
    si vecinos == 2 o 3 → sobrevive
    en otro caso → muere

si la celda está MUERTA:
    si vecinos == 3 → nace
    en otro caso → sigue muerta

Eso es todo. Notación canónica: B3/S23 (born en 3 vecinos, survives en 2-3).

1.2 Por qué importa para juegos

Conway puro casi nunca aparece en gameplay (es muy específico). Pero el patrón “regla local + actualización síncrona” es el mismo de:

  • Cuevas, fuego, agua (este tutorial).
  • Simulación de plagas, infecciones, virus.
  • Crecimiento de plantas, expansión bacteriana.
  • Sandboxes tipo Powder Toy y Sandboxels.

Aprendete Conway primero, los demás son derivados.

2. Cuevas roguelike — smoothing iterativo

Cueva procedural estilo Sebastian Lague: random fill + smoothing iterativo. Cada iteración, una celda con > 4 vecinos pared se vuelve pared; con < 4, se libera. Las cuevas emergen orgánicas y conectadas.

Figura 2 — Random fill al 45% + 5 iteraciones de smoothing. Cuevas orgánicas conectadas. Este es el algoritmo de Sebastian Lague que hizo famoso el approach en YouTube y miles de roguelikes lo usan.

2.1 Las reglas

1. Inicialización: cada celda es pared con probabilidad fillProb (típico 0.45)
2. Por N iteraciones (típico 4-6):
    para cada celda c:
        wallNeighbors = vecinos pared (incluyendo bordes del mapa como pared)
        si wallNeighbors >= 4 → c es pared
        en otro caso → c es libre

Una sola pasada de smoothing ya cierra cavidades pequeñas y suaviza. Más iteraciones = cavidades más grandes y conectadas.

2.2 Por qué funciona

La regla 4-vecinos es un filtro de mediana: tienden a desaparecer detalles pequeños y a sobrevivir formas grandes. Empezando desde ruido aleatorio uniforme, eso converge a “blobs” — exactamente lo que queremos para cuevas.

2.3 Lo que necesitas añadir en producción

  • FloodFill final: identificar regiones desconectadas y eliminarlas (o conectarlas con túneles BFS).
  • Spawn points y goals: encontrar las regiones grandes y poner el jugador / salida en cada extremo.
  • Decoración: una vez que la mazmorra es jugable, agregar piezas (cofres, enemigos) usando Poisson disk u otro método de distribución.

3. Fuego con humedad — CA probabilístico

Cada celda tiene humedad (color verde→ámbar). El fuego solo prende vecinos con humedad baja, y con probabilidad inversa a esa humedad. Click para encender; sube el slider y mira cómo el incendio se ahoga.

Figura 3 — Cada celda tiene humedad propia (verde→ámbar). El fuego solo prende vecinos con humedad baja, con probabilidad inversa. Sube el slider de humedad global y observa cómo el incendio se ahoga.

3.1 Las reglas

Cada celda tiene un estado FSM: 0 (sin prender), 1 (quemándose), 2 (quemado, no propaga).

para cada celda c con estado == 0:
    burning = count vecinos con estado == 1
    si burning > 0:
        prob = 0.85 * (1 - moisture[c]) * min(burning, 3) / 3
        si random() < prob → c.state = 1

para cada celda con estado == 1:
    si random() < burnoutChance → c.state = 2

Tres factores:

  1. Cuántos vecinos están prendidos (lineal hasta saturar en 3).
  2. Humedad de la celda (resta a la probabilidad).
  3. Aleatoriedad (irregularidad orgánica).

3.2 Variantes que aparecen en producción

  • Don’t Starve: viento direccional. La probabilidad para vecinos a sotavento es multiplicada.
  • Project Zomboid: tipos de material. Madera flammability alta, concreto baja, líquido cero.
  • Noita: cada material define flammability, burningTemp, burnoutTime. La interacción de materiales es lo que hace a Noita única.

4. Agua que cae — CA con prioridad direccional

Agua con reglas locales tipo Terraria/Sandboxels. Cada celda mira solo a sus vecinos directos: si abajo hay vacío, cae; si está bloqueada, intenta a los lados (con caída debajo). Sin físicas, solo CA.

Figura 4 — Click+drag para añadir agua, Shift+click para muros, Alt+click para borrar. La regla es local: si abajo está libre, cae; si bloqueado, intenta lateralmente con caída debajo. Sin física continua.

4.1 Las reglas (procesando de abajo hacia arriba)

para cada celda con agua, de Y máximo a 0:
    si celda(x, y+1) está vacía:
        mover agua a (x, y+1)
    sino:
        para dirección aleatoria (left, right):
            si celda(x+dx, y) está vacía Y celda(x+dx, y+1) está vacía:
                mover agua a (x+dx, y)
                break

Procesar de abajo hacia arriba evita “gotas teleport” (el agua sale de su celda y cae por gravedad en el mismo paso).

4.2 Por qué se ve “tipo Terraria”

Lo que percibís como agua viscosa es:

  • La regla “abajo primero” que la hace caer naturalmente.
  • La aleatoriedad de spread lateral que evita columnas verticales infinitas.
  • El check de “abajo libre” para spread que impide flotar.

Resultado: agua que llena vasijas, escurre por escalones, no flota. Tres reglas, comportamiento que parece físico.

4.3 Mejoras en producción

  • Presión de agua: cada celda tiene pressure que aumenta con altura. El spread se reorganiza para igualar presión.
  • Tipos de fluido: aceite flota sobre agua, lava se solidifica al tocar agua, etc. Cada par tiene reglas de interacción.
  • Optimización: solo procesar celdas “vivas” (con agua o con vecinos con agua). En grids 1000×1000, eso es la diferencia entre 60fps y stutter.

5. El patrón general — pasos sincrónicos vs asincrónicos

La mayoría de CA son sincrónicos: cada step se calcula desde el estado completo del paso anterior. Eso requiere dos buffers (lectura y escritura) — no podes modificar el board mientras lo lees.

function step(board) -> Board
    next = newBoard()
    for each cell c in board:
        next[c] = applyRule(board, c)
    return next

Asincrónicos (modificar in-place mientras se itera) son más rápidos pero el comportamiento puede cambiar. Conway requiere sincrónico; agua puede ser asincrónico (si procesas en orden bottom-up, el resultado es similar).

Saber cuál usar es decisión de diseño:

  • Conway, fuego: sincrónico. La regla mira “el estado actual”, no parchazos parciales.
  • Agua, partículas físicas: típicamente asincrónico con orden cuidadoso.

6. Pseudocódigo — un step genérico

function caStep(board, rule) -> Board
    next = newBoard(board.cols, board.rows)
    for each cell c in board:
        next[c] = rule(board, c)
    return next

// rules:
function conwayRule(board, c)
    n = countNeighbors8(board, c, alive)
    if board[c] is alive:  return n == 2 or n == 3 ? alive : dead
    else:                   return n == 3 ? alive : dead

function caveRule(board, c)
    walls = countNeighbors8(board, c, wall, oobAsWall = true)
    return walls >= 4 ? wall : empty

function fireRule(board, c)
    if board[c] == burning: return random() < burnoutChance ? burnt : burning
    if board[c] == burnt:   return burnt
    burning = countNeighbors8(board, c, burning)
    if burning == 0: return empty
    prob = 0.85 * (1 - moisture[c]) * min(burning, 3) / 3
    return random() < prob ? burning : empty

7. Implementación en Unity / C#

using UnityEngine;

public static class CellularAutomaton {
    public static int[,] Step(int[,] board, System.Func<int[,], int, int, int> rule) {
        int W = board.GetLength(0), H = board.GetLength(1);
        var next = new int[W, H];
        for (int y = 0; y < H; y++)
            for (int x = 0; x < W; x++)
                next[x, y] = rule(board, x, y);
        return next;
    }

    public static int CountNeighbors(int[,] board, int x, int y, int target, bool oobIs = false) {
        int W = board.GetLength(0), H = board.GetLength(1);
        int c = 0;
        for (int dy = -1; dy <= 1; dy++)
            for (int dx = -1; dx <= 1; dx++) {
                if (dx == 0 && dy == 0) continue;
                int nx = x + dx, ny = y + dy;
                if (nx < 0 || ny < 0 || nx >= W || ny >= H) {
                    if (oobIs) c++;
                    continue;
                }
                if (board[nx, ny] == target) c++;
            }
        return c;
    }

    // Conway
    public static int ConwayRule(int[,] board, int x, int y) {
        int n = CountNeighbors(board, x, y, 1);
        return board[x, y] == 1 ? (n == 2 || n == 3 ? 1 : 0) : (n == 3 ? 1 : 0);
    }

    // Cave smoothing
    public static int CaveRule(int[,] board, int x, int y) {
        int walls = CountNeighbors(board, x, y, 1, oobIs: true);
        return walls >= 4 ? 1 : 0;
    }
}

8. En otros engines

  • Godot: el patrón se traduce directo. TileMap para visualizar; el board lo mantienes en un int[,].
  • Unreal: para grids grandes, usar TArray<int> flat con índice manual. Para visualización, UInstancedStaticMeshComponent por celda (escala a 100k+ celdas).
  • JavaScript / TypeScript: la lib del sitio (~/lib/viz/sim/cellular) trae todas las primitivas usadas en este tutorial.

9. Quiz

Pon a prueba lo que entendiste

Responde una por una. La explicación aparece al elegir, correcta o no.

  1. Para generar una cueva roguelike con cellular automata, ¿qué regla aplicas?

  2. Tu fuego con humedad se propaga demasiado uniforme y se ve fake. ¿Qué le falta?

  3. Quieres que tu agua de Terraria llene una vasija sin teleportarse al fondo en un solo frame. ¿Cómo procesas las celdas?

  4. Conway's Game of Life es Turing-completo. ¿Qué significa eso para tu juego?

  5. Tu CA de fuego con humedad necesita procesar 50000 celdas por step. ¿Cómo optimizás?

10. Siguientes pasos