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ón | Reglas |
|---|---|
| Conway’s Life | Soporte vital: 2-3 vecinos sobreviven; 3 vecinos = nace. Emerge de simulación. |
| Cuevas roguelike | Smoothing iterativo: ≥4 vecinos pared → es pared; resto, libre. |
| Fuego con humedad | Vecinos quemándose × (1 - humedad propia) = probabilidad de prender. |
| Agua tipo Terraria | Si 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:
- Cuántos vecinos están prendidos (lineal hasta saturar en 3).
- Humedad de la celda (resta a la probabilidad).
- 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
pressureque 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.
TileMappara visualizar; el board lo mantienes en unint[,]. - Unreal: para grids grandes, usar
TArray<int>flat con índice manual. Para visualización,UInstancedStaticMeshComponentpor 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.
Para generar una cueva roguelike con cellular automata, ¿qué regla aplicas?
Tu fuego con humedad se propaga demasiado uniforme y se ve fake. ¿Qué le falta?
Quieres que tu agua de Terraria llene una vasija sin teleportarse al fondo en un solo frame. ¿Cómo procesas las celdas?
Conway's Game of Life es Turing-completo. ¿Qué significa eso para tu juego?
Tu CA de fuego con humedad necesita procesar 50000 celdas por step. ¿Cómo optimizás?