625 lines
24 KiB
Text
625 lines
24 KiB
Text
[#_lesson_26_characters_that_move_themselves]
|
|
== Lezione 26: Personaggi che si muovono da soli
|
|
|
|
image::learn/26-navigation.png[La lezione finita, 480]
|
|
|
|
In questa lezione costruisci una stanza con un muro che la taglia a metà e ci
|
|
metti dentro due personaggi. Uno lo mandi in un angolo premendo un pulsante.
|
|
L'altro sceglie un posto a caso, ci va, e quando arriva ne sceglie un altro,
|
|
all'infinito.
|
|
|
|
Nessuno dei due va a sbattere contro il muro, e tu non scrivi mai una riga che
|
|
dica come aggirarlo. Questa è una novità. Ogni lezione fino a questa ha mosso
|
|
le cose dove dicevi tu. Questa è la prima lezione in cui il gioco capisce
|
|
qualcosa da solo.
|
|
|
|
[#_the_problem_with_walking_toward_something]
|
|
=== Il problema di camminare verso qualcosa
|
|
|
|
Ecco il modo ovvio per far camminare un personaggio verso un punto, ed è il
|
|
modo in cui tutti lo scrivono la prima volta. Prendi la destinazione, sottrai
|
|
dove sei e fai un passettino lungo la linea tra le due, a ogni fotogramma,
|
|
finché non ci arrivi.
|
|
|
|
In una stanza vuota funziona benissimo.
|
|
|
|
Metti un muro nella stanza e smette di funzionare, e smette in un modo che
|
|
sembra stupido più che rotto: il personaggio marcia contro il muro e resta lì a
|
|
strusciarcisi contro, a un metro da una porta che non riesce a vedere, perché
|
|
niente in «fai un passo verso la destinazione» ha la minima idea che nel mondo
|
|
ci siano delle forme.
|
|
|
|
Non puoi uscirne a forza di toppe. Girare di lato quando tocchi un muro fa
|
|
incastrare il personaggio nel primo angolo che trova. E non basta nemmeno
|
|
sapere dove sono i muri, perché la strada attorno a un muro può essere lunga,
|
|
può allontanarsi dalla destinazione prima di tornare indietro, e può anche non
|
|
esistere affatto.
|
|
|
|
Quello che serve è un modo per fare una vera domanda: *come cammino da qui a
|
|
lì?* Per rispondere serve qualcosa che conosca la forma del pavimento.
|
|
|
|
[#_what_a_navigation_mesh_is]
|
|
=== Che cos'è una navmesh
|
|
|
|
Mettiti in piedi in una stanza e guarda il pavimento. Su una parte potresti
|
|
camminare. Su un'altra no: è sotto un tavolo, dentro un muro, oltre il bordo di
|
|
un balcone, o in cima a un gradino troppo alto da salire. Ora immagina di
|
|
dipingere la parte su cui potresti camminare e di buttare via tutto il resto --
|
|
i muri, i mobili, il soffitto, lo scenario. Quello che ti resta è una forma
|
|
piatta, o più d'una, con dei buchi dove ci sono i pilastri e dei vuoti dove ci
|
|
sono i dislivelli.
|
|
|
|
Quella forma dipinta è una *navmesh* (mesh di navigazione), ed è tutto ciò che
|
|
serve a un personaggio per pianificare una camminata. Non è il livello. È una
|
|
mappa semplificata delle parti del livello su cui una persona può stare in
|
|
piedi, e siccome è semplice, alle domande su di essa si risponde in fretta.
|
|
|
|
Singe la crea per te dal livello che hai già. Gli dici quanto sono grandi i
|
|
personaggi, gli consegni la geometria, e lui calcola che cosa è percorribile:
|
|
pavimenti e rampe entro la pendenza che consenti, gradini non più alti di
|
|
quanto consenti, con un margine tolto attorno a ogni muro e a ogni dislivello,
|
|
così un personaggio che ha una sua larghezza non attraversa gli spigoli. Questo
|
|
si chiama *precalcolo*, succede una volta sola, e il risultato si può salvare
|
|
in un file e caricare la volta dopo.
|
|
|
|
[#_a_room_with_a_wall_in_it]
|
|
=== Una stanza con un muro
|
|
|
|
Crea una cartella chiamata `navigation` e dentro comincia un file chiamato
|
|
`navigation.singe`. Questa parte è una scena tridimensionale, che è
|
|
l'argomento della ventiduesima lezione: una *scena* è un mondo disegnato in tre
|
|
dimensioni, e tutto ciò che contiene è appeso a un *nodo*, cioè una posizione,
|
|
una rotazione e una scala a cui puoi attaccare una forma.
|
|
|
|
Se hai saltato quella lezione, puoi seguire lo stesso questa. Prendi i prossimi
|
|
due blocchi come «un pavimento grigio di venti unità per venti, tre muri color
|
|
sabbia piantati sopra e una telecamera che guarda giù su tutto quanto».
|
|
|
|
[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)
|
|
----
|
|
|
|
Qui le unità sono metri, perché è quello che presuppone il mondo
|
|
tridimensionale di Singe. I primi due muri attraversano la stanza a metà con un
|
|
varco di tre metri tra loro: una porta. Il terzo è un moncone che sporge nella
|
|
metà lontana, per dare ai personaggi qualcosa da aggirare una volta passati.
|
|
|
|
Aggiungi una luce e una telecamera, così c'è qualcosa da vedere:
|
|
|
|
[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
|
|
----
|
|
|
|
Eseguilo con `Singe -R navigation`. Un pavimento grigio, un muro con un varco
|
|
e un moncone, visti dall'alto e da dietro.
|
|
|
|
[#_bake_the_floor]
|
|
=== Precalcola il pavimento
|
|
|
|
Sei righe trasformano quella stanza in una mappa:
|
|
|
|
[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)
|
|
----
|
|
|
|
Mettile dopo i muri e prima della luce. Poi aggiungi una riga a
|
|
`onOverlayUpdate`, sotto `overlayClear`, così puoi vedere che cosa hai
|
|
ottenuto:
|
|
|
|
[source,lua]
|
|
----
|
|
navDraw(nav, 0, 170, 190)
|
|
----
|
|
|
|
Eseguilo di nuovo. Una ragnatela di triangoli verde-azzurri è stesa sul
|
|
pavimento, e si ferma prima dei muri. Lo spazio che lascia non è una
|
|
decorazione: è il margine di 0,4 metri che serve a un personaggio con un raggio
|
|
di 0,4 metri perché le sue spalle non passino attraverso la pietra. La porta è
|
|
ancora una porta nella mesh, perché tre metri bastano per passarci; fai il
|
|
varco di mezzo metro e il blu si chiuderebbe sopra, e per tutti i personaggi
|
|
del tuo gioco lì non ci sarebbe proprio nessuna porta.
|
|
|
|
Guardala per un momento, perché `navDraw` è lo strumento che prenderai in mano
|
|
ogni volta che un personaggio fa qualcosa di inspiegabile. Nove volte su dieci
|
|
la risposta è sul pavimento davanti a te.
|
|
|
|
[#_somebody_to_walk_it]
|
|
=== Qualcuno che ci cammini
|
|
|
|
Un personaggio è un nodo. Che aspetto abbia non è affare della navigazione,
|
|
quindi costruisci la cosa più economica che mostri da che parte è rivolto: una
|
|
scatola con un naso.
|
|
|
|
[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
|
|
----
|
|
|
|
Questa va insieme a `addWall`. Le serve un colore, quindi aggiungine uno
|
|
accanto agli altri materiali:
|
|
|
|
[source,lua]
|
|
----
|
|
local walkerLook = materialNew()
|
|
materialSetColor(walkerLook, 225, 85, 60)
|
|
materialSetEmissive(walkerLook, 70, 18, 10)
|
|
----
|
|
|
|
E adesso la riga per cui esiste questa lezione. Dopo `navBuild`:
|
|
|
|
[source,lua]
|
|
----
|
|
local walker = makeWalker(-7, 7, walkerLook)
|
|
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
|
|
----
|
|
|
|
`navAgentNew` mette il nodo sulla mesh e restituisce un *agente*: la parola del
|
|
motore per una cosa che è disposto a far camminare al posto tuo. Da questo
|
|
momento la posizione di quel nodo è del motore. A ogni fotogramma lo sposta e
|
|
lo gira verso la direzione in cui va, e continuerà a farlo finché non gli dici
|
|
dove andare o non gli togli l'agente.
|
|
|
|
Eseguilo. Una scatola rossa con un naso compare nell'angolo vicino e non fa
|
|
assolutamente niente, perché non le hai ancora chiesto niente.
|
|
|
|
[#_sending_it_somewhere]
|
|
=== Mandarlo da qualche parte
|
|
|
|
Dagli un elenco di posti e un pulsante.
|
|
|
|
[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
|
|
----
|
|
|
|
Stampa il messaggio, così vedi che cosa sta succedendo. In `onOverlayUpdate`,
|
|
dopo `navDraw`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 2, message)
|
|
----
|
|
|
|
Eseguilo e premi il pulsante uno -- la *barra spaziatrice*, se non hai cambiato
|
|
niente. La scatola si gira, cammina fino all'angolo lontano della sua metà
|
|
della stanza e si ferma. Premi di nuovo. Cammina lungo il muro, trova la porta,
|
|
la attraversa, esce dall'altra parte e si ferma nell'angolo lontano. Premi una
|
|
terza volta e aggira il moncone fino al quarto angolo. Non le hai detto della
|
|
porta. Niente nel tuo script parla della porta.
|
|
L'ha trovata lei, perché la porta è un buco in una forma, e aggirare i buchi è
|
|
proprio ciò a cui serve la mesh.
|
|
|
|
Ogni voce di `posts` è fatta di tre numeri, una x, una y e una z, perché un
|
|
punto in un mondo tridimensionale ne vuole tre. La Y è in alto, e le tue sono
|
|
tutte a zero perché qui tutto sta per terra. La tabella `posts` è una tabella
|
|
di tabelle, come nella sesta lezione, e `posts[post]` è una di quelle interne.
|
|
|
|
[#_knowing_when_it_arrives]
|
|
=== Sapere quando arriva
|
|
|
|
La scatola si ferma quando arriva, ma il tuo script non sa che è arrivata.
|
|
Chiedi di essere avvisato:
|
|
|
|
[source,lua]
|
|
----
|
|
function onNavArrived(agent)
|
|
message = string.format("Arrived at post %d.", post)
|
|
end
|
|
----
|
|
|
|
Questa è una callback, la stessa idea di `onOverlayUpdate` nella prima lezione:
|
|
tu la scrivi, il motore la chiama. Questa viene eseguita una volta sola, nel
|
|
momento in cui un agente raggiunge la destinazione che gli è stata data. Non
|
|
quando è vicino, e non a ogni fotogramma dopo -- una volta, all'arrivo.
|
|
|
|
Le viene passato l'agente, perché un gioco ne ha più di uno. Quell'argomento è
|
|
il numero che ti ha restituito `navAgentNew`, e confrontarlo è il modo per
|
|
capire quale dei tuoi personaggi è appena arrivato dove stava andando.
|
|
|
|
[#_when_there_is_no_path]
|
|
=== Quando non c'è un percorso
|
|
|
|
Premi il pulsante una quinta volta e il messaggio dice `No path to post 5.
|
|
Staying put.`, e la scatola non si muove.
|
|
|
|
La postazione cinque è a `{ 30, 0, 30 }`, cioè venti metri fuori da una stanza
|
|
larga venti metri. Lì non c'è pavimento percorribile, non c'è una strada per
|
|
arrivarci e non c'è niente di sensato che un personaggio possa farci.
|
|
`navAgentMoveTo` lo dice restituendo `false`, e lascia l'agente esattamente
|
|
com'era.
|
|
|
|
Questa è la parte che i principianti saltano, ed è quella che morde. I giochi
|
|
veri chiedono di continuo camminate impossibili, perché la destinazione di
|
|
solito è il giocatore, e il giocatore va dappertutto: in piedi su una cassa, a
|
|
metà di un salto, in acqua, dietro una porta che si è chiusa, o in un varco che
|
|
la navmesh ha tolto perché troppo stretto. Ognuno di questi casi fa restituire
|
|
`false` a un `navAgentMoveTo` perfettamente ragionevole.
|
|
|
|
Che cosa voglia dire lo decide il tuo script. Una guardia che non riesce a
|
|
raggiungerti dovrebbe tornare a fare la ronda, o camminare fino al punto più
|
|
vicino che *può* raggiungere e aspettare lì, o gridare. Quello che non deve
|
|
fare è niente, mentre il tuo codice ignora in silenzio un `false` restituito e
|
|
tu ti chiedi per un'ora perché un nemico su venti è congelato.
|
|
|
|
Qui aiutano altre due chiamate, e vale la pena conoscerle prima che ti
|
|
servano. `navNearest` prende un punto qualsiasi e restituisce il posto più
|
|
vicino sulla mesh, o `nil` se lì vicino non c'è niente, ed è così che trasformi
|
|
«dov'è il giocatore» in «un posto dove un personaggio può davvero stare in
|
|
piedi». E `navPath` fa la stessa domanda di `navAgentMoveTo` senza muovere
|
|
nessuno: restituisce gli angoli del percorso come una tabella di punti, o `nil`
|
|
quando un percorso non c'è. Puoi misurarlo, disegnarlo o decidere che la
|
|
camminata è troppo lunga per prendersi il disturbo.
|
|
|
|
Disegnarlo è il trucco utile. Aggiungi questo 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
|
|
----
|
|
|
|
Ora una linea gialla va dalla scatola a dovunque stia andando, piegandosi
|
|
attraverso la porta e accorciandosi mentre cammina. Mandala alla postazione
|
|
cinque e la linea non viene proprio disegnata, perché `navPath` ha restituito
|
|
`nil` e l'istruzione `if` l'ha intercettato. Stai guardando la decisione che sta prendendo
|
|
l'agente, disegnata sul pavimento.
|
|
|
|
[#_a_second_walker]
|
|
=== Un secondo camminatore
|
|
|
|
Un personaggio che evita i muri è navigazione. Due personaggi che evitano i
|
|
muri *e si evitano a vicenda* sono quello che il motore ti dà davvero, e
|
|
costano quattro righe in più.
|
|
|
|
Aggiungi un materiale blu accanto a quello rosso, poi questo sotto il primo
|
|
agente:
|
|
|
|
[source,lua]
|
|
----
|
|
local wanderer = makeWalker(6, -6, wandererLook)
|
|
local wandererAgent = navAgentNew(nav, wanderer, AGENT_RADIUS, AGENT_HEIGHT, 1.4)
|
|
----
|
|
|
|
Mandalo da qualche parte per farlo partire, dopo la telecamera:
|
|
|
|
[source,lua]
|
|
----
|
|
navAgentMoveTo(wandererAgent, navRandomPoint(nav))
|
|
----
|
|
|
|
E insegna a `onNavArrived` a distinguerli:
|
|
|
|
[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` sceglie un posto sulla mesh percorribile -- uno qualsiasi, mai
|
|
dentro un muro -- e restituisce tre numeri, che sono esattamente quello che
|
|
`navAgentMoveTo` vuole subito dopo. Siccome la callback scatta all'arrivo e la
|
|
sua risposta è un altro viaggio, adesso la scatola blu vaga per la stanza
|
|
finché il gioco resta acceso, con quattro righe e nessuno stato.
|
|
|
|
Guarda i due incontrarsi nella porta. Nessuno si pianta di colpo e nessuno
|
|
passa attraverso l'altro: tutti e due si scostano un po', per tempo, e si
|
|
incrociano. Sono gli agenti che si girano attorno a vicenda, ed è la
|
|
differenza tra personaggi e mobili.
|
|
|
|
Lo script finito è `learn/26-navigation.singe`.
|
|
|
|
[#_what_just_happened_22]
|
|
=== Cos'è appena successo
|
|
|
|
[source,lua]
|
|
----
|
|
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
|
|
----
|
|
|
|
Avvia una navmesh, e i quattro numeri sono la descrizione della persona a cui
|
|
è destinata: larga 0,8 metri, alta 1,8 metri, capace di camminare su una
|
|
pendenza fino a quarantacinque gradi e di salire un gradino fino a 0,3 metri.
|
|
|
|
Ognuno di questi cambia la forma che ne esce. Un personaggio più largo perde
|
|
più pavimento nel margine attorno ai muri e può trovare chiusa una porta. Uno
|
|
più alto non passa sotto una trave bassa. Consenti una pendenza più ripida e
|
|
una rampa diventa una strada; consentine meno e diventa una scogliera.
|
|
|
|
Il raggio decide anche con quanta precisione viene misurato il pavimento,
|
|
quindi un raggio molto piccolo su un livello molto grande richiede molto tempo
|
|
di precalcolo. Prima di scegliere i tuoi numeri vale la pena leggere la voce
|
|
del Riferimento per `navNew`.
|
|
|
|
[source,lua]
|
|
----
|
|
navAddNode(nav, ground)
|
|
navBuild(nav)
|
|
----
|
|
|
|
`navAddNode` consegna la geometria -- un nodo e tutto ciò che gli sta appeso
|
|
sotto, così com'è nel mondo in quel momento, quindi sistema le cose prima di
|
|
aggiungerle. Una chiamata prende un intero modello caricato. Contano solo le
|
|
forme vere: una luce, un suono o un corpo fisico da soli non aggiungono niente.
|
|
|
|
`navBuild` fa il precalcolo, ed è la chiamata lenta di questa lezione: niente
|
|
che noteresti per una stanza come questa, un decimo di secondo per una
|
|
cattedrale su un computer desktop, parecchi secondi su un Raspberry Pi. Fallo
|
|
mentre è visibile una schermata del titolo, non mentre il giocatore aspetta.
|
|
|
|
Dopo, la geometria viene buttata via e la mesh è chiusa. Non puoi aggiungere
|
|
altro. Se il tuo livello cambia forma -- si apre una porta, crolla un ponte --
|
|
precalcoli un'altra mesh, e `navDelete` libera quella vecchia.
|
|
|
|
Su una macchina lenta preferiresti non precalcolare affatto, quindi `navSave`
|
|
scrive la mesh finita in un file e `navLoad` la rilegge, costruita e pronta.
|
|
Precalcola una volta sulla tua macchina, distribuisci il file con il gioco, e
|
|
il giocatore non aspetta mai.
|
|
|
|
[source,lua]
|
|
----
|
|
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
|
|
----
|
|
|
|
Un agente è un nodo che il motore ha accettato di far camminare al posto tuo.
|
|
Il raggio e l'altezza qui sono quelli che usa per stare alla larga dagli altri
|
|
agenti, e l'ultimo numero è la sua velocità massima in metri al secondo -- 2,2
|
|
è un passo svelto, 1,4 è una passeggiata, 6 è uno scatto.
|
|
|
|
Da qui in poi, non impostare tu la posizione di quel nodo. Due cose che
|
|
muovono lo stesso nodo si fanno la guerra, e la guerra si vede come un
|
|
movimento a scatti. Se il tuo personaggio deve essere spintonato dalla fisica
|
|
oltre che camminare, c'è una chiamata apposta -- `navAgentSetPlayer` passa
|
|
invece la guida a un controller della fisica -- e la voce del riferimento che
|
|
la riguarda spiega quando ti serve.
|
|
|
|
[source,lua]
|
|
----
|
|
local ok = navAgentMoveTo(walkerAgent, x, y, z)
|
|
----
|
|
|
|
Imposta la destinazione. Non ci cammina; dice dov'è il «lì», e il motore se ne
|
|
occupa nei fotogrammi successivi. Puoi richiamarla in qualsiasi momento e la
|
|
nuova destinazione sostituisce quella vecchia, ed è così che una guardia
|
|
insegue un giocatore che corre: la ripunti una o due volte al secondo, non
|
|
sessanta.
|
|
|
|
E restituisce `true` o `false`, e tu devi guardare.
|
|
|
|
[source,lua]
|
|
----
|
|
navAgentStop(walkerAgent)
|
|
----
|
|
|
|
Sopra non è usata, ma è l'altra metà della coppia: dimentica la destinazione,
|
|
e l'agente rallenta fino a fermarsi dov'è. `onNavArrived` non scatta, perché
|
|
non è arrivato. Usala quando il motivo del viaggio è venuto meno.
|
|
|
|
[#_fast_is_not_the_point]
|
|
=== Il punto non è la velocità
|
|
|
|
Un'altra chiamata, perché è quella che trasforma tutto questo da una
|
|
dimostrazione in qualcosa che sembra un gioco.
|
|
|
|
[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` ti dice come si sta muovendo l'agente in questo momento:
|
|
non dove sta andando, ma quanto veloce e in che direzione si sta davvero
|
|
spostando in questo fotogramma. Guarda il numero mentre la scatola parte e
|
|
mentre si ferma. Accelera dolcemente e rallenta dolcemente.
|
|
|
|
Quel numero è ciò a cui appendi un'animazione. Un personaggio le cui gambe si
|
|
muovono alla velocità a cui si sta davvero spostando sembra vivo; uno il cui
|
|
ciclo di camminata va a ritmo fisso mentre accelera sembra che stia
|
|
pattinando. Con un vero modello al posto della scatola confronteresti quella
|
|
velocità con un paio di soglie e riprodurresti un'animazione da fermo, una
|
|
camminata o una corsa -- che è l'argomento della ventitreesima lezione, e
|
|
l'esempio del riferimento sotto `navAgentGetVelocity` è scritto proprio per
|
|
quello.
|
|
|
|
Ed è questo il vero punto della lezione. Un personaggio che prende la linea
|
|
dritta e veloce e si struscia lungo un muro è peggio di uno che parte un
|
|
secondo dopo, si gira come si deve, passa dalla porta e rallenta quando arriva.
|
|
Nessuno che guarda un gioco misura quanto in fretta lo ha raggiunto un nemico.
|
|
Se ne accorge subito, senza saper dire perché, quando qualcosa si muove in un
|
|
modo in cui una cosa viva non si muoverebbe.
|
|
|
|
[#_try_it_26]
|
|
=== Prova tu
|
|
|
|
. *Chiudi la porta.* Cambia i primi due muri così che il varco tra loro sia di
|
|
mezzo metro invece che di tre: `addWall(-4, 0, 13, 0.6)` e
|
|
`addWall(6.5, 0, 7, 0.6)`. Eseguilo e guarda che cosa mostra `navDraw` prima
|
|
di premere qualsiasi cosa. Poi manda la scatola dall'altra parte e guarda che
|
|
cosa fa.
|
|
. *Fai ingrassare tutti.* Imposta `AGENT_RADIUS` a `2.0` ed eseguilo. La porta
|
|
è ancora larga tre metri e i triangoli blu lì sono spariti. Non c'è niente di
|
|
rotto; la mesh adesso sta rispondendo a una domanda diversa.
|
|
. *Mandala dove c'è il mouse.* Prendi il punto sotto il puntatore del mouse con
|
|
`sceneUnproject`, agganciale alla mesh con `navNearest` e manda lì la scatola
|
|
quando premi il pulsante. `navNearest` che restituisce `nil` è il caso che
|
|
devi gestire, e succede ogni volta che il puntatore è fuori dal pavimento.
|
|
. *Aggiungi altri quattro vagabondi.* Metti le quattro righe del camminatore
|
|
blu in un ciclo `for`, come nella quarta lezione, tieni gli agenti in una
|
|
tabella e lasciali vagare tutti. Guarda la porta quando tre di loro la vogliono
|
|
nello stesso momento.
|
|
. *Precalcolala una volta sola.* Chiama `navSave(nav, "room.nav")` dopo
|
|
`navBuild`, poi sostituisci `navNew`, le chiamate a `navAddNode` e `navBuild`
|
|
con un unico `navLoad("room.nav", AGENT_RADIUS, AGENT_HEIGHT)`. Ora cambia la
|
|
posizione di un muro ed eseguilo di nuovo. Quello che vedi è la trappola che
|
|
viene con le mesh precalcolate, e vale la pena incontrarla una volta apposta.
|
|
|
|
[#_break_it_on_purpose_25]
|
|
=== Rompilo apposta
|
|
|
|
Il precalcolo chiude la mesh. Prova ad aggiungere un'altra cosa a cose fatte --
|
|
una cassa che hai messo tardi, o un muro che avevi dimenticato -- mettendo
|
|
questa riga subito dopo `navBuild(nav)`:
|
|
|
|
[source,lua]
|
|
----
|
|
navAddNode(nav, ground)
|
|
----
|
|
|
|
Singe si ferma prima che il gioco parta, indicando la riga del tuo file in
|
|
cui si trova quella chiamata:
|
|
|
|
----
|
|
68:navAddNode: The mesh is already built.
|
|
----
|
|
|
|
La forma di un errore del motore è la riga, la chiamata che ha protestato e la
|
|
lamentela, e questa lamentela vuol dire quello che dice: `navBuild` ha
|
|
consumato la geometria che gli era stata data e non c'è più niente a cui
|
|
aggiungere.
|
|
|
|
La soluzione non è mai spostare la chiamata a `navAddNode`. È accorgerti che
|
|
hai la geometria di due livelli e vuoi due mesh, o che hai precalcolato troppo
|
|
presto. Aggiungi tutto, poi costruisci, e costruisci per ultimo.
|
|
|
|
[#_what_you_learned_26]
|
|
=== Cosa hai imparato
|
|
|
|
* Fare passi verso una destinazione funziona in una stanza vuota e fallisce
|
|
appena c'è un muro, e nessuna quantità di toppe lo sistema.
|
|
* Una navmesh è una mappa semplificata del pavimento su cui un personaggio può
|
|
stare in piedi, con i muri, i dislivelli e il margine attorno a loro tolti.
|
|
* `navNew` descrive il personaggio a cui è destinata la mesh, `navAddNode`
|
|
consegna la geometria del livello e `navBuild` la precalcola. Dopo il
|
|
precalcolo non si può aggiungere altro.
|
|
* `navSave` e `navLoad` tengono una mesh precalcolata in un file, così il
|
|
giocatore non deve mai aspettarla.
|
|
* `navDraw` ti mostra la mesh, ed è la prima cosa da guardare quando un
|
|
personaggio si comporta in modo strano.
|
|
* `navAgentNew` affida un nodo al motore perché lo faccia camminare; da quel
|
|
momento non spostare tu quel nodo.
|
|
* `navAgentMoveTo` imposta una destinazione e restituisce `false` quando non
|
|
c'è modo di arrivarci. Gestirlo è compito tuo, e succede spesso.
|
|
* `onNavArrived` viene chiamata una volta quando un agente raggiunge la sua
|
|
destinazione, e le viene detto quale agente.
|
|
* `navPath` risponde alla stessa domanda senza muovere nessuno, e `nil` vuol
|
|
dire nessun percorso.
|
|
* Gli agenti si girano attorno l'un l'altro oltre che attorno al livello.
|
|
* `navAgentGetVelocity` è il modo in cui un ciclo di camminata si accorda al
|
|
terreno percorso.
|
|
|
|
[#_next_time_26]
|
|
=== La prossima volta
|
|
|
|
Le tue due scatole adesso girano per una stanza in silenzio. La ventisettesima
|
|
lezione parla di quello che ci metti dietro: musica che va in loop come si deve
|
|
e resta a tempo con il video, e il MIDI, che permette a un gioco di suonare uno
|
|
strumento -- o a un vero strumento di suonare il gioco.
|