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

642 lines
25 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_lesson_26_characters_that_move_themselves]
== Leçon 26 : Des personnages qui se déplacent seuls
image::learn/26-navigation.png[La leçon terminée, 480]
Dans cette leçon, vous construisez une pièce coupée en deux par un mur, et vous
y mettez deux personnages. L'un, vous l'envoyez dans un coin d'un appui sur un
bouton. L'autre choisit un endroit au hasard, y marche, et quand il arrive en
choisit un autre, indéfiniment.
Aucun des deux ne se cogne dans le mur, et vous n'écrivez jamais une ligne qui
dit comment le contourner. C'est nouveau. Jusqu'ici, chaque leçon a déplacé
les choses là où vous leur disiez d'aller. C'est la première leçon où le jeu
trouve quelque chose tout seul.
[#_the_problem_with_walking_toward_something]
=== Le problème quand on marche vers quelque chose
Voici la façon évidente de faire marcher un personnage jusqu'à un point, et
c'est celle que tout le monde écrit en premier. Prenez la cible, soustrayez
l'endroit où vous êtes, et avancez un peu le long de la ligne qui les relie, à
chaque image, jusqu'à y arriver.
Ça marche à merveille dans une pièce vide.
Mettez un mur dans la pièce et ça ne marche plus, et ça ne marche plus d'une
façon qui a l'air bête plutôt que cassée : le personnage marche droit dans le
mur et reste là à frotter contre lui, à un mètre d'une porte qu'il ne voit
pas, parce que rien dans « avancer vers la cible » n'a la moindre idée que le
monde contient des formes.
Vous ne vous en sortirez pas à coups de rustines. Faire un écart quand on
touche un mur coince votre personnage dans le premier coin qu'il trouve. Et il
ne suffit pas non plus de savoir où sont les murs, parce que le chemin qui
contourne un mur peut être long, peut s'éloigner de la cible avant d'y
revenir, et peut ne pas exister du tout.
Ce qu'il faut, c'est un moyen de poser une vraie question : *comment aller à
pied d'ici à là-bas ?* Pour y répondre, il faut quelque chose qui connaît la
forme du sol.
[#_what_a_navigation_mesh_is]
=== Ce qu'est un maillage de navigation
Tenez-vous dans une pièce et regardez le sol. Sur une partie, vous pourriez
marcher. Sur une autre, non : elle est sous une table, dans un mur, au-delà du
bord d'un balcon, ou en haut d'une marche trop haute pour être gravie.
Imaginez maintenant que vous peignez la partie où vous pourriez marcher, et
que vous jetez tout le reste -- les murs, les meubles, le plafond, le décor. Il
vous reste une forme plate, ou plusieurs, avec des trous là où sont les
piliers et des vides là où sont les à-pics.
Cette forme peinte est un *maillage de navigation*, et c'est tout ce dont un
personnage a besoin pour prévoir un trajet. Ce n'est pas le niveau. C'est une
carte simplifiée des parties du niveau où une personne peut se tenir debout,
et parce qu'elle est simple, on peut répondre vite aux questions qui la
concernent.
Singe le fabrique pour vous à partir du niveau que vous avez déjà. Vous dites
quelle taille font les personnages, vous lui donnez la géométrie, et il
détermine ce qui est praticable : les sols et les rampes dans la limite de la
pente que vous autorisez, les marches pas plus hautes que vous ne l'autorisez,
avec une marge retirée autour de chaque mur et de chaque à-pic pour qu'un
personnage qui a une largeur n'accroche pas un coin. Cela s'appelle le
*précalcul*, ça se fait une fois, et le résultat peut être enregistré dans un
fichier et chargé la fois suivante.
[#_a_room_with_a_wall_in_it]
=== Une pièce avec un mur dedans
Créez un dossier appelé `navigation` et commencez dedans un fichier appelé
`navigation.singe`. Cette partie est une scène en trois dimensions,
le sujet de la leçon vingt-deux : une *scène* est un monde dessiné en trois
dimensions, et tout ce qu'elle contient est accroché à un *nœud*, c'est-à-dire
une position, une rotation et une échelle auxquelles vous pouvez attacher une
forme.
Si vous avez sauté cette leçon, vous pouvez quand même suivre celle-ci. Prenez
les deux blocs suivants pour « un sol gris de vingt unités de côté, trois murs
beiges posés dessus, et une caméra qui regarde le tout d'en haut ».
[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)
----
Les unités sont ici des mètres, parce que c'est ce que suppose le monde en
trois dimensions de Singe. Les deux premiers murs traversent le milieu de la
pièce avec un espace de trois mètres entre eux : une porte. Le troisième est
un bout de mur qui avance dans la moitié du fond, pour que les personnages
aient quelque chose à contourner une fois passés.
Ajoutez une lumière et une caméra pour qu'il y ait quelque chose à voir :
[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
----
Lancez-le avec `Singe -R navigation`. Un sol gris, un mur avec une ouverture,
et un bout de mur, vus d'en haut et de derrière.
[#_bake_the_floor]
=== Précalculez le sol
Six lignes font de cette pièce une carte :
[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)
----
Mettez-les après les murs et avant la lumière. Puis ajoutez une ligne à
`onOverlayUpdate`, sous `overlayClear`, pour voir ce que vous avez obtenu :
[source,lua]
----
navDraw(nav, 0, 170, 190)
----
Relancez-le. Une toile de triangles bleu-vert est posée sur le sol, et elle
s'arrête avant les murs. L'espace qu'elle laisse n'est pas décoratif : ce sont
les 0,4 mètre de marge dont un personnage d'un rayon de 0,4 mètre a besoin pour
que ses épaules ne traversent pas la pierre. La porte est toujours une porte
dans le maillage, parce que trois mètres, c'est assez large pour passer ;
réduisez l'ouverture à un demi-mètre et le bleu se refermerait dessus, et pour
tous les personnages de votre jeu, il n'y aurait plus de porte du tout.
Regardez-le un instant, parce que `navDraw` est l'outil que vous prendrez
chaque fois qu'un personnage fera quelque chose d'inexplicable. Neuf fois sur
dix, la réponse est sur le sol, sous vos yeux.
[#_somebody_to_walk_it]
=== Quelqu'un pour y marcher
Un personnage est un nœud. Son apparence ne regarde pas la navigation, alors
construisez la chose la plus simple qui montre de quel côté il est tourné : une
boîte avec un nez.
[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
----
Cela va avec `addWall`. Il lui faut une couleur, alors ajoutez-en une à côté
des autres matériaux :
[source,lua]
----
local walkerLook = materialNew()
materialSetColor(walkerLook, 225, 85, 60)
materialSetEmissive(walkerLook, 70, 18, 10)
----
Et maintenant, la ligne pour laquelle cette leçon existe. Après `navBuild` :
[source,lua]
----
local walker = makeWalker(-7, 7, walkerLook)
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
`navAgentNew` pose le nœud sur le maillage et renvoie un *agent* : le mot du
moteur pour quelque chose qu'il veut bien faire marcher pour vous. À partir de
ce moment, la position de ce nœud appartient au moteur. À chaque image, il le
déplace et le tourne face à la direction où il va, et il continuera ainsi
jusqu'à ce que vous lui disiez où aller ou que vous retiriez l'agent.
Lancez-le. Une boîte rouge avec un nez apparaît dans le coin le plus proche et
ne fait absolument rien, parce que vous n'avez encore rien demandé.
[#_sending_it_somewhere]
=== L'envoyer quelque part
Donnez-lui une liste d'endroits et un bouton.
[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
----
Affichez le message pour voir ce qui se passe. Dans `onOverlayUpdate`, après
`navDraw` :
[source,lua]
----
overlayPrint(2, 2, message)
----
Lancez-le et appuyez sur le bouton un -- la *barre d'espace*, par défaut. La
boîte se tourne, marche jusqu'au coin opposé de sa moitié de la pièce, et
s'arrête. Appuyez encore. Elle longe le mur, trouve la porte, la franchit,
ressort de l'autre côté, et s'arrête dans le coin du fond. Appuyez une
troisième fois et elle contourne le bout de mur jusqu'au quatrième coin. Vous
ne lui avez pas parlé de la porte. Rien dans votre script ne
mentionne la porte. Elle l'a trouvée, parce que la porte est un trou dans une
forme et que contourner les trous, c'est à ça que sert le maillage.
Chaque entrée de `posts` est faite de trois nombres, un x, un y et un z, parce
qu'un point dans un monde en trois dimensions en demande trois. Y va vers le
haut, et les vôtres valent tous zéro parce que tout ici est au sol. La table
`posts` est une table de tables, comme à la leçon six, et `posts[post]` est
l'une des tables intérieures.
[#_knowing_when_it_arrives]
=== Savoir quand il arrive
La boîte s'arrête quand elle arrive, mais votre script ne sait pas qu'elle est
arrivée. Demandez à être prévenu :
[source,lua]
----
function onNavArrived(agent)
message = string.format("Arrived at post %d.", post)
end
----
C'est un callback, la même idée que `onOverlayUpdate` à la leçon un : vous
l'écrivez, le moteur l'appelle. Celui-ci s'exécute une fois, au moment où un
agent atteint la cible qu'on lui a donnée. Pas quand il en est près, et pas à
chaque image ensuite -- une fois, à l'arrivée.
Il reçoit l'agent, parce qu'un jeu en a plus d'un. Cet argument est le nombre
que `navAgentNew` a renvoyé, et c'est en le comparant que vous savez lequel de
vos personnages vient d'arriver là où il allait.
[#_when_there_is_no_path]
=== Quand il n'y a pas de chemin
Appuyez une cinquième fois sur le bouton et le message affiche `No path to post 5.
Staying put.`, et la boîte ne bouge pas.
Le poste cinq est en `{ 30, 0, 30 }`, soit vingt mètres en dehors d'une pièce de
vingt mètres de large. Il n'y a pas de sol praticable là-bas, aucun chemin pour
y aller, et rien de sensé qu'un personnage puisse y faire. `navAgentMoveTo` le
dit en renvoyant `false`, et laisse l'agent exactement comme il était.
C'est la partie que les débutants sautent, et c'est celle qui mord. Les vrais
jeux demandent sans cesse des trajets impossibles, parce que la cible est en
général le joueur et que le joueur va partout : debout sur une caisse, en
plein saut, dans l'eau, derrière une porte qui s'est fermée, ou de l'autre côté
d'un passage que le maillage de navigation a retiré parce qu'il était trop
étroit. Chacun de ces cas fait renvoyer `false` à un `navAgentMoveTo`
parfaitement raisonnable.
C'est votre script qui décide de ce que cela veut dire. Un garde qui ne peut
pas vous atteindre devrait reprendre sa ronde, ou marcher jusqu'au point le
plus proche qu'il *peut* atteindre et y attendre, ou crier. Ce qu'il ne doit
pas faire, c'est rien, pendant que votre code ignore en silence un `false`
renvoyé et que vous vous demandez pendant une heure pourquoi un ennemi sur
vingt reste figé.
Deux autres appels aident ici, et ils valent d'être connus avant que vous en
ayez besoin. `navNearest` prend n'importe quel point et renvoie l'endroit le
plus proche sur le maillage, ou `nil` s'il n'y a rien près de lui ; c'est ainsi
que vous transformez « là où est le joueur » en « un endroit où un personnage
peut vraiment se tenir ». Et `navPath` pose la même question que
`navAgentMoveTo` sans faire bouger personne : il renvoie les angles du trajet
sous forme de table de points, ou `nil` quand il n'y a pas de trajet. Vous
pouvez le mesurer, le dessiner, ou décider que le trajet est trop long pour
s'en donner la peine.
Le dessiner, c'est l'astuce utile. Ajoutez ceci à `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
----
Maintenant, une ligne jaune va de la boîte jusqu'à l'endroit où elle se rend,
en s'infléchissant par la porte et en raccourcissant à mesure qu'elle avance.
Envoyez-la au poste cinq et la ligne n'est pas dessinée du tout, parce que
`navPath` a renvoyé `nil` et que le `if` l'a intercepté. Vous regardez la
décision que prend l'agent, dessinée sur le sol.
[#_a_second_walker]
=== Un deuxième marcheur
Un personnage qui évite les murs, c'est de la navigation. Deux personnages qui
évitent les murs *et s'évitent entre eux*, c'est ce que le moteur vous donne
vraiment, et cela coûte quatre lignes de plus.
Ajoutez un matériau bleu à côté du rouge, puis ceci sous le premier agent :
[source,lua]
----
local wanderer = makeWalker(6, -6, wandererLook)
local wandererAgent = navAgentNew(nav, wanderer, AGENT_RADIUS, AGENT_HEIGHT, 1.4)
----
Envoyez-le quelque part pour le mettre en route, après la caméra :
[source,lua]
----
navAgentMoveTo(wandererAgent, navRandomPoint(nav))
----
Et apprenez à `onNavArrived` à distinguer les deux :
[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` choisit un endroit sur le maillage praticable -- n'importe où
dessus, jamais dans un mur -- et renvoie trois nombres, ce qui est exactement
ce que `navAgentMoveTo` attend ensuite. Comme le callback se déclenche à
l'arrivée et que sa réponse est un autre trajet, la boîte bleue erre
maintenant dans la pièce tant que le jeu tourne, avec quatre lignes et aucun
état.
Regardez-les se croiser dans la porte. Aucun ne s'arrête net et aucun ne passe
à travers l'autre : tous deux s'écartent un peu, à temps, et se croisent. Ce
sont les agents qui se contournent l'un l'autre, et c'est la différence entre
des personnages et des meubles.
Le script terminé est `learn/26-navigation.singe`.
[#_what_just_happened_22]
=== Ce qui vient de se passer
[source,lua]
----
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
----
Commence un maillage de navigation, et les quatre nombres décrivent la personne
à qui il est destiné : 0,8 mètre de large, 1,8 mètre de haut, capable de
monter une pente jusqu'à quarante-cinq degrés, et de gravir une marche jusqu'à
0,3 mètre.
Chacun de ces nombres change la forme qui en sort. Un personnage plus large
perd plus de sol dans la marge autour des murs et peut trouver une porte
fermée. Un plus grand ne peut pas passer sous une poutre basse. Autorisez une
pente plus raide et une rampe devient une route ; autorisez-en moins et elle
devient une falaise.
Le rayon décide aussi de la finesse avec laquelle le sol est mesuré, si bien
qu'un très petit rayon sur un très grand niveau prend longtemps à précalculer.
L'entrée de la Référence pour `navNew` mérite d'être lue avant de choisir vos
nombres.
[source,lua]
----
navAddNode(nav, ground)
navBuild(nav)
----
`navAddNode` transmet de la géométrie -- un nœud et tout ce qui est accroché en
dessous, tel qu'il se trouve dans le monde à cet instant, alors placez les
choses avant de les ajouter. Un seul appel prend tout un modèle chargé. Seules
comptent les vraies formes : une lumière, un son ou un corps physique seul
n'ajoute rien.
`navBuild` fait le précalcul, et c'est l'appel lent de cette leçon : rien que
vous remarqueriez pour une pièce comme celle-ci, un dixième de seconde pour
une cathédrale sur un ordinateur de bureau, plusieurs secondes sur un
Raspberry Pi. Faites-le pendant qu'un écran titre est affiché, pas pendant que
le joueur attend.
Ensuite, la géométrie est jetée et le maillage est fermé. Vous ne pouvez plus
rien ajouter. Si votre niveau change de forme -- une porte s'ouvre, un pont
s'effondre -- vous précalculez un autre maillage, et `navDelete` libère
l'ancien.
Sur une machine lente, vous préféreriez ne pas précalculer du tout, alors
`navSave` écrit le maillage terminé dans un fichier et `navLoad` le relit,
construit et prêt. Précalculez une fois sur votre propre machine, livrez le
fichier avec le jeu, et le joueur n'attend jamais.
[source,lua]
----
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
Un agent est un nœud que le moteur a accepté de faire marcher pour vous. Le
rayon et la hauteur servent ici à le tenir à l'écart des autres agents, et le
dernier nombre est sa vitesse maximale en mètres par seconde -- 2,2 est une
marche vive, 1,4 une flânerie, 6 un sprint.
À partir de là, ne réglez plus vous-même la position de ce nœud. Deux choses
qui déplacent un même nœud se battent, et la bagarre ressemble à des saccades.
Si votre personnage doit être bousculé par la physique en plus de marcher, il y
a un appel pour cela -- `navAgentSetPlayer` confie plutôt le pilotage à un
contrôleur physique -- et son entrée dans la Référence explique quand vous en
avez besoin.
[source,lua]
----
local ok = navAgentMoveTo(walkerAgent, x, y, z)
----
Règle la cible. Il n'y marche pas ; il dit où est « là-bas », et le moteur s'en
occupe au fil des images suivantes. Vous pouvez le rappeler à tout moment et la
nouvelle cible remplace l'ancienne ; c'est ainsi qu'un garde suit un joueur qui
court : réorientez-le une ou deux fois par seconde, pas soixante.
Et il renvoie `true` ou `false`, et vous devez regarder.
[source,lua]
----
navAgentStop(walkerAgent)
----
Pas utilisé plus haut, mais c'est l'autre moitié de la paire : il oublie la
cible, et l'agent ralentit jusqu'à s'arrêter là où il est. `onNavArrived` ne se
déclenche pas, puisqu'il n'est pas arrivé. Servez-vous-en quand la raison du
trajet a disparu.
[#_fast_is_not_the_point]
=== Aller vite n'est pas le but
Encore un appel, parce que c'est lui qui transforme cette démonstration en
quelque chose qui ressemble à un jeu.
[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` vous dit comment l'agent se déplace en ce moment : pas où
il va, mais à quelle vitesse et dans quelle direction il avance réellement à
cette image. Regardez le nombre quand la boîte se met en route et quand elle
s'arrête. Il monte en douceur et redescend en douceur.
C'est à ce nombre que vous accrochez une animation. Un personnage dont les
jambes bougent à la vitesse à laquelle il avance vraiment a l'air vivant ; un
personnage dont le cycle de marche tourne à un rythme fixe pendant qu'il
accélère a l'air de patiner. Avec un vrai modèle à la place de la boîte, vous
compareriez cette vitesse à deux ou trois seuils et joueriez une animation
d'attente, de marche ou de course -- c'est le sujet de la leçon vingt-trois, et
l'exemple de la Référence sous `navAgentGetVelocity` est écrit exactement pour
cela.
Et c'est là le vrai sujet de cette leçon. Un personnage qui prend la ligne
droite rapide et frotte le long d'un mur fait pire qu'un autre qui part une
seconde plus tard, se tourne correctement, fait le tour par la porte et ralentit
en arrivant. Personne, en regardant un jeu, ne mesure à quelle vitesse un
ennemi l'a atteint. On remarque, tout de suite et sans pouvoir dire pourquoi,
quand quelque chose bouge d'une façon dont un être vivant ne bougerait pas.
[#_try_it_26]
=== Essayez
. *Fermez la porte.* Changez les deux premiers murs pour que l'espace entre eux
fasse un demi-mètre au lieu de trois : `addWall(-4, 0, 13, 0.6)` et
`addWall(6.5, 0, 7, 0.6)`. Lancez-le et regardez ce que montre `navDraw`
avant d'appuyer sur quoi que ce soit. Puis envoyez la boîte de l'autre côté
et voyez ce qu'elle fait.
. *Rendez tout le monde gros.* Mettez `AGENT_RADIUS` à `2.0` et lancez-le. La
porte fait toujours trois mètres de large et les triangles bleus en ont
disparu. Rien n'est cassé ; le maillage répond maintenant à une autre
question.
. *Envoyez-la vers la souris.* Prenez le point sous le pointeur de la souris
avec `sceneUnproject`, ramenez-le sur le maillage avec `navNearest`, et
envoyez-y la boîte quand le bouton est enfoncé. `navNearest` qui renvoie
`nil`, c'est le cas que vous devez gérer, et il se produit chaque fois que le
pointeur n'est pas sur le sol.
. *Ajoutez quatre autres promeneurs.* Mettez les quatre lignes du marcheur bleu
dans une boucle `for`, comme à la leçon quatre, gardez les agents dans une
table, et laissez-les tous errer. Regardez la porte quand trois d'entre eux
la veulent en même temps.
. *Précalculez-le une fois.* Appelez `navSave(nav, "room.nav")` après
`navBuild`, puis remplacez `navNew`, les appels à `navAddNode` et `navBuild`
par un seul `navLoad("room.nav", AGENT_RADIUS, AGENT_HEIGHT)`. Changez
maintenant la position d'un mur et relancez. Ce que vous voyez est le piège
qui va avec les maillages précalculés, et il vaut la peine de le rencontrer
une fois exprès.
[#_break_it_on_purpose_25]
=== Cassez-le exprès
Le précalcul ferme le maillage. Essayez d'ajouter une chose de plus après
coup -- une caisse placée trop tard, ou un mur oublié -- en mettant cette ligne
juste après `navBuild(nav)` :
[source,lua]
----
navAddNode(nav, ground)
----
Singe s'arrête avant que le jeu démarre, en donnant la ligne de votre
fichier où se trouve cet appel :
----
68:navAddNode: The mesh is already built.
----
Une erreur du moteur a toujours la même forme : la ligne, l'appel qui a
protesté, et la plainte, et cette plainte veut dire ce qu'elle dit : `navBuild`
a consommé la géométrie qu'on lui avait donnée et il n'y a plus rien à quoi
ajouter.
Le remède, ce n'est jamais de déplacer l'appel à `navAddNode`. C'est de
remarquer que vous avez la géométrie de deux niveaux et qu'il vous faut deux
maillages, ou que vous avez précalculé trop tôt. Ajoutez tout, puis
construisez, et construisez en dernier.
[#_what_you_learned_26]
=== Ce que vous avez appris
* Avancer vers une cible marche dans une pièce vide et échoue dès qu'il y a un
mur, et aucune rustine n'y change rien.
* Un maillage de navigation est une carte simplifiée du sol où un personnage
peut se tenir, dont on a retiré les murs, les à-pics et la marge autour
d'eux.
* `navNew` décrit le personnage auquel le maillage est destiné, `navAddNode`
transmet la géométrie du niveau, et `navBuild` la précalcule. Après le
précalcul, on ne peut plus rien ajouter.
* `navSave` et `navLoad` gardent un maillage précalculé dans un fichier pour
que le joueur ne l'attende jamais.
* `navDraw` vous montre le maillage, et c'est la première chose à regarder
quand un personnage se conduit bizarrement.
* `navAgentNew` confie un nœud au moteur pour qu'il le fasse marcher ; à partir
de là, ne déplacez plus ce nœud vous-même.
* `navAgentMoveTo` règle une cible et renvoie `false` quand il n'y a aucun
moyen d'y arriver. Gérer ce cas, c'est votre travail, et cela arrive souvent.
* `onNavArrived` est appelé une fois quand un agent atteint sa cible, et on lui
dit quel agent.
* `navPath` répond à la même question sans faire bouger personne, et `nil` veut
dire pas de trajet.
* Les agents se contournent entre eux en plus de contourner le niveau.
* `navAgentGetVelocity` permet d'accorder un cycle de marche au terrain
parcouru.
[#_next_time_26]
=== La prochaine fois
Vos deux boîtes se promènent maintenant dans une pièce en silence. La leçon
vingt-sept parle de ce que vous mettez derrière elles : de la musique qui
boucle proprement et reste au pas de la vidéo, et le MIDI, qui permet à un jeu
de jouer d'un instrument -- ou à un vrai instrument de jouer au jeu.