Seek, arrive, flee, pursue, wander and flocking movement for agents, as velocities your game applies.
Import blitz.steering As S gives your program these, with S. in front. Each name leads to its part of the reference.
CreateSteering:SteeringWorld(options:SteeringOptions)AddAgent:Agent(world:SteeringWorld, options:AgentOptions)AgentMotion(agent:Agent, x:Float, y:Float, z:Float, vx:Float, vy:Float, vz:Float)SteerTo(agent:Agent, x:Float, y:Float, z:Float, mode:Int = SteerMode.Arrive)FollowAgent(agent:Agent, target:Agent, mode:Int = SteerMode.Pursue)WanderAgent(agent:Agent, enabled:Int = True)FlockAgent(agent:Agent, flock:Int, options:FlockOptions, enabled:Int = True)AgentVelocity:Velocity(agent:Agent)UseNeighbours(world:SteeringWorld, source:NeighbourSource)UpdateSteering(world:SteeringWorld, seconds:Double)SteeringOptionsdimensions:Int = SteeringDimensions.XZ, seed:Long = 1L, avoidance:AvoidanceAgentOptionsmaximumSpeed:Float = 4, acceleration:Float = 8, arrivalRadius:Float = 2, neighbourRadius:Float = 6, radius:Float = 0.25FlockOptionsseparation:Float = 1.5, alignment:Float = 0.5, cohesion:Float = 0.3AvoidanceResultx:Float, y:Float, z:FloatVelocityX:Float(), Y:Float(), Z:Float()SteeringWorldAgentID:Long()AvoidanceAdjust(x:Float, y:Float, z:Float, radius:Float, vx:Float, vy:Float, vz:Float, seconds:Double, result:AvoidanceResult)NeighbourSourceBeginFrame(dimensions:Int), IndexAgent(id:Long, x:Float, y:Float, z:Float), EndFrame(), Query(x:Float, y:Float, z:Float, radius:Float, output:Long List)SteeringDimensionsXZ, XYZSteerModeSeek, Arrive, Flee, Pursue, EvadeAn optional part. Import blitz.steering.physics as well to use these.
CreatePhysicsAvoidance:PhysicsAvoidance(world:Int, options:PhysicsAvoidanceOptions)PhysicsAvoidanceOptionslookAhead:Float = 0.75, maximumDistance:Float = 8, padding:Float = 0.05, heightOffset:Float, mask:Int = -1, ignoredBody:IntPhysicsAvoidance Implements Steering.AvoidanceAdjust(x:Float,y:Float,z:Float,radius:Float,vx:Float,vy:Float,vz:Float,seconds:Double,result:Steering.AvoidanceResult)An optional part. Import blitz.steering.spatial as well to use these.
CreateSpatialNeighbours:SpatialNeighbours(cellSize:Float = 4, dimensions:Int = Steering.SteeringDimensions.XZ)SpatialNeighbours Implements Steering.NeighbourSourceBeginFrame(dimensions:Int), IndexAgent(id:Long, x:Float, y:Float, z:Float), EndFrame(), Query(x:Float, y:Float, z:Float, radius:Float, output:Long List)Steering makes characters move as if they mean it: heading for a place and slowing down to stop there, chasing someone by aiming for where they're going, running away, wandering about, or moving as a flock that stays together without bumping into itself. Each frame, you tell it where everyone is, and it tells you how fast, and which way, each of them would like to move. Moving them is up to you, so it works with your own movement, collisions and physics.

