564 lines
23 KiB
Text
564 lines
23 KiB
Text
[#_lesson_24_physics]
|
||
== Leçon 24 : La physique
|
||
|
||
image::learn/24-physics.png[La leçon terminée, 480]
|
||
|
||
Jusqu'ici, tout ce qui bougeait dans vos jeux bougeait parce que vous faisiez
|
||
l'arithmétique. Un nombre augmentait de deux à chaque image ; s'il dépassait un
|
||
bord, vous le faisiez repartir dans l'autre sens. C'est la façon honnête de
|
||
faire bouger un jeu, et la plupart des jeux sont faits de ça.
|
||
|
||
Cette leçon confie l'arithmétique à quelqu'un d'autre. Un *moteur physique*
|
||
simule le monde -- la gravité, les chutes, les glissades, les empilements, les
|
||
rebonds, les roulades, et les choses qui se cognent -- et déplace vos nœuds à
|
||
votre place. À la fin, vous aurez un plateau de caisses et un canon qui les
|
||
renverse. La dernière section explique quand ne rien faire de tout ça, et c'est
|
||
la partie à lire deux fois.
|
||
|
||
[#_what_it_does_and_what_it_does_not]
|
||
=== Ce qu'elle fait, et ce qu'elle ne fait pas
|
||
|
||
Donnez un *corps* à un nœud et la simulation le prend en charge. À partir de
|
||
là, il tombe sous l'effet de la gravité, atterrit sur ce qui se trouve en
|
||
dessous, bascule quand on le frappe hors de son centre, repose dans une pile,
|
||
glisse jusqu'à s'arrêter, et le nœud suit partout où la simulation le met.
|
||
Vous arrêtez de régler sa position. C'est le marché : vous cédez la position et
|
||
la rotation de ce nœud, et en échange vous obtenez un comportement que vous
|
||
n'avez pas eu à écrire.
|
||
|
||
Ce qu'elle ne fait pas, c'est quoi que ce soit qui concerne votre jeu. Elle ne
|
||
sait pas ce qu'est un score, ni que la caisse rouge est celle qui rapporte des
|
||
points, ni que tomber du bord coûte une vie. Elle n'anime pas un personnage qui
|
||
marche, elle ne sait pas quelle sensation doit donner un saut, et elle ne fera
|
||
pas bouger un objet d'une façon que vous pouvez prévoir exactement. La physique
|
||
est une machine à produire un désordre plausible.
|
||
|
||
Les unités sont les mètres, les kilogrammes et les secondes. La gravité vaut
|
||
`0, -9.81, 0` sauf si vous en décidez autrement, ce qui est celle de la Terre,
|
||
vers le bas, en mètres par seconde au carré. Une caisse d'une unité de large
|
||
fait un mètre de large, et elle tombe comme telle. Si votre scène est
|
||
construite à une autre échelle, tout aura l'air de tomber dans du sirop ou
|
||
dans un dessin animé, et la solution est de construire en mètres.
|
||
|
||
[#_a_floor_and_one_crate]
|
||
=== Un sol et une caisse
|
||
|
||
Créez un dossier, créez `crates.singe`, et tapez ceci. C'est une scène, une
|
||
dalle, une caisse au-dessus de la dalle, et trois lignes de physique.
|
||
|
||
[source,lua]
|
||
----
|
||
sceneEnable(true)
|
||
sceneSetBackground(16, 18, 26)
|
||
sceneSetAmbient(40, 42, 50)
|
||
|
||
local stone = materialNew()
|
||
materialSetColor(stone, 110, 110, 120)
|
||
materialSetRoughness(stone, 0.9)
|
||
|
||
local wood = materialNew()
|
||
materialSetColor(wood, 190, 140, 80)
|
||
materialSetRoughness(wood, 0.8)
|
||
|
||
local floor = nodeNew()
|
||
nodeSetMesh(floor, meshBox(16, 0.4, 16), stone)
|
||
nodeSetPosition(floor, 0, -0.2, 0)
|
||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
||
|
||
local crate = nodeNew()
|
||
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood)
|
||
nodeSetPosition(crate, 0.3, 4, 0)
|
||
nodeSetRotation(crate, 20, 0, 15)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
||
bodySetMass(crate, 4)
|
||
|
||
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||
nodeSetPosition(sun, -5, 9, 6)
|
||
nodeLookAt(sun, 0, 0, 0)
|
||
lightSetIntensity(sun, 1.7)
|
||
lightSetShadow(sun, true)
|
||
|
||
local camera = nodeNew()
|
||
nodeSetPosition(camera, 0, 4, 11)
|
||
nodeLookAt(camera, 0, 1, 0)
|
||
cameraSet(camera)
|
||
|
||
|
||
function onOverlayUpdate()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Lancez-le avec `Singe -R crates`. La caisse tombe de quatre mètres de haut,
|
||
atterrit sur un coin parce que vous l'avez inclinée, bascule sur une face, et
|
||
s'arrête. Appuyez sur *F5* pour la relancer et regardez-la encore une fois.
|
||
|
||
Rien dans `onOverlayUpdate` ne déplace quoi que ce soit. Les deux appels à
|
||
`bodyNew` ont tout fait.
|
||
|
||
[#_what_just_happened_20]
|
||
=== Ce qui vient de se passer
|
||
|
||
[source,lua]
|
||
----
|
||
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
||
----
|
||
|
||
`bodyNew` donne un corps à un nœud. Elle prend le nœud, la sorte de corps dont
|
||
il s'agit, la forme que la simulation doit utiliser pour lui, et les
|
||
dimensions de cette forme.
|
||
|
||
Il y a trois sortes, et choisir la bonne, c'est l'essentiel du savoir-faire.
|
||
|
||
* `BODY_STATIC` ne bouge jamais. Il est placé là où se trouve le nœud quand
|
||
vous créez le corps, et il y reste pour toujours. Les sols, les murs, le
|
||
terrain, la borne, le niveau. Déplacer son nœud après coup ne déplace pas le
|
||
corps, et c'est un piège à retenir : statique veut dire statique.
|
||
* `BODY_DYNAMIC` est déplacé par la simulation. Il tombe, rebondit, glisse, se
|
||
fait pousser, et entraîne son nœud avec lui. C'est une caisse, un tonneau,
|
||
une balle, un débris.
|
||
* `BODY_KINEMATIC` va là où *vous* mettez son nœud, et écarte de son chemin les
|
||
corps dynamiques sans jamais être poussé lui-même. Une plateforme mobile, une
|
||
raquette, une porte sur un rail, le poing d'un personnage qui frappe. C'est
|
||
le pont entre votre arithmétique et la simulation : vous le pilotez, il les
|
||
pilote.
|
||
|
||
La forme n'est pas le maillage. C'est une doublure grossière que la simulation
|
||
utilise parce qu'elle peut répondre à des questions sur une boîte bien plus
|
||
vite que sur neuf cents triangles. `SHAPE_BOX` prend une largeur, une hauteur
|
||
et une profondeur ; `SHAPE_SPHERE` prend un rayon ; `SHAPE_CAPSULE` et
|
||
`SHAPE_CYLINDER` prennent un rayon et une hauteur. Il y a `SHAPE_HULL`, qui
|
||
enveloppe au plus près le maillage qui se trouve sous le nœud -- la réponse
|
||
habituelle pour un modèle -- et `SHAPE_MESH`, les triangles exacts, que le
|
||
décor statique peut utiliser et qu'un objet qui tombe ne peut pas utiliser.
|
||
|
||
C'est à vous de donner les bonnes dimensions. Rien ne vérifie que le corps que
|
||
vous avez décrit correspond au maillage que vous avez dessiné, et une caisse
|
||
dessinée à `0.8` avec un corps créé à `2` flottera au-dessus du sol et
|
||
renversera des choses à distance. Quand quelque chose se comporte comme s'il
|
||
avait une autre taille, c'est qu'il l'a.
|
||
|
||
[source,lua]
|
||
----
|
||
bodySetMass(crate, 4)
|
||
----
|
||
|
||
La masse d'un corps dynamique vient de sa forme, à la densité de l'eau, ce qui
|
||
rend les choses bien plus lourdes que vous ne l'attendez : une boîte de la
|
||
taille d'une porte pèse environ cent quarante kilogrammes, et rien de ce que
|
||
vous pourrez lui lancer ne la fera bouger. Réglez la masse de tout ce qui doit
|
||
paraître léger. Quatre kilogrammes, c'est une caisse qu'on peut soulever.
|
||
|
||
La masse compte parce que les forces se répartissent selon elle. La même
|
||
poussée envoie plus loin une chose légère, et une impulsion égale à la masse
|
||
multipliée par la vitesse est ce qu'il faut pour mettre quelque chose en
|
||
mouvement à cette vitesse. Réglez d'abord la masse et les forces ensuite,
|
||
sinon vous tournerez en rond.
|
||
|
||
[#_making_things_move]
|
||
=== Faire bouger les choses
|
||
|
||
Trois appels poussent un corps dynamique, et ils ne sont pas interchangeables.
|
||
|
||
`bodySetVelocity(node, x, y, z)` dit « tu te déplaces maintenant à cette
|
||
vitesse », en remplaçant ce que le corps faisait. C'est un service, un lancement, une
|
||
apparition avec déjà un peu de vitesse.
|
||
|
||
`bodyApplyImpulse(node, ix, iy, iz)` est un coup : un coup de pied, une
|
||
explosion, une batte qui rencontre une balle. Elle s'ajoute à ce que le corps
|
||
faisait déjà. Donnez-lui un point du monde sous la forme de trois nombres de
|
||
plus et elle frappe *là* au lieu du milieu, si bien qu'une caisse frappée près
|
||
du haut bascule au lieu de glisser.
|
||
|
||
`bodyApplyForce(node, fx, fy, fz)` est une poussée qui dure : une fusée, un
|
||
ventilateur, un tapis roulant. Une force appliquée pendant une seule image ne
|
||
fait presque rien, ce qui surprend ceux qui s'attendaient à une impulsion.
|
||
|
||
La gravité elle-même, c'est `physicsSetGravity(x, y, z)`, et elle s'applique
|
||
au monde entier. La Lune, c'est `physicsSetGravity(0, -1.62, 0)`, l'espace, que
|
||
des zéros, et un jeu avec la gravité de côté est à une ligne de là.
|
||
|
||
Deux réglages de plus complètent le ressenti d'un objet.
|
||
`bodySetBounce(node, amount)` va de `0`, qui s'arrête net, à `1`, qui repart
|
||
avec tout ce qu'il avait en arrivant ; la valeur par défaut est un `0.1` à
|
||
peine perceptible. `bodySetFriction(node, amount)` vaut `0` pour la glace et
|
||
`0.5` par défaut. Les deux se combinent entre les deux choses qui se touchent,
|
||
donc un sol glacé demande le petit nombre sur le sol, pas sur tout ce qui
|
||
marche dessus.
|
||
|
||
[#_knowing_when_things_touch]
|
||
=== Savoir quand les choses se touchent
|
||
|
||
[source,lua]
|
||
----
|
||
function onCollision(nodeA, nodeB, x, y, z, speed)
|
||
if speed > 4 then
|
||
knocks = knocks + 1
|
||
end
|
||
end
|
||
----
|
||
|
||
Ici, `knocks` est une variable à vous, qui attend en haut du fichier avec
|
||
`local knocks = 0`, et le jouet plus bas l'affiche.
|
||
|
||
`onCollision` est un callback, comme `onOverlayUpdate` : vous l'écrivez, le
|
||
moteur l'appelle. Il arrive une fois pour chaque nouveau contact -- deux corps
|
||
qui restent en contact ne le signalent pas de nouveau -- avec les deux nœuds,
|
||
le point du monde où ils se sont rencontrés, et la vitesse à laquelle ils se
|
||
sont rencontrés. C'est la vitesse qui permet à un petit choc et à un fracas de
|
||
ne pas sonner pareil, et c'est pour ça que le test ci-dessus ignore le doux
|
||
tassement d'une pile.
|
||
|
||
L'autre moitié de la paire, c'est `onTrigger`. Faites d'un corps un
|
||
déclencheur avec `bodySetTrigger(node, true)` et il arrête de pousser les
|
||
choses : il devient une zone qui signale ce qui y entre et en sort, par
|
||
`onTrigger(trigger, other, entered)`. Une ligne d'arrivée, une porte, une
|
||
fosse, un objet à ramasser. C'est le descendant en 3D des tests de collision
|
||
de la leçon onze, et pour tout ce qui ressemble à « le joueur est-il arrivé
|
||
ici », c'est l'appel qu'il vous faut plutôt qu'un corps solide.
|
||
|
||
[#_the_toy]
|
||
=== Le jouet
|
||
|
||
Maintenant, le tout. Partez du script ci-dessus et transformez-le en ceci : un
|
||
plateau avec quatre murs, six caisses en pyramide, et une boule d'acier que
|
||
vous pouvez viser et tirer. Les morceaux sont tous des choses que vous avez
|
||
déjà rencontrées.
|
||
|
||
Tout ce dont le nouveau code se souvient va en haut du fichier, au-dessus de
|
||
la scène :
|
||
|
||
[source,lua]
|
||
----
|
||
local crates = {}
|
||
local balls = {}
|
||
local aimX = 0
|
||
local aimLeft = false
|
||
local aimRight = false
|
||
local knocks = 0
|
||
local showShapes = false
|
||
----
|
||
|
||
Deux handles partagés de plus vont avec les matériaux : un métal poli pour la
|
||
boule et le repère de visée, et un seul maillage de caisse pour les six
|
||
caisses.
|
||
|
||
[source,lua]
|
||
----
|
||
local steel = materialNew()
|
||
materialSetColor(steel, 200, 205, 215)
|
||
materialSetMetallic(steel, 1)
|
||
materialSetRoughness(steel, 0.25)
|
||
|
||
local crateMesh = meshBox(0.8, 0.8, 0.8)
|
||
----
|
||
|
||
Les murs du plateau sont quatre boîtes statiques, créées dans une boucle à
|
||
partir d'une table d'emplacements, et c'est la leçon six qui fait un travail
|
||
honnête :
|
||
|
||
[source,lua]
|
||
----
|
||
local wallMesh = meshBox(16, 1, 0.4)
|
||
|
||
for _, side in ipairs({ { 0, -8, 0 }, { 0, 8, 0 }, { -8, 0, 90 }, { 8, 0, 90 } }) do
|
||
local wall = nodeNew()
|
||
nodeSetMesh(wall, wallMesh, stone)
|
||
nodeSetPosition(wall, side[1], 0.5, side[2])
|
||
nodeSetRotation(wall, 0, side[3], 0)
|
||
bodyNew(wall, BODY_STATIC, SHAPE_BOX, 16, 1, 0.4)
|
||
end
|
||
----
|
||
|
||
Chaque entrée est un X, un Z et une rotation autour de Y. Deux murs sont
|
||
couchés en travers, au fond et devant ; deux sont tournés de quatre-vingt-dix
|
||
degrés pour faire les côtés. Un seul maillage sert aux quatre, et le corps est
|
||
créé après que le nœud a été placé et tourné, parce qu'un corps statique prend
|
||
la position et la rotation du nœud au moment où il est créé.
|
||
|
||
Les caisses viennent d'une fonction, pour que six caisses fassent six lignes :
|
||
|
||
[source,lua]
|
||
----
|
||
local function crateAt(x, y)
|
||
local crate = nodeNew()
|
||
|
||
nodeSetMesh(crate, crateMesh, wood)
|
||
nodeSetPosition(crate, x, y, -2)
|
||
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
||
bodySetMass(crate, 4)
|
||
crates[#crates + 1] = crate
|
||
end
|
||
----
|
||
|
||
Elle garde chaque caisse qu'elle crée dans la table `crates`, et c'est comme
|
||
ça que le callback de dessin peut plus tard leur poser une question à toutes.
|
||
Six appels construisent la pyramide, trois sur le sol, deux sur celles-ci, et
|
||
une au sommet :
|
||
|
||
[source,lua]
|
||
----
|
||
crateAt(-0.9, 0.4)
|
||
crateAt(0, 0.4)
|
||
crateAt(0.9, 0.4)
|
||
crateAt(-0.45, 1.25)
|
||
crateAt(0.45, 1.25)
|
||
crateAt(0, 2.1)
|
||
----
|
||
|
||
Les rangées du haut se trouvent à un doigt au-dessus de celles du dessous et
|
||
tombent en place dès que le jeu démarre. Laisser un petit écart est plus
|
||
facile que d'empiler exactement, et c'est plus gentil pour la simulation que
|
||
de lui donner une pile déjà tassée.
|
||
|
||
Tirer crée une nouvelle boule à chaque fois :
|
||
|
||
[source,lua]
|
||
----
|
||
local function fire()
|
||
local ball = nodeNew()
|
||
|
||
nodeSetMesh(ball, meshSphere(0.35, 24), steel)
|
||
nodeSetPosition(ball, aimX, 1.2, 6)
|
||
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
|
||
bodySetMass(ball, 3)
|
||
bodySetBounce(ball, 0.3)
|
||
bodySetVelocity(ball, 0, 2, -14)
|
||
balls[#balls + 1] = ball
|
||
if #balls > 5 then
|
||
nodeDelete(balls[1])
|
||
table.remove(balls, 1)
|
||
end
|
||
end
|
||
----
|
||
|
||
Regardez l'ordre. Le nœud est créé, reçoit une forme à dessiner, est *placé*,
|
||
et ce n'est qu'ensuite qu'il reçoit un corps. Un corps dynamique commence là où
|
||
se trouve son nœud, et après ça la circulation se fait dans l'autre sens : le
|
||
corps déplace le nœud. Régler vous-même ensuite la position d'un nœud
|
||
dynamique, c'est se battre contre la simulation, et la simulation gagne. Quand
|
||
quelque chose doit être déplacé, donnez-lui une vitesse, frappez-le avec une
|
||
impulsion, ou faites comme ici -- créez-en un nouveau et laissez partir
|
||
l'ancien.
|
||
|
||
Laisser partir l'ancien, c'est `nodeDelete`, qui libère le nœud, tout ce qui se
|
||
trouve sous lui, et le corps qu'il portait. Sans ces trois lignes, chaque tir
|
||
resterait dans le monde pour toujours, et quelques centaines de boules plus
|
||
tard, la simulation ferait énormément de travail sur des choses que personne
|
||
ne voit. Faire le ménage fait partie de l'usage de la physique.
|
||
|
||
Les commandes et l'affichage forment le reste :
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON1 then
|
||
fire()
|
||
elseif what == SWITCH_BUTTON3 then
|
||
showShapes = not showShapes
|
||
if showShapes then
|
||
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
|
||
else
|
||
physicsSetDebug(DEBUG_NONE)
|
||
end
|
||
elseif what == SWITCH_LEFT then
|
||
aimLeft = true
|
||
elseif what == SWITCH_RIGHT then
|
||
aimRight = true
|
||
end
|
||
end
|
||
----
|
||
|
||
Espace tire, Maj allume et éteint la vue de la physique, et les flèches,
|
||
tenues enfoncées, font glisser le point de lancement. Une plaque de métal
|
||
plate sans corps, déplacée à chaque image en `aimX`, montre d'où partira la
|
||
prochaine boule. Le script complet, avec le callback de relâchement et le reste
|
||
de la mise en place, est `24-physics.singe` dans le dossier `learn`.
|
||
|
||
`physicsSetDebug` est l'appel le plus utile de cette leçon. Il dessine ce que
|
||
la simulation contient réellement -- les vraies formes en fil de fer, et une
|
||
croix rouge partout où deux corps se sont touchés pendant ce pas de la
|
||
simulation -- par-dessus votre scène. La première fois qu'une caisse flotte à
|
||
une main au-dessus du sol, ou passe au travers, ou renverse quelque chose à
|
||
deux mètres de distance, allumez-le et la réponse sera à l'écran. Ajoutez
|
||
`DEBUG_STATIC` pour voir aussi le sol et les murs. Il ne coûte rien tant qu'il
|
||
est éteint.
|
||
|
||
Enfin, le comptage :
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
local over = 0
|
||
local settled = true
|
||
|
||
if aimLeft then
|
||
aimX = math.max(aimX - 0.08, -5)
|
||
end
|
||
if aimRight then
|
||
aimX = math.min(aimX + 0.08, 5)
|
||
end
|
||
nodeSetPosition(marker, aimX, 0.05, 6)
|
||
|
||
for _, crate in ipairs(crates) do
|
||
local pitch, _, roll = nodeGetRotation(crate)
|
||
|
||
if math.abs(pitch) > 30 or math.abs(roll) > 30 then
|
||
over = over + 1
|
||
end
|
||
if not bodyIsResting(crate) then
|
||
settled = false
|
||
end
|
||
end
|
||
|
||
overlayClear()
|
||
overlayPrint(2, 2, "Arrows aim. Space fires. Shift shows the shapes.")
|
||
overlayPrint(2, 4, "knocked over " .. over .. " of " .. #crates .. " hard hits " .. knocks)
|
||
if settled then
|
||
overlayPrint(2, 6, "everything has settled")
|
||
end
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Une caisse compte comme renversée quand elle penche de plus de trente degrés,
|
||
ce qui est une règle de jeu écrite en une ligne d'arithmétique par-dessus ce
|
||
que la simulation a fait. `nodeGetRotation` donne trois angles et ce code veut
|
||
le premier et le troisième, donc celui du milieu est attrapé dans une variable
|
||
appelée `_`. Ce tiret bas est un nom ordinaire qui n'a rien de spécial, utilisé
|
||
par convention pour dire « celui-là, je l'ignore ».
|
||
|
||
`bodyIsResting` dit si la simulation a mis un corps en sommeil parce qu'il a
|
||
cessé de bouger. C'est comme ça que vous savez qu'un tir est terminé : pas
|
||
avec une minuterie, pas au jugé, mais quand toutes les caisses dorment. Un jeu
|
||
de bowling compte les points de son tour exactement à ce moment-là.
|
||
|
||
[#_when_not_to_use_it]
|
||
=== Quand ne pas s'en servir
|
||
|
||
La physique est l'outil le plus tentant de n'importe quel moteur, et celui qui
|
||
gâche le plus de week-ends. Trois choses sont vraies à son sujet, et aucune
|
||
n'est la faute du moteur.
|
||
|
||
*Elle coûte cher.* Chaque corps dynamique demande du travail à chaque pas,
|
||
soixante fois par seconde, que quelqu'un le regarde ou non. Quarante caisses,
|
||
ça va. Quatre mille, non, et sur une petite machine, quatre cents non plus.
|
||
|
||
*Elle est imprévisible.* Lancez votre jouet deux fois et les caisses
|
||
atterrissent autrement. C'est ce qui lui donne l'air vivant, et c'est aussi
|
||
pour ça qu'un jeu ne peut pas compter sur elle pour quoi que ce soit qui doit
|
||
se passer pareil à chaque fois. Une porte qui doit s'ouvrir, une plateforme
|
||
qui doit arriver, un saut que le joueur doit pouvoir réussir : tout ça, c'est
|
||
de l'arithmétique, ou un tween, ou un corps cinématique sur un trajet que vous
|
||
avez écrit. Ne confiez jamais une chose que le joueur doit faire à une
|
||
simulation qui peut la laisser tomber derrière une caisse.
|
||
|
||
*Elle est difficile à régler.* La masse, le frottement, le rebond, la forme et
|
||
la taille tirent tous les uns sur les autres, et « ça ne fait pas le bon
|
||
effet » a rarement une seule cause. Chaque heure que vous passez à donner un
|
||
bon ressenti à une caisse physique est une heure que vous n'avez pas passée
|
||
sur le jeu.
|
||
|
||
Sortez-la donc quand le désordre *est* le but : des choses qui basculent, des
|
||
débris qui volent, une pile qui se tasse, une boule qui cogne partout dans une
|
||
pièce, un véhicule qui roule sur un terrain accidenté, un ragdoll qui dégringole
|
||
un escalier. Personne ne peut écrire ça à la main, et personne ne peut vous
|
||
dire exactement à quoi ça doit ressembler.
|
||
|
||
Ne la sortez pas quand vous savez ce qui doit se passer. Une balle qui vole en
|
||
ligne droite, c'est deux lignes de la leçon deux. Une plateforme qui monte et
|
||
descend, c'est `math.sin`. Un joueur qui marche et saute comme les joueurs s'y
|
||
attendent demande un contrôleur de personnage (`playerNew`, dans la
|
||
référence), parce qu'une personne n'est pas une boîte et ne se comporte jamais
|
||
comme une boîte. Un ennemi qui patrouille demande un trajet. Un menu qui glisse
|
||
à l'écran demande un tween.
|
||
|
||
La règle qui sert le mieux : la physique pour les choses que le joueur
|
||
regarde, et l'arithmétique pour les choses que le joueur fait.
|
||
|
||
[#_try_it_24]
|
||
=== Essayez
|
||
|
||
. *Changez le monde.* Ajoutez `physicsSetGravity(0, -1.62, 0)` vers le haut du
|
||
fichier et tirez de nouveau. Puis essayez `0, -30, 0`. Les mêmes caisses,
|
||
une autre planète, une ligne.
|
||
. *Changez la boule.* Rendez la boule dix fois plus lourde avec
|
||
`bodySetMass(ball, 30)`, puis dix fois plus légère avec `0.3`, en gardant la
|
||
même vitesse. Puis remettez la masse et changez plutôt le `-14`. Deux façons
|
||
de frapper plus fort, et elles ne donnent pas la même sensation.
|
||
. *Changez les caisses en glace.* Ajoutez `bodySetFriction(crate, 0.02)` dans
|
||
`crateAt`, et ajoutez une ligne qui donne aussi au sol un faible frottement.
|
||
Regardez la pile refuser de tenir debout dès le départ.
|
||
. *Faussez la forme exprès.* Dans `crateAt`, changez les dimensions du corps en
|
||
`2, 2, 2` pendant que le maillage reste à `0.8`. Lancez, regardez les caisses
|
||
flotter et se bousculer de nulle part, puis appuyez sur Maj et regardez la
|
||
vue de débogage l'expliquer en une seule image.
|
||
. *Comptez les points comme il faut.* Utilisez `bodyIsResting` sur chaque
|
||
caisse pour remarquer quand le tir est terminé, et seulement alors comptez
|
||
combien sont renversées et ajoutez-le à un score. C'est la partie d'un jeu de
|
||
bowling qui compte les points.
|
||
|
||
[#_break_it_on_purpose_23]
|
||
=== Cassez-le exprès
|
||
|
||
Ajoutez sous le `bodyNew` du sol une ligne qui rend le sol lourd, ce qu'il est
|
||
raisonnable de croire que vous voulez :
|
||
|
||
[source,lua]
|
||
----
|
||
bodySetMass(floor, 200)
|
||
----
|
||
|
||
Singe arrête le jeu avec quelque chose comme :
|
||
|
||
----
|
||
32:bodySetMass: Node 1 is not a dynamic body, or the mass is not positive.
|
||
----
|
||
|
||
Le premier nombre est la ligne dans votre propre fichier, donc le vôtre sera
|
||
différent.
|
||
|
||
Le nombre après `Node` est le handle du nœud, le nombre que `nodeNew` a
|
||
renvoyé. La plainte est exacte : la masse appartient aux choses que la
|
||
simulation déplace, et un corps statique n'est déplacé par rien, donc il n'a
|
||
pas de masse à régler. Le demander est le signe que le corps n'est pas du bon
|
||
type pour ce que vous aviez en tête -- et si vous voulez vraiment un sol lourd
|
||
qu'on puisse déplacer, ce qu'il vous faut, c'est un corps dynamique avec une
|
||
grande masse, ou un corps cinématique que vous déplacez vous-même.
|
||
|
||
Le même message apparaît pour une masse de `0`, que l'on écrit en pensant
|
||
« sans poids ». « Sans poids » n'est pas une masse. C'est un monde sans
|
||
gravité, ou `playerSetGravityScale` sur un contrôleur de personnage.
|
||
|
||
[#_what_you_learned_24]
|
||
=== Ce que vous avez appris
|
||
|
||
* Un corps s'attache à un nœud, et à partir de là, le nœud et le corps ne font
|
||
qu'un.
|
||
* Un corps statique ne bouge jamais, un corps dynamique est déplacé par la
|
||
simulation, et un corps cinématique est déplacé par vous et pousse tout le
|
||
reste de côté.
|
||
* La forme de collision est une doublure du maillage, et c'est à vous de faire
|
||
en sorte que ses dimensions correspondent à ce que vous avez dessiné.
|
||
* Par défaut, la masse est le volume de la forme à la densité de l'eau, donc
|
||
réglez-la sur tout ce qui doit paraître léger.
|
||
* `bodySetVelocity` remplace le mouvement d'un corps, `bodyApplyImpulse` est un
|
||
coup, et `bodyApplyForce` est une poussée qui dure.
|
||
* Ne réglez pas la position d'un nœud dynamique ; supprimez-le et créez-en un
|
||
nouveau quand vous devez le faire repartir de zéro.
|
||
* `onCollision` signale chaque nouveau contact avec son point et sa vitesse, et
|
||
un corps déclencheur signale les arrivées et les départs par `onTrigger`.
|
||
* `bodyIsResting` vous dit quand tout s'est arrêté, ce qui est en général le
|
||
moment où un tour est fini.
|
||
* `physicsSetDebug` dessine la vue de la simulation elle-même, et elle répond à
|
||
la plupart des questions de physique plus vite que la lecture du code.
|
||
* La physique coûte cher, ne se répète pas, et demande beaucoup de réglages.
|
||
Servez-vous-en pour le désordre, pas pour les choses dont le jeu dépend.
|
||
|
||
[#_next_time_24]
|
||
=== La prochaine fois
|
||
|
||
Des caisses qui tombent, c'est une sorte de spectacle. L'autre sorte est faite
|
||
de mille petites choses qui vivent une demi-seconde : des étincelles, de la
|
||
fumée, du feu, de la pluie et de la poussière. La prochaine leçon parle des
|
||
émetteurs, et elle finit par une explosion. C'est la leçon vingt-cinq.
|