The World Vol 3 · Forces of Nature
ch 31 / 105
Chapter 31

Mannerisms Before Minds

Motion aimed as force

Everything that has moved in The Hollow so far was moved from outside it. Weight pulls down, wind pushes east, a point of attraction hauls things toward itself, and drag leans back against whatever is travelling. Release a leaf and it goes wherever that arithmetic sends it. Watch for a thousand ticks and you will never catch it choosing, because there is nothing in a leaf to choose with.

A behavior is a force, exactly like weight and wind and drag. Work out the velocity the body would have if it were already doing what it wants, subtract the velocity it actually has, and hand the difference to ApplyForce. An animal reads differently on screen, and the difference arrives long before you know anything about the animal. It heads somewhere. It slows down as it gets there. It turns away from things it does not care for, and when nothing in particular is happening it still moves, drifting about instead of standing perfectly still. A beetle manages all four. None of it needs a brain. All of it needs the motion to be aimed.

The obvious way to aim motion is to write the velocity down. Work out the direction from the creature to whatever it is after, multiply by how fast that creature goes, assign the result to Vel. Two lines, and on screen the creature does indeed set off toward the target.

It has also just left the world. An assignment does not add to the velocity, it replaces it, so every other force acting on that body during the tick is thrown away unread. Wind cannot blow the creature. Water cannot slow it. A slope cannot make it stumble, and nothing can shove it, because a body whose velocity is dictated once a tick has no memory of a push. Worse, every system that touches bodies now needs a list of which bodies are steered so it can skip them, and any system that forgets produces motion that flickers between two answers.

This is the payoff the accumulator was built for. It was written so that any system could add a force knowing nothing about the body and nothing about the other systems, and a behavior is precisely such a system: it works out one vector, calls one method, and goes away. Composition comes free. A walker being pushed by a gust while heading for a tuft of grass is one sum of two vectors, and neither the gust nor the grass has to be told the other exists.

Four things follow from that. The steering force itself, plus the two limits that make it an animal instead of an arrow. Four behaviors built out of it, each with a flaw the numbers show plainly. The arithmetic of combining several when they disagree. And a walker that grazes, bolts when something comes too close, settles, and goes back to grazing, with nothing anywhere in it that could be called a mind.

One seek by hand

A body wants to reach a point. Two numbers describe what the body is capable of: the fastest it can travel, and the hardest it can push itself around. Both are properties of the creature, in the same way mass is, and everything in this chapter comes out of them. Work one tick of it by hand before writing a line.

∑ Interlude — a desire, a reality, and the difference

A walker stands at (40, 100). The grass is at (136, 28). The walker's top speed is 2.4 world pixels a tick and the hardest it can steer is 0.3. Right now it is drifting south-west at (−0.480, 1.760), which is the wrong way entirely.

Step one: where is the target from here? Subtract the positions, (136 − 40, 28 − 100), giving (96, −72). Its length is 120, since 96 by 72 is a three-four-five triangle scaled by 24. Throw the length away and keep the direction: (0.8, −0.6).

Step two: the desired velocity. That direction at full speed is 2.4 × (0.8, −0.6), which is (1.920, −1.440). This is the velocity the walker would have if it were already doing what it wants.

Step three: the steering force. Subtract what the walker has from what it wants. (1.920 − (−0.480), −1.440 − 1.760) is (2.400, −3.200), and that vector has length 4.000. Notice what it contains. It points north-east, partly because the grass is north-east and partly because the walker was going south and that has to be undone first. The difference cancels the wrong motion and supplies the right motion in a single vector.

Step four: the cut. Four is far more than 0.3, so the walker cannot push that hard. Keep the direction, set the length to 0.3: multiply by 0.3 ÷ 4.0, giving (0.180, −0.240).

desired = unit(target − position) × maxSpeed
steering = desired − velocity, cut to maxForce

That force goes to the accumulator, the tick divides by a mass of 1, and the velocity becomes (−0.300, 1.520). The walker is still going south. It has turned by about four degrees. That is what a limited steering force buys: turning takes time, and time spent turning is most of what makes a moving thing look like it has a body.

target − positionthe vector from where the body is to where it wants to be
unit(v)that vector's direction with its length thrown away: divide both numbers by the length
maxSpeedthe fastest this body travels, in world pixels per tick; 2.4 here
desiredthe velocity the body wants: the direction to the target, at maxSpeed
maxForcethe largest steering force this body can produce; 0.3 here
steeringdesired minus current, cut down to maxForce: one force, for the accumulator
▣ Build · stage 1 — a gait, and the one method under all of it
// internal/field/steer.go

// Gait is what a body is physically able to do: the fastest it can
// travel and the hardest it can push itself around. Every steering
// behavior in this world answers with a force no larger than MaxForce,
// aiming at a velocity no faster than MaxSpeed, and those two numbers
// are the whole difference between a creature and a thrown stone.
type Gait struct {
	MaxSpeed float64 // world pixels per tick
	MaxForce float64 // the largest steering force this body can produce
}

