Steering Behaviors IV: follow leader, queue y path following
Sigue a un líder, formar filas, recorrer caminos. La capa táctica del steering.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Hasta aquí los behaviors han sido individuales: un agente, un target o un threat, y reglas locales. Funcionan, pero no escalan a situaciones tácticas: un escuadrón siguiendo a su capitán, NPCs entrando en una tienda en orden, una patrulla por un circuito.
En este tutorial vivimos esos tres casos:
- Follow Leader — un grupo se acomoda detrás de un líder móvil, sin pisarse, sin meterse en su camino.
- Queue — agentes que llegan al mismo punto forman una fila natural, sin necesidad de un sistema de turnos central.
- Path Following — el agente recorre un camino. Variante (a) waypoints discretos, variante (b) path tube de Reynolds.
Todos los behaviors aquí son forces que enchufas en el SteeringController de Steering Behaviors III. El composite es lo que hace posible combinarlos sin que el código se rompa.
1. Demo
2. Follow Leader
2.1 La intuición
Quieres que un grupo siga a su capitán. La solución ingenua: que cada follower haga seek(leader.position). Resultado: todos se apilan literalmente sobre el leader, lo empujan y lo bloquean. Ridículo.
Reynolds resolvió esto con tres ideas combinadas:
- El target NO es el leader — es un punto detrás del leader (en
-leader.heading * behindDist). - Hay una zona prohibida delante del leader — si un follower entra, recibe una
forcede evade que lo aparta a un costado. - Los followers se separan entre sí — separation behavior, igual que en flocking.
2.2 Follow Leader paso a paso
-
Obtener el
headingdel leader.leader.heading = normalize(leader.velocity)(si está parado, conserva el último heading válido) -
Calcular el target detrás del leader.
behindPoint = leader.position - leader.heading * behindDist -
Calcular la posición prohibida frente al leader.
aheadPoint = leader.position + leader.heading * aheadDist -
Para cada follower:
- Si
distance(follower, aheadPoint) < aheadRadius→ aplicarevade(aheadPoint)con peso alto. Está estorbando. - Aplicar
arrive(behindPoint)— al estar detrás, frena suavemente al llegar. - Aplicar
separationcontra los demás followers, peso medio.
- Si
-
Pasar el resultado al
SteeringControllerpara que lo combine con peso normal.
2.3 Por qué arrive y no seek
Si los followers usan seek(behindPoint), llegan al punto a velocidad máxima, se pasan, regresan y oscilan — exactamente el problema de Steering Behaviors I. Arrive les hace desacelerar al llegar, así se asientan detrás del leader y mantienen la formación cuando este se detiene.
2.4 Variantes que valen la pena
- Formaciones — en vez de un único
behindPoint, asignar a cada follower un offset relativo al leader (formación V, columna, escuadrón). Cada slot es untargetdiferente. - Slots dinámicos — los followers compiten por los slots libres más cercanos. Útil si los followers entran y salen del grupo.
- Predicción del leader — usar
pursuiten vez deseek/arrivepuro hacia elbehindPoint. Hace que el grupo “anticipe” giros del leader.
3. Queue
3.1 El problema
Diez agentes corren al mismo target. Si todos hacen seek(goal), llegan a la vez y se encajonan en un grumo. Lo quieres es que formen una fila natural.
3.2 La idea
No hay un sistema central que asigne turnos. Cada agente decide localmente:
- Sigo a mi
target(seek o arrive normal). - Si tengo otro agente delante de mí (en un cono frontal corto), bajo mi velocidad deseada.
- Aplico separation para no chocar lateralmente.
La combinación produce el orden emergente.
Cada agente reaparece a la izquierda cuando llega al goal. Mira cómo se forma la fila sola.
3.3 Detección del cono frontal
Para saber si hay alguien delante, proyectamos cada vecino sobre la dirección del agente:
heading = normalize(agent.velocity)
for each other in agents:
offset = other.position - agent.position
along = dot(offset, heading)
if along < 0 or along > coneLength: continue # detrás o muy lejos
perp = length(offset - heading * along)
if perp > coneWidth: continue # no está en el carril
# Hay alguien delante: tomar el más cercano (menor along)
Si encuentras a alguien, escalas la velocidad deseada según along / coneLength (más cerca = más freno).
3.4 Por qué este patrón es robusto
- Sin coordinación central — no hace falta un manager que reparta turnos.
- Resilient — si quitas o agregas un agente, la fila se reacomoda sola.
- Escalable — con grid espacial, el cono cuesta O(vecinos) por agente, no O(N).
- Combinable — el “brake si hay alguien delante” es una
forcemás en el controller.
4. Path Following
4.1 ¿Por qué existe?
Hay momentos donde el agente debe seguir una ruta predefinida: una patrulla, un circuito de carrera, una ruta de NPC en una ciudad, un escolta. No estás persiguiendo un target móvil — estás recorriendo geometría fija.
Hay dos formas clásicas de modelar esto. Las dos sirven; resuelven matices distintos.
4.2 Variante (a): Waypoints discretos
El path es una lista de puntos [w0, w1, w2, ...]. El agente:
- Apunta al waypoint actual.
- Aplica
seek(en intermedios) oarrive(en el último, si no hace loop). - Cuando entra en
arriveRadiusdel waypoint actual, avanza al siguiente índice.
Arrastra los waypoints. Click en el canvas vacío añade uno nuevo. El círculo punteado es el arriveRadius del waypoint activo: cuando el agente entra, salta al siguiente.
Pros y contras
- Pro: trivial de implementar, fácil de visualizar, fácil de tunear.
- Pro: encaja bien con grafos de navegación (un waypoint = un nodo del grafo).
- Contra: el agente toma curvas con esquinas duras en cada waypoint. Si pasas a 60 km/h, hace zigzag.
- Contra: si
arriveRadiuses muy pequeño, el agente da vueltas alrededor del waypoint sin “tocarlo”. Si es muy grande, corta esquinas demasiado pronto.
4.3 Variante (b): Path tube de Reynolds
La idea: el path no son puntos, es una polilínea con grosor. Mientras el agente esté dentro del tubo, está libre — no hay force correctora. Solo cuando se sale, el sistema lo empuja de vuelta.
El truco del 0.5 — predicción:
- Calcular la posición futura:
predicted = position + velocity * predictAhead. - Encontrar el segmento del path más cercano a
predicted. - Proyectar
predictedsobre ese segmento → puntoproj. - Si
distance(predicted, proj) > tubeRadius→ aplicarseek(proj)para regresar al tubo. - Si
distance(predicted, proj) ≤ tubeRadius→ no aplicar nada (o un seek suave hacia el waypoint siguiente para mantener avance).
La línea verde es el path; el área verde claro es el tubo. La línea punteada es la predicción. Cuando esa predicción cae fuera del tubo (línea roja), el agente recibe una corrección.
Pros y contras
- Pro: curvas suaves. El agente puede tomar un atajo tangente entre segmentos sin zigzag.
- Pro: se puede combinar con otros behaviors sin que peleen — el path solo corrige cuando es necesario.
- Contra: más matemática (proyección sobre segmentos).
- Contra: si los segmentos son muy desiguales, “el más cercano” puede saltar de uno a otro de forma confusa.
4.4 Tabla comparativa: ¿waypoints o tube?
| Aspecto | Waypoints (a) | Path tube (b) — Reynolds |
|---|---|---|
| Modelo del path | Lista de puntos discretos | Polilínea con grosor (tubo) |
| Trayectoria visible | Esquinas duras en cada waypoint | Curvas suaves, atajos tangentes |
| Coste por frame | O(1) — solo el waypoint actual | O(segmentos) — proyectar en cada uno |
| Tuning crítico | arriveRadius (overshoot vs zigzag) | tubeRadius (rigidez vs deriva) y predictAhead |
| Encaja con grafos de navegación | Directo (1 waypoint = 1 nodo) | Requiere convertir el grafo a polilínea |
| Combina con otros behaviors | Aceptable, pero pelea con avoidance | Excelente — solo corrige cuando hace falta |
| Cuándo elegirlo | Patrullas con paradas, paths cortos, mobile (CPU justa) | Carreras, vuelos, persecuciones largas, sensación arcade |
Patrón pragmático: en producción se ven los dos. Waypoints para NPCs de baja prioridad y patrullas; tube para vehículos, vuelo, y todo lo que necesite “feel” arcade.
4.5 Loops, idas y vueltas, y reverso
Tres modos típicos:
- One-shot — del waypoint 0 al N-1, frenar al final con arrive. Una patrulla a un puesto.
- Loop — al llegar al N-1, volver al 0. Circuitos, rondas.
- Ping-pong — al llegar al N-1, invertir dirección hacia 0; al llegar a 0, volver a invertir. Patrullas de ida y vuelta.
Implementación: una variable direction ∈ {+1, -1} y una rama en el “avanzar índice” para flipearla en los extremos.
5. Pseudocódigo completo
function followLeader(follower: Agent, leader: Agent, params: FollowParams) -> Vec2
heading = leader.headingOrLastValid()
behindPoint = leader.position - heading * params.behindDist
aheadPoint = leader.position + heading * params.aheadDist
force = Vec2(0, 0)
# 1) Si el follower está delante del leader, evade
if distance(follower.position, aheadPoint) < params.aheadRadius
force += evade(follower, virtualThreatAt(aheadPoint, leader.velocity)) * 2.0
# 2) Arrive al punto detrás
force += arrive(follower, behindPoint, params.slowRadius)
# 3) Separación contra otros followers
force += separation(follower, otherFollowers, params.sepRadius) * 0.6
return limit(force, follower.maxForce)
function queueBrake(agent: Agent, others: Agent[], params: QueueParams) -> Float
if length(agent.velocity) < epsilon: return 1.0 # sin freno
heading = normalize(agent.velocity)
closestAlong = +inf
for each other in others
if other == agent: continue
offset = other.position - agent.position
along = dot(offset, heading)
if along < 0 or along > params.coneLength: continue
perp = distance(offset, heading * along)
if perp > params.coneWidth: continue
if along < closestAlong: closestAlong = along
if closestAlong == +inf: return 1.0
return clamp(closestAlong / params.coneLength, 0, 1)
function pathFollowWaypoints(agent: Agent, path: PathState) -> Vec2
target = path.waypoints[path.currentIdx]
if distance(agent.position, target) < path.arriveRadius
path.advance() # avanza índice según mode (loop, one-shot, ping-pong)
isFinal = (path.mode == "one-shot" and path.currentIdx == last)
return isFinal
? arrive(agent, target, path.slowRadius)
: seek(agent, target)
function pathFollowTube(agent: Agent, path: Path, params: TubeParams) -> Vec2
predicted = agent.position + agent.velocity * params.predictAhead
bestSeg, proj, dist = findClosestSegment(predicted, path.segments)
if dist > params.tubeRadius
# fuera del tubo: seek al punto proyectado
return seek(agent, proj)
else
# dentro: mantener avance hacia el siguiente waypoint del segmento
nextWp = path.waypoints[bestSeg + 1]
return seek(agent, nextWp)
6. Implementación en Unity / C#
using System.Collections.Generic;
using UnityEngine;
public class FollowLeader : SteeringBehavior {
public Transform leader;
public List<SteeringAgent> otherFollowers;
public float behindDist = 2f;
public float aheadDist = 1.5f;
public float aheadRadius = 1.5f;
public float slowRadius = 1f;
public float sepRadius = 1.2f;
Vector3 lastHeading = Vector3.forward;
Vector3 lastLeaderPos;
public override Vector3 Calculate(SteeringAgent agent) {
if (leader == null) return Vector3.zero;
var leaderVel = (leader.position - lastLeaderPos) / Mathf.Max(Time.deltaTime, 0.001f);
lastLeaderPos = leader.position;
if (leaderVel.sqrMagnitude > 0.01f) lastHeading = leaderVel.normalized;
var behindPoint = leader.position - lastHeading * behindDist;
var aheadPoint = leader.position + lastHeading * aheadDist;
var force = Vector3.zero;
// 1) Evade si está en zona prohibida
if (Vector3.Distance(agent.transform.position, aheadPoint) < aheadRadius) {
var away = (agent.transform.position - aheadPoint).normalized * agent.maxSpeed;
force += (away - agent.velocity) * 2f;
}
// 2) Arrive al behindPoint
var to = behindPoint - agent.transform.position;
var dist = to.magnitude;
var speed = agent.maxSpeed * Mathf.Min(1f, dist / slowRadius);
var desired = to.normalized * speed;
force += desired - agent.velocity;
// 3) Separation
foreach (var o in otherFollowers) {
if (o == agent) continue;
var off = agent.transform.position - o.transform.position;
var d = off.magnitude;
if (d > 0.01f && d < sepRadius) {
var pushSpeed = agent.maxSpeed * (1f - d / sepRadius);
force += (off.normalized * pushSpeed - agent.velocity) * 0.6f;
}
}
return force;
}
}
public class PathFollowWaypoints : SteeringBehavior {
public Vector3[] waypoints;
public float arriveRadius = 0.8f;
public float slowRadius = 1.5f;
public bool loop = true;
int currentIdx;
public override Vector3 Calculate(SteeringAgent agent) {
if (waypoints == null || waypoints.Length == 0) return Vector3.zero;
var target = waypoints[currentIdx];
var dist = Vector3.Distance(agent.transform.position, target);
var isFinal = !loop && currentIdx == waypoints.Length - 1;
if (dist < arriveRadius && !isFinal) {
currentIdx = (currentIdx + 1) % waypoints.Length;
target = waypoints[currentIdx];
}
var to = target - agent.transform.position;
var speed = isFinal ? agent.maxSpeed * Mathf.Min(1f, dist / slowRadius) : agent.maxSpeed;
var desired = to.normalized * speed;
return desired - agent.velocity;
}
}
public class PathFollowTube : SteeringBehavior {
public Vector3[] waypoints;
public float tubeRadius = 1f;
public float predictAhead = 0.5f;
public override Vector3 Calculate(SteeringAgent agent) {
if (waypoints == null || waypoints.Length < 2) return Vector3.zero;
var predicted = agent.transform.position + agent.velocity * predictAhead;
Vector3 bestProj = waypoints[0];
float bestDist = float.MaxValue;
int bestSeg = 0;
for (int i = 0; i < waypoints.Length - 1; i++) {
var (proj, dist) = ProjectOnSegment(predicted, waypoints[i], waypoints[i + 1]);
if (dist < bestDist) { bestProj = proj; bestDist = dist; bestSeg = i; }
}
Vector3 aim;
if (bestDist > tubeRadius) {
aim = bestProj;
} else {
aim = waypoints[Mathf.Min(bestSeg + 1, waypoints.Length - 1)];
}
var desired = (aim - agent.transform.position).normalized * agent.maxSpeed;
return desired - agent.velocity;
}
static (Vector3, float) ProjectOnSegment(Vector3 p, Vector3 a, Vector3 b) {
var ab = b - a;
var ap = p - a;
var lenSq = Mathf.Max(ab.sqrMagnitude, 0.0001f);
var t = Mathf.Clamp01(Vector3.Dot(ap, ab) / lenSq);
var proj = a + ab * t;
return (proj, Vector3.Distance(p, proj));
}
}
7. En otros engines
- Godot: para path following, Godot trae
Path3D+PathFollow3Dque ya hacen el trabajo de proyección. Para follow leader y queue, las clases composite se transcriben directo aResource. - Unreal:
USplineComponentcubre paths con curvas. Follow leader vive en elAAIControllero comoUBehaviorTreetask. - JavaScript / Canvas: la lib del sitio implementa los tres patrones en
~/lib/viz/sim/.
8. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Implementas follow leader con seek directo al leader. Los followers se apilan sobre él. ¿Mejor primer fix?
En queue, dos agentes están exactamente en línea uno detrás del otro pero ambos siguen avanzando a maxSpeed. ¿Causa probable?
Patrullas en una ciudad, NPCs lentos, CPU justa en mobile. ¿Path following recomendado?
En path tube, el agente vibra entre dentro y fuera del tubo. ¿Causa más probable?
¿Por qué follow leader necesita ZONA PROHIBIDA delante del leader si los followers ya apuntan al behindPoint?
Necesitas un agente que recorra un camino circular y al final empiece de nuevo, sin frenar al cerrar el loop. ¿Modo apropiado?