singe/docs/es/lessons/26-navigation.adoc

619 lines
24 KiB
Text

[#_lesson_26_characters_that_move_themselves]
== Lección 26: Personajes que se mueven solos
image::learn/26-navigation.png[La lección terminada, 480]
En esta lección construyes una habitación con una pared que la cruza por el
medio y pones dos personajes en ella. A uno lo mandas a una esquina pulsando un
botón. El otro elige un sitio al azar, camina hasta allí y, cuando llega, elige
otro sitio, para siempre.
Ninguno de los dos se mete en la pared, y nunca escribes una línea que diga cómo
rodearla. Eso es nuevo. Todas las lecciones hasta esta han movido las cosas
adonde tú les decías. Esta es la primera lección en la que el juego averigua
algo por sí mismo.
[#_the_problem_with_walking_toward_something]
=== El problema de caminar hacia algo
Esta es la forma obvia de hacer que un personaje camine hasta un punto, y es la
que todo el mundo escribe primero. Toma el destino, réstale dónde estás y da un
pasito a lo largo de la línea que los une, en cada fotograma, hasta llegar.
Funciona de maravilla en una habitación vacía.
Pon una pared en la habitación y deja de funcionar, y deja de funcionar de una
forma que parece estúpida más que rota: el personaje marcha contra la pared y se
queda ahí restregándose contra ella, a un metro de una puerta que no puede ver,
porque nada en «da un paso hacia el destino» tiene la menor idea de que el mundo
tiene formas.
No puedes salir de esto a base de parches. Apartarte cuando tocas una pared hace
que tu personaje se atasque en la primera esquina que encuentre. Tampoco basta
con saber dónde están las paredes, porque el camino que rodea una pared puede
ser largo, puede alejarse del destino antes de volver y puede no existir en
absoluto.
Lo que hace falta es una forma de hacer una pregunta de verdad: *¿cómo camino de
aquí a allí?* Para responderla hace falta algo que conozca la forma del suelo.
[#_what_a_navigation_mesh_is]
=== Qué es una malla de navegación
Ponte en una habitación y mira el suelo. Por una parte podrías caminar. Por otra
no: está debajo de una mesa, dentro de una pared, más allá del borde de un
balcón o encima de un escalón demasiado alto para subirlo. Ahora imagina que
pintas la parte por la que podrías caminar y tiras todo lo demás -- las paredes,
los muebles, el techo, el decorado. Lo que te queda es una forma plana, o
varias, con agujeros donde están las columnas y huecos donde están los
desniveles.
Esa forma pintada es una *malla de navegación*, y es todo lo que necesita un
personaje para planear un paseo. No es el nivel. Es un mapa simplificado de las
partes del nivel donde una persona puede estar de pie, y como es sencillo, las
preguntas sobre él se pueden responder deprisa.
Singe la hace por ti a partir del nivel que ya tienes. Le dices el tamaño de los
personajes, le pasas la geometría y averigua qué es transitable: suelos y rampas
dentro de la pendiente que permitas, escalones no más altos de lo que permitas,
con un margen recortado alrededor de cada pared y de cada desnivel para que un
personaje con anchura no atraviese una esquina. Eso se llama *precalcular*,
ocurre una vez, y el resultado se puede guardar en un archivo y cargar la
próxima vez.
[#_a_room_with_a_wall_in_it]
=== Una habitación con una pared
Crea una carpeta llamada `navigation` y empieza dentro un archivo llamado
`navigation.singe`. Esta parte es una escena tridimensional, que es el
tema de la lección veintidós: una *escena* es un mundo dibujado en tres
dimensiones, y todo lo que hay en ella cuelga de un *nodo*, que es una posición,
una rotación y una escala a las que puedes sujetar una forma.
Si te saltaste esa lección, puedes seguir esta igualmente. Toma los dos bloques
siguientes como «un suelo gris de veinte unidades de lado, tres paredes de color
tostado sobre él y una cámara que lo mira todo desde arriba».
[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)
----
Las unidades aquí son metros, porque eso es lo que da por hecho el mundo
tridimensional de Singe. Las dos primeras paredes cruzan la mitad de la
habitación con un hueco de tres metros entre ellas: una puerta. La tercera es un
tabique corto que se mete en la mitad del fondo, para que los personajes tengan
algo que rodear una vez que la hayan cruzado.
Añade una luz y una cámara para que haya algo que ver:
[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
----
Ejecútalo con `Singe -R navigation`. Un suelo gris, una pared con un hueco y un
tabique, vistos desde arriba y desde atrás.
[#_bake_the_floor]
=== Precalcula el suelo
Seis líneas convierten esa habitación en un mapa:
[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)
----
Ponlas después de las paredes y antes de la luz. Luego añade una línea a
`onOverlayUpdate`, debajo de `overlayClear`, para ver lo que has obtenido:
[source,lua]
----
navDraw(nav, 0, 170, 190)
----
Ejecútalo otra vez. Una red de triángulos verdeazulados se extiende sobre el
suelo, y se detiene antes de llegar a las paredes. El hueco que deja no es
decoración: son los 0,4 metros de holgura que necesita un personaje con un radio
de 0,4 metros para que sus hombros no atraviesen la piedra. La puerta sigue
siendo una puerta en la malla, porque tres metros es lo bastante ancho para
pasar; haz el hueco de medio metro y el azul se cerraría sobre él, y para todos
los personajes de tu juego allí no habría ninguna puerta.
Míralo un momento, porque `navDraw` es la herramienta que buscarás cada vez que
un personaje haga algo inexplicable. Nueve de cada diez veces la respuesta está
en el suelo, delante de ti.
[#_somebody_to_walk_it]
=== Alguien que lo recorra
Un personaje es un nodo. Su aspecto no es asunto de la navegación, así que
construye lo más barato que muestre hacia dónde mira: una caja con nariz.
[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
----
Eso va con `addWall`. Necesita un color, así que añade uno junto a los demás
materiales:
[source,lua]
----
local walkerLook = materialNew()
materialSetColor(walkerLook, 225, 85, 60)
materialSetEmissive(walkerLook, 70, 18, 10)
----
Y ahora la línea por la que existe esta lección. Después de `navBuild`:
[source,lua]
----
local walker = makeWalker(-7, 7, walkerLook)
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
`navAgentNew` pone el nodo sobre la malla y te devuelve un *agente*: la palabra
del motor para algo que está dispuesto a hacer caminar por ti. Desde este
momento, el motor es el dueño de la posición de ese nodo. En cada fotograma lo
mueve y lo gira para que mire hacia donde va, y seguirá haciéndolo hasta que le
digas adónde ir o retires el agente.
Ejecútalo. Una caja roja con nariz aparece en la esquina más cercana y no hace
absolutamente nada, porque todavía no le has pedido nada.
[#_sending_it_somewhere]
=== Mandarla a algún sitio
Dale una lista de sitios y un botón.
[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
----
Imprime el mensaje para ver qué está pasando. En `onOverlayUpdate`, después de
`navDraw`:
[source,lua]
----
overlayPrint(2, 2, message)
----
Ejecútalo y pulsa el botón uno -- la *barra espaciadora* por defecto. La caja
gira, camina hasta la esquina más lejana de su propia mitad de la habitación y
se para. Pulsa otra vez. Camina a lo largo de la pared, encuentra la puerta, la
cruza, sale por el otro lado y se para en la esquina más lejana. Pulsa una
tercera vez y rodea el tabique hasta la cuarta esquina. No le has hablado de la puerta. Nada en tu script menciona la puerta. La
ha encontrado, porque la puerta es un agujero en una forma y rodear agujeros es
justo para lo que sirve la malla.
Cada entrada de `posts` son tres números, una x, una y, y una z, porque un punto
en un mundo tridimensional necesita tres. La Y es hacia arriba, y las tuyas son
todas cero porque aquí todo está en el suelo. La tabla `posts` es una tabla de
tablas, de la lección seis, y `posts[post]` es una de las de dentro.
[#_knowing_when_it_arrives]
=== Saber cuándo llega
La caja se para cuando llega, pero tu script no sabe que ha llegado. Pide que te
avisen:
[source,lua]
----
function onNavArrived(agent)
message = string.format("Arrived at post %d.", post)
end
----
Esto es un callback, la misma idea que `onOverlayUpdate` en la lección uno: tú
lo escribes y el motor lo llama. Este se ejecuta una vez, en el momento en que
un agente alcanza el destino que se le dio. No cuando está cerca, y no en cada
fotograma después -- una vez, al llegar.
Recibe el agente, porque un juego tiene más de uno. Ese argumento es el número
que te devolvió `navAgentNew`, y compararlo es la forma de saber cuál de tus
personajes acaba de llegar adonde iba.
[#_when_there_is_no_path]
=== Cuando no hay camino
Pulsa el botón una quinta vez y el mensaje dice `No path to post 5.
Staying put.`, y la caja no se mueve.
El puesto cinco está en `{ 30, 0, 30 }`, que está veinte metros fuera de una
habitación de veinte metros de ancho. Allí no hay suelo transitable, no hay
ninguna ruta hasta él y un personaje no puede hacer nada sensato al respecto.
`navAgentMoveTo` lo dice devolviendo `false`, y deja el agente exactamente como
estaba.
Esta es la parte que los principiantes se saltan, y es la que muerde. Los juegos
de verdad piden caminatas imposibles constantemente, porque el destino suele ser
el jugador y el jugador se mete en todas partes: subido a un cajón, en mitad de
un salto, en el agua, detrás de una puerta que se cerró o al otro lado de un
hueco que la malla de navegación recortó por demasiado estrecho. Cada uno de
esos casos hace que un `navAgentMoveTo` perfectamente razonable devuelva
`false`.
Tu script decide qué significa eso. Un guardia que no puede alcanzarte debería
volver a patrullar, o caminar hasta el punto más cercano al que *sí* pueda
llegar y esperar allí, o gritar. Lo que no debe hacer es nada, mientras tu
código ignora en silencio un `false` devuelto y tú te preguntas durante una hora
por qué uno de cada veinte enemigos está congelado.
Hay otras dos llamadas que ayudan aquí, y merece la pena conocerlas antes de
necesitarlas. `navNearest` recibe cualquier punto y devuelve el sitio más
cercano de la malla, o `nil` si no hay nada cerca, que es como conviertes «donde
está el jugador» en «un sitio donde un personaje puede estar de pie de verdad».
Y `navPath` hace la misma pregunta que `navAgentMoveTo` sin mover a nadie:
devuelve las esquinas de la ruta como una tabla de puntos, o `nil` cuando no hay
ruta. Puedes medirla, dibujarla o decidir que el paseo es demasiado largo para
molestarse.
Dibujarla es el truco útil. Añade esto a `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
----
Ahora una línea amarilla va desde la caja hasta donde sea que esté yendo,
doblándose por la puerta y acortándose a medida que camina. Mándala al puesto
cinco y la línea no se dibuja, porque `navPath` devolvió `nil` y el `if` lo
detectó. Estás viendo la decisión que está tomando el agente, dibujada en el
suelo.
[#_a_second_walker]
=== Un segundo caminante
Un personaje que esquiva paredes es navegación. Dos personajes que esquivan las
paredes *y se esquivan entre sí* es lo que el motor te da de verdad, y cuesta
cuatro líneas más.
Añade un material azul junto al rojo, y luego esto debajo del primer agente:
[source,lua]
----
local wanderer = makeWalker(6, -6, wandererLook)
local wandererAgent = navAgentNew(nav, wanderer, AGENT_RADIUS, AGENT_HEIGHT, 1.4)
----
Mándalo a algún sitio para ponerlo en marcha, después de la cámara:
[source,lua]
----
navAgentMoveTo(wandererAgent, navRandomPoint(nav))
----
Y enseña a `onNavArrived` a distinguirlos:
[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` elige un sitio de la malla transitable -- cualquiera, nunca
dentro de una pared -- y devuelve tres números, que es exactamente lo que quiere
`navAgentMoveTo` a continuación. Como el callback salta al llegar y su respuesta
es otro viaje, la caja azul ahora vaga por la habitación mientras dure el juego,
con cuatro líneas y sin ningún estado.
Mira cómo se encuentran los dos en la puerta. Ninguno se para en seco y ninguno
atraviesa al otro: los dos se apartan un poco, con tiempo, y se cruzan. Eso son
los agentes esquivándose entre sí, y es la diferencia entre personajes y
muebles.
El script terminado es `learn/26-navigation.singe`.
[#_what_just_happened_22]
=== Qué acaba de pasar
[source,lua]
----
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
----
Empieza una malla de navegación, y los cuatro números son la descripción de la
persona para la que es: 0,8 metros de ancho, 1,8 metros de alto, capaz de
caminar por una pendiente de hasta cuarenta y cinco grados y capaz de subir un
escalón de hasta 0,3 metros.
Cada uno de ellos cambia la forma que sale. Un personaje más ancho pierde más
suelo por el margen alrededor de las paredes y puede encontrarse una puerta
cerrada. Uno más alto no puede pasar por debajo de una viga baja. Permite una
pendiente más empinada y una rampa se convierte en un camino; permite menos y se
convierte en un acantilado.
El radio también decide con qué detalle se mide el suelo, así que un radio muy
pequeño en un nivel muy grande tarda mucho en precalcularse. Merece la pena leer
la entrada de la referencia para `navNew` antes de elegir tus números.
[source,lua]
----
navAddNode(nav, ground)
navBuild(nav)
----
`navAddNode` le pasa geometría -- un nodo y todo lo que cuelga de él, tal como
está en el mundo en ese momento, así que coloca las cosas antes de añadirlas.
Una sola llamada se lleva un modelo cargado entero. Solo cuentan las formas de
verdad: una luz, un sonido o un cuerpo físico por sí solos no añaden nada.
`navBuild` hace el precálculo, y es la llamada lenta de esta lección: nada
que notes para una habitación como esta, una décima de segundo para
una catedral en un ordenador de sobremesa, varios segundos en una Raspberry Pi.
Hazlo mientras se ve una pantalla de título, no mientras el jugador espera.
Después, la geometría se tira y la malla queda cerrada. No puedes añadir más. Si
tu nivel cambia de forma -- se abre una puerta, se cae un puente -- precalculas
otra malla, y `navDelete` libera la anterior.
En una máquina lenta preferirías no precalcular nada, así que `navSave` escribe
la malla terminada en un archivo y `navLoad` la vuelve a leer, construida y
lista. Precalcula una vez en tu propia máquina, distribuye el archivo con el
juego y el jugador no espera nunca.
[source,lua]
----
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
Un agente es un nodo que el motor ha aceptado hacer caminar por ti. El radio y
la altura de aquí son lo que usa para mantenerse apartado de otros agentes, y el
último número es su velocidad máxima en metros por segundo -- 2,2 es un paso
ligero, 1,4 es un paseo tranquilo y 6 es un esprint.
A partir de aquí, no ajustes tú la posición de ese nodo. Dos cosas que mueven un
mismo nodo se pelean, y la pelea se ve como tirones. Si tu personaje necesita
que la física lo empuje además de caminar, hay una llamada para eso --
`navAgentSetPlayer` cede el manejo a un controlador de física -- y la entrada de
la referencia sobre ella explica cuándo te conviene.
[source,lua]
----
local ok = navAgentMoveTo(walkerAgent, x, y, z)
----
Fija el destino. No camina hasta allí; dice dónde está «allí», y el motor se
pone a ello en los fotogramas siguientes. Puedes volver a llamarla en cualquier
momento y el nuevo destino sustituye al anterior, que es como un guardia sigue a
un jugador que corre: vuelve a apuntarlo una o dos veces por segundo, no sesenta.
Y devuelve `true` o `false`, y tienes que mirarlo.
[source,lua]
----
navAgentStop(walkerAgent)
----
No se usa arriba, pero es la otra mitad de la pareja: olvida el destino y el
agente frena hasta quedarse quieto donde está. `onNavArrived` no salta, porque
no ha llegado. Úsala cuando la razón del viaje haya desaparecido.
[#_fast_is_not_the_point]
=== Lo rápido no es lo importante
Una llamada más, porque es la que convierte esto de una demostración en algo que
parece un juego.
[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` te dice cómo se está moviendo el agente ahora mismo: no
adónde va, sino a qué velocidad y en qué dirección está viajando de verdad en
este fotograma. Mira el número cuando la caja arranca y cuando se detiene.
Acelera suavemente y frena suavemente.
De ese número es del que cuelgas una animación. Un personaje cuyas piernas se
mueven a la velocidad a la que viaja de verdad parece vivo; uno cuyo ciclo de
paso va a un ritmo fijo mientras acelera parece que patina. Con un modelo de
verdad en lugar de la caja, compararías esa velocidad con un par de umbrales y
reproducirías una animación de reposo, de andar o de correr -- que es el tema de
la lección veintitrés, y el ejemplo de la referencia en `navAgentGetVelocity`
está escrito justo para eso.
Y eso es lo importante de verdad de esta lección. Un personaje que toma la línea
recta rápida y se restriega a lo largo de una pared es peor que uno que arranca
un segundo más tarde, gira como es debido, da la vuelta por la puerta y frena
al llegar. Nadie que mire un juego mide lo deprisa que un enemigo llegó hasta
él. Lo que nota, al instante y sin saber decir por qué, es cuando algo se mueve
de una forma en que no se movería un ser vivo.
[#_try_it_26]
=== Pruébalo
. *Cierra la puerta.* Cambia las dos primeras paredes para que el hueco entre
ellas sea de medio metro en lugar de tres: `addWall(-4, 0, 13, 0.6)` y
`addWall(6.5, 0, 7, 0.6)`. Ejecútalo y mira lo que muestra `navDraw` antes de
pulsar nada. Luego manda la caja al otro lado y mira qué hace.
. *Engorda a todo el mundo.* Pon `AGENT_RADIUS` a `2.0` y ejecútalo. La puerta
sigue teniendo tres metros de ancho y los triángulos azules han desaparecido
de ella. No hay nada roto; la malla ahora está respondiendo a otra pregunta.
. *Mándala al ratón.* Toma el punto que hay bajo el puntero del ratón con
`sceneUnproject`, ajústalo a la malla con `navNearest` y manda la caja allí
cuando se pulse el botón. Que `navNearest` devuelva `nil` es el caso que
tienes que tratar, y ocurre siempre que el puntero está fuera del suelo.
. *Añade cuatro caminantes más.* Pon las cuatro líneas del caminante azul en un
bucle `for` de la lección cuatro, guarda los agentes en una tabla y deja que
vaguen todos. Mira la puerta cuando tres de ellos la quieran a la vez.
. *Precalcúlala una vez.* Llama a `navSave(nav, "room.nav")` después de
`navBuild`, y luego sustituye `navNew`, las llamadas a `navAddNode` y
`navBuild` por un único `navLoad("room.nav", AGENT_RADIUS, AGENT_HEIGHT)`.
Ahora cambia la posición de una pared y vuelve a ejecutarlo. Lo que ves es la
trampa que viene con las mallas precalculadas, y merece la pena encontrársela
una vez a propósito.
[#_break_it_on_purpose_25]
=== Rómpelo a propósito
Precalcular cierra la malla. Intenta añadir una cosa más a toro pasado -- un
cajón que colocaste tarde, o una pared que se te olvidó -- poniendo esta línea
justo después de `navBuild(nav)`:
[source,lua]
----
navAddNode(nav, ground)
----
Singe se para antes de que arranque el juego, nombrando la línea de tu archivo
en la que está esa llamada:
----
68:navAddNode: The mesh is already built.
----
La forma de un error del motor es la línea, la llamada que protestó y la queja,
y esta queja significa lo que dice: `navBuild` consumió la geometría que se le
dio y ya no queda nada a lo que añadir.
El arreglo nunca es mover la llamada a `navAddNode`. Es darte cuenta de que
tienes geometría para dos niveles y quieres dos mallas, o de que precalculaste
demasiado pronto. Añádelo todo, luego construye, y construye lo último.
[#_what_you_learned_26]
=== Lo que has aprendido
* Dar pasos hacia un destino funciona en una habitación vacía y falla en cuanto
hay una pared, y ningún parche lo arregla.
* Una malla de navegación es un mapa simplificado del suelo donde un personaje
puede estar de pie, sin las paredes, los desniveles ni la holgura a su
alrededor.
* `navNew` describe el personaje para el que es la malla, `navAddNode` le pasa
la geometría del nivel y `navBuild` la precalcula. Después del precálculo no
se puede añadir nada más.
* `navSave` y `navLoad` guardan una malla precalculada en un archivo para que el
jugador nunca tenga que esperarla.
* `navDraw` te muestra la malla, y es lo primero que hay que mirar cuando un
personaje se porta de forma rara.
* `navAgentNew` le da un nodo al motor para que lo haga caminar; a partir de
ahí, no muevas tú ese nodo.
* `navAgentMoveTo` fija un destino y devuelve `false` cuando no hay forma de
llegar. Tratar eso es cosa tuya, y pasa a menudo.
* `onNavArrived` se llama una vez cuando un agente alcanza su destino, y se le
dice qué agente es.
* `navPath` responde a la misma pregunta sin mover a nadie, y `nil` significa
que no hay ruta.
* Los agentes se esquivan entre sí además de esquivar el nivel.
* `navAgentGetVelocity` es como un ciclo de paso se ajusta al suelo que se
recorre.
[#_next_time_26]
=== La próxima vez
Tus dos cajas ahora se pasean por una habitación en silencio. La lección
veintisiete trata de lo que pones detrás de ellas: música que se repite como es
debido y que va a compás del vídeo, y MIDI, que permite a un juego tocar un
instrumento -- o a un instrumento de verdad tocar el juego.