// Steer turns a desired velocity into a force for the accumulator: the
// velocity the body wants, minus the velocity it actually has, cut down
// to what the body can push with. It is the only place in this file
// where a behavior becomes a force.
func (g Gait) Steer(b Body, desired Vec2) Vec2 {
	f := desired.Sub(b.Vel)
	if f.Len() > g.MaxForce {
		f = f.Unit().Scale(g.MaxForce)
	}
	return f
}

// Seek is the force that carries a body toward a target at full speed:
// the desired velocity points at the target and is as long as the body
// can go.
func (g Gait) Seek(b Body, target Vec2) Vec2 {
	return g.Steer(b, target.Sub(b.Pos).Unit().Scale(g.MaxSpeed))
}
$ go run ./cmd/steer -mode hand
steer: one seek, mass 1.0, max speed 2.4, max force 0.3
       target 136.000,28.000
  tick        position        current       desired      steering  |steer|         force
     1  40.000,100.000   -0.480,1.760  1.920,-1.440  2.400,-3.200    4.000  0.180,-0.240
     2  39.700,101.520   -0.300,1.520  1.908,-1.456  2.208,-2.976    3.706  0.179,-0.241
     3  39.579,102.799   -0.121,1.279  1.896,-1.471  2.018,-2.750    3.411  0.177,-0.242
     4  39.635,103.836    0.056,1.037  1.886,-1.484  1.830,-2.521    3.115  0.176,-0.243
     5  39.867,104.631    0.232,0.794  1.877,-1.496  1.644,-2.290    2.819  0.175,-0.244
     6  40.275,105.181    0.407,0.551  1.868,-1.506  1.461,-2.057    2.523  0.174,-0.245
     7  40.856,105.487    0.581,0.306  1.861,-1.516  1.280,-1.822    2.226  0.172,-0.245
     8  41.609,105.548    0.754,0.061  1.854,-1.524  1.101,-1.584    1.929  0.171,-0.246
       after 8 ticks: at 42.534,105.362 doing 0.925,-0.186, 121.329 from the target

Row 1 is the hand calculation, column for column. The interesting column is |steer|: it starts at 4.000 and comes down every tick, because each tick the walker's velocity gets a little closer to the velocity it wanted, so there is less difference left to fix. The cut is doing all the work in the meantime. Every entry in the force column has length 0.3 exactly, and the direction of that 0.3 turns slowly as the walker comes round.

Look at the position column and nothing much happens: eight ticks in, the walker has crept from y 100 to y 105, the wrong way, still shedding the southward motion it started with. Then read the current-velocity column beside it, where the southward 1.760 has fallen to 0.061 and the eastward number has climbed from −0.480 to 0.754. The turn was happening the whole time. A creature that could reverse instantly would have skipped all of that, and skipping it is what makes a thing look like a cursor instead of an animal.

One tick of one seek, from two velocities to one force Two boxes on the left: desired, holding 1.920 comma minus 1.440, and current, holding minus 0.480 comma 1.760. Lines from both meet and lead into a highlighted box called steering, holding 2.400 comma minus 3.200 with the note length 4.000. An arrow leads from there to a box called the force, holding 0.180 comma minus 0.240, noted as cut to 0.300. An arrow leads down from that box to a box that divides by a mass of 1.0 and is spent by the update. Two lines beneath say that max speed 2.4 sets the desired vector's length and max force 0.3 sets how much of the difference gets through. ONE TICK OF ONE SEEK desired 1.920, -1.440 current -0.480, 1.760 steering 2.400, -3.200 length 4.000 the force 0.180, -0.240 cut to 0.300 ÷ mass 1.0 spent by the update max speed 2.4 sets the desired vector's length max force 0.3 sets how much of the difference gets through

Figure 31.1 — the whole of steering: a velocity the body wants, minus the one it has, cut to what it can push with, handed to the accumulator like any other force.

Arrival radius 9.600

Seek has one behavior and it never stops having it. The desired velocity is full speed at the target from any distance, including from half a pixel away, so a body that reaches its target is still being told to travel at 2.4 pixels a tick. It sails past, turns round over the next several ticks, comes back, and sails past the other way.

$ go run ./cmd/steer -mode seek -ticks 240
steer: seek only, target 96.000,64.000, max speed 2.4, max force 0.3
  tick  distance   speed
    20    44.780   2.400
    40     2.920   2.100
    60     9.080   0.900
    80     8.920   0.300
   100     5.480   1.500
   120     3.680   2.100
   140     8.320   0.900
   160     9.680   0.300
   180     4.720   1.500
   200     2.920   2.100
   220     9.080   0.900
   240     8.920   0.300
       nearest approach 0.820 at tick 39, and it turned round to come back 13 times
       at tick 240 it is 8.920 away doing 0.300

