[#_lesson_25_particles] == Leçon 25 : Les particules image::learn/25-particles.png[La leçon terminée, 480] À la fin de cette leçon, un vaisseau traversera le bas de l'écran en traînant du feu derrière son réacteur, et un bouton le fera exploser en cent quarante étincelles tournoyantes qui rebondissent sur le sol. Vous n'écrirez pas une seule ligne qui dessine une étincelle. C'est toute l'idée de cette leçon. Vous décrivez le feu ; le moteur le fabrique. [#_why_you_do_not_draw_the_sparks] === Pourquoi vous ne dessinez pas les étincelles Vous pourriez faire autrement. Vous en savez assez : une table d'étincelles, comme à la leçon six, chacune avec un x, un y et une vitesse ; une boucle dans `onOverlayUpdate` qui les déplace toutes, les estompe, retire celles qui meurent et les dessine. Ça marche. Ce sont aussi deux cents lignes de comptabilité que chaque jeu réécrit, et ça tourne en Lua, une étincelle à la fois, soixante fois par seconde. Singe le fait pour vous à la place. Vous lui donnez une *recette* -- combien d'étincelles par seconde, combien de temps vit chacune, à quelle vitesse et dans quelle direction elles partent, ce qui les attire, à quoi elles ressemblent et vers quelle couleur elles s'estompent -- et il s'occupe lui-même des étincelles. Ce qui contient la recette et l'exécute s'appelle un *émetteur*. Un émetteur n'est pas une image, et ce n'est pas un objet de votre jeu. C'est la description d'une sorte de feu, rangée quelque part où vous pouvez la désigner, et il peut produire ce feu partout où vous le demandez. [#_start_with_something_that_moves] === Commencez par quelque chose qui bouge Les particules sont plus convaincantes quand elles sortent de quelque chose, alors commencez par un vaisseau. Créez un dossier, copiez-y le dossier `art` du livre, et mettez ceci dans `particles.singe` : [source,lua] ---- dofile("Singe/Framework.singe") local SHIP_WIDTH = 32 local SHIP_HEIGHT = 24 local SHIP_SPEED = 120 local shipSprite = spriteLoad(DIR .. "art/spaceship.png") local shipX = (overlayGetWidth() - SHIP_WIDTH) / 2 local shipY = overlayGetHeight() - 56 local movingLeft = false local movingRight = false local lastTicks = singeGetTicks() function onInputPressed(what) if what == SWITCH_LEFT then movingLeft = true elseif what == SWITCH_RIGHT then movingRight = true end end function onInputReleased(what) if what == SWITCH_LEFT then movingLeft = false elseif what == SWITCH_RIGHT then movingRight = false end end function onOverlayUpdate() local now = singeGetTicks() local seconds = (now - lastTicks) / 1000 lastTicks = now if movingLeft then shipX = shipX - SHIP_SPEED * seconds end if movingRight then shipX = shipX + SHIP_SPEED * seconds end overlayClear() spriteDraw(shipSprite, shipX, shipY) return OVERLAY_UPDATED end ---- Lancez-le avec `Singe -R particles` et pilotez avec les touches fléchées gauche et droite. Rien de nouveau ici : un sprite de la leçon neuf, les contacts de la leçon trois, et `singeGetTicks` pour que le vaisseau avance à la même vitesse quelle que soit la fréquence d'images. La seule ligne qui mérite d'être signalée est `dofile("Singe/Framework.singe")`. C'est elle qui définit `DIR`, le dossier où se trouve votre script, si bien que `DIR .. "art/spaceship.png"` trouve l'image quel que soit le dossier depuis lequel vous avez lancé Singe. [#_give_it_an_exhaust] === Donnez-lui un échappement Maintenant, le feu. Ajoutez ceci au-dessus des callbacks, après les variables du vaisseau : [source,lua] ---- local exhaust = emitterNew() emitterSetBlend(exhaust, PARTICLE_ADD) emitterSetRate(exhaust, 90) emitterSetLife(exhaust, 0.15, 0.40) emitterSetDirection(exhaust, 0, 1) emitterSetSpread(exhaust, 12) emitterSetSpeed(exhaust, 40, 90) emitterSetSize(exhaust, 7, 1) emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0) emitterSetMax(exhaust, 120) emitterStart(exhaust) ---- Et deux lignes dans `onOverlayUpdate`. La première va juste avant `overlayClear`, la seconde juste après `spriteDraw` : [source,lua] ---- emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2) ---- [source,lua] ---- emitterDraw(exhaust) ---- Enregistrez. Le vaisseau repose maintenant sur une courte langue de flamme jaune, et la flamme le suit quand vous le pilotez. [#_blow_it_up] === Faites-le exploser Un échappement tourne indéfiniment. Une explosion n'arrive qu'une fois, ce que le moteur traite comme une autre sorte de demande faite au même émetteur. D'abord, deux fichiers de plus et un nombre de plus. Ceux-ci vont à côté du sprite du vaisseau, en haut : [source,lua] ---- local sparkSprite = spriteLoad(DIR .. "art/star.png") local boomSound = soundLoad(DIR .. "art/boom.wav") ---- et celui-ci va à côté de `shipX` et `shipY`, vingt unités au-dessus du bas de l'écran : [source,lua] ---- local ground = overlayGetHeight() - 20 ---- Maintenant, la deuxième recette, sous la première : [source,lua] ---- local boom = emitterNew() emitterSetTexture(boom, sparkSprite) emitterSetBlend(boom, PARTICLE_ADD) emitterSetLife(boom, 0.4, 1.1) emitterSetSpeed(boom, 40, 220) emitterSetSpread(boom, 180) emitterSetGravity(boom, 0, 160) emitterSetDrag(boom, 0.9) emitterSetSize(boom, 10, 2, 0.4) emitterSetSpin(boom, -240, 240) emitterSetColor(boom, 255, 255, 220, 255, 255, 70, 20, 0) emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground) emitterSetTrail(boom, 4, 2) emitterSetMax(boom, 400) ---- Remarquez qu'il manque `emitterStart`. Celui-ci ne va jamais émettre en continu. Ajoutez une fonction pour le déclencher, au-dessus de `onInputPressed` : [source,lua] ---- function explode() if not flying then return end flying = false emitterStop(exhaust) emitterSetPosition(boom, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT / 2) emitterBurst(boom, 140) soundPlay(boomSound) timerAfter(RESPAWN_MS, function() flying = true emitterStart(exhaust) end) end ---- Il faut encore trois choses : `local RESPAWN_MS = 1400` avec les autres constantes, `local flying = true` avec les autres variables, et une branche dans `onInputPressed` : [source,lua] ---- elseif what == SWITCH_BUTTON1 then explode() ---- Enfin, le vaisseau ne doit pas être là pendant qu'il est en morceaux. Entourez le pilotage et le dessin de `onOverlayUpdate` d'un `if flying then ... end`, et ajoutez `emitterDraw(boom)` à côté de l'autre appel de dessin. Le script terminé est `learn/25-particles.singe`. Appuyez sur la *barre d'espace* -- le bouton un, par défaut -- et le vaisseau se disloque en un nuage d'étoiles qui culbutent, retombent en arc, heurtent le sol et rebondissent. [#_what_just_happened_21] === Ce qui vient de se passer Il y a énormément de nouveaux appels dans cette leçon, et ils ont tous la même forme : l'émetteur d'abord, puis ce que vous réglez. Prenez-les par groupes. [source,lua] ---- local exhaust = emitterNew() ---- Crée un émetteur et renvoie un *handle* : un nombre dont le moteur se sert pour le retrouver, exactement comme le nombre que vous donne `spriteLoad`. Sans argument, vous obtenez un émetteur en deux dimensions, qui vit dans les coordonnées de l'overlay et que l'on dessine depuis `onOverlayUpdate`. Donnez plutôt un nœud à `emitterNew` et vous en obtenez un en trois dimensions, qui vit dans la scène et se dessine tout seul ; tout le reste de cette leçon est pareil pour les deux. Un nouvel émetteur a déjà une recette complète -- cinquante particules par seconde, blanches, une ou deux secondes de vie, qui dérivent vers le haut de l'écran -- alors chaque appel qui suit, c'est vous qui n'êtes pas d'accord avec une valeur par défaut. L'entrée de la Référence pour `emitterNew` les donne toutes. [source,lua] ---- emitterSetRate(exhaust, 90) emitterSetLife(exhaust, 0.15, 0.40) ---- Quatre-vingt-dix particules par seconde, chacune vivant entre quinze centièmes et quatre dixièmes de seconde. Ces deux nombres décident ensemble de la quantité de feu : quatre-vingt-dix par seconde qui vivent un tiers de seconde, cela fait environ trente à l'écran à tout moment. Partout où un appel d'émetteur prend un minimum et un maximum, chaque particule reçoit sa propre valeur, tirée au hasard entre les deux. C'est de là que vient l'irrégularité. Un feu où chaque flamme vivrait exactement aussi longtemps que toutes les autres aurait l'air d'une machine. [source,lua] ---- emitterSetDirection(exhaust, 0, 1) emitterSetSpread(exhaust, 12) emitterSetSpeed(exhaust, 40, 90) ---- Dans quelle direction elles partent, quelle est la largeur de l'éventail, et à quelle vitesse. Le Y de l'overlay croît *vers le bas*, comme depuis la leçon un, donc `(0, 1)` va vers le bas de l'écran -- par l'arrière d'un vaisseau qui pointe vers le haut. `emitterSetSpread` est la moitié de l'angle de l'éventail : douze degrés font un jet serré, quatre-vingt-dix l'ouvrent sur la moitié du cercle, et cent quatre-vingts, c'est toutes les directions à la fois. [source,lua] ---- emitterSetSize(exhaust, 7, 1) emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0) ---- C'est grâce à ces deux-là que la flamme ressemble à une flamme. Une particule n'a pas une taille et une couleur ; elle a une taille et une couleur *à sa naissance* et d'autres *à sa mort*, et elle glisse régulièrement des unes aux autres pendant la durée de vie qu'elle a tirée. Ainsi, chaque étincelle commence large de sept unités de l'overlay et jaune pâle, et finit large d'une unité et rouge orangé, avec un alpha de zéro, c'est-à-dire invisible. Elle rétrécit et s'efface parce que vous avez dit où elle finit, pas parce que quelque chose l'a estompée. `emitterSetColor` prend huit nombres, ce qui fait beaucoup à lire : rouge, vert, bleu et alpha au départ, puis rouge, vert, bleu et alpha à la fin. [source,lua] ---- emitterSetBlend(exhaust, PARTICLE_ADD) ---- La façon dont une particule se combine avec ce qui est derrière elle. `PARTICLE_ALPHA` est la peinture normale, et c'est la valeur par défaut : servez-vous-en pour la fumée, la poussière, la pluie et les gravats. `PARTICLE_ADD` ajoute plutôt sa lumière à l'image, si bien que deux étincelles l'une sur l'autre brillent plus qu'une seule et que rien ne devient jamais plus sombre. Le feu, les étincelles, les lasers et la magie sont additifs. La fumée ne l'est pas, et une fumée dessinée en mode additif ressemble à de la vapeur éclairée de l'intérieur. [source,lua] ---- emitterSetMax(exhaust, 120) emitterStart(exhaust) ---- `emitterSetMax` est la taille de la réserve : combien de particules de cet émetteur peuvent exister à la fois. Quand la réserve est pleine, rien de nouveau ne naît tant que quelque chose n'est pas mort. Quatre-vingt-dix par seconde qui vivent jusqu'à quatre dixièmes de seconde demandent environ trente-six places, donc cent vingt, c'est confortable. Réglez-la avant de démarrer, parce que la changer ensuite jette toutes les particules vivantes. `emitterStart` ouvre le robinet. À partir de là, l'émetteur fabrique des particules à chaque image, que vous le dessiniez ou non, jusqu'à ce que `emitterStop` le referme. Notez ce que `emitterStop` ne fait *pas* : il arrête les nouvelles particules, et celles qui sont déjà en vie finissent leur vie et s'effacent normalement. C'est pour cela que l'échappement s'étiole au lieu de disparaître quand le vaisseau explose. Si vous voulez qu'elles disparaissent à l'instant même, `emitterClear` les tue. [source,lua] ---- emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2) ---- C'est ainsi qu'un émetteur suit quelque chose. Un émetteur en deux dimensions émet depuis un seul point, et ce point est là où vous l'avez mis en dernier, alors vous le réglez à chaque image d'après ce qui bouge : la position d'un sprite, le nœud d'un corps physique, la souris. Les nouvelles particules apparaissent au nouvel endroit ; celles qui sont déjà en l'air restent là où elles sont nées, ce qui est exactement ce qu'il faut pour un échappement, parce qu'un échappement, ça reste derrière. Un émetteur en trois dimensions est créé sur un nœud et suit ce nœud tout seul, sans aucun appel dans votre mise à jour. Et quand vous voulez *vraiment* que tout le nuage se déplace avec la chose -- un bouclier, une aura, le panache d'un propulseur vu de l'extérieur -- `emitterSetLocal` active cela. [source,lua] ---- emitterDraw(exhaust) ---- Les émetteurs en deux dimensions sont dessinés quand vous le demandez, une fois par image, depuis `onOverlayUpdate`. L'ordre de vos appels à `emitterDraw` est l'ordre dans lequel ils s'empilent. Oublier cet appel est l'erreur que tout le monde fait une fois : l'émetteur continue de tourner, continue de puiser dans sa réserve, et n'affiche rien. Maintenant, les appels propres à l'explosion. [source,lua] ---- emitterSetTexture(boom, sparkSprite) ---- Sans texture, chaque particule est une tache ronde et floue que le moteur dessine lui-même ; c'est ce qu'utilise l'échappement, et c'est ce qu'il faut pour une flamme. Avec une texture, chaque particule porte cette image -- ici `star.png`, huit pixels sur huit. L'émetteur garde sa propre copie des pixels, si bien que vous pouvez décharger le sprite ensuite et que les particules continuent de marcher. [source,lua] ---- emitterSetGravity(boom, 0, 160) emitterSetDrag(boom, 0.9) ---- La gravité est une attraction constante, en unités de l'overlay par seconde et par seconde, et elle est positive vers le bas pour la même raison que l'échappement part avec un Y positif. Le freinage, c'est l'inverse : il retire de la vitesse, un peu à chaque image, si bien que les étincelles partent fort puis restent suspendues. Des étincelles avec de la gravité et sans freinage retombent tout droit en pluie comme un feu d'artifice ; c'est le freinage qui leur donne l'air de traverser de l'air. [source,lua] ---- emitterSetSpin(boom, -240, 240) emitterSetSize(boom, 10, 2, 0.4) ---- La rotation se compte en degrés par seconde, et une plage qui passe par zéro vous donne des particules qui tournent dans chaque sens. Elle se voit sur une texture et ne fait absolument rien à la tache intégrée, qui est ronde. Le troisième nombre de `emitterSetSize` est la variation. Il multiplie les deux tailles de chaque particule par un même facteur aléatoire -- ici n'importe où entre six dixièmes et un et quatre dixièmes -- pour que les étincelles ne soient pas toutes la même étoile à la même taille. [source,lua] ---- emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground) emitterSetTrail(boom, 4, 2) ---- `COLLIDE_FLOOR` pose un sol plat et invisible à travers le monde, à la hauteur que vous indiquez, et les particules rebondissent dessus. Les deux nombres qui précèdent la hauteur sont la part de vitesse qui survit à un rebond et la part de vitesse latérale qui est gommée. Cela ne coûte rien, parce que c'est une seule comparaison par particule, et cela donne aux débris l'air d'avoir atterri quelque part plutôt que de traverser le monde en tombant. Dans un jeu en trois dimensions, `COLLIDE_SCENE` fait plutôt rebondir les particules sur la vraie géométrie, ce qui coûte beaucoup plus cher. Une traînée garde les quelques dernières positions d'une particule et trace à travers elles un ruban qui s'estompe. Quatre positions et deux unités de large font de chaque étoile un court trait lumineux. [source,lua] ---- emitterBurst(boom, 140) ---- Et voici l'autre façon de faire des particules. `emitterStart` est un robinet ; `emitterBurst` est une poignée lancée d'un coup. L'émetteur n'a pas besoin d'émettre en continu, et celui-ci ne le fait jamais. Tout le reste -- la recette, la réserve, la position -- marche de la même façon. Cette distinction vaut la peine d'être retenue. Tout ce qui est continu est un flux que vous démarrez et arrêtez : un échappement, de la fumée, de la pluie, une torche, une cascade. Tout ce qui arrive à un instant précis est une salve : une explosion, une éclaboussure, un pas dans la poussière, un impact de balle, un éclair de bouche. Bien des effets sont les deux, avec un émetteur ou deux : un obus éclate, puis fume. [#_how_many_is_too_many] === Combien, c'est trop La réponse honnête, c'est que les particules coûtent peu à déplacer et cher à regarder. Déplacer dix mille particules, c'est de l'arithmétique, et un ordinateur capable de faire tourner un jeu, quel qu'il soit, le fait sans s'en apercevoir. Ce qui coûte, c'est le dessin, et ce que coûte le dessin, c'est à peu près le nombre de pixels de l'écran que couvrent les particules, chevauchements compris. Deux cents étincelles de huit pixels de large couvrent moins d'écran qu'une seule bouffée de fumée qui le remplit. Voilà la règle : petites et nombreuses, c'est presque gratuit ; grandes et superposées, ça ne l'est pas. Le mélange additif est la sorte coûteuse, parce que rien de ce qu'il dessine ne peut être sauté -- chaque couche s'ajoute à ce qui est dessous, alors la machine les peint toutes. Une douzaine d'éclairs additifs plein écran les uns sur les autres font ramper un Raspberry Pi 4, alors que dix mille minuscules étincelles qui rebondissent ne le dérangent pas. Donc : quelques milliers de petites particules, il n'y a pas de quoi s'inquiéter. Avant d'aller au-delà, affichez le compte à l'écran et surveillez-le. [source,lua] ---- overlayPrint(2, 4, "Particles alive: " .. (emitterGetCount(exhaust) + emitterGetCount(boom))) ---- `emitterGetCount`, c'est ce qui vous permet de dimensionner une réserve honnêtement plutôt qu'au jugé. Si le nombre reste pile à votre `emitterSetMax`, votre réserve est pleine et le moteur refuse en silence de fabriquer des particules que vous avez demandées. S'il n'en approche jamais, vous réservez de la mémoire pour rien. [#_try_it_25] === Essayez . *Transformez l'échappement en fumée.* Changez `emitterSetBlend(exhaust, PARTICLE_ADD)` en `PARTICLE_ALPHA`, réglez la couleur sur un gris qui s'estompe jusqu'à un gris transparent, et faites-la grossir au lieu de rétrécir avec `emitterSetSize(exhaust, 3, 14)`. Trois lignes ont fait une tout autre machine. . *Enlevez le freinage de l'explosion.* Supprimez la ligne `emitterSetDrag` et comparez. Puis remettez le freinage et enlevez plutôt la gravité. L'un des deux ressemble à un feu d'artifice et l'autre à un accident. . *Élargissez l'échappement.* Mettez `emitterSetSpread(exhaust, 180)`. Le vaisseau n'a plus l'air d'avoir un réacteur. L'ouverture travaille plus que vous ne le pensez. . *Lâchez-en mille d'un coup.* Changez `emitterBurst(boom, 140)` en `emitterBurst(boom, 1000)` et regardez le compte à l'écran. Il n'atteindra pas mille. Trouvez pourquoi à partir de `emitterSetMax`, puis agrandissez la réserve et rendez les étincelles quatre fois plus grosses, et voyez ce que cela fait à la fréquence d'images. . *Déclenchez l'explosion là où le vaisseau n'est pas.* Mettez `emitterSetPosition(boom, 40, 60)` dans `explode` à la place de la position du vaisseau. Un émetteur est la description d'un feu, pas une chose qui vit quelque part, et vous pouvez faire éclater le même en dix endroits dans la même image. [#_break_it_on_purpose_24] === Cassez-le exprès Les constantes -- `PARTICLE_ADD`, `COLLIDE_FLOOR`, `OVERLAY_UPDATED` -- sont des noms que Singe a donnés à des nombres. Écrivez-en une un peu de travers et Lua ne se plaint pas, parce qu'en Lua un nom qui n'a jamais reçu de valeur n'est pas une erreur : c'est `nil`, la valeur qui veut dire « rien ici ». Changez la ligne de mélange de l'échappement pour un nom qui a l'air tout aussi plausible : [source,lua] ---- emitterSetBlend(exhaust, PARTICLE_ADDITIVE) ---- Singe s'arrête avant que le jeu démarre, en donnant la ligne de votre fichier où se trouve cet appel : ---- 22:emitterSetBlend: Argument 2 must be a number. ---- Cette erreur n'a pas la même forme que les erreurs Lua de la leçon huit, parce qu'elle vient du moteur et pas de Lua. C'est le numéro de ligne, puis l'appel qui a protesté, puis la plainte. L'argument deux de `emitterSetBlend` est le mode de mélange, et ce qui est arrivé, c'est `nil`, parce que `PARTICLE_ADDITIVE` n'est pas un nom que quoi que ce soit ait jamais défini. « Must be a number » veut presque toujours dire ceci : une constante mal orthographiée, ou une variable que vous pensiez avoir réglée. Le remède, c'est de vérifier le nom dans la Référence, qui l'écrit `PARTICLE_ADD`. [#_what_you_learned_25] === Ce que vous avez appris * Un émetteur contient la recette d'une sorte de particule et s'occupe lui-même des particules, si bien que votre script n'en dessine jamais une seule. * `emitterNew` sans argument crée un émetteur en deux dimensions, dans les coordonnées de l'overlay ; avec un nœud, il en crée un en trois dimensions, dans la scène. * Chaque émetteur commence avec une recette complète, et chaque appel `emitterSet...` en change une partie. * Partout où un appel prend un minimum et un maximum, chaque particule tire sa propre valeur au hasard entre les deux, et c'est de là que vient l'irrégularité. * La taille et la couleur se règlent à la naissance et à la mort, et la particule glisse de l'une à l'autre au cours de sa vie. * `emitterStart` émet en continu jusqu'à `emitterStop` ; `emitterBurst` lance une poignée d'un coup. Les effets continus sont des flux, les effets ponctuels sont des salves. * `emitterStop` laisse les particules vivantes finir ; `emitterClear` les tue tout de suite. * Un émetteur en deux dimensions se déplace avec `emitterSetPosition` à chaque image et se dessine avec `emitterDraw` à chaque image. Un émetteur en trois dimensions suit son nœud et se dessine tout seul. * Les particules coûtent ce qu'elles couvrent à l'écran, pas ce qu'elles coûtent à déplacer, et le mélange additif est la sorte coûteuse. * `emitterSetMax` plafonne la réserve, et `emitterGetCount` vous dit si vous en avez deviné la bonne taille. [#_next_time_25] === La prochaine fois L'explosion a déplacé cent quarante choses sans qu'on lui dise où aucune devait aller, mais c'étaient des étincelles, et les étincelles se moquent de ce qu'elles percutent. La leçon vingt-six parle de quelque chose qui ne s'en moque pas : un personnage qui doit aller d'ici à là-bas, avec un mur sur le chemin.