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