Squad AI: coordinación táctica entre NPCs
Tokens, roles y blackboard compartido: cómo F.E.A.R., Halo y Killzone hacen que un grupo de enemigos parezca un equipo y no cuatro idiotas.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Pones cuatro enemigos en una sala. Cada uno tiene su behavior tree, su FOV, su raycast de visión. Los cuatro detectan al jugador en el mismo frame. Los cuatro deciden “flanquear por la derecha”. Los cuatro se asoman a la misma esquina como un coro de pingüinos sincronizados y mueren en orden.
El problema no es la IA individual. La IA individual está bien. El problema es que no hay nadie pensando por el grupo.
¿Qué es Squad AI?
Squad AI es una capa de coordinación que se monta sobre las IAs individuales. Reparte roles, comparte información mediante un blackboard, y usa tokens para evitar que dos NPCs hagan la misma acción al mismo tiempo.
El patrón nació en F.E.A.R. (2005), donde Jeff Orkin combinó GOAP individual con coordinación de squad. La saga Halo lo refinó con voice barks y reasignación dinámica. Killzone 2/3 lo usó para flanqueos predecibles pero creíbles. Helldivers 2 lo lleva al extremo cooperativo entre bots y jugadores.
¿Qué problema concreto resuelve?
- Evita el “coro tonto”: cuatro NPCs no toman la misma decisión a la vez.
- Reparte trabajo táctico: uno suprime con fuego, otro flanquea, otro reposiciona. Roles distintos producen drama.
- Comparte información: si un NPC vio al jugador, los demás ya lo saben aunque no tengan línea de visión.
- Da feedback al jugador: los voice barks (“¡cubriéndote!”, “¡está flanqueando!”) refuerzan que hay un equipo enfrente.
¿Cuándo NO usar Squad AI?
- Enemigos solitarios (un mini-boss, un cazador único): no hay squad que coordinar.
- Hordas masivas tipo Left 4 Dead: ahí pesa más el director central que el squad táctico.
- Equipos con menos de 5 días para implementar IA: el coste de coordinación encima de BT/GOAP individual no es trivial.
En esta página vas a ver:
- El problema del coro tonto y por qué la IA individual no lo arregla.
- Blackboard compartido, tokens y roles: las tres piezas centrales.
- Cómo se monta squad AI encima de tu BT o utility existente, sin reescribirlo.
- Un snippet Unity con
Squad : MonoBehaviourque orquesta a variosNpcAgent.
1. Demo
2. Cómo funciona Squad AI
2.1 El problema del coro tonto
Cada NPC, evaluado en aislamiento, toma la mejor decisión local. Eso normalmente está bien: por eso usas BT o utility. El problema aparece cuando varios NPCs comparten el mismo contexto: misma posición del jugador, misma cobertura disponible, misma evaluación de utility.
Resultado: todos eligen lo mismo al mismo tiempo.
Sin coordinación, los 4 NPCs evalúan lo mismo y deciden lo mismo: fila india por la misma esquina.
La IA individual no se equivoca: la opción óptima es flanquear. El error es que cuatro agentes evalúen lo óptimo sin saber que los otros tres ya lo decidieron también.
La coordinación no se resuelve haciendo a cada NPC “más inteligente”. Se resuelve añadiendo una capa que sabe lo que el grupo entero está haciendo.
2.2 El blackboard compartido
El blackboard es una estructura clave-valor accesible por todos los miembros del squad. Funciona como memoria colectiva: lo que un NPC ve, todos lo saben.
Cada miembro escribe lo que percibe y lee lo que necesita. Una sola línea de visión informa a todo el squad.
Cada NPC contribuye con lo que percibe y consulta lo que necesita. Si NPC1 ve al jugador y escribe su posición, NPC2 sin LOS puede igualmente disparar suprimiendo a esa ubicación. Eso es lo que hace que un squad parezca informado: una sola línea de visión informa a todos.
¿Cuándo se limpia el blackboard?
Datos viejos contaminan decisiones. Cada entrada del blackboard debe tener un TTL (time-to-live) o un timestamp:
function isStale(entry, now)
return now - entry.timestamp > TTL
Cuando knownPlayerPosition lleva 10 segundos sin actualizarse, el squad entra en estado “buscar” en vez de seguir disparando a un punto donde el jugador ya no está. Sin TTL, los NPCs disparan al fantasma del jugador hasta el fin de los tiempos.
2.3 ¿Qué son los tokens y por qué funcionan?
Un token es un recurso limitado del squad. Solo el NPC que tiene el token puede ejecutar la acción asociada.
Cada token es un recurso limitado. El NPC que lo reclama gana derecho a ejecutar la acción; el resto cae al siguiente comportamiento de su BT.
El BT o utility del NPC, antes de elegir “flanquear”, pide el token al squad. Si está disponible, lo reclama y procede. Si no, el flanqueo falla y el árbol cae al siguiente comportamiento (suprimir, esperar, reposicionar).
Esto produce variedad emergente sin escribir lógica explícita para repartir comportamientos. Cuatro NPCs con la misma IA, pidiendo un token de flanqueo único, terminan haciendo cuatro cosas distintas: uno flanquea, dos suprimen, uno espera. Al frame siguiente, si el flanker muere, el token se libera y otro toma su lugar.
2.4 Roles dentro del squad
Los roles son etiquetas que el squad asigna a sus miembros para sesgar sus decisiones. Cinco arquetipos clásicos:
| Rol | Qué hace | Cuándo se asigna |
|---|---|---|
| Leader | Toma decisiones de squad, prioriza objetivos | Designado al spawn, reasignado si muere |
| Suppressor | Fuego de cobertura sostenido desde posición fija | NPC con LOS y arma automática |
| Flanker | Busca rodear al jugador por un lado expuesto | NPC con buena movilidad y token FLANK |
| Retreater | Reposiciona a cobertura cuando recibe daño | NPC con HP bajo |
| Medic / Support | Cura aliados, da munición, reanima | Si tu juego lo soporta |
Los roles pueden ser estáticos (asignados al spawn según equipamiento: el del rifle siempre suprime) o dinámicos (reasignados según la situación: si el flanker pierde HP, pasa a retreater). Lo dinámico se siente más vivo, pero requiere reevaluación periódica.
function assignRoles(squad)
for each agent in squad.members
if agent.hp < 0.25
agent.role = RETREATER
else if agent.weapon.isAutomatic and agent.hasLOS
agent.role = SUPPRESSOR
else if squad.tryClaimToken(agent, FLANK)
agent.role = FLANKER
else
agent.role = ADVANCER
2.5 ¿Cómo se monta Squad AI sobre BT o GOAP individual?
Esta es la parte que más confunde: el squad no reemplaza al BT. Se monta encima.
El squad coordina con blackboard, tokens y roles. Cada NPC sigue ejecutando su propio BT y consulta al squad en hojas concretas.
El flujo en cada frame:
- Squad tick (1 vez por frame por squad): el squad actualiza su blackboard agregando percepción de todos los miembros, asigna roles, distribuye tokens prioritarios.
- NPC tick (1 vez por frame por NPC): cada NPC ejecuta su BT/utility como siempre, pero consulta al squad antes de tomar decisiones que requieren coordinación.
Una hoja del BT del NPC se ve así:
Sequence "Flanquear"
+-- Condition: ¿el squad me autoriza el token FLANK?
+-- Action: ir a la posición de flanqueo
+-- Action: disparar
Si el token no está disponible, la condition devuelve failure, el selector padre cae al siguiente subárbol (“suprimir desde mi posición”). El BT del NPC no sabe que hay un squad: solo sabe que esa acción a veces puede ejecutarla y a veces no.
2.6 Voice barks: la coordinación que el jugador escucha
Un voice bark es una línea corta que un NPC dice en voz alta: “¡Está flanqueando!”, “¡Cubriéndote!”, “¡Recargando!”. No son decorativos, hacen tres cosas:
- Telegrafían la coordinación al jugador. Un flanker silencioso pasa desapercibido. Uno que grita “¡rodeando!” hace que el squad parezca pensar.
- Dan feedback de gameplay. “¡Recargando, cúbreme!” le dice al jugador “este es buen momento para asomarte”: loop tensión-alivio.
- Crean personalidad. Mismo squad, voces distintas, percepción distinta.
Los voice barks se disparan desde el squad, no desde el NPC. Cuando el squad asigna FLANKER, encola la línea con un cooldown global para evitar diálogos solapados.
2.7 Reasignación dinámica: el squad reacciona a la muerte
El squad debe sobrevivir a la muerte de sus miembros. Cuando un NPC muere:
- Libera todos sus tokens. El FLANK vuelve al pool inmediatamente.
- Pierde su rol. Si era leader, otro NPC asume.
- Notifica al blackboard:
deadAllies += 1.
En el siguiente tick, otro NPC reclama el FLANK liberado y toma el relevo. Eso produce la sensación de “siguen viniendo, siguen pensando”. Lo opuesto: squads cuyo flanker muere y los demás siguen suprimiendo a una posición vacía toda la pelea.
function onAgentDeath(agent)
for each token in agent.claimedTokens
squad.releaseToken(agent, token)
if agent.role == LEADER
squad.electNewLeader()
squad.blackboard.deadAllies += 1
squad.bark("HOMBRE_CAIDO")
3. Pseudocódigo
class Squad
members : List<Agent>
blackboard : Map<String, Entry>
tokenPool : Map<String, Int> # {"flank": 1, "suppress": 2, "grenade": 1}
leaderId : Int
class Entry
value : Any
timestamp : Float
function squadTick(squad, now)
aggregatePerception(squad, now)
pruneStaleEntries(squad, now)
reassignRoles(squad)
function aggregatePerception(squad, now)
for each agent in squad.members
if agent.canSeePlayer
squad.blackboard["knownPlayerPosition"] = Entry(agent.playerPos, now)
squad.blackboard["lastSeenTimestamp"] = Entry(now, now)
function pruneStaleEntries(squad, now)
for each key, entry in squad.blackboard
if now - entry.timestamp > TTL[key]
squad.blackboard.remove(key)
function tryClaimToken(squad, agent, tokenKey) -> Bool
if squad.tokenPool[tokenKey] > 0
squad.tokenPool[tokenKey] -= 1
agent.claimedTokens.add(tokenKey)
return true
return false
function releaseToken(squad, agent, tokenKey)
squad.tokenPool[tokenKey] += 1
agent.claimedTokens.remove(tokenKey)
function onAgentDeath(squad, agent)
for each token in agent.claimedTokens.copy()
releaseToken(squad, agent, token)
if agent.id == squad.leaderId
squad.leaderId = pickHealthiestMember(squad)
El squad se llama una vez por frame antes de los NPCs individuales. Cada NPC consulta tokens y blackboard durante su propio tick.
4. Implementación en Unity / C#
Un esqueleto mínimo. El Squad es un MonoBehaviour que vive en un GameObject del nivel; los NpcAgent se registran al spawn.
using System.Collections.Generic;
using UnityEngine;
public class Squad : MonoBehaviour {
[SerializeField] List<NpcAgent> members = new();
readonly Dictionary<string, int> tokenPool = new() {
{ "flank", 1 },
{ "suppress", 2 },
{ "grenade", 1 }
};
readonly Dictionary<string, BlackboardEntry> blackboard = new();
const float PLAYER_POS_TTL = 8f;
void Update() {
AggregatePerception();
PruneStaleEntries();
ReassignRoles();
}
void AggregatePerception() {
foreach (var m in members) {
if (m == null || m.IsDead) continue;
if (m.CanSeePlayer)
Write("knownPlayerPosition", m.LastPlayerPos);
}
}
void PruneStaleEntries() {
var now = Time.time;
var stale = new List<string>();
foreach (var kv in blackboard)
if (now - kv.Value.timestamp > PLAYER_POS_TTL) stale.Add(kv.Key);
foreach (var k in stale) blackboard.Remove(k);
}
public bool TryClaim(NpcAgent agent, string token) {
if (!tokenPool.TryGetValue(token, out var count) || count <= 0) return false;
tokenPool[token] = count - 1;
agent.ClaimedTokens.Add(token);
return true;
}
public void Release(NpcAgent agent, string token) {
if (!agent.ClaimedTokens.Remove(token)) return;
tokenPool[token] = tokenPool.GetValueOrDefault(token) + 1;
}
public void NotifyDeath(NpcAgent agent) {
foreach (var t in new List<string>(agent.ClaimedTokens)) Release(agent, t);
}
public bool TryRead<T>(string key, out T value) {
if (blackboard.TryGetValue(key, out var e) && e.value is T t) {
value = t; return true;
}
value = default; return false;
}
void Write(string key, object value) =>
blackboard[key] = new BlackboardEntry { value = value, timestamp = Time.time };
void ReassignRoles() { /* asignar Suppressor, Flanker, Retreater según HP/LOS */ }
}
public class BlackboardEntry { public object value; public float timestamp; }
Y en el NpcAgent, la hoja del BT que pide el token antes de flanquear:
public Status TryFlank() {
if (!squad.TryClaim(this, "flank")) return Status.Failure;
if (!squad.TryRead<Vector3>("knownPlayerPosition", out var pos)) {
squad.Release(this, "flank");
return Status.Failure;
}
MoveTo(ComputeFlankPosition(pos));
return Status.Running;
}
5. En otros engines
- Godot: usa un
Nodeautoload comoSquadManagero agrupa los NPCs conadd_to_group("squad_alpha")y comparte unResourcecustom como blackboard. La lógica de tokens es idéntica. - Unreal: el patrón canónico es un
UAISquadComponenten un actor coordinador. Combínalo con EQS (Environment Query System) para resolver posiciones de flanqueo y cobertura — EQS hace por ti el trabajo espacial que en Unity tendrías que escribir a mano. - JavaScript / Web: cualquier objeto plano
{ members, blackboard, tokens }con métodostryClaim/releasesirve. El cuello de botella no es el lenguaje, es la coherencia entre la capa squad y la IA individual.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Tus 4 enemigos flanquean los 4 al mismo tiempo por la misma esquina. ¿Qué pieza falta en tu squad AI?
Tu blackboard sigue mostrando `knownPlayerPosition` aunque el jugador se haya ido hace 30 segundos. Los NPCs disparan a un punto vacío. ¿Qué le falta al blackboard?
El NPC con el token FLANK muere a medio flanqueo. Si no haces nada especial, ¿qué pasa con el token?
¿Cuál es la relación correcta entre Squad AI y el Behavior Tree individual de cada NPC?