Thirteen turns in 240 ticks and the pattern repeats exactly every 40, because after the first pass the whole thing is one motion along one line: overshoot by about nine pixels, decelerate at 0.3 a tick, come back, overshoot again. This is not a bug and it is not useless. A moth round a lamp does this. A creature circling something it has not decided about does this. It is never an arrival.

Arrival needs the desired velocity to know how close the body is. Outside some radius, full speed as before. Inside it, ask for a speed proportional to the distance remaining, so that the request reaches zero exactly where the target is. The braking radius is the only new number, and it cannot be chosen freely.

∑ Interlude — how much room a stop needs

The walker is doing 2.4 and has to reach 0. The strongest force it can produce is 0.3, and dividing by its mass of 1 gives a deceleration of 0.3 pixels per tick per tick, so shedding 2.4 takes eight ticks. During those eight ticks it is still moving, at speeds running down from 2.4 to 0, averaging 1.2, so it covers about 8 × 1.2 = 9.6 pixels while stopping.

stopping distance = maxSpeed × maxSpeed × mass ÷ (2 × maxForce)

2.4 × 2.4 × 1 ÷ 0.6 is 9.6, the same answer written once instead of in two steps. A braking radius under 9.6 asks this walker for a stop it has no way to perform: it will still be near full speed when it runs out of room, and it will go past. A radius of 24 leaves plenty of margin, and inside it the desired speed is 0.1 of whatever the distance is, so the walker settles by taking a tenth off the gap every tick.

maxForce ÷ massthe deceleration available: how much speed one tick of maximum steering removes
brakethe braking radius: the distance at which the body starts asking for less than full speed
maxSpeed × r ÷ brakethe speed wanted at distance r inside the radius: full speed at the edge, zero at the target
▣ Build · stage 2 — away, and all the way in
// internal/field/steer.go

// Flee is Seek with the desired velocity turned around: away from the
// thing rather than toward it, and otherwise the same arithmetic.
func (g Gait) Flee(b Body, from Vec2) Vec2 {
	return g.Steer(b, b.Pos.Sub(from).Unit().Scale(g.MaxSpeed))
}

// Arrive is Seek with a braking radius. Outside the radius the body
// wants full speed. Inside it, the speed it wants falls off in
// proportion to how far it still has to go, reaching zero at the target
// itself.
func (g Gait) Arrive(b Body, target Vec2, brake float64) Vec2 {
	d := target.Sub(b.Pos)
	r := d.Len()
	speed := g.MaxSpeed
	if r < brake {
		speed = g.MaxSpeed * r / brake
	}
	return g.Steer(b, d.Unit().Scale(speed))
}

// StopRadius is the shortest braking radius this gait can honour on a
// body of mass m: the distance the body covers while shedding its top
// speed at the hardest it can push. A braking radius under this number
// asks for a stop the body cannot perform.
func (g Gait) StopRadius(m float64) float64 {
	return g.MaxSpeed * g.MaxSpeed * m / (2 * g.MaxForce)
}
$ go run ./cmd/steer -mode arrive -ticks 240
steer: arrive, target 96.000,64.000, braking radius 24.0
       shortest radius this gait can stop inside: 9.600
  tick  distance   speed   wanted
    20    44.780   2.400    2.400
    40     7.274   0.808    0.808
    60     0.884   0.098    0.098
    80     0.108   0.012    0.012
   100     0.013   0.001    0.001
   120     0.002   0.000    0.000
   140     0.000   0.000    0.000
   160     0.000   0.000    0.000
   180     0.000   0.000    0.000
   200     0.000   0.000    0.000
   220     0.000   0.000    0.000
   240     0.000   0.000    0.000
       ends 0.0000 from the target doing 0.0000

The speed and wanted columns are equal from tick 40 onward, which says the walker is getting exactly what it asks for: at 24 pixels out the request is small enough that a 0.3 force can keep up with it, so the cut stops binding and the body tracks its desire precisely. The distance then falls by a tenth every tick, which is a curve that halves roughly every seven ticks and never quite reaches zero. By tick 140 the difference is smaller than a printed thousandth, and on a screen of whole pixels it stopped mattering around tick 80.

Flee is one subtraction reversed, and the smallness of that change is the point. The direction to run is the direction from the frightening thing to the body, which is the same vector as before with its ends swapped. Everything else, the top speed, the cut, the accumulator, is untouched. Behaviors written this way tend to be one line, because all the machinery lives in Steer and each behavior only has to answer the question of what velocity it would like.

Wander with memory

A creature that only ever seeks, arrives and flees stands perfectly still whenever it has nothing to do, and stillness is the single most artificial thing a living creature can do on screen. It needs an idle motion: a drift that never repeats and never settles.

