619 lines
24 KiB
Text
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.
|