642 lines
25 KiB
Text
642 lines
25 KiB
Text
[#_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.
|