The tempting version is a random direction every tick, drawn from the world's seeded stream so it still replays. It does not work, and the reason it does not work is not the one people expect. Successive random directions have nothing to do with each other, so over any handful of ticks they cancel, and what the accumulator receives averages out to almost nothing. The body dithers on the spot. What is needed is randomness that remembers: an angle that gets nudged a little each tick instead of being redrawn. Keep that angle on a small circle projected in front of the body, seek the point it names, and the body commits to a direction for a while before drifting off it.

▣ Build · stage 3 — a heading that drifts, against one that does not
// internal/field/steer.go

// Wander is a heading that drifts. The body projects a circle a little
// in front of itself, keeps one angle on that circle from tick to tick,
// nudges the angle by a small seeded amount, and seeks whatever point
// the angle names. The angle is what carries across ticks, and carrying
// across ticks is the entire trick.
type Wander struct {
	Ahead  float64 // how far in front of the body the circle sits
	Radius float64 // how far off the heading its target may sit
	Jitter float64 // the most the angle may change in one tick, in radians

	angle float64 // where on the circle the target is, right now
}

// Target is the point the wander is steering for this tick: the circle's
// centre plus one radius in the direction the drifting angle names. The
// angle is measured from the body's own heading, so the circle turns
// with the body instead of standing still in the world.
func (w *Wander) Target(b Body, rng *rand.Rand) Vec2 {
	w.angle += (rng.Float64()*2 - 1) * w.Jitter
	head := b.Vel.Unit()
	if head == (Vec2{}) {
		head = Vec2{X: 1}
	}
	centre := b.Pos.Add(head.Scale(w.Ahead))
	a := math.Atan2(head.Y, head.X) + w.angle
	return centre.Add(Vec2{X: math.Cos(a), Y: math.Sin(a)}.Scale(w.Radius))
}

// Force is the wander as a force: seek the point on the circle.
func (w *Wander) Force(g Gait, b Body, rng *rand.Rand) Vec2 {
	return g.Seek(b, w.Target(b, rng))
}

// Jump is the wander that does not work, kept so the two can be run
// against each other: a direction drawn fresh every tick with nothing
// whatever carried over from the last one.
func Jump(g Gait, b Body, rng *rand.Rand) Vec2 {
	a := rng.Float64() * 2 * math.Pi
	return g.Steer(b, Vec2{X: math.Cos(a), Y: math.Sin(a)}.Scale(g.MaxSpeed))
}
$ go run ./cmd/steer -mode wander -ticks 240 -shot wander.png
steer: two wanders from 96.000,64.000, seed 5, 240 ticks
       circle: 12 ahead, radius 8, jitter 0.50 radians a tick
  tick   circle: speed   travelled      turn      jump: speed   travelled      turn
    40          2.391        92.2       6.1           1.138        37.1      14.8
    80          2.000       180.9       6.9           0.766        62.4      20.6
   120          2.388       273.3       6.6           1.075        85.0      23.3
   160          2.251       365.5       6.7           0.962       119.0      21.1
   200          2.353       452.9       6.9           0.301       136.4      26.7
   240          2.400       541.6       7.0           0.833       168.4      25.0
       circle: mean speed 2.257, path 541.6, mean turn 7.0 degrees a tick  frame 3b1fe4642101
       jump:   mean speed 0.702, path 168.4, mean turn 25.0 degrees a tick  frame 4f500817bec9
wrote circle-wander.png
wrote jump-wander.png

Same seed, same stream, same starting velocity, and the two walkers behave nothing alike. The circle wanderer holds 2.257 of a possible 2.4 and covers 541 pixels, turning about seven degrees a tick, which is a long unhurried curve. The jump wanderer averages 0.702, covers 168, and turns 25 degrees a tick: it swings three and a half times as hard while getting a third as far. A creature that turns constantly and goes nowhere is what a viewer reads as broken.

The two PNGs put it beyond argument. circle-wander.png is a long loose trail that crosses the valley, wraps at the edge and comes back through. In jump-wander.png the trail is a short scribble knotted around a single patch of ground the size of two cells. Both trails were drawn from the same numbers, one stream, one seed, and both frames hash the same on every run.

The 52-tick alarm

Now put it together. A walker feeding at a tuft of grass wants two things at once: to be at the tuft, and to keep shifting about while it is there. That is Arrive at the tuft plus a smaller helping of Wander, added, since adding forces is what the accumulator has always done. Then a predator comes over the rim, and the walker wants a third thing that flatly contradicts the first two.

▣ Build · stage 4 — grazing, and the arithmetic of being frightened
// cmd/steer/main.go — the tick

// Stream 3 of this seed: the terrain draws from stream 0, the world's
// laws from stream 1, released bodies from stream 2 and chapter 30's
// weathers from streams 4 to 7, so a walker's wandering disturbs none
// of them.
rng := rand.New(rand.NewPCG(seed, 3))

...

pred = predAt(t, park)
r := pred.Sub(b.Pos).Len()
if r < startle {
	fear = 1
} else {
	fear *= 0.94
}