Open chase.bb and run it. Run from the red hunters with the arrow keys. The lines show where each hunter is heading: watch them aim ahead of you as you run. Hold Space and they run away instead.
; Run from the hunters with the arrow keys. They head for where you're
; going, not where you are, and keep out of each other's way. Hold Space to
; scare them off. Escape quits.
Dialect "modern"
Import blitz.steering As S
Const SCALE:Float = 20 ; 20 pixels to a unit: the window is 40 by 24 units.
Type Hunter
Field agent:S.Agent
Field x:Float
Field z:Float
Field vx:Float
Field vz:Float
End Type
Graphics 800, 480, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79
; The steering world works out how each hunter wants to move, and the
; program moves them. You are an agent too, so that the hunters can follow you.
Local world:S.SteeringWorld = S.CreateSteering(Null)
Local you:S.Agent = S.AddAgent(world, Null)
Local x:Float = 20
Local z:Float = 18
; The hunters are a flock whose only rule is to keep apart.
Local apart:S.FlockOptions = New S.FlockOptions
apart.alignment = 0
apart.cohesion = 0
Local index:Int
For index = 0 To 5
Local hunter:Hunter = New Hunter
hunter.agent = S.AddAgent(world, Null)
S.FlockAgent hunter.agent, 1, apart
hunter.x = 4 + index * 6.4
hunter.z = 4
Next
Delete apart
Local previous:Int = MilliSecs()
While Not KeyDown(1)
Local now:Int = MilliSecs()
Local seconds:Double = Double(now - previous) / 1000D
previous = now
; Run at 6 units a second, faster than the hunters' 4, inside the field.
Local vx:Float = Float(KeyDown(205) - KeyDown(203)) * 6
Local vz:Float = Float(KeyDown(208) - KeyDown(200)) * 6
x = x + vx * Float(seconds)
z = z + vz * Float(seconds)
If x < 1 Or x > 39 Then x = x - vx * Float(seconds)
If z < 2 Or z > 23 Then z = z - vz * Float(seconds)
; Tell the world where everyone is, how they're moving and what each
; hunter wants. Then update them all at once.
S.AgentMotion you, x, 0, z, vx, 0, vz
For hunter = Each Hunter
S.AgentMotion hunter.agent, hunter.x, 0, hunter.z, hunter.vx, 0, hunter.vz
If KeyDown(57)
S.FollowAgent hunter.agent, you, S.SteerMode.Evade
Else
S.FollowAgent hunter.agent, you, S.SteerMode.Pursue
EndIf
Next
S.UpdateSteering world, seconds
; Move each hunter the way it asks, and draw it with a line showing
; where it's heading.
Cls
Color 30, 72, 105
Rect SCALE, 2 * SCALE, 38 * SCALE, 21 * SCALE, True
Color 240, 90, 80
For hunter = Each Hunter
Local velocity:S.Velocity = S.AgentVelocity(hunter.agent)
hunter.vx = velocity.X()
hunter.vz = velocity.Z()
hunter.x = hunter.x + hunter.vx * Float(seconds)
hunter.z = hunter.z + hunter.vz * Float(seconds)
Oval hunter.x * SCALE - 8, hunter.z * SCALE - 8, 16, 16, True
Line hunter.x * SCALE, hunter.z * SCALE, (hunter.x + hunter.vx) * SCALE, (hunter.z + hunter.vz) * SCALE
Next
Color 250, 210, 60
Oval x * SCALE - 9, z * SCALE - 9, 18, 18, True
Color 255, 255, 255
Text 12, 12, "Arrows: run Space: scare the hunters off"
Flip
Wend
Delete world ; This deletes the agents too.
Delete Each Hunter
End
A steering world holds an agent for each character, including you, so that the hunters can follow you. Each frame, the program tells the world where every agent is and how it's moving, says what each hunter wants, and updates the world once. Then it reads each hunter's velocity, the speed and direction it would like to move in, and moves it. Steering works on flat ground, in X and Z, so this 2D program uses Z for down the screen.
SteerTo heads for a point, and slows down over the last 2 units to stop on
it. Add S.SteerMode.Seek to rush there at full speed, or S.SteerMode.Flee
to run away from the point instead.
S.SteerTo agent, 30, 0, 12 ; Go to x 30, z 12 and stop. S.SteerTo agent, 30, 0, 12, S.SteerMode.Flee ; Run away from it.
A wandering agent moves at full speed, turning a little this way and that.
S.WanderAgent agent ; Wander about... S.WanderAgent agent, False ; ...and slow down to a stop.
Put the agents in the same flock. They keep apart, line up with each other and
stay together, as well as doing whatever else you ask. FlockOptions says
how much of each; the hunters above only keep apart.
S.FlockAgent bird, 1, Null ; Flock number 1, the usual way. S.WanderAgent bird ; And wander about together.
Flocks with different numbers take no notice of each other.
Give it options of its own when you add it.
Local quick:S.AgentOptions = New S.AgentOptions quick.maximumSpeed = 7 ; Units a second. quick.acceleration = 20 ; How quickly it can change its speed and direction. Local hare:S.Agent = S.AddAgent(world, quick) Delete quick
Import blitz.steering.physics As P too. It looks ahead through your physics
world, and turns agents aside from the shapes in their way. Give it to the
steering world when you make it:
Local looks:P.PhysicsAvoidanceOptions = New P.PhysicsAvoidanceOptions looks.heightOffset = 0.6 ; Look ahead 0.6 units up, clear of the floor. Local avoider:P.PhysicsAvoidance = P.CreatePhysicsAvoidance(physicsWorld, looks) Delete looks Local settings:S.SteeringOptions = New S.SteeringOptions settings.avoidance = avoider Local walkers:S.SteeringWorld = S.CreateSteering(settings) Delete settings
It only nudges agents aside, and can't stop them walking into a wall, so keep
moving them with your own collisions, as obstacle_course.bb does. Delete the
steering world before the avoider, and the avoider before the physics world.
Make a world in three dimensions. Agents then use Y as well.
Local sky:S.SteeringOptions = New S.SteeringOptions sky.dimensions = S.SteeringDimensions.XYZ Local birds:S.SteeringWorld = S.CreateSteering(sky) Delete sky
Blip's tip
Steering never moves anything itself. If your characters stand still, check that you add each velocity to its position yourself. Before every
UpdateSteering, tell the world where each agent really is withAgentMotion: where it ended up after your walls and collisions, not just where it wanted to go.
AgentMotion for every agent,
then UpdateSteering once for them all, so that every agent sees where the
others were at the same moment.X() and Z() from the one AgentVelocity
gives you, but don't delete it. It changes at the next update.Five more complete programs come with the module:
The reference below has every function, option and rule.
blitz.steering.physicsblitz.steering.spatial, which needs blitz.spatialCompute desired velocities for agents whose movement belongs to your game.
The core never moves an entity, advances a clock, resolves a collision or polls
input. Its only source dependency is blitz.random. Extended with
object-lifecycle-v1 is required. Blitz3D+ comes with this module and the
modules it uses, so any Extended program can Import blitz.steering. The
indexed example also uses blitz.spatial; importing the core does not import it.
Open examples/quick_start.bb and run it. Click to move the blue
circle toward a destination. playground.bb adds all six goal modes and an
arrow-controlled target. flock.bb draws generated cones: +/- changes group
size, S changes separation, Space pauses, Escape closes. Every example has a
finite --verify mode that asserts its working interactions and cleans up.
Dialect "modern" Import blitz.steering As S Local world:S.SteeringWorld = S.CreateSteering(Null) Local agent:S.Agent = S.AddAgent(world, Null) S.SteerTo agent, 10, 0, 5 ; Each game frame: capture ALL agent motion before calling UpdateSteering. S.AgentMotion agent, x, y, z, vx, vy, vz S.UpdateSteering world, seconds Local desired:S.Velocity = S.AgentVelocity(agent) vx = desired.X() vy = desired.Y() vz = desired.Z() ; Apply through your existing movement/collision controller; then report its ; actual resulting position/velocity as the next frame's AgentMotion snapshot. Delete world
| Operation | Contract |
|---|---|
CreateSteering(options):SteeringWorld | Caller owns world. Explicit Null selects XZ, seed 1, no avoidance. Copies options, borrows the provider. |
AddAgent(world, options):Agent | World owns new agent; caller may Delete it early. Explicit Null selects defaults. Positive per-world Long ID is never reused. |
AgentMotion(agent,x,y,z,vx,vy,vz) | Copy a complete finite Float position/velocity snapshot. Does not publish output or move anything. |
SteerTo(agent,x,y,z,mode=SteerMode.Arrive) | Replace goal with Seek, Arrive or Flee; clears following/wander. |
FollowAgent(agent,target,mode=SteerMode.Pursue) | Replace goal with Pursue/Evade. Target must be another live agent in this world. Borrow it; deletion clears inbound goals. |
WanderAgent(agent,enabled=True) | Replace point/follow goal with seeded wander, or clear the goal when False. Flocking remains independent. |
FlockAgent(agent,flock,options,enabled=True) | Positive group ID; copy FlockOptions, explicit Null for defaults. False disables this agent's membership. |
UseNeighbours(world,source) | Borrow a NeighbourSource; explicit Null restores direct scans. Does not change agent identities or outputs. |
UpdateSteering(world,seconds) | Compute all candidates from snapshots, then publish all borrowed outputs together. Nonnegative finite Double seconds. |
AgentVelocity(agent):Velocity | Borrow read-only X()/Y()/Z() result, reused until next update, invalidated when agent dies. Never delete it. |
agent.ID():Long | Stable identity in its world. Does not identify an engine entity. |
Delete agent | Unregister, clear inbound target goals, delete owned stream/vectors/views. Leaves other agents/world/provider intact. |
Delete world | Clear links and delete all remaining agents and containers. Does not delete game entities or borrowed provider. |
Options are public caller-owned records; delete them after the operation copies them. Factory constructors, mutable motion/target/stream fields and containers are private. Velocity and AvoidanceResult constructors are also private; deleting those borrowed records raises a programming error.
SteeringOptions: dimensions=SteeringDimensions.XZ (2) or .XYZ (3),
seed:Long=1L, avoidance:Avoidance=Null. XZ discards Y position, target and
velocity after validating all supplied numbers; results always have Y=0.
AgentOptions: maximumSpeed=4, acceleration=8, arrivalRadius=2,
neighbourRadius=6, radius=0.25, all Float. Speed/acceleration may be zero;
radii must be positive; all must be finite. Units are game units, seconds,
units/second and units/second². Maximum 65,536 agents per world bounds registries;
that is a resource limit, not a direct-scan performance promise.
FlockOptions: nonnegative finite separation=1.5, alignment=0.5,
cohesion=0.3. Settings copy per agent. Different groups do not interact;
an agent's own neighbour radius controls its inclusive local neighbourhood.
Seek points at maximum speed; Flee points away; Arrive linearly reduces that
speed within arrivalRadius. A coincident point goal requests zero velocity.
Pursuit/Evasion predicts target motion for
min(2 seconds, distance/(own maximum speed + target speed)); a zero divisor
uses zero prediction. These local behaviours do not find global routes.
Wander maintains a normalised direction per agent, perturbing it using its own
PCG stream. Each positive update consumes two draws in XZ or three in XYZ;
jitter scales by 2 * min(seconds,1). Normalisation handles zero direction.
World seed plus stable agent ID selects the stream: changing another agent's
activity/evaluation order does not consume this stream. Changing creation order
changes IDs; changing timestep partition changes wander's draw schedule. Neither
is advertised as equivalent to preserving per-agent random state.
Flocking scans snapshots, averages neighbour velocities and positions, and adds alignment/cohesion to the goal. Separation sums directions weighted by combined agent radius divided by distance (denominator floor 1e-6); coincident agents use opposite X directions ordered by ID. Weighted separation claims the speed budget first; goal/alignment/cohesion use the residual magnitude. Avoidance runs on that candidate next, before final acceleration/speed constraints. No temporary objects or neighbour containers are created during an ordinary core update.
Acceleration bounds the change from caller-reported motion, using seconds, then maximum speed caps the result. If external physics reports an already overspeed velocity, the speed cap takes priority and can exceed the acceleration bound. Zero seconds instead copies the current snapshot exactly (projected onto XZ), without clamping, random draws or avoidance calls. It is not a hidden clock tick. Large finite deltas saturate toward the candidate without overflowing an acceleration-times-delta product. Geometry promotes to Double before arithmetic; outputs round to Float. Compare physical bounds with Float rounding tolerance (e.g. 1e-5 at these example scales), not bit-exact real-number inequalities.
All AgentMotion calls precede the world update. Neighbours and targets read those snapshots, never freshly computed outputs. Candidate results publish in a second pass. Reversing evaluation and accumulation order changes results only by Float rounding (within 1e-5 at these scales); identical order/seed/snapshots repeat exactly in the current runtime. The core is synchronous and main-thread only. No worker/parallel claim is made.
Implement the public Avoidance.Adjust(x,y,z,radius,vx,vy,vz,seconds,result)
method. It receives the proposed velocity and an agent-owned reusable
AvoidanceResult already containing that velocity. Fill its writable x/y/z
fields with finite components; do not retain or delete it. XZ projects Y back to
zero. The provider is borrowed for the world's life and can be shared by separate
worlds updated sequentially. Delete worlds before the provider.
Callbacks must return promptly. They cannot update/create/mutate/delete steering resources, including another world. Read-only result queries see the previous complete publication. No callbacks run on zero time or destruction. Programming errors are RuntimeError: Null/deleted resources, invalid numbers/modes/groups, foreign/self targets, deleted providers, borrowed-view deletion or callback mutation. Such errors terminate the program; there is no transactional callback rollback or exception recovery contract.
Avoidance is a local suggestion, subsequently limited by acceleration. It is not collision resolution, a dead-end escape guarantee or a pathfinder. Your movement controller remains authoritative. The optional providers below do not become core dependencies.
blitz.steering.spatial)CreateSpatialNeighbours(cellSize=4, dimensions=SteeringDimensions.XZ) returns
an owned SpatialNeighbours. Pass it to UseNeighbours(world, provider) and
delete worlds before the provider. Match dimensions explicitly. The provider
owns an accepted spatial index, generation Map and reusable ID List; it borrows
no agents. Sequential worlds may share it: each positive update replaces its
complete snapshot, removes stale IDs, and queries sorted unique Long identities.
Zero time performs no provider calls. indexed_flock.bb adds I to switch between
direct and indexed queries while preserving the same moving flock.
Custom NeighbourSource implements BeginFrame(dimensions),
IndexAgent(id,x,y,z), EndFrame() and
Query(x,y,z,radius,output:Long List). The core builds all snapshots before
querying; callbacks have the same mutation guards as Avoidance. Output is
borrowed scratch, cleared before each call: fill ascending unique positive IDs
from the current snapshot. Extra candidates are allowed; omitting a point inside
the inclusive sphere is a provider error. The core applies the exact radius/group
filter and rejects duplicate, unsorted or foreign IDs. Do not retain/delete the
output. Spatial's documented cell-coordinate/resource limits still apply.
Index maintenance and query traversal reuse storage; growing/moving memberships can allocate in the spatial index. Fixed cell size is a caller choice, not auto tuning. Small or dense flocks can favour direct scans.
blitz.steering.physics)CreatePhysicsAvoidance(physicsWorld, options) returns an owned
PhysicsAvoidance implementing the core callback. Pass explicit Null for default
options. The provider borrows the live physics world and optional ignored body,
owns exactly one reusable query, and releases it on Delete. Delete steering
worlds before the provider, then delete the physics world. A prematurely freed
world/body is diagnosed on use; destructor cleanup tolerates world invalidation.
PhysicsAvoidanceOptions copies Float lookAhead=0.75 seconds,
maximumDistance=8, padding=0.05, heightOffset=0, Int mask=-1 and
ignoredBody=0. Lookahead/distance must be positive; padding nonnegative; all
Floats finite. The ignored body must belong to the borrowed world. Sensors are
excluded. XZ snapshots discard Y, so choose heightOffset for the character's
cast centre (the course uses 0.6). For XYZ it offsets the supplied Y. A cast
centre within the agent radius of a floor collider counts as penetrating it, so
raise heightOffset above the agent radius or exclude the floor with mask. Radius plus
padding becomes the cast diameter; centre/diameter must remain representable Floats.
An overlap query at the agent's own radius first stops proposed movement from
existing penetration; it reports up to 64 touching shapes, so corners, doorways
and tile seams are ordinary. Padding only widens the casts, so an agent that is
merely close to a surface keeps steering. Proposed movement shorter than
0.000001 units over the lookahead is returned unchanged without casting. The
provider then sphere-casts straight and at +/-30, +/-60, +/-90 degrees in a
plane through the proposed direction, with a perpendicular basis that also
handles vertical motion. Displacement is bounded by lookahead and maximum
distance. Clearance fraction weighted toward forward progress chooses the
candidate; positive-side ties are stable. An unobstructed straight path exits
after its first cast. At most one overlap plus seven casts runs per moving
agent. There are no per-call game resource allocations, world steps or entity
writes. This local fan cannot guarantee a route around concave traps or enforce
collision safety after acceleration limits. obstacle_course.bb shows the
actual runtime's game-owned swept character controller, checks overlap every
frame, and reaches a goal beyond three generated walls. Space pauses, R resets,
1/2 select opposite destinations, Escape closes.