518 lines
22 KiB
Text
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.
|