feed := walk.Arrive(b, tuft, 24).Add(w.Force(walk, b, rng).Scale(0.6))
flight := walk.Flee(b, pred)

b.ApplyForce(combine(blend, feed, flight, fear, r < startle))
b.Step()
// cmd/steer/main.go

// combine settles the disagreement when feeding and flight both want the
// tick. Three rules, and the run's -blend flag picks between them.
func combine(blend string, feed, flight field.Vec2, fear float64, near bool) field.Vec2 {
	switch blend {
	case "sum": // fixed weights: both behaviors are always heard
		if near {
			return feed.Add(flight.Scale(2))
		}
		return feed
	case "switch": // flight wins outright, and stops the instant it can
		if near {
			return flight
		}
		return feed
	}
	// flight wins outright, and lets go slowly
	return flight.Scale(fear).Add(feed.Scale(1 - fear))
}

Three ways to settle a disagreement, and they are easier to tell apart on paper than in a run. The weighted sum keeps both behaviors in the accumulator all the time and lets the weights decide who dominates. The override lets flight take the tick outright and hands it straight back the moment the predator is one pixel outside the radius. The third does both: while the predator is inside the radius the alarm is 1 and grazing has no vote at all, and afterwards the alarm falls by six percent a tick, so the walker spends the next fifty or so ticks somewhere between fleeing and feeding.

Park the predator where it will not go away, so the disagreement has to be resolved rather than outlived, and each rule shows its character in one number: how long it let something dangerous stay close.

$ go run ./cmd/steer -mode graze -ticks 240 -blend sum -park | tail -3
   240    0.734      43.28   0.350              11.28  cacf47c06635  b76935018c65ca58
       the predator got within 12.93, spent 79 ticks inside the radius,
       and the fastest the walker ran while it was in there was 2.448
$ go run ./cmd/steer -mode graze -ticks 240 -blend switch -park | tail -3
   240    0.690      44.02   0.326              12.07  dff6c9b6d87c  a6cddf5964a9c8c8
       the predator got within 13.99, spent 89 ticks inside the radius,
       and the fastest the walker ran while it was in there was 2.400
$ go run ./cmd/steer -mode graze -ticks 240 -blend fear -park | tail -3
   240    0.101      83.21   1.658              70.75  aabdb05d264d  976c3857a01fbb0c
       the predator got within 13.99, spent 52 ticks inside the radius,
       and the fastest the walker ran while it was in there was 2.400

The middle line of each summary carries the verdict. Under the weighted sum the walker spends 79 of 240 ticks with a predator inside its radius, and the reason is in the arithmetic: the grass is on the far side of the predator, so the grazing force points at the danger while the flight force points away, and their sum is a compromise neither behavior would have chosen. The walker hovers, half-feeding, eleven pixels from its tuft and forty-three from a predator. Any fixed weight buys the same problem at a different distance. Raise it and the walker can never feed within sight of anything; lower it and it feeds while being eaten.

The plain override does worse, at 89 ticks, and the fault is the opposite one. The moment the predator is a pixel outside the radius the walker forgets entirely and turns back toward the grass, which walks it into the radius again, which turns it round again. It orbits the boundary. The decaying alarm cuts that to 52 ticks by taking the two apart: the trigger is still a hard radius, but the letting-go is gradual, so the walker carries its fright out past the boundary and is well clear before grazing gets a say. Of the three, it is the one to keep, and the reason is that it stops treating a threshold and a state as the same thing.

⚠ Worked failure — the bolt that was a teleport

While tuning the startle, the first version of it felt sluggish: the walker took most of a second to get up to speed, and a frightened animal ought to be quicker than that. The obvious lever is the steering limit, so it went up, and then up again, and then off altogether, since a cap that never binds might as well not exist. Watch the ticks around the moment the predator crosses the radius:

$ go run ./cmd/steer -mode graze -ticks 40 -blend fear -force 0 -from 33 -to 40 | head -14
steer: a walker grazing at 150.000,64.000, seed 5, flight overrides grazing, and the fear decays
       startle radius 40, predator walks west at 1.600 and keeps going
  tick     fear   predator   speed          from tuft         frame           numbers
     0    0.000     178.18   0.400             120.15  054c46392206  75fb133e21c2537a
    20    0.000      99.52   2.141              73.47  349270a205c9  000a10a1ca7b2393
t= 33  fear 0.000  pred  50.83  vel 2.351,0.262  speed 2.366  pos 106.776,64.568
t= 34  fear 0.000  pred  46.89  vel 2.393,0.129  speed 2.397  pos 109.169,64.697
t= 35  fear 0.000  pred  42.92  vel 2.403,-0.071  speed 2.404  pos 111.572,64.626
t= 36  fear 1.000  pred  38.92  vel -2.395,0.162  speed 2.400  pos 109.177,64.788
t= 37  fear 1.000  pred  39.72  vel -2.394,0.168  speed 2.400  pos 106.783,64.956
t= 38  fear 0.940  pred  40.52  vel -2.105,0.140  speed 2.109  pos 104.678,65.096
t= 39  fear 0.884  pred  41.04  vel -1.856,0.144  speed 1.862  pos 102.822,65.240
t= 40  fear 0.831  pred  41.30  vel -1.635,0.105  speed 1.638  pos 101.188,65.345
    40    0.831      42.94   1.638              48.83  2027168a252a  dbfbf71e0317bbb4

