Decisiones Avanzado 19 min de lectura

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 : MonoBehaviour que orquesta a varios NpcAgent.

1. Demo

Demo — Squad AI con tokens y roles Cuatro enemigos coordinándose: un suppressor fija al jugador, un flanker busca línea por el otro lado, los demás cubren rutas. Reasigna en tiempo real.

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.

El coro tonto
jugadorcoberturaNPC 1NPC 2NPC 3NPC 4todos eligen el mismo flanco

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.

Blackboard compartido
SquadBlackboardknownPlayerPosition : Vec3lastSeenTimestamp : floatsuppressionLevel : [0..1]lastKnownCover : Vec3playerHealthEstimate : floatdeadAllies : intNPC 1escribeNPC 2lee y decideNPC 3lee y decide

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.

Token pool del squad
SquadTokensflanksuppressgrenadeadvancesolo cuenta = tokens libresNPC ANPC BNPC CNPC DFLANKSUPPRESSSUPPRESSesperando

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:

RolQué haceCuándo se asigna
LeaderToma decisiones de squad, prioriza objetivosDesignado al spawn, reasignado si muere
SuppressorFuego de cobertura sostenido desde posición fijaNPC con LOS y arma automática
FlankerBusca rodear al jugador por un lado expuestoNPC con buena movilidad y token FLANK
RetreaterReposiciona a cobertura cuando recibe dañoNPC con HP bajo
Medic / SupportCura aliados, da munición, reanimaSi 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.

Squad encima del BT individual
Squad Layerblackboardtokens + rolesNPC 1BT / UtilityNPC 2BT / UtilityNPC 3BT / Utility

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:

  1. 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.
  2. 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:

  1. Telegrafían la coordinación al jugador. Un flanker silencioso pasa desapercibido. Uno que grita “¡rodeando!” hace que el squad parezca pensar.
  2. Dan feedback de gameplay. “¡Recargando, cúbreme!” le dice al jugador “este es buen momento para asomarte”: loop tensión-alivio.
  3. 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:

  1. Libera todos sus tokens. El FLANK vuelve al pool inmediatamente.
  2. Pierde su rol. Si era leader, otro NPC asume.
  3. 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 Node autoload como SquadManager o agrupa los NPCs con add_to_group("squad_alpha") y comparte un Resource custom como blackboard. La lógica de tokens es idéntica.
  • Unreal: el patrón canónico es un UAISquadComponent en 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étodos tryClaim/release sirve. 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.

  1. Tus 4 enemigos flanquean los 4 al mismo tiempo por la misma esquina. ¿Qué pieza falta en tu squad AI?

  2. 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?

  3. El NPC con el token FLANK muere a medio flanqueo. Si no haces nada especial, ¿qué pasa con el token?

  4. ¿Cuál es la relación correcta entre Squad AI y el Behavior Tree individual de cada NPC?