Decisiones Intermedio 13 min de lectura

Behavior Trees: la IA que todos copian de los AAA

Aprende a reemplazar cientos de líneas de if/else usando árboles de comportamiento.

Publicado: · Por Juanjo "Banyo" López

0. Introducción

Si vienes de FSM y HFSM sabes cuándo una FSM se vuelve insostenible: 15 estados, 40 transiciones, una regla compartida que tienes que escribir 8 veces. Un Behavior Tree (BT) no piensa mejor que una FSM — piensa igual, pero se lee, se modifica y se reusa mejor.

¿Qué problema concreto resuelve?

  • Reutilización por subárboles. Un subárbol “PerseguirYAtacar” se enchufa en 3 enemigos distintos sin tocar lógica.
  • Prioridad explícita. El orden de los hijos en un Selector ES la prioridad. No hay condiciones cruzadas implícitas.
  • Acciones que tardan varios frames. El estado running es nativo: una acción “moverse al waypoint” devuelve running mientras dure y el árbol respeta esa rama.
  • Composición. Mismas piezas (Selector, Sequence, Condition, Action), distintas combinaciones, comportamientos distintos.

¿De dónde sale el patrón?

Halo 2 (2004) lo popularizó como alternativa moderna a la HFSM. Unreal lo trae de fábrica con BehaviorTree + Blackboard, en Unity es uno de los assets más vendidos año tras año, y casi cualquier juego AAA con IA táctica usa BTs o derivados (Utility, GOAP).

¿Cuándo NO usarlo?

  • IA muy reactiva con pocos comportamientos (3-7 estados): FSM plana sigue ganando en simpleza.
  • Necesitas planificación a varios pasos (“para matar al jefe debo recoger 3 items y desactivar 2 generadores”): un BT puro se queda corto, mira GOAP.
  • El equipo no tiene tiempo para aprender el patrón y la IA es secundaria al juego: HFSM cumple.

En esta página vas a ver:

  • Los cuatro tipos de nodos que resuelven el 95% de los casos, cada uno con una viz interactiva.
  • Cómo se evalúa un árbol en runtime, paso a paso.
  • El estado running y por qué cambia todo respecto a una FSM.
  • El snippet Unity con una implementación mínima sin dependencias.

1. Demo

Demo — Behavior Tree en vivo Mueve el cursor (jugador). El árbol se colorea según qué nodo triunfa, cuál falla y cuál está en curso.
Mueve el cursor como el jugador. Mira el árbol a la derecha: los nodos verdes tienen éxito, rojos fallan, el azul está corriendo.

2. Concepto

No hay matemática aquí: un BT es flujo de control. El único “estado” interno es la lista de nodos actualmente corriendo.

Cada nodo, al ser evaluado (tick), devuelve uno de tres valores:

  • success — el nodo cumplió.
  • failure — el nodo no pudo cumplir.
  • running — el nodo aún está trabajando, evaluame de nuevo el próximo frame.

Los nodos compuestos combinan los resultados de sus hijos según reglas simples. Veamos los cuatro tipos centrales.

2.1 Selector — “OR con cortocircuito”

Pregunta que resuelve: “¿alguno de mis hijos puede cumplir?”

Evalúa hijos en orden:

  • En cuanto un hijo devuelve success → el Selector devuelve success (cortocircuito).
  • Si un hijo devuelve running → el Selector devuelve running (cortocircuito).
  • Solo si todos devuelven failure → el Selector devuelve failure.

Paso a paso

  1. Inicializa el cursor en el primer hijo.

  2. Llama a tick() del hijo actual.

  3. Según el resultado:

    • success → devolver success, salir.
    • running → devolver running, salir.
    • failure → avanzar al siguiente hijo.
  4. Si te quedas sin hijos sin ningún success/running, devolver failure.

Selector (?)
running
child 0
failure
child 1
running
child 2
success

El Selector es un OR con cortocircuito. Evalúa hijos en orden y devuelve el primer success o running; si todos dan failure, el selector devuelve failure. Cambia los resultados de los hijos: los nodos no visitados se atenúan.

Cambia los resultados de los hijos. Vas a ver cómo el Selector corta en el primer hijo “exitoso” (success o running) y los siguientes ni se evalúan (se atenúan visualmente).

2.2 Sequence — “AND con cortocircuito”

Pregunta que resuelve: “¿pueden todos mis hijos cumplir, en orden?”