Read the velocity column across ticks 35 and 36. The walker is travelling east at 2.403 pixels a tick, and one tick later it is travelling west at 2.395, at full speed both ways, with nothing in between. No deceleration, no turn, no ticks spent changing its mind. On screen the sprite does not run away; it is somewhere else, facing the other way, on the next frame. The word for that is a teleport, and no amount of frightening it harder was going to fix it, because the problem was that the fright had become infinitely effective.

The proof is in the plainest run there is. Take the cap off the single seek from the first section:

$ go run ./cmd/steer -mode hand -force 0
steer: one seek, mass 1.0, max speed 2.4, max force +Inf
       target 136.000,28.000
  tick        position        current       desired      steering  |steer|         force
     1  40.000,100.000   -0.480,1.760  1.920,-1.440  2.400,-3.200    4.000  2.400,-3.200
     2   41.920,98.560   1.920,-1.440  1.920,-1.440   0.000,0.000    0.000   0.000,0.000
     3   43.840,97.120   1.920,-1.440  1.920,-1.440  -0.000,0.000    0.000  -0.000,0.000
     4   45.760,95.680   1.920,-1.440  1.920,-1.440  0.000,-0.000    0.000  0.000,-0.000
     5   47.680,94.240   1.920,-1.440  1.920,-1.440   0.000,0.000    0.000   0.000,0.000
     6   49.600,92.800   1.920,-1.440  1.920,-1.440   0.000,0.000    0.000   0.000,0.000
     7   51.520,91.360   1.920,-1.440  1.920,-1.440  -0.000,0.000    0.000  -0.000,0.000
     8   53.440,89.920   1.920,-1.440  1.920,-1.440   0.000,0.000    0.000   0.000,0.000
       after 8 ticks: at 55.360,88.480 doing 1.920,-1.440, 100.800 from the target

On tick 1 the current velocity becomes the desired velocity, to the digit. From tick 2 onward the steering force reads 0.000 forever, because there is no difference left between what the walker wants and what it has. Follow the arithmetic and the reason is unavoidable: the force is desired minus current, the accumulator divides by a mass of 1, and the update adds the result to the velocity, so the new velocity is current plus desired minus current, which is desired. An uncapped steering force on a body of mass 1 is an assignment written the long way round, and everything the first section said about assignment applies to it. The cap is what keeps steering a force instead of an instruction.

One number decides where that boundary sits. The steering force can never be longer than twice the top speed, since it is the difference between two vectors of length at most 2.4. So any cap of 4.8 or more, on a body of mass 1, binds on no tick ever. At 0.3 it binds constantly, and a walker takes eight ticks to turn round. Between those two lies every animal in this world.

With the cap back at 0.3, let the predator walk across the valley instead of parking, and watch the whole encounter from grazing to grazing.

$ go run ./cmd/steer -mode graze -ticks 240 -blend fear -shot startle.png
steer: a walker grazing at 150.000,64.000, seed 5, flight overrides grazing, and the fear decays
       startle radius 40, predator walks west at 1.600 and keeps going
  tick     fear   predator   speed          from tuft         frame           numbers
     0    0.000     178.18   0.400             120.15  054c46392206  75fb133e21c2537a
    20    0.000     102.02   2.346              75.95  55c019b6268b  66961ee5d5669540
    40    1.000      25.64   1.201              31.34  d97ffb418332  0fe50327d2d6d14f
    60    1.000      30.25   2.400              55.62  cf0653926fc7  b5300eaf6074c36f
    80    0.395      75.36   1.932              86.15  c66c5efa53b1  68a760b7fadf9114
   100    0.115     116.04   1.546              99.40  75862d3f02ec  f4d9484e1df7a8b2
   120    0.033     133.45   2.375              58.78  8a5113209e7c  84e15e4377fc5059
   140    0.010     164.69   1.598              13.78  9fd98fbbc369  1cb40e777723efd4
   160    0.003     197.29   0.194               0.84  09d1731e946e  555ecba46888cd3c
   180    0.001     230.00   0.191               0.09  d4044a8d87db  d157efb8012a10ff
   200    0.000     262.17   0.162               0.50  5c70c2cbef73  2c1f7c2d1566df8e
   220    0.000     294.06   0.135               0.58  5c70c2cbef73  ddb17063e51e6d50
   240    0.000     325.32   0.151               0.87  09d1731e946e  0f1cf32fa743996d
       the predator got within 13.99, spent 29 ticks inside the radius,
       and the fastest the walker ran while it was in there was 2.400
