Steering Behaviors I: seek, flee, arrive
Aprende a crear movimientos orgánicos en tus juegos. Entenderás lo que son los agentes y aprenderás a calcular fuerzas hacia un target.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Cuando un enemigo se mueve hacia el jugador en línea recta y con velocidad constante, el resultado se siente robótico. Craig Reynolds resolvió ese problema en 1999 con steering behaviors: en lugar de teletransportar la velocidad deseada, le aplicas una fuerza (force) que lo empuja hacia su objetivo (target).
Antes de tocar la demo, conviene fijar tres conceptos que van repetirse durante todo el tutorial.
Si ya los conoces, puedes ir directo al seudocódigo
¿Qué es un agente?
Un agente es cualquier entidad del juego que se mueve por su cuenta: un enemigo, un NPC, un compañero, un proyectil guiado. En código, lo modelas como un objeto que mínimo tiene:
position— dónde está (Vec2oVec3).velocity— hacia dónde va y a qué ritmo. (Vec2oVec3).- Límites:
maxSpeed(Lo rápido que puede ir) ymaxForce(Su límite en que cambia de dirección).
El agente no tiene “inteligencia” en el sentido clásico. Solo aplica reglas locales cada frame. La sensación de “estar vivo” emerge de cómo esas reglas se combinan.
¿Qué son los steering behaviors?
El comportamiento de movimiento, o dicho de otro modo, el como se mueven los objetos. Los Steering Behaviors son funciones puras que calculan una fuerza para asignar a un agente un movimiento. Esta fuerza corrige la velocidad actual. Cada comportamiento responde a una sola pregunta:
- Seek: ¿cómo me acerco a este punto?
- Flee: ¿cómo me alejo de este punto?
- Arrive: ¿cómo me detengo sin pasarme?
La magia de los Steering Behaviors que que puedes sumar sus fuerzas:
total = seek(...) + 0.5 * flee(...)
¿Qué son las fuerzas?
Una fuerza es un vector (Vec2 o Vec3) que representa una corrección a la velocidad del agente. La regla universal de Reynolds es:
fuerza = velocidad_deseada − velocidad_actual
La “velocidad deseada” es a dónde el agente quiere ir, sin embargo esta limitada por su velocidad. Es como si el agente tuviera a un perrito con correa que lo esta jalando hacia un lugar.
Ademas, la fuerza del perrito también es limitada, ahi es donde entra maxForce. No es lo mismo pasear a un Chihuahua que a un Husky.
En cada frame la posicion del agente se calcula a partir de la fuerza de este modo.:
velocity += force * dt
position += velocity * dt
Los valores de force y velocity serán limitados por maxForce y maxSpeed correspondientemente.
Eso es todo. El resto del tutorial son variaciones sobre cómo calcular esa “velocidad deseada” según la situación.
Ahora sí, mueve el cursor sobre la demo y observa cómo el agente no salta a tu posición: empuja hacia ella.
1. Demo
2. Concepto y matemáticas
La idea cabe en una línea:
fuerza_correccion = deseo − situacion_actual.
Mi deseo (desired) es hacia donde me quiero dirigir, se representa como un vector que va desde el agente hacia el objetivo (target).
Arrastra el agente o el target: la longitud del vector cambia con la distancia.
Antes dijimos que una fuerza es un vector, respetando esa idea, mi deseo sera un vector al igual que mi situacion_actual.
Después de todo, no puedo obtener perros restando peras a plumas. Sino que debo tener el mismo tipo de dato siempre.
Mi desired se calcula con una resta de vectores: la position del target menos la position del agente.
position_target - position_agent
Pero esto no estara limitado, si estoy muy lejos, entonces mi fuerza sera muy grande.
Arrastra los targets en cada panel. Sin normalizar, la magnitud crece linealmente con la distancia — por eso necesitamos limitarla.
Para limitar esta fuerza, puedo hacerlo limitando el vector con un valor maximo, en este caso, a mi maxSpeed asi mi perro que jala de mi correa se limita y no alcanza velocidades de un tren magnético.
normalize(position_target - position_agent) * maxSpeed
El círculo violeta es el "alcance" de maxSpeed. Arrastra el target dentro y fuera: la flecha violeta es el deseo limitado, la gris es el crudo.
Una vez que tengo definido mi desired, entonces puedo ajustar mi velocity, haciendo un cálculo a partir de restar estos valores.
Nota: Si tienes problemas en entender por que se restan, busca operaciones de vectores.
Una vez teniendo esto, mi fuerza de correccion (steer) se calcula limitando con maxForce:
normalize(desired - velocity) * maxForce
Al tener esta force, solo queda actualizar la velocity y la position del agente, recordando:
velocity += force * dt
position += velocity * dt
2.1 Seek paso a paso
-
Calculo mi deseo (velocidad deseada hacia el
target)desired = normalize(target − position) * maxSpeed -
Calculo mi fuerza de correccion (
steer| steering force)steer = desired − velocity(limitado amaxForce) -
Actualizo mi
velocityactualvelocity += steer * dt(luego clamp amaxSpeed) -
Actualizo mi
positionactualposition += velocity * dt
Ese ciclo se debe aplicar en cada frame, asi el ajuste es organico y se siente natural.
2.2 Arrive: desacelerar al llegar
Nuestro Seek funciona perfecto para perseguir pero no para al estar cerca. El agente llega al target a su velocidad máxima, se pasa y regresa terminando en una oscilacion infinita.
Seek puro: el agente llega al target con demasiada velocidad y oscila a su alrededor sin parar. Esto es lo que Arrive viene a corregir.
Lo normal es que cuando el agente este cerca del target empiece a desacelerar, si esta lejos entonces que se comporte normalmente.
Para esto necesitaremos un nuevo concepto: slowRadius o radio de frenado. Es un radio alrededor del target que nos indicara cuando empezar a frenar.
- Si
dist > slowRadius→ me comporto como seek (velocidad deseada =maxSpeed). - Si
dist < slowRadius→ escalo la velocidad deseada en proporción a qué tan cerca estoy.
En seudocodigo:
desired_speed = maxSpeed * min(1, dist / slowRadius)
Cuando dist → 0, ya no tengo necesidad de un deseo, es decir, se volveria 0 y el agente frena solo. Es como el perrito en mi correa atrapara la pelota y ahora se sienta a masticarla.
2.3 Flee: la huida
Flee es el espejo de seek. Mismo cálculo, signo invertido.
Donde seek decía “quiero ir hacia el target”, flee dice “quiero alejarme del threat”. Mi desired ya no apunta del agente al target — apunta del threat hacia el agente.
desired = normalize(position - threat) * maxSpeed
La formula es la misma, solo se cambia el orden a la resta. El resto del pipeline (steer, velocity, position) es idéntico.
2.3.1 El “Arrive invertido”: zona segura
Flee tiene el mismo problema que tenía seek: nunca para. El agente huye, huye, huye… aunque ya esté lejísimos del threat.
En un principio pareceria normal, pero mientras mas pasa el tiempo mas se evidencia su falta de “Inteligencia”.
La idea es la misma que en el Arrive, pero en espejo. En vez de “frenar al acercarme al target”, quiero “frenar al alejarme del threat”.
Introduzco un safeRadius: un radio alrededor del threat a partir del cual ya me considero a salvo.
- Si
dist < safeRadius→ estoy en peligro, huyo. Y huyo más fuerte cuanto más cerca esté. - Si
dist >= safeRadius→ ya estoy lejos. Eldesiredpasa a ser velocidad cero: el agente frena suavemente conmaxForcey se queda quieto.
La fórmula del desired cuando estoy en peligro queda:
scale = maxSpeed * (1 - dist / safeRadius)
desired = normalize(position - threat) * scale
La velocidad deseada no decae al acercarme al target, decae al alejarme del threat. Cuando dist → safeRadius, scale → 0 y la force de huida se desvanece justo en la frontera.
Activa el toggle Zona segura en la demo y juega con safeRadius: vas a ver el círculo punteado y al agente quedándose quieto fuera de él, en lugar de pegarse a la pared.
Mueve el cursor: el agente huye y se queda confinado al lienzo. Activa Zona segura para que deje de huir cuando ya esté lejos — el círculo punteado marca safeRadius. Enter o Espacio pausan.
Intenta crearlo a partir de lo que aprendiste. Antes de ver el seudocódigo del siguiente apartado, prueba escribirlo de cero: una función flee(agent, threat, safeRadius) que devuelva una force con la zona segura aplicada como acabamos de describir. Si te atascas, recuerda que es seek con la resta al revés y Arrive con el slowRadius también al revés.
3. Pseudocódigo
function seek(agent: Agent, target: Vec2) -> Vec2
desired = normalize(target - agent.position) * agent.maxSpeed
steer = desired - agent.velocity
return limit(steer, agent.maxForce)
function arrive(agent: Agent, target: Vec2, slowRadius: Float) -> Vec2
toTarget = target - agent.position
dist = length(toTarget)
# velocidad objetivo decae linealmente dentro del radio
speed = agent.maxSpeed * min(1, dist / slowRadius)
desired = normalize(toTarget) * speed
steer = desired - agent.velocity
return limit(steer, agent.maxForce)
function apply(agent: Agent, force: Vec2, dt: Float)
agent.velocity = limit(agent.velocity + force * dt, agent.maxSpeed)
agent.position = agent.position + agent.velocity * dt
El patrón es siempre el mismo: cada comportamiento devuelve una fuerza, las sumas (ponderadas si hace falta) y las aplicas al final del frame.
4. Implementación en Unity / C#
using UnityEngine;
public class SteeringAgent : MonoBehaviour {
public float maxSpeed = 5f;
public float maxForce = 12f;
public float slowRadius = 2.5f;
public Transform target;
Vector3 velocity;
void Update() {
if (target == null) return;
var steer = Arrive(target.position);
velocity = Vector3.ClampMagnitude(velocity + steer * Time.deltaTime, maxSpeed);
transform.position += velocity * Time.deltaTime;
if (velocity.sqrMagnitude > 0.01f)
transform.forward = velocity.normalized;
}
Vector3 Arrive(Vector3 t) {
var toTarget = t - transform.position;
var dist = toTarget.magnitude;
var speed = maxSpeed * Mathf.Min(1f, dist / slowRadius);
var desired = toTarget.normalized * speed;
return Vector3.ClampMagnitude(desired - velocity, maxForce);
}
}
5. En otros engines
- Godot:
CharacterBody3D.velocityreemplaza al vector manual. Sumasteer * deltaen_physics_processy llama amove_and_slide(). - Unreal: en
ACharacterusaAddMovementInput(dir.GetSafeNormal(), 1.0f). La ley de steering vive en tuAIController, no en el movement component. - JavaScript / Canvas: es exactamente lo que hace la demo de arriba. Un
requestAnimationFrame, unVec2y el mismo algoritmo.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Subes mucho maxForce y dejas maxSpeed bajo. ¿Qué ves en el agente?
¿Qué hace la diferencia entre seek puro y arrive?
Si no limitas la fuerza (maxForce → ∞), ¿qué pasa?