Director AI: el pacing dinámico de Left 4 Dead
La IA invisible que decide cuándo soltar la horda y cuándo darte un respiro. Tension curve, stress del jugador y spawning adaptativo: el modelo de Left 4 Dead.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Spawnear enemigos a ritmo fijo es la receta más rápida para que tu juego de acción se sienta plano. Si la presión nunca baja, el jugador se fatiga y se desensibiliza. Si nunca sube, se aburre. El problema no es la dificultad — es el pacing.
Valve resolvió esto en 2008 con un sistema que llamaron AI Director. No controla zombies individuales: controla cuándo aparecen, qué tipo y a qué ritmo, persiguiendo una curva dramática parecida a la de una película. Un buen director te suelta una horda, te deja respirar 30 segundos viendo un pasillo vacío, y justo cuando piensas que ya pasó lo peor — te pone otra.
El patrón se ha repetido en Alien Isolation (donde el Xenomorph tiene su propio director que decide cuándo aparecer), en Resident Evil 4 (las “notas de Hunk” sobre tensión), y en Hades (variación de encuentros según racha). Es uno de los meta-sistemas más rentables que puedes meter en un proyecto.
¿Qué cubre este tutorial?
- Qué es la tension curve y por qué la dificultad lineal aburre.
- Cómo estimar el stress del jugador con 4 inputs baratos.
- Las reglas de transición entre Build Up, Peak, Fade y Relax.
- Las reglas duras de spawning que evitan romper la inmersión.
- Implementación de un Director mínimo en Unity con menos de 60 líneas.
1. Demo
[Visualización pendiente: gráfico de tension curve vs spawning rate]
2. Concepto
El AI Director es un sistema meta que ajusta pacing, dificultad y eventos en tiempo real basándose en una estimación del estrés del jugador. No controla NPCs individuales: controla cuándo aparecen, qué tipo y a qué ritmo. Acuñado por Valve para Left 4 Dead (2008).
Esa definición se desarma en tres piezas. Vamos una por una.
2.1 ¿Qué es la tension curve?
La tension curve viene de dramaturgia clásica: una escena se construye, llega a un pico, decae, y empieza otra. No es una recta ascendente; es una sierra.
El Director persigue una forma dramática (build → peak → relax → build), no una rampa creciente.
Si grafícas el estrés del jugador en un juego de acción bien dirigido, la curva se parece a esto. Los picos te dejan grabado el momento. Los valles te dan tiempo para procesarlo y oír tu propia respiración. Sin valles, los picos pierden valor — todo es ruido.
El Director no persigue la dificultad máxima. Persigue la forma de esa curva.
2.2 ¿Cómo se mide el estrés del jugador?
No tienes acceso al pulso real del jugador, pero puedes estimarlo. La heurística clásica de L4D combina 4 inputs baratos:
- HP bajo:
1 - hp/maxHp. Cuanto menos vida, más estrés. - Ammo bajo:
1 - ammo/maxAmmo. Quedarse sin balas es ansiedad pura. - Tiempo en combate: combate sostenido sube el estrés; tiempo desde el último daño lo baja.
- Compañeros caídos: incapacitados penalizan fuerte (en L4D, un downed teammate dispara estrés a casi máximo).
La fórmula es una suma ponderada, clampada a [0, 1]:
stress = 0.4 * hpLack + 0.3 * ammoLack + 0.3 * incapsRatio
Importa suavizar la señal con un lerp o un EMA. Si el stress salta de 0.2 a 0.9 en un frame porque el jugador se cayó por un barranco, el Director va a sobrereaccionar. Un suavizado del 10% por frame elimina esos artefactos sin perder reactividad.
stress = lerp(stress, computeStressRaw(), 0.1)
2.3 ¿Cómo decide cuándo soltar la horda?
El Director vive en una pequeña FSM de 4 estados:
Relax → BuildUp → Peak → Fade → Relax. Cada flecha es una condición de transición; las cotas duras (minRelax, maxPeak) impiden bucles infinitos.
- Build Up: spawnea enemigos a ritmo creciente. Persigue subir el stress.
- Peak / Sustain Peak: horda activa, intensidad alta. Dura un máximo de
maxPeaksegundos. - Fade: deja de spawnear, los enemigos restantes se acaban solos. El stress empieza a bajar.
- Relax: silencio. Pasillos vacíos, items visibles, música en off. Dura un mínimo de
minRelaxsegundos.
El truco está en las dos cotas duras: minRelax garantiza que después de un peak hay calma palpable; maxPeak garantiza que ningún peak se vuelve permanente aunque el jugador se atasque. Sin esas cotas, el sistema oscila o se queda clavado.
2.4 ¿Qué spawnea cada estado?
Cada estado tiene su tabla de spawning:
| Estado | Frecuencia | Tipos | Posición |
|---|---|---|---|
| BuildUp | media, creciente | comunes en grupos pequeños | fuera de LOS, lateral |
| Peak | alta | horda + 1 special (Tank, Boomer) | múltiples puntos a la vez |
| Fade | cero spawns nuevos | — | — |
| Relax | cero spawns | — (a veces 1 común aislado para mantener tensión baja) | — |
Los specials (los enemigos infrecuentes con habilidad única) son el cetro del Director. En L4D, un Smoker en Build Up cambia el ritmo entero — el jugador deja de avanzar para mirar atrás. Eso es pacing, no dificultad.
2.5 Item placement adaptativo
El Director también modula items. La regla:
- En Build Up, esconde munición y medkits o ponlos lejos del path.
- En Relax, deja un medkit visible justo en el path. Premio por sobrevivir.
- L4D2 además rastrea la escasez detectada: si el equipo lleva 3 minutos con ammo bajo, baja el umbral y empieza a soltar más cajas.
Esto resuelve un problema real de balance: un jugador que sufrió en el último encuentro recibe un respiro material, no solo temporal. Un jugador que llegó intacto encuentra el mismo lugar vacío.
2.6 ¿Cuándo el director rompe la inmersión?
Si el jugador siente al Director, ya lo perdiste. La confianza en el mundo se evapora cuando los enemigos aparecen de la nada o cuando un medkit se materializa en el suelo. Reglas duras, no negociables:
- No spawnear en LOS del jugador. Nunca. Usa raycast contra cada miembro del equipo.
- No spawnear en la misma zona dos veces seguidas. Mantén un short-term memory de los últimos N spawn points usados.
- Items en posiciones plausibles: una caja sobre una mesa, no en mitad de un pasillo limpio. Los spawners de items deben ser puntos pre-autorizados por el level designer.
- No spawnear durante una cinemática o un scripted moment. Suena obvio. No lo es.
- Spawn budget por zona: cada área tiene un máximo de enemigos activos. Si el budget está lleno, el Director espera.
El AI Director de Alien Isolation lleva esto al extremo: el Xenomorph tiene un “director” propio que nunca lo teletransporta. Si el jugador no debería poder ver al Xeno, el director lo mueve por ductos invisibles — pero el Xeno físicamente atraviesa el mapa.
2.7 Director vs DDA puro
Es fácil confundirlos. No son lo mismo:
| DDA | Director | |
|---|---|---|
| Optimiza | dificultad por skill | experiencia dramática |
| Modifica | daño, HP enemigo, aim assist | timing, frecuencia, tipo de spawn |
| Mide | win rate, deaths, completion time | stress estimado, tiempo en cada estado |
| Granularidad | continua | discreta (estados) |
En L4D2 hay ambos. El DDA ajusta cuánto daño hacen los specials según el skill del equipo; el Director ajusta cuándo aparecen. Son ejes ortogonales. Puedes tener uno sin el otro, pero en producción suelen complementarse.
3. Pseudocódigo
class Director
state: enum { BuildUp, Peak, Fade, Relax }
stress: Float # 0..1, suavizado por EMA
timeInState: Float
minRelax, maxPeak: Float
function update(director: Director, players: List<Player>, dt: Float)
raw = computeStress(players)
director.stress = lerp(director.stress, raw, 0.1)
director.timeInState += dt
transition(director)
applyStateRules(director, dt)
function computeStress(players) -> Float
avgHp = mean(p.hp / p.maxHp for p in players)
ammoLack = mean(1 - p.ammo / p.maxAmmo for p in players)
incaps = sum(1 for p in players if p.incapped) / count(players)
raw = 0.4 * (1 - avgHp) + 0.3 * ammoLack + 0.3 * incaps
return clamp01(raw)
function transition(d: Director)
if d.state == Relax and d.timeInState > d.minRelax and d.stress < 0.3
enter(d, BuildUp)
elif d.state == BuildUp and d.stress > 0.6
enter(d, Peak)
elif d.state == Peak and (d.stress > 0.85 or d.timeInState > d.maxPeak)
enter(d, Fade)
elif d.state == Fade and d.stress < 0.4
enter(d, Relax)
function enter(d: Director, newState)
d.state = newState
d.timeInState = 0
function applyStateRules(d: Director, dt: Float)
match d.state:
BuildUp: spawnCommons(rate = 0.5 + d.timeInState * 0.1)
Peak: spawnHorde(); maybeSpawnSpecial()
Fade: # nada
Relax: placeItemsIfPath()
El bucle es siempre el mismo: leer mundo → suavizar → transicionar → aplicar reglas. No hay nada más. La complejidad emerge de las tablas que cuelgan de cada estado, no del flujo.
4. Implementación en Unity / C#
using System.Collections.Generic;
using UnityEngine;
public class Director : MonoBehaviour {
public enum State { Relax, BuildUp, Peak, Fade }
[Header("Tuning")]
public float minRelax = 30f;
public float maxPeak = 25f;
public float smoothing = 0.1f;
[Header("Refs")]
public List<PlayerStats> players;
public SpawnerService spawner;
public State state = State.Relax;
public float stress;
public float timeInState;
void Update() {
float dt = Time.deltaTime;
stress = Mathf.Lerp(stress, ComputeStress(), smoothing);
timeInState += dt;
Transition();
Tick(dt);
}
float ComputeStress() {
if (players.Count == 0) return 0f;
float hpLack = 0f, ammoLack = 0f, incaps = 0f;
foreach (var p in players) {
hpLack += 1f - p.HpRatio;
ammoLack += 1f - p.AmmoRatio;
incaps += p.Incapped ? 1f : 0f;
}
int n = players.Count;
return Mathf.Clamp01(0.4f * hpLack/n + 0.3f * ammoLack/n + 0.3f * incaps/n);
}
void Transition() {
switch (state) {
case State.Relax:
if (timeInState > minRelax && stress < 0.3f) Enter(State.BuildUp); break;
case State.BuildUp:
if (stress > 0.6f) Enter(State.Peak); break;
case State.Peak:
if (stress > 0.85f || timeInState > maxPeak) Enter(State.Fade); break;
case State.Fade:
if (stress < 0.4f) Enter(State.Relax); break;
}
}
void Enter(State s) { state = s; timeInState = 0f; }
void Tick(float dt) {
switch (state) {
case State.BuildUp: spawner.SpawnCommons(0.5f + timeInState * 0.1f, dt); break;
case State.Peak: spawner.SpawnHorde(dt); spawner.MaybeSpecial(dt); break;
case State.Relax: spawner.PlaceItemsAlongPath(); break;
// Fade: nada
}
}
}
SpawnerService es donde viven las reglas duras: no spawnear en LOS, no repetir zona, respetar budget. El Director solo dice “ahora dame horda”. El dónde y el cómo plausible lo decide el servicio.
5. En otros engines
- Godot: el Director vive en un
Nodeautoload (singleton) registrado en Project Settings. La FSM es trivial con unenumymatch; conecta señales (signal) para que el spawner reaccione a cambios de estado sin acoplamiento directo. - Unreal: implementa el Director como
UGameInstanceSubsystem(vida igual al GameInstance) o como componente delGameModesi es por partida.FTimerManagerpara los timers de estado;UDataAssetpara las tablas de spawn por estado. - JavaScript / Web: cualquier objeto con
tick(dt)llamado desde tu game loop. Como el Director no toca rendering, es trivial portarlo entre engines.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Tu Director sube la intensidad cada vez que detecta que el jugador parece estresado. En testing, los playtesters dicen que el juego se siente injusto. ¿Qué pasa?
Quieres garantizar que después de un Peak fuerte haya al menos 30 segundos de calma, sin excepciones. ¿Dónde se enforce eso?
Tu Director entra en Peak y nunca sale. Reviste y ves que la única salida es 'stress > 0.85'. ¿Qué falta?
Activas el Director y los playtesters reportan que 'los enemigos aparecen mágicamente delante de ellos'. Tu spawner respeta el budget y los tipos. ¿Qué regla dura falta?