570 lines
22 KiB
Text
570 lines
22 KiB
Text
[#_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.
|