[#_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.