wrote startle.png

Read the alarm column against the distance to the grass. The walker crosses the valley and gets within 31 pixels of the tuft. The predator arrives, the alarm goes to 1, and by tick 60 the walker is running flat out and 55 pixels from the tuft, going the wrong way. It peaks at 99 pixels away around tick 100 with the alarm down to 0.115, turns, comes back through 58 and 13, and from tick 160 onward it is within a pixel of the grass, moving at a fifth of a pixel a tick: feeding, shifting about, occasionally taking a step. At tick 240 startle.png is a walker standing on its tuft with the whole valley empty behind it.

Nothing in that walker holds a plan. There is no state machine, no decision, no if-tree about what to do next. There is one number that says how alarmed it is, three behaviors added up with that number deciding the weights, and a mass. What a viewer sees is an animal that was eating, got a fright, ran, calmed down over several seconds and came back to its dinner, and every part of that reading was supplied by the viewer.

Behavior as caller

The accumulator asked every force to answer one question, what vector do you contribute this tick, and refused to care where the answer came from. Weight computes it from mass. Drag computes it from velocity. A field computes it from position. A steering behavior computes it from a desired velocity, and that is the only new idea in this chapter: an intention can be written as a velocity, and the gap between an intention and a fact is a force. Once that translation exists, wanting composes with physics without either side being modified, because both of them are just numbers being added.

The two limits do the rest. Top speed decides how long the desired vector is, and it is the difference between a mouse and a horse. The steering cap decides how much of the difference gets through in one tick, and it is the difference between a horse and a cursor. Every character trait these walkers have came out of those two numbers plus a braking radius: how fast, how sharply, how carefully. That is a small enough set of dials that a creature's whole manner of moving can be written down as three numbers and handed to something that generates creatures.

It also settles a question about minds before the question needs an answer. Deciding that the water matters more than the berries is one kind of work; getting to the water without walking into a rock is another, and this chapter did the second one. A chooser hands over a target, a thing to avoid, and how much each matters; the mannerisms turn that into forces and the accumulator turns the forces into motion.

The alarm earns its place as a number between 0 and 1 instead of a true-or-false for the same reason. A threshold answers a question about the world; a state answers a question about the animal, and animals stay frightened for a while. Any quantity that decays like that smooths a hard trigger into a behavior with duration: hunger after a meal, exhaustion after a sprint, wariness after being chased. One multiply a tick, and a switch becomes a mood.

Movement that reads as intent

✓ Checkpoint — what you can now do
  • Given a position, a target, a current velocity and a gait's two numbers, I can produce the desired velocity, the raw steering force, its length and the force after the cut, and check all four against -mode hand.
  • I can say what a steering force becomes when the cut never binds, and point at the column in the uncapped run that proves it.
  • From a top speed, a mass and a steering limit I can compute the shortest braking radius that gait can honour, and say what a smaller one does when the body reaches the target.
  • I can explain, from the path and turn columns rather than the picture, why a direction drawn afresh each tick leaves a body dithering while a drifting angle carries it across the valley.
  • Given the three parked-predator summaries, I can say which combination rule left something dangerous close for longest, and name the arithmetic that did it.
  • I can add a fifth behavior to this walker without touching the update, the accumulator, the gait, or any behavior already there.
⚡ Exercises — try first, then reveal
Exercise 1 — ask for a stop the walker cannot make. The braking radius floor for this gait is 9.600. Run arrive with a radius of 6 and watch the ticks either side of the target. Predict how far past it goes before it turns.

At 6.380 out it is still doing 2.400, because the desired speed only starts falling inside 6 and the body needs 9.6 pixels to shed 2.4. It crosses the target at tick 39 with 1.800 still on the clock and keeps going.

$ go run ./cmd/steer -mode arrive -ticks 64 -brake 6 -from 36 -to 46
steer: arrive, target 96.000,64.000, braking radius 6.0
       shortest radius this gait can stop inside: 9.600
  tick  distance   speed   wanted
    20    44.780   2.400    2.400
t= 36  distance   6.380  wanted  2.400  speed  2.400  pos 90.556,67.327
t= 37  distance   3.980  wanted  2.400  speed  2.400  pos 92.604,66.075
t= 38  distance   1.880  wanted  1.592  speed  2.100  pos 94.396,64.980
t= 39  distance   0.080  wanted  0.752  speed  1.800  pos 95.932,64.042
t= 40  distance   1.420  wanted  0.032  speed  1.500  pos 97.212,63.260
    40     1.420   1.500    0.032
