590 lines
22 KiB
Text
590 lines
22 KiB
Text
== Lesson 26: Characters That Move Themselves
|
|
|
|
image::learn/26-navigation.png[The finished lesson, 480]
|
|
|
|
In this lesson you build a room with a wall across the middle of it and put
|
|
two characters in it. One of them you send to a corner with a button press.
|
|
The other picks somewhere at random, walks there, and when it arrives picks
|
|
somewhere else, forever.
|
|
|
|
Neither of them walks into the wall, and you never write a line that says how
|
|
to go round it. That is new. Every lesson up to this one has moved things
|
|
where you told them to go. This is the first lesson where the game works out
|
|
something for itself.
|
|
|
|
=== The Problem with Walking Toward Something
|
|
|
|
Here is the obvious way to make a character walk to a point, and it is the way
|
|
everybody writes first. Take the target, subtract where you are, and step a
|
|
little along the line between them, every frame, until you get there.
|
|
|
|
It works beautifully in an empty room.
|
|
|
|
Put a wall in the room and it stops working, and it stops working in a way
|
|
that looks stupid rather than broken: the character marches into the wall and
|
|
stands there grinding against it, a metre from a doorway it cannot see,
|
|
because nothing in "step toward the target" has any idea that the world has
|
|
shapes in it.
|
|
|
|
You cannot patch your way out of this. Turning aside when you touch a wall
|
|
gets your character stuck in the first corner it finds. Nor is it enough to
|
|
know where the walls are, because the route round a wall may be long, may go
|
|
away from the target before it comes back, and may not exist at all.
|
|
|
|
What is needed is a way to ask a real question: *how do I walk from here to
|
|
there?* Answering it needs something that knows the shape of the floor.
|
|
|
|
=== What a Navigation Mesh Is
|
|
|
|
Stand in a room and look at the floor. Some of it you could walk on. Some of
|
|
it you could not: it is under a table, inside a wall, off the edge of a
|
|
balcony, or up a step too high to climb. Now imagine painting the part you
|
|
could walk on, and throwing away everything else -- the walls, the furniture,
|
|
the ceiling, the scenery. What you are left with is a flat shape, or several,
|
|
with holes in it where the pillars are and gaps where the drops are.
|
|
|
|
That painted shape is a *navigation mesh*, and it is all a character needs in
|
|
order to plan a walk. It is not the level. It is a simplified map of the parts
|
|
of the level a person can stand on, and because it is simple, questions about
|
|
it can be answered quickly.
|
|
|
|
Singe makes it for you from the level you already have. You say how big the
|
|
characters are, hand it the geometry, and it works out what is walkable:
|
|
floors and ramps within the slope you allow, steps no higher than you allow,
|
|
with a margin trimmed off around every wall and every drop so a character with
|
|
width does not clip a corner. That is called *baking*, it happens once, and
|
|
the result can be saved to a file and loaded next time.
|
|
|
|
=== A Room with a Wall in It
|
|
|
|
Start a new file. This part is a three dimensional scene, which is lesson
|
|
twenty-two's subject: a *scene* is a world drawn in three dimensions, and
|
|
everything in it hangs on a *node*, which is a position, a rotation, and a
|
|
scale that you can attach a shape to.
|
|
|
|
If you skipped that lesson you can still follow this one. Take the next two
|
|
blocks as "a grey floor twenty units square, three tan walls standing on it,
|
|
and a camera looking down at the lot".
|
|
|
|
[source,lua]
|
|
----
|
|
local ROOM = 20
|
|
local WALL_HEIGHT = 2
|
|
local AGENT_RADIUS = 0.4
|
|
local AGENT_HEIGHT = 1.8
|
|
local WALK_SPEED = 2.2
|
|
|
|
sceneEnable(true)
|
|
sceneSetBackground(28, 32, 42, 255)
|
|
sceneSetAmbient(80, 86, 100)
|
|
|
|
local floorLook = materialNew()
|
|
materialSetColor(floorLook, 96, 102, 110)
|
|
materialSetRoughness(floorLook, 0.9)
|
|
|
|
local wallLook = materialNew()
|
|
materialSetColor(wallLook, 150, 128, 104)
|
|
materialSetRoughness(wallLook, 0.8)
|
|
|
|
local ground = nodeNew()
|
|
nodeSetMesh(ground, meshPlane(ROOM, ROOM), floorLook)
|
|
|
|
local wallMesh = meshBox(1, WALL_HEIGHT, 1)
|
|
local walls = {}
|
|
|
|
|
|
function addWall(x, z, width, depth)
|
|
local wall = nodeNew()
|
|
nodeSetMesh(wall, wallMesh, wallLook)
|
|
nodeSetScale(wall, width, 1, depth)
|
|
nodeSetPosition(wall, x, WALL_HEIGHT / 2, z)
|
|
walls[#walls + 1] = wall
|
|
end
|
|
|
|
|
|
addWall(-4, 0, 12, 0.6)
|
|
addWall(7.5, 0, 5, 0.6)
|
|
addWall(-3, -6, 0.6, 8)
|
|
----
|
|
|
|
The units here are metres, because that is what Singe's three dimensional
|
|
world assumes. The first two walls run across the middle of the room with a
|
|
three metre gap between them: a doorway. The third is a stub sticking into the
|
|
far half, to give the characters something to walk round once they are
|
|
through.
|
|
|
|
Add a light and a camera so there is something to see:
|
|
|
|
[source,lua]
|
|
----
|
|
local sun = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(sun, 8, 14, 6)
|
|
nodeLookAt(sun, 0, 0, 0)
|
|
lightSetIntensity(sun, 3.5)
|
|
lightSetShadow(sun, true)
|
|
|
|
local camera = nodeNew()
|
|
nodeSetPosition(camera, 0, 17, 19)
|
|
nodeLookAt(camera, 0, 0, 0)
|
|
cameraSet(camera)
|
|
cameraSetPerspective(55, 0.1, 120)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Run it. A grey floor, a wall with a gap, and a stub, seen from above and
|
|
behind.
|
|
|
|
=== Bake the Floor
|
|
|
|
Three lines turn that room into a map:
|
|
|
|
[source,lua]
|
|
----
|
|
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
|
|
navAddNode(nav, ground)
|
|
for _, wall in ipairs(walls) do
|
|
navAddNode(nav, wall)
|
|
end
|
|
navBuild(nav)
|
|
----
|
|
|
|
Put them after the walls and before the light. Then add one line to
|
|
`onOverlayUpdate`, under `overlayClear`, so you can see what you got:
|
|
|
|
[source,lua]
|
|
----
|
|
navDraw(nav, 0, 170, 190)
|
|
----
|
|
|
|
Run it again. A web of blue-green triangles is laid over the floor, and it
|
|
stops short of the walls. The gap it leaves is not decoration: it is the 0.4
|
|
metres of clearance a character with a radius of 0.4 metres needs so that its
|
|
shoulders do not go through the stone. The doorway is still a doorway in the
|
|
mesh, because three metres is wide enough to walk through; make the gap half a
|
|
metre and the blue would close across it, and as far as every character in
|
|
your game was concerned there would be no door there at all.
|
|
|
|
Look at it for a moment, because `navDraw` is the tool you will reach for
|
|
every time a character does something inexplicable. Nine times in ten the
|
|
answer is on the floor in front of you.
|
|
|
|
=== Somebody to Walk It
|
|
|
|
A character is a node. What it looks like is not navigation's business, so
|
|
build the cheapest thing that shows which way it is facing: a box with a nose
|
|
on it.
|
|
|
|
[source,lua]
|
|
----
|
|
function makeWalker(x, z, look)
|
|
local walker = nodeNew()
|
|
nodeSetPosition(walker, x, 0, z)
|
|
local body = nodeNew()
|
|
nodeSetParent(body, walker)
|
|
nodeSetMesh(body, meshBox(0.7, AGENT_HEIGHT, 0.7), look)
|
|
nodeSetPosition(body, 0, AGENT_HEIGHT / 2, 0)
|
|
local nose = nodeNew()
|
|
nodeSetParent(nose, body)
|
|
nodeSetMesh(nose, meshSphere(0.22, 16), look)
|
|
nodeSetPosition(nose, 0, 0.4, -0.45)
|
|
return walker
|
|
end
|
|
----
|
|
|
|
That goes with `addWall`. It needs a colour, so add one beside the other
|
|
materials:
|
|
|
|
[source,lua]
|
|
----
|
|
local walkerLook = materialNew()
|
|
materialSetColor(walkerLook, 225, 85, 60)
|
|
materialSetEmissive(walkerLook, 70, 18, 10)
|
|
----
|
|
|
|
And now the line this lesson exists for. After `navBuild`:
|
|
|
|
[source,lua]
|
|
----
|
|
local walker = makeWalker(-7, 7, walkerLook)
|
|
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
|
|
----
|
|
|
|
`navAgentNew` puts the node on the mesh and hands back an *agent*: the engine's
|
|
word for something it is willing to walk around for you. From this moment the
|
|
engine owns that node's position. Every frame it moves it, and turns it to
|
|
face the way it is going, and it will keep doing that until you tell it where
|
|
to go or take the agent away.
|
|
|
|
Run it. A red box with a nose appears in the near corner and does absolutely
|
|
nothing, because you have not asked for anything yet.
|
|
|
|
=== Sending It Somewhere
|
|
|
|
Give it a list of places and a button.
|
|
|
|
[source,lua]
|
|
----
|
|
local posts = {
|
|
{ 7, 0, 7 },
|
|
{ 7, 0, -7 },
|
|
{ -7, 0, -7 },
|
|
{ -7, 0, 7 },
|
|
{ 30, 0, 30 }
|
|
}
|
|
local post = 0
|
|
local message = "Button 1 sends the red one somewhere."
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what ~= SWITCH_BUTTON1 then
|
|
return
|
|
end
|
|
post = post % #posts + 1
|
|
local target = posts[post]
|
|
if navAgentMoveTo(walkerAgent, target[1], target[2], target[3]) then
|
|
message = string.format("Walking to post %d.", post)
|
|
else
|
|
message = string.format("No path to post %d. Staying put.", post)
|
|
end
|
|
end
|
|
----
|
|
|
|
Print the message so you can see what is going on. In `onOverlayUpdate`, after
|
|
`navDraw`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 2, message)
|
|
----
|
|
|
|
Run it and press button one -- the *space bar* by default. The box turns, walks
|
|
to the far corner of its own half of the room, and stops. Press again. It
|
|
walks along the wall, finds the doorway, goes through it, comes out the other
|
|
side, goes round the stub, and stops in the far corner. You did not tell it
|
|
about the doorway. Nothing in your script mentions the doorway. It found it,
|
|
because the doorway is a hole in a shape and going round holes is what the
|
|
mesh is for.
|
|
|
|
Each `posts` entry is three numbers, an x, a y, and a z, because a point in a
|
|
three dimensional world needs three. Y is up, and yours are all zero because
|
|
everything here is on the ground. The `posts` table is a table of tables, from
|
|
lesson six, and `posts[post]` is one of the inner ones.
|
|
|
|
=== Knowing When It Arrives
|
|
|
|
The box stops when it gets there, but your script does not know that it has.
|
|
Ask to be told:
|
|
|
|
[source,lua]
|
|
----
|
|
function onNavArrived(agent)
|
|
message = string.format("Arrived at post %d.", post)
|
|
end
|
|
----
|
|
|
|
This is a callback, the same idea as `onOverlayUpdate` in lesson one: you write
|
|
it, the engine calls it. This one runs once, the moment an agent reaches the
|
|
target it was given. Not when it is near, and not every frame afterward --
|
|
once, on arrival.
|
|
|
|
It is handed the agent, because a game has more than one. That argument is the
|
|
number `navAgentNew` gave back, and comparing it is how you tell which of your
|
|
characters has just got where it was going.
|
|
|
|
=== When There Is No Path
|
|
|
|
Press the button a fifth time and the message reads `No path to post 5.
|
|
Staying put.`, and the box does not move.
|
|
|
|
Post five is at `{ 30, 0, 30 }`, which is ten metres outside a room twenty
|
|
metres across. There is no walkable floor there, there is no route to it, and
|
|
there is nothing sensible for a character to do about it. `navAgentMoveTo`
|
|
says so by handing back `false`, and it leaves the agent exactly as it was.
|
|
|
|
This is the part beginners skip, and it is the part that bites. Real games ask
|
|
for impossible walks constantly, because the target is usually the player and
|
|
the player gets everywhere: standing on a crate, mid-jump, in the water,
|
|
behind a door that closed, or through a gap the nav mesh trimmed away as too
|
|
narrow. Every one of those makes a perfectly reasonable `navAgentMoveTo`
|
|
return `false`.
|
|
|
|
Your script decides what that means. A guard who cannot reach you should go
|
|
back to patrolling, or walk to the nearest point it *can* reach and wait
|
|
there, or shout. What it must not do is nothing while your code quietly
|
|
ignores a returned `false` and you wonder for an hour why one enemy in twenty
|
|
is frozen.
|
|
|
|
Two other calls help here, and they are worth knowing before you need them.
|
|
`navNearest` takes any point and hands back the closest spot on the mesh, or
|
|
`nil` if there is nothing near it, which is how you turn "where the player is"
|
|
into "somewhere a character can actually stand". And `navPath` asks the same
|
|
question as `navAgentMoveTo` without moving anybody: it hands back the corners
|
|
of the route as a table of points, or `nil` when there is no route. You can
|
|
measure it, draw it, or decide the walk is too long to bother with.
|
|
|
|
Drawing it is the useful trick. Add this to `onOverlayUpdate`:
|
|
|
|
[source,lua]
|
|
----
|
|
local target = posts[post]
|
|
if target ~= nil then
|
|
local x, y, z = nodeGetWorldPosition(walker)
|
|
local route = navPath(nav, x, y, z, target[1], target[2], target[3])
|
|
if route ~= nil then
|
|
for i = 2, #route do
|
|
local a = route[i - 1]
|
|
local b = route[i]
|
|
lineDraw(a[1], a[2] + 0.1, a[3], b[1], b[2] + 0.1, b[3], 255, 215, 60)
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Now a yellow line runs from the box to wherever it is going, bending through
|
|
the doorway, shortening as it walks. Send it to post five and the line is not
|
|
drawn at all, because `navPath` returned `nil` and the `if` caught it. You are
|
|
watching the decision that the agent is making, drawn on the floor.
|
|
|
|
=== A Second Walker
|
|
|
|
One character avoiding walls is navigation. Two characters avoiding walls
|
|
*and each other* is what the engine actually gives you, and it costs four more
|
|
lines.
|
|
|
|
Add a blue material beside the red one, then this under the first agent:
|
|
|
|
[source,lua]
|
|
----
|
|
local wanderer = makeWalker(6, -6, wandererLook)
|
|
local wandererAgent = navAgentNew(nav, wanderer, AGENT_RADIUS, AGENT_HEIGHT, 1.4)
|
|
----
|
|
|
|
Send it somewhere to start it off, after the camera:
|
|
|
|
[source,lua]
|
|
----
|
|
navAgentMoveTo(wandererAgent, navRandomPoint(nav))
|
|
----
|
|
|
|
And teach `onNavArrived` to tell the two apart:
|
|
|
|
[source,lua]
|
|
----
|
|
function onNavArrived(agent)
|
|
if agent == wandererAgent then
|
|
navAgentMoveTo(agent, navRandomPoint(nav))
|
|
else
|
|
message = string.format("Arrived at post %d.", post)
|
|
end
|
|
end
|
|
----
|
|
|
|
`navRandomPoint` picks somewhere on the walkable mesh -- anywhere on it, never
|
|
inside a wall -- and hands back three numbers, which is exactly what
|
|
`navAgentMoveTo` wants next. Because the callback fires on arrival and its
|
|
answer is another journey, the blue box now wanders the room for as long as
|
|
the game runs, out of four lines and no state at all.
|
|
|
|
Watch the two of them meet in the doorway. Neither one stops dead and neither
|
|
one walks through the other: they both lean aside a little, in good time, and
|
|
pass. That is the agents steering round each other, and it is the difference
|
|
between characters and furniture.
|
|
|
|
The finished script is `learn/26-navigation.singe`.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
|
|
----
|
|
|
|
Starts a navigation mesh, and the four numbers are a description of the person
|
|
it is for: 0.4 metres wide, 1.8 metres tall, able to walk a slope of up to
|
|
forty-five degrees, and able to climb a step of up to 0.3 metres.
|
|
|
|
Every one of those changes the shape that comes out. A wider character loses
|
|
more floor to the margin around walls and may find a doorway closed. A taller
|
|
one cannot walk under a low beam. Allow a steeper slope and a ramp becomes a
|
|
road; allow less and it becomes a cliff.
|
|
|
|
The radius also decides how finely the floor is measured, so a very small
|
|
radius over a very large level takes a long time to bake. The manual's entry
|
|
for `navNew` is worth reading before you pick your numbers.
|
|
|
|
[source,lua]
|
|
----
|
|
navAddNode(nav, ground)
|
|
navBuild(nav)
|
|
----
|
|
|
|
`navAddNode` hands over geometry -- a node and everything hanging under it, as
|
|
it stands in the world at that moment, so place things before you add them.
|
|
One call takes a whole loaded model. Only actual shapes count: a light, a
|
|
sound, or a physics body on its own adds nothing.
|
|
|
|
`navBuild` does the baking, and it is the slow call in this lesson: a fraction
|
|
of a second for a room like this one, a tenth of a second for a cathedral on a
|
|
desktop machine, several seconds on a Raspberry Pi. Do it while a title screen
|
|
is up, not while the player waits.
|
|
|
|
Afterward the geometry is thrown away and the mesh is closed. You cannot add
|
|
more. If your level changes shape -- a door opens, a bridge falls -- you bake
|
|
another mesh, and `navDelete` frees the old one.
|
|
|
|
On a slow machine you would rather not bake at all, so `navSave` writes the
|
|
finished mesh to a file and `navLoad` reads it back, built and ready. Bake once
|
|
on your own machine, ship the file with the game, and the player never waits.
|
|
|
|
[source,lua]
|
|
----
|
|
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
|
|
----
|
|
|
|
An agent is a node the engine has agreed to walk about for you. The radius and
|
|
height here are what it uses to keep clear of other agents, and the last
|
|
number is its top speed in metres a second -- 2.2 is a brisk walk, 1.4 is an
|
|
amble, 6 is a sprint.
|
|
|
|
From here on, do not set that node's position yourself. Two things moving one
|
|
node fight, and the fight looks like stuttering. If your character needs to
|
|
be shoved about by physics as well as walk, there is a call for that --
|
|
`navAgentSetPlayer` hands the steering to a physics controller instead -- and
|
|
the manual's entry for it explains when you want it.
|
|
|
|
[source,lua]
|
|
----
|
|
local ok = navAgentMoveTo(walkerAgent, x, y, z)
|
|
----
|
|
|
|
Sets the target. It does not walk there; it says where "there" is, and the
|
|
engine gets on with it over the following frames. You may call it again at any
|
|
moment and the new target replaces the old one, which is how a guard follows a
|
|
player who is running: re-aim it once or twice a second, not sixty times.
|
|
|
|
And it hands back `true` or `false`, and you must look.
|
|
|
|
[source,lua]
|
|
----
|
|
navAgentStop(walkerAgent)
|
|
----
|
|
|
|
Not used above, but the other half of the pair: it forgets the target and the
|
|
agent slows to a stand where it is. `onNavArrived` does not fire, because it
|
|
did not arrive. Use it when the reason for the journey has gone away.
|
|
|
|
=== Fast Is Not the Point
|
|
|
|
One more call, because it is what turns this from a demonstration into
|
|
something that looks like a game.
|
|
|
|
[source,lua]
|
|
----
|
|
local vx, vy, vz = navAgentGetVelocity(walkerAgent)
|
|
local speed = math.sqrt(vx * vx + vz * vz)
|
|
overlayPrint(2, 4, string.format("Speed %.2f", speed))
|
|
----
|
|
|
|
`navAgentGetVelocity` tells you how the agent is moving right now: not where
|
|
it is going, but how fast and in which direction it is actually travelling
|
|
this frame. Watch the number as the box sets off and as it comes to a halt. It
|
|
eases in and eases out.
|
|
|
|
That number is what you hang an animation on. A character whose legs move at
|
|
the speed it is actually travelling looks alive; one whose walk cycle runs at
|
|
a fixed rate while it accelerates looks like it is skating. With a real model
|
|
in place of the box you would compare that speed against a couple of
|
|
thresholds and play an idle, a walk, or a run -- which is lesson
|
|
twenty-three's subject, and the manual's example under
|
|
`navAgentGetVelocity` is written for exactly that.
|
|
|
|
Which is the real point of this lesson. A character that takes the fast
|
|
straight line and grinds along a wall is worse than one that sets off a second
|
|
later, turns properly, goes round through the door, and slows down when it
|
|
arrives. Nobody watching a game measures how quickly an enemy reached them.
|
|
They notice, immediately and without being able to say why, when something
|
|
moves in a way a living thing would not.
|
|
|
|
=== Try It
|
|
|
|
. *Close the door.* Change the first two walls so the gap between them is half
|
|
a metre instead of three: `addWall(-4, 0, 13, 0.6)` and
|
|
`addWall(6.5, 0, 7, 0.6)`. Run it and look at what `navDraw` shows before
|
|
you press anything. Then send the box across and see what it does.
|
|
. *Make everybody fat.* Set `AGENT_RADIUS` to `2.0` and run it. The doorway is
|
|
still three metres wide and the blue triangles are gone from it. Nothing is
|
|
broken; the mesh is answering a different question now.
|
|
. *Send it to the mouse.* Take the point under the mouse pointer with
|
|
`sceneUnproject`, snap it onto the mesh with `navNearest`, and send the box
|
|
there when the button is pressed. `navNearest` returning `nil` is the case
|
|
you must handle, and it happens whenever the pointer is off the floor.
|
|
. *Add four more wanderers.* Put the blue walker's four lines in a `for` loop
|
|
from lesson four, keep the agents in a table, and let them all wander. Watch
|
|
the doorway when three of them want it at once.
|
|
. *Bake it once.* Call `navSave(nav, "room.nav")` after `navBuild`, then
|
|
replace `navNew`, the `navAddNode` calls, and `navBuild` with a single
|
|
`navLoad("room.nav", AGENT_RADIUS, AGENT_HEIGHT)`. Now change a wall's
|
|
position and run it again. What you see is the trap that comes with baked
|
|
meshes, and it is worth meeting once on purpose.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Baking closes the mesh. Try to add one more thing after the fact -- a crate you
|
|
placed late, or a wall you forgot -- by putting this line straight after
|
|
`navBuild(nav)`:
|
|
|
|
[source,lua]
|
|
----
|
|
navAddNode(nav, ground)
|
|
----
|
|
|
|
Singe stops before the window opens, naming the line that call is on in your
|
|
file:
|
|
|
|
----
|
|
69:navAddNode: The mesh is already built.
|
|
----
|
|
|
|
The shape of an engine error is the line, the call that objected, and the
|
|
complaint, and this complaint means what it says: `navBuild` consumed the
|
|
geometry it was given and there is no longer anything to add to.
|
|
|
|
The fix is never to move the `navAddNode` call. It is to notice that you have
|
|
two levels' worth of geometry and want two meshes, or that you baked too
|
|
early. Add everything, then build, and build last.
|
|
|
|
=== What You Learned
|
|
|
|
* Stepping toward a target works in an empty room and fails the moment there
|
|
is a wall, and no amount of patching fixes it.
|
|
* A navigation mesh is a simplified map of the floor a character can stand on,
|
|
with the walls, the drops, and the clearance around them taken out.
|
|
* `navNew` describes the character the mesh is for, `navAddNode` hands over
|
|
the level's geometry, and `navBuild` bakes it. After the bake, nothing more
|
|
can be added.
|
|
* `navSave` and `navLoad` keep a baked mesh in a file so the player never
|
|
waits for it.
|
|
* `navDraw` shows you the mesh, and it is the first thing to look at when a
|
|
character behaves strangely.
|
|
* `navAgentNew` gives a node to the engine to walk about; from then on, do not
|
|
move that node yourself.
|
|
* `navAgentMoveTo` sets a target and hands back `false` when there is no way
|
|
to get there. Handling that is your job, and it happens often.
|
|
* `onNavArrived` is called once when an agent reaches its target, and is told
|
|
which agent.
|
|
* `navPath` answers the same question without moving anyone, and `nil` means
|
|
no route.
|
|
* Agents steer round each other as well as round the level.
|
|
* `navAgentGetVelocity` is how a walk cycle matches the ground being covered.
|
|
|
|
=== Next Time
|
|
|
|
Your two boxes now walk around a room in silence. Lesson twenty-seven is about
|
|
what you put behind them: music that loops properly and stays in step with
|
|
video, and MIDI, which lets a game play an instrument -- or lets a real
|
|
instrument play the game.
|