[#_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.