Steering

Seek, arrive, flee, pursue, wander and flocking movement for agents, as velocities your game applies.

Commands

Import blitz.steering As S gives your program these, with S. in front. Each name leads to its part of the reference.

Functions

Types

Interfaces

Enums

blitz.steering.physics

An optional part. Import blitz.steering.physics as well to use these.

Functions

Types

blitz.steering.spatial

An optional part. Import blitz.steering.spatial as well to use these.

Functions

Types

Guide

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.

Six red hunters closing in on a yellow dot, spread out so that they don't bump into each other

Try it

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.

How do I...

Walk to a place and stop there?

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.

Wander about?

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.

Move as a flock?

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.

Make one faster than the others?

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

Steer round walls?

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.

Steer things that fly or swim?

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 with AgentMotion: where it ended up after your walls and collisions, not just where it wanted to go.

Good to know

More examples

Five more complete programs come with the module:

The reference below has every function, option and rule.

Reference

Needs
blitz.random
Optional parts
blitz.steering.physics
blitz.steering.spatial, which needs blitz.spatial
Examples
flock.bb, indexed_flock.bb, obstacle_course.bb, playground.bb, quick_start.bb

Compute 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

API and ownership

OperationContract
CreateSteering(options):SteeringWorldCaller owns world. Explicit Null selects XZ, seed 1, no avoidance. Copies options, borrows the provider.
AddAgent(world, options):AgentWorld 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):VelocityBorrow read-only X()/Y()/Z() result, reused until next update, invalidated when agent dies. Never delete it.
agent.ID():LongStable identity in its world. Does not identify an engine entity.
Delete agentUnregister, clear inbound target goals, delete owned stream/vectors/views. Leaves other agents/world/provider intact.
Delete worldClear 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.

Algorithms, time and determinism

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.

Avoidance callback

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.

Indexed neighbours (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.

Physics avoidance (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.