t= 41  distance   2.620  wanted  0.568  speed  1.200  pos 98.236,62.634
t= 42  distance   3.520  wanted  1.048  speed  0.900  pos 99.003,62.165
t= 43  distance   4.120  wanted  1.408  speed  0.600  pos 99.515,61.852
t= 44  distance   4.420  wanted  1.648  speed  0.300  pos 99.771,61.695
t= 45  distance   4.420  wanted  1.768  speed  0.000  pos 99.771,61.695
t= 46  distance   4.120  wanted  1.768  speed  0.300  pos 99.515,61.852
    60     0.004   0.003    0.003
    64     0.000   0.000    0.000
       ends 0.0005 from the target doing 0.0003

It ends up 4.420 past the target, stops dead on tick 45, and starts back. Every tick after that happens inside the braking radius, where the speed it asks for falls along with the distance, so it converges instead of orbiting: the run ends half a thousandth of a pixel out. Compare the wanted and speed columns on the rows before the overshoot. The gap between them is the request the walker's legs are refusing.

Exercise 2 — take the cap off something lighter. The uncapped seek on a mass of 1 turned into an assignment. Predict what it does on a mass of 0.5 before running it, then run -mode hand -force 0 -mass 0.5.

Dividing by half a mass doubles the acceleration, so the velocity gains twice the difference it needed and lands as far past the desired velocity as it started short of it. Next tick it does the same thing in reverse. The velocity reflects across the desired velocity every tick, forever.

$ go run ./cmd/steer -mode hand -force 0 -mass 0.5
steer: one seek, mass 0.5, max speed 2.4, max force +Inf
       target 136.000,28.000
  tick        position        current       desired      steering  |steer|         force
     1  40.000,100.000   -0.480,1.760  1.920,-1.440  2.400,-3.200    4.000  2.400,-3.200
     2   44.320,95.360   4.320,-4.640  1.934,-1.421  -2.386,3.219    4.007  -2.386,3.219
     3   43.868,97.158   -0.452,1.798  1.919,-1.441  2.371,-3.239    4.014  2.371,-3.239
     4   48.159,92.478   4.291,-4.680  1.935,-1.420  -2.356,3.259    4.022  -2.356,3.259
     5   47.738,94.318   -0.421,1.839  1.919,-1.442  2.340,-3.281    4.030  2.340,-3.281
     6   51.996,89.595   4.259,-4.723  1.935,-1.419  -2.323,3.303    4.039  -2.323,3.303
     7   51.609,91.479   -0.388,1.884  1.918,-1.443  2.306,-3.327    4.048  2.306,-3.327
     8   55.832,86.710   4.224,-4.770  1.936,-1.418  -2.287,3.352    4.058  -2.287,3.352
       after 8 ticks: at 55.481,88.643 doing -0.351,1.934, 100.801 from the target

The odd ticks read −0.452, 1.798 and the even ticks 4.291, −4.680: a body doing six pixels one frame and under two the next, sign flipped, on a field 192 across. It still closes on the target, and at exactly the rate the mass-1 run managed: 100.801 away after eight ticks against that run's 100.800. The two alternating velocities average to the desired one, so the progress is right and the motion is a vibration.

Exercise 3 — feed in a crosswind. Give the arriving walker a steady wind of 0.200 east as well, and predict exactly where it comes to rest. It is not the target.

At rest the desired speed is the distance times 0.1, so arrive's force is 0.1 times the distance, toward the target. The wind is 0.200 the other way. They cancel where 0.1 × distance = 0.200, which is 2.000 pixels downwind.

// cmd/exp/main.go — the body of main, and nothing else
walk := field.Gait{MaxSpeed: 2.4, MaxForce: 0.3}
wind := field.Vec2{X: 0.2, Y: 0}
target := field.Vec2{X: 96, Y: 64}
b := field.NewBody(field.Vec2{X: 24, Y: 108}, 1)

fmt.Printf("arrive into a steady wind %.3f,%.3f, braking radius 24\n", wind.X, wind.Y)
fmt.Println("  tick  distance   speed   downwind of the tuft")
for t := 1; t <= 200; t++ {
	b.ApplyForce(walk.Arrive(b, target, 24))
	b.ApplyForce(wind)
	b.Step()
	if t%25 == 0 {
		fmt.Printf("  %4d %9.3f %7.3f %14.3f\n",
			t, target.Sub(b.Pos).Len(), b.Vel.Len(), b.Pos.X-target.X)
	}
}
$ go run ./cmd/exp
arrive into a steady wind 0.200,0.000, braking radius 24
  tick  distance   speed   downwind of the tuft
    25    26.057   2.567        -21.349
    50     6.482   0.060          6.479
    75     2.372   0.041          2.372
   100     2.027   0.003          2.027
   125     2.002   0.000          2.002
   150     2.000   0.000          2.000
   175     2.000   0.000          2.000
   200     2.000   0.000          2.000

2.000, to the last printed digit, and it holds there. Nobody wrote a rule about standing downwind. It fell out of two forces balancing, which is what putting a behavior in the accumulator buys you: the creature obeys the weather without knowing there is any.