Pathfinding says where to go; steering makes the movement smooth and lifelike. Learn seek, flee, and arrive behaviors, and how combining simple rules produces flocking and crowds.
Steering behaviors compute a small acceleration each frame that nudges an agent’s velocity, producing smooth, momentum-based movement instead of teleporting. Seek steers toward a target by desiring a velocity straight at it and correcting toward that desire; flee is the opposite. These primitives, built on the vectors from the Game Math course, are the atoms of natural movement.
def sub(a, b): return (a[0]-b[0], a[1]-b[1])
def seek(agent, target, max_speed):
# desired velocity points straight at the target, at full speed
to_target = sub(target, agent.pos)
desired = normalize(to_target)
desired = (desired[0]*max_speed, desired[1]*max_speed)
# steering = the correction from current velocity toward the desired one
steer = sub(desired, agent.vel)
return steer # apply as acceleration this frame
def flee(agent, threat, max_speed):
steer = seek(agent, threat, max_speed)
return (-steer[0], -steer[1]) # flee = reverse of seek
# steering nudges velocity each frame -> smooth, momentum-based motion.Seek makes an agent overshoot and orbit its target because it never slows. Arrive fixes this by scaling the desired speed down within a braking radius, so the agent decelerates and stops cleanly on the target. This small refinement is the difference between a jittery agent circling a waypoint and one that pulls up smoothly — essential for following paths and reaching destinations.
def arrive(agent, target, max_speed, slow_radius=3.0):
to_target = sub(target, agent.pos)
dist = (to_target[0]**2 + to_target[1]**2) ** 0.5
if dist < 0.01:
return (0.0, 0.0) # arrived
# ramp speed down inside the braking radius so we stop ON the target
speed = max_speed * min(dist / slow_radius, 1.0)
desired = normalize(to_target)
desired = (desired[0]*speed, desired[1]*speed)
return sub(desired, agent.vel) # steering correction
# without 'arrive', seek overshoots + orbits the target. arrive brakes to a stop.
# use it to follow A* waypoints smoothly (previous lesson).The power of steering is composition: you compute several behaviors and blend their forces. An agent can seek a goal while avoiding obstacles and separating from neighbors, all at once, by summing (often weighted) the steering vectors. This "sum of simple urges" approach yields emergent, complex-looking movement from a handful of trivial rules — a hallmark of good game AI.
def combine(agent, behaviors):
# behaviors: list of (steering_vector, weight)
total = [0.0, 0.0]
for steer, weight in behaviors:
total[0] += steer[0] * weight
total[1] += steer[1] * weight
return total
# an agent that pursues a goal, avoids walls, and keeps its distance:
steer = combine(agent, [
(seek(agent, goal, 5), 1.0),
(avoid_obstacles(agent), 2.0), # weighted higher -> safety first
(separation(agent, neighbors), 1.5),
])
agent.vel = clamp(add(agent.vel, steer), max_speed)
# summing simple urges -> emergent, believable movement.Craig Reynolds’ classic result: realistic flocking (birds, fish, crowds) emerges from three local steering rules applied to each agent — separation (avoid crowding neighbors), alignment (match neighbors’ heading), and cohesion (steer toward the group’s center). No agent knows the global pattern; the coordinated swarm arises purely from everyone following the same simple rules. It is the definitive example of emergent behavior in games.
def flocking(agent, neighbors):
# three local rules, each a steering vector, then combine them:
sep = separation(agent, neighbors) # 1. avoid crowding neighbors
ali = alignment(agent, neighbors) # 2. match neighbors' average heading
coh = cohesion(agent, neighbors) # 3. steer toward neighbors' center
return combine(agent, [(sep, 1.5), (ali, 1.0), (coh, 1.0)])
# each agent only looks at nearby neighbors — no global coordination — yet a
# realistic flock/school/crowd EMERGES. (Reynolds' "boids", 1986.)
# used for birds, fish, crowds, swarm enemies.