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
runninges nativo: una acción “moverse al waypoint” devuelverunningmientras 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
runningy por qué cambia todo respecto a una FSM. - El snippet Unity con una implementación mínima sin dependencias.
1. Demo
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 devuelvesuccess(cortocircuito). - Si un hijo devuelve
running→ el Selector devuelverunning(cortocircuito). - Solo si todos devuelven
failure→ el Selector devuelvefailure.
Paso a paso
-
Inicializa el cursor en el primer hijo.
-
Llama a
tick()del hijo actual. -
Según el resultado:
success→ devolversuccess, salir.running→ devolverrunning, salir.failure→ avanzar al siguiente hijo.
-
Si te quedas sin hijos sin ningún
success/running, devolverfailure.
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 devuelvefailure(cortocircuito). - Si un hijo devuelve
running→ el Sequence devuelverunning(cortocircuito). - Solo si todos devuelven
success→ el Sequence devuelvesuccess.
Paso a paso
-
Inicializa el cursor en el primer hijo.
-
Llama a
tick()del hijo actual. -
Según el resultado:
failure→ devolverfailure, salir.running→ devolverrunning, salir.success→ avanzar al siguiente hijo.
-
Si todos devuelven
success, devolversuccess.
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:
successcuando termina bien.failurecuando no pudo (ej: el path se bloqueó).runningmientras 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.
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:
| Decorador | Qué hace |
|---|---|
| Inverter | Invierte el resultado: success ↔ failure. running queda igual. |
| Repeater(N) | Ejecuta el hijo N veces seguidas; devuelve success al terminar todas las repeticiones. |
| UntilSuccess | Sigue 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?
| Necesidad | FSM/HFSM | Behavior Tree |
|---|---|---|
| Pocos estados (< 10), transiciones claras | Ideal | Overkill |
| Acciones que tardan varios frames | Manual con flags | Nativo (estado running) |
| Reuso de comportamientos entre agentes | Bajo | Alto (subárboles enchufables) |
| Prioridad explícita entre comportamientos | Implícita en transiciones | Explícita en orden de hijos del Selector |
| Editor visual maduro en Unity | Animator | Asset (Behavior Designer, NodeCanvas, etc.) |
| Coste de aprendizaje | Bajo | Medio |
| Adecuado para IA táctica AAA | Limitado | Estándar de la industria |
| Fácil de debugear paso a paso | Sí (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+Blackboardde 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.
Un Selector tiene 3 hijos que devuelven: failure, running, success. ¿Qué devuelve el Selector?
Un Sequence tiene 3 hijos que devuelven: success, running, success. ¿Qué devuelve el Sequence?
¿Por qué conviene poner las condiciones baratas al inicio de una Sequence?
Una Action devuelve Running durante varios ticks. ¿Qué implica eso para el árbol?
Quieres que un enemigo huya MIENTRAS dispara al jugador. ¿Cómo lo modelas en BT?