singe/docs/de/lessons/25-particles.adoc

518 lines
22 KiB
Text

[#_lesson_25_particles]
== Lektion 25: Partikel
image::learn/25-particles.png[Die fertige Lektion, 480]
Am Ende dieser Lektion fliegt ein Raumschiff über den unteren Rand des Bildschirms und zieht
Feuer aus seinem Triebwerk hinter sich her, und ein Knopf sprengt es in hundertvierzig wirbelnde
Funken, die vom Boden abprallen. Du schreibst keine einzige Zeile, die einen Funken zeichnet.
Das ist die ganze Idee dieser Lektion. Du beschreibst das Feuer; die Engine macht es.
[#_why_you_do_not_draw_the_sparks]
=== Warum du die Funken nicht zeichnest
Du könntest es auch andersherum machen. Du weißt genug: eine Tabelle von Funken aus Lektion
sechs, jeder mit einem x, einem y und einer Geschwindigkeit; eine Schleife in `onOverlayUpdate`,
die jeden einzelnen davon bewegt, ihn verblassen lässt, ihn wegwirft, wenn er stirbt, und ihn
zeichnet. Das funktioniert. Es sind aber auch zweihundert Zeilen Buchhaltung, die jedes Spiel neu
schreibt, und sie laufen in Lua, ein Funke nach dem anderen, sechzigmal pro Sekunde.
Singe macht das stattdessen für dich. Du gibst ihm ein *Rezept* -- wie viele Funken pro Sekunde,
wie lange jeder lebt, wie schnell und in welche Richtung sie losfliegen, was an ihnen zieht, wie
sie aussehen und zu welcher Farbe sie verblassen -- und es verwaltet die Funken selbst. Das Ding,
das das Rezept enthält und ausführt, heißt *Emitter*.
Ein Emitter ist kein Bild und auch kein Objekt in deinem Spiel. Er ist die Beschreibung einer Art
von Feuer, aufbewahrt an einem Ort, auf den du zeigen kannst, und er kann dieses Feuer überall
machen, wo du ihn darum bittest.
[#_start_with_something_that_moves]
=== Fang mit etwas an, das sich bewegt
Partikel wirken überzeugender, wenn sie von etwas ausgehen, also fang mit einem Raumschiff an.
Leg einen Ordner an, kopier den Ordner `art` des Buchs hinein und schreib das hier in
`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
----
Starte es mit `Singe -R particles` und steuere mit den Pfeiltasten nach links und rechts. Hier
ist nichts neu: ein Sprite aus Lektion neun, die Schalter aus Lektion drei und `singeGetTicks`,
damit sich das Schiff mit derselben Geschwindigkeit bewegt, egal wie hoch die Bildrate ist.
Die eine Zeile, auf die es sich zu zeigen lohnt, ist `dofile("Singe/Framework.singe")`. Sie
definiert `DIR`, den Ordner, in dem dein Skript liegt, also findet `DIR .. "art/spaceship.png"`
das Bild, egal aus welchem Ordner du Singe gestartet hast.
[#_give_it_an_exhaust]
=== Gib ihm einen Abgasstrahl
Jetzt das Feuer. Füg das hier über den Callbacks ein, nach den Variablen des Schiffs:
[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)
----
Und zwei Zeilen in `onOverlayUpdate`. Die erste kommt direkt vor `overlayClear`, die zweite
direkt nach `spriteDraw`:
[source,lua]
----
emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2)
----
[source,lua]
----
emitterDraw(exhaust)
----
Speichere. Das Schiff sitzt jetzt auf einer kurzen Zunge aus gelber Flamme, und die Flamme fliegt
mit, wenn du steuerst.
[#_blow_it_up]
=== Spreng es in die Luft
Ein Abgasstrahl läuft für immer. Eine Explosion passiert einmal, und das behandelt die Engine als
eine andere Art von Auftrag an denselben Emitter.
Erst noch zwei Dateien und eine Zahl. Die beiden Dateien kommen oben neben das Sprite des
Schiffs:
[source,lua]
----
local sparkSprite = spriteLoad(DIR .. "art/star.png")
local boomSound = soundLoad(DIR .. "art/boom.wav")
----
und das hier kommt neben `shipX` und `shipY`, zwanzig Einheiten über dem unteren Rand des
Bildschirms:
[source,lua]
----
local ground = overlayGetHeight() - 20
----
Jetzt das zweite Rezept, unter dem ersten:
[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)
----
Fällt dir auf, dass `emitterStart` fehlt? Dieser Emitter wird nie strömen.
Füg über `onInputPressed` eine Funktion ein, die ihn auslöst:
[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
----
Dafür braucht es noch drei Dinge: `local RESPAWN_MS = 1400` bei den anderen Konstanten,
`local flying = true` bei den anderen Variablen und einen Zweig in `onInputPressed`:
[source,lua]
----
elseif what == SWITCH_BUTTON1 then
explode()
----
Zum Schluss sollte das Schiff nicht da sein, solange es in Stücken ist. Pack das Steuern und das
Zeichnen in `onOverlayUpdate` in `if flying then ... end` und füg `emitterDraw(boom)` neben dem
anderen Zeichenaufruf ein. Das fertige Skript ist `learn/25-particles.singe`.
Drück die *Leertaste* -- standardmäßig Knopf eins -- und das Schiff zerfällt in eine Wolke
taumelnder Sterne, die im Bogen nach unten fliegen, auf den Boden schlagen und abprallen.
[#_what_just_happened_21]
=== Was gerade passiert ist
In dieser Lektion gibt es sehr viele neue Aufrufe, und sie haben alle dieselbe Form: zuerst der
Emitter, dann das, was du einstellst. Nimm sie dir in Gruppen vor.
[source,lua]
----
local exhaust = emitterNew()
----
Macht einen Emitter und gibt ein *Handle* zurück: eine Zahl, mit der die Engine ihn wiederfindet,
genau wie die Zahl, die dir `spriteLoad` gibt. Ohne Argument bekommst du einen zweidimensionalen
Emitter, der in Overlay-Koordinaten lebt und aus `onOverlayUpdate` gezeichnet wird. Gib
`emitterNew` stattdessen einen Knoten, und du bekommst einen dreidimensionalen, der in der Szene
lebt und sich selbst zeichnet; alles andere in dieser Lektion ist für beide gleich.
Ein neuer Emitter hat schon ein vollständiges Rezept -- fünfzig Partikel pro Sekunde, weiß, ein
bis zwei Sekunden Lebensdauer, langsam den Bildschirm hinauftreibend -- also widersprichst du mit
jedem Aufruf danach einem Standardwert. Der Eintrag zu `emitterNew` in der Referenz listet sie alle
auf.
[source,lua]
----
emitterSetRate(exhaust, 90)
emitterSetLife(exhaust, 0.15, 0.40)
----
Neunzig Partikel pro Sekunde, und jedes lebt zwischen fünfzehn Hundertstel und vier Zehntel
Sekunden. Diese beiden Zahlen zusammen entscheiden, wie viel Feuer es gibt: Neunzig pro Sekunde,
die eine Drittelsekunde leben, heißt, dass in jedem Moment etwa dreißig auf dem Bildschirm sind.
Überall, wo ein Emitter-Aufruf einen kleinsten und einen größten Wert nimmt, bekommt jedes
Partikel seinen eigenen Wert, zufällig zwischen den beiden gewählt. Daher kommt das Zerfranste.
Ein Feuer, in dem jede Flamme genau so lange lebte wie jede andere, sähe aus wie eine Maschine.
[source,lua]
----
emitterSetDirection(exhaust, 0, 1)
emitterSetSpread(exhaust, 12)
emitterSetSpeed(exhaust, 40, 90)
----
In welche Richtung sie losfliegen, wie breit der Fächer ist und wie schnell. Das Y des Overlays
wächst *nach unten*, wie schon seit Lektion eins, also ist `(0, 1)` den Bildschirm hinunter --
hinten aus einem Schiff heraus, das nach oben zeigt. `emitterSetSpread` ist der halbe Winkel des
Fächers: Zwölf Grad sind ein enger Strahl, neunzig öffnen ihn zum halben Kreis, und hundertachtzig
sind alle Richtungen auf einmal.
[source,lua]
----
emitterSetSize(exhaust, 7, 1)
emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0)
----
Diese beiden sind der Grund, warum die Flamme wie eine Flamme aussieht. Ein Partikel hat nicht
eine Größe und eine Farbe; es hat eine Größe und eine Farbe *bei der Geburt* und eine andere *beim
Tod*, und es gleitet gleichmäßig von der einen zur anderen, über die Lebensdauer, die es gezogen
hat. Also fängt jeder Funke sieben Overlay-Einheiten breit und blassgelb an und endet eine Einheit
breit und orangerot mit einem Alpha von null, und das ist unsichtbar. Er schrumpft und verblasst,
weil du gesagt hast, wo er endet, nicht weil ihn irgendetwas verblassen ließ.
`emitterSetColor` nimmt acht Zahlen, und das ist viel zu lesen: Rot, Grün, Blau und Alpha am
Anfang, dann Rot, Grün, Blau und Alpha am Ende.
[source,lua]
----
emitterSetBlend(exhaust, PARTICLE_ADD)
----
Wie sich ein Partikel mit dem verbindet, was dahinter ist. `PARTICLE_ALPHA` ist normales Malen und
der Standard: Nimm es für Rauch, Staub, Regen und Trümmer. `PARTICLE_ADD` addiert stattdessen sein
Licht zum Bild, also sind zwei Funken übereinander heller als einer, und nichts wird je dunkler.
Feuer, Funken, Laser und Magie sind additiv. Rauch nicht, und additiv gezeichneter Rauch sieht aus
wie Dampf, der von innen beleuchtet wird.
[source,lua]
----
emitterSetMax(exhaust, 120)
emitterStart(exhaust)
----
`emitterSetMax` ist die Größe des Pools: wie viele Partikel dieses Emitters gleichzeitig
existieren dürfen. Ist der Pool voll, wird nichts Neues geboren, bis etwas stirbt. Neunzig pro
Sekunde, die bis zu vier Zehntel Sekunden leben, brauchen etwa sechsunddreißig, also sind
hundertzwanzig bequem. Stell es ein, bevor du startest, denn wenn du es später änderst, wird
jedes lebende Partikel weggeworfen.
`emitterStart` dreht den Hahn auf. Von da an macht der Emitter in jedem Frame Partikel, ob du ihn
zeichnest oder nicht, bis `emitterStop` ihn wieder zudreht. Beachte, was `emitterStop` *nicht*
tut: Es stoppt neue Partikel, und die, die schon leben, leben ihr Leben zu Ende und verblassen
ganz normal. Darum verweht der Abgasstrahl, statt zu verschwinden, wenn das Schiff explodiert.
Wenn du sie sofort weghaben willst, tötet `emitterClear` sie.
[source,lua]
----
emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2)
----
So folgt ein Emitter etwas. Ein zweidimensionaler Emitter lässt alles an einem Punkt entstehen,
und dieser Punkt ist dort, wo du ihn zuletzt hingesetzt hast, also setzt du ihn in jedem Frame
nach dem, was sich gerade bewegt: der Position eines Sprites, dem Knoten eines Physikkörpers, der
Maus. Neue Partikel erscheinen am neuen Ort; die schon in der Luft sind, bleiben, wo sie geboren
wurden, und genau das ist richtig für einen Abgasstrahl, denn Abgas bleibt zurück.
Ein dreidimensionaler Emitter wird an einem Knoten gemacht und folgt diesem Knoten von selbst,
ganz ohne Aufruf in deinem Update. Und wenn du *doch* willst, dass die ganze Wolke mit dem Ding
mitreist -- ein Schild, eine Aura, ein Triebwerksstrahl von außen gesehen -- schaltet
`emitterSetLocal` das ein.
[source,lua]
----
emitterDraw(exhaust)
----
Zweidimensionale Emitter werden gezeichnet, wenn du darum bittest, einmal pro Frame, aus
`onOverlayUpdate`. Die Reihenfolge deiner Aufrufe von `emitterDraw` ist die Reihenfolge, in der
sie übereinanderliegen. Diesen Aufruf zu vergessen ist der Fehler, den jeder einmal macht: Der
Emitter läuft weiter, verbraucht weiter seinen Pool und zeigt nichts.
Jetzt die eigenen Aufrufe der Explosion.
[source,lua]
----
emitterSetTexture(boom, sparkSprite)
----
Ohne Textur ist jedes Partikel ein weicher runder Klecks, den die Engine selbst zeichnet; das
benutzt der Abgasstrahl, und für eine Flamme ist es richtig. Mit einer Textur trägt jedes Partikel
dieses Bild -- hier `star.png`, acht mal acht Pixel. Der Emitter nimmt sich seine eigene Kopie der
Pixel, also darfst du das Sprite danach entladen, und die Partikel funktionieren weiter.
[source,lua]
----
emitterSetGravity(boom, 0, 160)
emitterSetDrag(boom, 0.9)
----
Schwerkraft ist ein gleichmäßiger Zug, in Overlay-Einheiten pro Sekunde pro Sekunde, und sie ist
nach unten positiv, aus demselben Grund, aus dem der Abgasstrahl mit positivem Y feuert. Der
Luftwiderstand (Drag) ist das Gegenteil: Er nimmt Geschwindigkeit weg, in jedem Frame ein bisschen,
also fliegen die Funken hart hinaus und bleiben dann in der Luft hängen. Funken mit Schwerkraft und
ohne Luftwiderstand regnen gerade nach unten wie ein Feuerwerk; erst der Luftwiderstand lässt sie
aussehen, als bewegten sie sich durch Luft.
[source,lua]
----
emitterSetSpin(boom, -240, 240)
emitterSetSize(boom, 10, 2, 0.4)
----
Die Drehung ist in Grad pro Sekunde, und ein Bereich, der über null hinweggeht, lässt manche in
die eine und manche in die andere Richtung drehen. Man sieht sie auf einer Textur, und am
eingebauten Klecks bewirkt sie gar nichts, denn der ist rund.
Die dritte Zahl bei `emitterSetSize` ist die Variation. Sie skaliert beide Größen jedes Partikels
mit einem einzigen zufälligen Faktor -- hier irgendwo von null Komma sechs bis eins Komma vier --
damit die Funken nicht alle derselbe Stern in derselben Größe sind.
[source,lua]
----
emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground)
emitterSetTrail(boom, 4, 2)
----
`COLLIDE_FLOOR` legt in der Höhe, die du angibst, einen flachen, unsichtbaren Boden quer durch die
Welt, und Partikel prallen davon ab. Die beiden Zahlen davor sagen, wie viel Geschwindigkeit einen
Aufprall übersteht und wie viel seitliche Geschwindigkeit abgerieben wird. Das kostet nichts, weil
es ein Vergleich pro Partikel ist, und es lässt Trümmer aussehen, als wären sie irgendwo gelandet,
statt durch die Welt zu fallen. In einem dreidimensionalen Spiel lässt `COLLIDE_SCENE` Partikel
stattdessen von der echten Geometrie abprallen, und das kostet sehr viel mehr.
Ein Schweif merkt sich die letzten paar Orte, an denen ein Partikel war, und zeichnet ein
verblassendes Band durch sie hindurch. Vier Positionen und zwei Einheiten Breite machen aus jedem
Stern einen kurzen Streifen.
[source,lua]
----
emitterBurst(boom, 140)
----
Und das ist die andere Art, Partikel zu machen. `emitterStart` ist ein Hahn; `emitterBurst` ist
eine Handvoll, auf einmal geworfen. Der Emitter muss dafür nicht strömen, und dieser hier tut es
nie. Alles andere -- das Rezept, der Pool, die Position -- funktioniert genauso.
Diese Unterscheidung solltest du dir merken. Alles, was andauert, ist ein Strom, den du startest
und stoppst: Abgas, Rauch, Regen, eine Fackel, ein Wasserfall. Alles, was in einem Moment
passiert, ist ein Stoß: eine Explosion, ein Platschen, ein Schritt im Staub, ein Kugeleinschlag,
ein Mündungsfeuer. Viele Effekte sind beides, aus einem Emitter oder aus zweien: Eine Granate
platzt und raucht dann.
[#_how_many_is_too_many]
=== Wie viele sind zu viele
Die ehrliche Antwort ist, dass Partikel billig zu bewegen und teuer anzusehen sind.
Zehntausend Partikel zu bewegen ist Rechnen, und ein Computer, der überhaupt ein Spiel laufen
lassen kann, schafft das, ohne es zu merken. Was kostet, ist das Zeichnen, und was das Zeichnen
kostet, ist ungefähr die Zahl der Bildschirmpixel, die die Partikel bedecken, Überlappungen
mitgezählt. Zweihundert Funken, acht Pixel breit, bedecken weniger vom Bildschirm als eine
Rauchwolke, die ihn ausfüllt. Das ist die Regel: klein und viele ist fast umsonst, groß und
überlappend nicht.
Additives Mischen ist die teure Art, denn nichts, was es zeichnet, kann übersprungen werden --
jede Schicht addiert sich zu dem, was darunter ist, also malt der Rechner alle. Ein Dutzend
additive Blitze über den ganzen Bildschirm, übereinander, bremst einen Raspberry Pi 4 zum
Schneckentempo aus, während ihn zehntausend winzige hüpfende Funken nicht stören.
Also: Ein paar tausend kleine Partikel sind kein Grund zur Sorge. Bevor du darüber hinausgehst,
bring die Anzahl auf den Bildschirm und behalte sie im Auge.
[source,lua]
----
overlayPrint(2, 4, "Particles alive: " .. (emitterGetCount(exhaust) + emitterGetCount(boom)))
----
Mit `emitterGetCount` bemisst du einen Pool ehrlich, statt zu raten. Steht die Zahl genau bei
deinem `emitterSetMax`, ist dein Pool voll, und die Engine weigert sich still, Partikel zu machen,
um die du gebeten hast. Kommt sie nie in die Nähe, reservierst du Speicher für nichts.
[#_try_it_25]
=== Probier es aus
. *Mach aus dem Abgasstrahl Rauch.* Ändere `emitterSetBlend(exhaust,
PARTICLE_ADD)` in `PARTICLE_ALPHA`, stell die Farbe auf Grau, das zu durchsichtigem Grau
verblasst, und lass ihn mit `emitterSetSize(exhaust, 3, 14)` wachsen statt schrumpfen. Drei
Zeilen haben eine andere Maschine gemacht.
. *Nimm der Explosion den Luftwiderstand.* Lösch die Zeile mit `emitterSetDrag` und vergleiche.
Dann setz den Luftwiderstand wieder ein und nimm stattdessen die Schwerkraft weg. Das eine sieht
aus wie ein Feuerwerk und das andere wie ein Unfall.
. *Mach den Abgasstrahl breiter.* Setz `emitterSetSpread(exhaust, 180)`. Das Schiff sieht nicht
mehr aus, als hätte es ein Triebwerk. Die Streuung leistet mehr, als du denkst.
. *Wirf tausend auf einmal.* Ändere `emitterBurst(boom, 140)` in
`emitterBurst(boom, 1000)` und beobachte die Anzahl auf dem Bildschirm. Sie wird keine tausend
erreichen. Finde anhand von `emitterSetMax` heraus, warum nicht, dann vergrößere den Pool, mach
die Funken viermal so groß und sieh, was das mit der Bildrate macht.
. *Zünde die Explosion dort, wo das Schiff nicht ist.* Schreib `emitterSetPosition(boom,
40, 60)` in `explode` statt der Position des Schiffs. Ein Emitter ist die Beschreibung eines
Feuers, kein Ding, das irgendwo lebt, und du kannst denselben im selben Frame an zehn Stellen
zünden.
[#_break_it_on_purpose_24]
=== Mach es absichtlich kaputt
Die Konstanten -- `PARTICLE_ADD`, `COLLIDE_FLOOR`, `OVERLAY_UPDATED` -- sind Namen, die Singe
Zahlen gegeben hat. Schreib eine davon leicht falsch, und Lua beschwert sich nicht, denn in Lua ist
ein Name, der nie einen Wert bekommen hat, kein Fehler: Er ist `nil`, der Wert, der „hier ist
nichts“ bedeutet.
Ändere die Mischzeile des Abgasstrahls in einen Namen, der genauso naheliegend klingt:
[source,lua]
----
emitterSetBlend(exhaust, PARTICLE_ADDITIVE)
----
Singe hält an, bevor das Spiel startet, und nennt die Zeile in deiner Datei, in der dieser
Aufruf steht:
----
22:emitterSetBlend: Argument 2 must be a number.
----
Dieser Fehler hat eine andere Form als die Lua-Fehler aus Lektion acht, weil er von der Engine
kommt und nicht von Lua. Erst kommt die Zeilennummer, dann der Aufruf, der sich gewehrt hat, dann
die Beschwerde. Argument zwei von `emitterSetBlend` ist der Mischmodus, und angekommen ist `nil`,
weil `PARTICLE_ADDITIVE` kein Name ist, den je irgendetwas definiert hat.
„Must be a number“ heißt fast immer genau das: eine falsch geschriebene Konstante oder eine
Variable, von der du dachtest, du hättest sie gesetzt. Die Lösung ist, den Namen mit der Referenz
zu vergleichen, und dort heißt er `PARTICLE_ADD`.
[#_what_you_learned_25]
=== Was du gelernt hast
* Ein Emitter enthält ein Rezept für eine Art von Partikel und verwaltet die Partikel selbst,
also zeichnet dein Skript nie eins.
* `emitterNew` ohne Argument macht einen zweidimensionalen Emitter in Overlay-Koordinaten; mit
einem Knoten macht es einen dreidimensionalen in der Szene.
* Jeder Emitter fängt mit einem vollständigen Rezept an, und jeder Aufruf von `emitterSet...`
ändert einen Teil davon.
* Überall, wo ein Aufruf einen kleinsten und einen größten Wert nimmt, zieht jedes Partikel
seinen eigenen Wert zufällig dazwischen, und daher kommt das Zerfranste.
* Größe und Farbe werden für die Geburt und für den Tod festgelegt, und das Partikel gleitet über
sein Leben von der einen zur anderen.
* `emitterStart` strömt bis `emitterStop`; `emitterBurst` wirft eine Handvoll auf einmal.
Andauernde Effekte sind Ströme, kurze sind Stöße.
* `emitterStop` lässt die lebenden Partikel zu Ende leben; `emitterClear` tötet sie sofort.
* Ein zweidimensionaler Emitter wird in jedem Frame mit `emitterSetPosition` bewegt und in jedem
Frame mit `emitterDraw` gezeichnet. Ein dreidimensionaler folgt seinem Knoten und zeichnet sich
selbst.
* Partikel kosten, was sie auf dem Bildschirm bedecken, nicht, was ihre Bewegung kostet, und
additives Mischen ist die teure Art.
* `emitterSetMax` begrenzt den Pool, und `emitterGetCount` sagt dir, ob du die Größe richtig
geraten hast.
[#_next_time_25]
=== Beim nächsten Mal
Die Explosion hat hundertvierzig Dinge bewegt, ohne dass ihr jemand gesagt hat, wohin eins davon
fliegen soll, aber es waren Funken, und Funken ist es egal, wogegen sie stoßen. In Lektion
sechsundzwanzig geht es um etwas, dem es nicht egal ist: eine Figur, die von hier nach dort muss,
und eine Wand im Weg.