El espejo del Selector:

  • En cuanto un hijo devuelve failure → el Sequence devuelve failure (cortocircuito).
  • Si un hijo devuelve running → el Sequence devuelve running (cortocircuito).
  • Solo si todos devuelven success → el Sequence devuelve success.

Paso a paso

  1. Inicializa el cursor en el primer hijo.

  2. Llama a tick() del hijo actual.

  3. Según el resultado:

    • failure → devolver failure, salir.
    • running → devolver running, salir.
    • success → avanzar al siguiente hijo.
  4. Si todos devuelven success, devolver success.

Sequence (→)
running
child 0
success
child 1
running
child 2
success

El Sequence es un AND con cortocircuito. Evalúa hijos en orden y devuelve el primer failure o running; solo devuelve success si todos los hijos triunfan.

Mismo patrón que el Selector pero invertido. Aquí lo que corta es el primer failure (o running).

2.3 Condition — leer el mundo

Pregunta que resuelve: “¿se cumple X en este momento?”

Una Condition es una hoja que lee el mundo y devuelve success o failure. Nunca devuelve running (es instantánea), nunca cambia el estado del juego (es read-only).

condition("¿ve al jugador?")  → distance(player) < visionRadius ? success : failure
condition("¿HP < 20?")        → hp < 20 ? success : failure

2.4 Action — hacer algo en el mundo

Pregunta que resuelve: “haz X y avísame cómo te fue”.

Una Action es una hoja que modifica el mundo y puede tardar varios ticks. Devuelve:

  • success cuando termina bien.
  • failure cuando no pudo (ej: el path se bloqueó).
  • running mientras esté trabajando.

Aquí está la diferencia más importante respecto a una FSM: una acción que tarda 5 frames no necesita un estado especial. Devuelve running 5 veces y al sexto success. Toda la lógica del árbol se queda esperándola.

tick 0
Sequence — devuelve success
condition
success (siempre)
action: caminar
success — terminó
action: avisar
success

El nodo action: caminar tarda 3 ticks. Mientras devuelve running, el Sequence devuelve running y nunca llega al tercer hijo. Cuando finalmente da success, el Sequence avanza al siguiente hijo y termina con success.

El Sequence superior tiene 3 hijos. El del medio (acción “caminar”) tarda 3 ticks. Mientras devuelve running, el Sequence devuelve running y nunca llega al tercer hijo. Cuando finalmente da success, el Sequence avanza y devuelve success. Eso se repite en loop.

2.5 Decoradores — modificadores de un solo hijo

Los decoradores son nodos que envuelven a un único hijo y modifican su resultado. Los más usados:

DecoradorQué hace
InverterInvierte el resultado: successfailure. running queda igual.
Repeater(N)Ejecuta el hijo N veces seguidas; devuelve success al terminar todas las repeticiones.
UntilSuccessSigue ticking el hijo hasta que devuelva success.
Cooldown(t)Si el hijo dio success, lo bloquea por t segundos devolviendo failure mientras tanto.
TimeLimit(t)Si el hijo no termina en t segundos, lo aborta con failure.

Los decoradores son refinamiento, no fundamento. Con Selector + Sequence + Condition + Action ya tienes un BT funcional; los decoradores lo hacen más expresivo.

2.6 Ejemplo mental — el árbol de la demo

Selector
 ├─ Sequence: Atacar
 │   ├─ Condition: ¿Ve al jugador?
 │   ├─ Condition: ¿Está cerca?
 │   └─ Action: Atacar
 ├─ Sequence: Perseguir
 │   ├─ Condition: ¿Ve al jugador?
 │   └─ Action: Perseguir
 └─ Action: Patrullar

Se lee como: “Si veo y está cerca, ataco. Si solo veo, persigo. Si no, patrullo.” Esa misma lógica en if/else funciona, pero cambiar la prioridad ataque ↔ perseguir en un árbol es mover una línea. En if/else ese cambio es riesgoso.

2.7 BT vs FSM — ¿cuándo migrar?

NecesidadFSM/HFSMBehavior Tree
Pocos estados (< 10), transiciones clarasIdealOverkill
Acciones que tardan varios framesManual con flagsNativo (estado running)
Reuso de comportamientos entre agentesBajoAlto (subárboles enchufables)
Prioridad explícita entre comportamientosImplícita en transicionesExplícita en orden de hijos del Selector
Editor visual maduro en UnityAnimatorAsset (Behavior Designer, NodeCanvas, etc.)
Coste de aprendizajeBajoMedio
Adecuado para IA táctica AAALimitadoEstándar de la industria
Fácil de debugear paso a pasoSí (un estado activo)Sí (rama running visible en cada tick)

Regla práctica: si tu HFSM ya tiene 3 niveles de jerarquía, o un sub-estado se repite en 3 padres, migra a BT. La inversión paga rápido.

3. Pseudocódigo

enum Status: Success, Failure, Running

# Hojas
function condition(check: Func -> Bool) -> Status
    return check() ? Success : Failure

function action(run: Func -> Status) -> Status
    return run()   # devuelve Running, Success o Failure

# Compuestos
function selector(children: Node[]) -> Status
    for each c in children
        s = tick(c)
        if s == Success: return Success
        if s == Running: return Running
        # Failure → seguir con el siguiente
    return Failure

function sequence(children: Node[]) -> Status
    for each c in children
        s = tick(c)
        if s == Failure: return Failure
        if s == Running: return Running
        # Success → seguir con el siguiente
    return Success

# Decoradores
function inverter(child: Node) -> Status
    s = tick(child)
    if s == Success: return Failure
    if s == Failure: return Success
    return Running   # running queda igual

El agente llama tick(root) una vez por frame. El árbol se evalúa de arriba abajo y devuelve lo que el agente debe hacer ese frame.

4. Implementación en Unity / C#

Una implementación mínima, sin dependencias. El asset la amplía con editor visual y decoradores.

using System;
using System.Collections.Generic;

public enum Status { Success, Failure, Running }

public abstract class Node {
    public abstract Status Tick();
}

public class Condition : Node {
    readonly Func<bool> check;
    public Condition(Func<bool> c) { check = c; }
    public override Status Tick() => check() ? Status.Success : Status.Failure;
}

public class Action : Node {
    readonly Func<Status> run;
    public Action(Func<Status> r) { run = r; }
    public override Status Tick() => run();
}

public class Selector : Node {
    readonly List<Node> children;
    public Selector(params Node[] c) { children = new List<Node>(c); }
    public override Status Tick() {
        foreach (var n in children) {
            var s = n.Tick();
            if (s == Status.Success) return Status.Success;
            if (s == Status.Running) return Status.Running;
        }
        return Status.Failure;
    }
}

public class Sequence : Node {
    readonly List<Node> children;
    public Sequence(params Node[] c) { children = new List<Node>(c); }
    public override Status Tick() {
        foreach (var n in children) {
            var s = n.Tick();
            if (s == Status.Failure) return Status.Failure;
            if (s == Status.Running) return Status.Running;
        }
        return Status.Success;
    }
}

public class Inverter : Node {
    readonly Node child;
    public Inverter(Node c) { child = c; }
    public override Status Tick() {
        var s = child.Tick();
        if (s == Status.Success) return Status.Failure;
        if (s == Status.Failure) return Status.Success;
        return Status.Running;
    }
}

Uso típico:

var root = new Selector(
    new Sequence(           // atacar si veo y está cerca
        new Condition(() => CanSeePlayer()),
        new Condition(() => Distance() < attackRange),
        new Action(() => DoAttack())
    ),
    new Sequence(           // perseguir si veo
        new Condition(() => CanSeePlayer()),
        new Action(() => Chase())
    ),
    new Action(() => Patrol())
);

void Update() { root.Tick(); }

5. En otros engines

  • Godot: no trae BTs nativos, pero hay addons (Beehave, LimboAI). La arquitectura de arriba se porta directo a GDScript.
  • Unreal: el BehaviorTree + Blackboard de fábrica es excelente y cuesta cero configurarlo. Si trabajas en Unreal, úsalo antes que rodar tu propia implementación.
  • JavaScript: ideal para prototipos. Cualquier dev de web arma este árbol en una tarde y es suficiente para un juego pequeño de itch.io.

6. Quiz

Pon a prueba lo que entendiste

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

  1. Un Selector tiene 3 hijos que devuelven: failure, running, success. ¿Qué devuelve el Selector?

  2. Un Sequence tiene 3 hijos que devuelven: success, running, success. ¿Qué devuelve el Sequence?

  3. ¿Por qué conviene poner las condiciones baratas al inicio de una Sequence?

  4. Una Action devuelve Running durante varios ticks. ¿Qué implica eso para el árbol?

  5. Quieres que un enemigo huya MIENTRAS dispara al jugador. ¿Cómo lo modelas en BT?