576 lines
24 KiB
Text
576 lines
24 KiB
Text
[#_lesson_10_sound]
|
|
== Lektion 10: Sound
|
|
|
|
image::learn/10-sound.png[Die fertige Lektion, 480]
|
|
|
|
Ein Spiel ohne Sound fühlt sich kaputt an, und zwar auf eine Art, die man schwer benennen kann.
|
|
Die Schüsse fliegen los, und nichts passiert. Die Felsen zerbrechen, und nichts passiert. Leg auf
|
|
jedes davon ein Geräusch von einer halben Sekunde, und dasselbe Spiel fühlt sich plötzlich an, als
|
|
wäre es aus etwas gemacht.
|
|
|
|
Diese Lektion gibt deinem Spiel einen Schuss, eine Explosion und Musik darunter. Unterwegs stellt
|
|
sie dir deinen fünften Callback vor: den, mit dem die Engine dir sagt, dass ein Sound fertig
|
|
abgespielt ist.
|
|
|
|
[#_one_noise]
|
|
=== Ein Geräusch
|
|
|
|
Im Grafik-Kit, das du in Lektion neun kopiert hast, stecken zwei Sounds, `shoot.wav` und
|
|
`boom.wav`, und die Explosion ist von den beiden leichter zu hören. Leg neben deinem Ordner `art`
|
|
eine neue Datei an und tipp das ein.
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(boom)
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Press space.")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
function onShutdown()
|
|
soundUnload(boom)
|
|
end
|
|
----
|
|
|
|
Starte es und tipp drei- oder viermal schnell auf die Leertaste. Die Knalle türmen sich
|
|
übereinander, statt einander abzuschneiden.
|
|
|
|
Hältst du die Leertaste gedrückt, bekommst du genau einen Knall, keinen ganzen Strom davon. Das ist
|
|
dasselbe Verhalten, das du in Lektion drei kennengelernt hast: Eine gehaltene Taste ist ein
|
|
Drücken, einmal gemeldet, und die Engine wirft die Wiederholungen weg, die deine Tastatur schickt.
|
|
Darum braucht das Lenken `onInputReleased` und eine Variable, statt Tastendrücke zu zählen.
|
|
|
|
[#_what_just_happened_6]
|
|
=== Was gerade passiert ist
|
|
|
|
Die Form ist dieselbe wie in Lektion neun: einmal oben laden, das Handle benutzen, am Ende
|
|
zurückgeben.
|
|
|
|
[source,lua]
|
|
----
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
----
|
|
|
|
`soundLoad` liest eine Audiodatei, behält sie im Speicher und gibt ein Handle zurück -- dieselbe
|
|
Idee wie ein Sprite-Handle, eine Zahl, die für die Engine „diesen Clip da“ bedeutet. `DIR` steht
|
|
aus demselben Grund da wie in Lektion neun: Der Sound wohnt im Ordner deines Spiels, und das Spiel
|
|
muss ihn finden können, egal von wo aus Singe gestartet wurde.
|
|
|
|
Alles, was Lektion neun übers Laden gesagt hat, gilt hier Wort für Wort. Lade oben in der Datei,
|
|
einmal. Ein `soundLoad` in `onOverlayUpdate` lädt den Clip sechzigmal pro Sekunde und gibt nie
|
|
einen zurück, und ein paar Minuten später stirbt das Spiel daran.
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(boom)
|
|
----
|
|
|
|
Spiel ihn ab. Mehr gehört zum Abspielen eines Soundeffekts nicht, und um den Rest kümmert sich die
|
|
Engine: Der Clip fängt an, er hört auf, und er kommt deinem Programm nie in die Quere.
|
|
|
|
Aber `soundPlay` gibt auch etwas zurück, und das ist *nicht* dieselbe Zahl, die du hineingegeben
|
|
hast. Ändere die Zeile und sieh hin:
|
|
|
|
[source,lua]
|
|
----
|
|
local channel = soundPlay(boom)
|
|
debugPrint("clip " .. boom .. " on channel " .. channel)
|
|
----
|
|
|
|
Tipp ein halbes Dutzend Mal schnell auf die Leertaste und beobachte die beiden Zahlen. Die
|
|
Clip-Nummer ändert sich nie. Die Kanalnummer steigt -- 0, dann 1, dann 2 -- und sobald die früheren
|
|
Knalle vorbei sind, fällt sie zurück und fängt an, die kleinen Nummern wiederzuverwenden.
|
|
|
|
Das ist die eine Sache beim Sound, über die jeder stolpert, also lohnt es sich, sie langsam zu
|
|
sagen. *Hier sind zwei verschiedene Arten von Zahlen im Spiel.*
|
|
|
|
Das *Clip-Handle* von `soundLoad` ist die Aufnahme. Davon gibt es eins. Es ändert sich nicht, es
|
|
spielt weder, noch spielt es nicht, es ist der Sound, der im Speicher sitzt und darauf wartet,
|
|
benutzt zu werden.
|
|
|
|
Der *Kanal* von `soundPlay` ist eine Aufführung dieser Aufnahme. Singe hat sechzehn davon,
|
|
nummeriert von `0` bis `15`, und jeder Sound, den man gerade hört, belegt einen. Darum überlappen
|
|
sich die Knalle: Das zweite Drücken hat das erste nicht unterbrochen, es hat eine zweite Aufführung
|
|
auf dem nächsten freien Kanal gestartet.
|
|
|
|
Alles, was mit einem *Sound, den du hörst*, etwas tut, nimmt einen Kanal: `soundStop`,
|
|
`soundIsPlaying`, `soundPause`, `soundResume`. Alles, was mit der *Aufnahme* etwas tut, nimmt das
|
|
Handle: `soundPlay` und `soundUnload`. Gib das eine dort hinein, wo das andere hingehört, und du
|
|
bekommst nicht immer einen Fehler, denn `3` ist eine gültige Kanalnummer und auch ein völlig
|
|
brauchbares Clip-Handle -- du bekommst Stille, oder der falsche Sound hört auf, und das ist viel
|
|
schwerer zu finden.
|
|
|
|
Sechzehn ist viel, aber nicht endlos. Wenn alle Kanäle belegt sind, spielt nichts, und
|
|
`soundPlay` gibt statt einer Kanalnummer `-1` zurück. Ein Spiel, das sehr viele Sounds auf einmal
|
|
abfeuert, sollte damit rechnen, und gleich schreibst du eins.
|
|
|
|
[#_volume]
|
|
=== Lautstärke
|
|
|
|
Es gibt drei Lautstärken, und sie werden miteinander multipliziert.
|
|
|
|
Die *Gesamtlautstärke* der Effekte gilt für alles. `soundGetVolume` liest sie, und
|
|
`soundSetVolume` setzt sie, auf einer Skala von `0` bis `63`.
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(40)
|
|
----
|
|
|
|
Lass es. Diese Zahl gehört dem Spieler, nicht dir: Sie startet bei dem, was er beim Starten des
|
|
Spiels verlangt hat, und ein Spiel, das sie überschreibt, ist ein Spiel, das in irgendjemandes
|
|
stillem Zimmer zu laut ist. Ändere die Gesamtlautstärke nur, wenn der Spieler dich darum bittet,
|
|
über einen Optionsbildschirm. `soundGetVolume` gibt es, damit ein Optionsbildschirm zeigen kann,
|
|
wie hoch sie gerade ist, statt zu raten.
|
|
|
|
Wonach du greifen solltest, ist die *Kanallautstärke*, die unter der Gesamtlautstärke sitzt. Gib
|
|
`soundPlay` ein drittes Argument, und diese eine Aufführung ist leiser:
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(shootClip, 0, 35)
|
|
----
|
|
|
|
Die `0` in der Mitte gibt an, wie oft wiederholt wird, und die musst du angeben, weil die
|
|
Lautstärke danach kommt. `0` heißt: einmal abspielen. `-1` heißt: in Schleife, bis etwas es
|
|
anhält, und so würdest du ein Motorbrummen oder Wind machen. Der Eintrag zu `soundPlay` in der Referenz
|
|
listet alle drei Argumente auf.
|
|
|
|
Die dritte Lautstärke ist die Entfernung, für Sounds, die in einer 3D-Welt platziert sind. Die
|
|
kommt in Teil vier dran, wenn das Spiel in drei Dimensionen umzieht.
|
|
|
|
Musik hat eine eigene Lautstärke, und die hat eine andere Skala. Gleich mehr dazu.
|
|
|
|
[#_stopping_and_not_stopping]
|
|
=== Anhalten und Nicht-Anhalten
|
|
|
|
`soundStop` nimmt einen Kanal und schneidet ihn dort ab, wo er gerade ist:
|
|
|
|
[source,lua]
|
|
----
|
|
soundStop(engineChannel)
|
|
engineChannel = -1
|
|
----
|
|
|
|
Beachte die zweite Zeile. Sobald ein Kanal aufhört, darf die Engine genau diese Nummer an das
|
|
nächste `soundPlay` vergeben, also gehört eine Kanalnummer, die du noch festhältst, jetzt
|
|
vielleicht zu einem völlig anderen Sound. Wirf sie weg, sobald du ihn anhältst. `-1` ist das
|
|
Nützliche, was du dort hineinschreibst, denn das ist derselbe Wert, den dir `soundPlay` gibt, wenn
|
|
es gar nichts abspielen konnte, und so deckt ein einziger Test beides ab: `if channel >= 0 then`.
|
|
|
|
`soundStop` ist für einen Sound, der jetzt falsch ist -- das Motorgeräusch, wenn der Motor
|
|
ausgeht, der Alarm, wenn jemand auf den Alarm reagiert hat. Für einen kurzen Effekt ist es das
|
|
falsche Werkzeug. Eine halbsekündige Explosion, die nach einer Zehntelsekunde abgehackt wird,
|
|
klingt wie ein Fehler, weil sie einer ist.
|
|
|
|
Also tu meistens einfach gar nichts. Lass ihn zu Ende spielen. Ein Soundeffekt, den du startest und
|
|
an den du nie wieder denkst, hört von selbst auf und gibt seinen Kanal von selbst frei, und genau
|
|
das willst du.
|
|
|
|
Das gilt sogar am Ende. `soundUnload` schneidet einen Clip, der noch spielt, nicht ab: Die Engine
|
|
hält den Ton am Leben, bis der letzte Kanal, der ihn benutzt, aufgehört hat. Du kannst in
|
|
`onShutdown` entladen, ohne dir Sorgen zu machen, dass du etwas zum Schweigen gebracht hast.
|
|
|
|
`soundIsPlaying(channel)` beantwortet, ob ein Kanal gerade Geräusche macht. Wofür man es
|
|
hauptsächlich braucht, ist nicht Neugier, sondern Zurückhaltung: ein Alarm, der einmal ertönen soll
|
|
und nicht jedes Mal neu anfängt, wenn das Spiel merkt, dass die Gefahr immer noch da ist.
|
|
|
|
[source,lua]
|
|
----
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
Lies die Bedingung laut vor: Spiel den Alarm ab, wenn es keinen Alarmkanal gibt oder wenn der, den
|
|
du hast, aufgehört hat. Sonst lass ihn in Ruhe.
|
|
|
|
[#_the_sound_that_stutters]
|
|
=== Der Sound, der stottert
|
|
|
|
Jetzt kommt der Fehler. Jeder schreibt ihn, meistens innerhalb einer Woche, nachdem er Sound
|
|
entdeckt hat.
|
|
|
|
Angenommen, du willst, dass das Schiff weiterfeuert, solange der Knopf gedrückt ist, statt einmal
|
|
pro Drücken. Du würdest es so machen, wie du lenkst: eine Variable `firing`, die in
|
|
`onInputPressed` auf `true` gesetzt wird und in `onInputReleased` zurück auf `false`, und der
|
|
Schuss wird in `onOverlayUpdate` erzeugt, solange sie wahr ist. Das ist ein völlig vernünftiger
|
|
Wunsch, und das Geräusch sieht aus, als gehörte es genau dorthin, zum Schuss:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if firing then
|
|
shots[#shots + 1] = { x = shipX, y = shipY }
|
|
soundPlay(shootClip)
|
|
end
|
|
...
|
|
----
|
|
|
|
Was herauskommt, ist kein Schuss. Es ist ein surrendes Rasseln oder ein harter, flacher Ton oder
|
|
ein Geräusch, als ginge eine kleine Maschine kaputt.
|
|
|
|
`onOverlayUpdate` läuft etwa sechzigmal pro Sekunde. Halt den Knopf eine Sekunde lang gedrückt, und
|
|
du hast sechzig Aufführungen eines Clips verlangt, der ungefähr eine Fünftelsekunde dauert, und
|
|
jede fängt über der letzten an. In jedem Moment spielen etwa elf davon, jede einen Frame hinter der
|
|
vorigen, und was du hörst, sind sechzig Kopien desselben Anfangs, übereinandergestapelt.
|
|
|
|
Die Lösung ist kein schlauerer Sound. Sie besteht darin, den Sound dort abzuspielen, wo das
|
|
*Ereignis* passiert, und nicht dort, wo der *Zustand* wahr ist:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputPressed` passiert einmal, wenn der Knopf runtergeht. Ein Drücken, ein Schuss, ein
|
|
Geräusch.
|
|
|
|
Wenn du wirklich Dauerfeuer willst, ist die Antwort dieselbe, nur in anderer Form: Mach das Feuern
|
|
selbst zu einem Ereignis. Zähl Frames, genau wie der Laufzyklus sie in Lektion neun gezählt hat,
|
|
und erzeuge nur jeden achten Frame einen Schuss. Das Geräusch gehört zum Schuss, also spielt es
|
|
sieben- oder achtmal pro Sekunde statt sechzigmal, und das ist ein Maschinengewehr statt eines
|
|
Rasselns.
|
|
|
|
Das ist die Regel, und es lohnt sich, sie aus dieser Lektion mitzunehmen: *Sounds gehören zu
|
|
Ereignissen, nicht zu Bedingungen*. Ein Drücken, ein Treffer, ein Tod, ein aufgesammeltes Ding --
|
|
jedes davon passiert einmal und verdient einen Sound. „Der Knopf ist unten“ und „das Schiff bewegt
|
|
sich“ sind keine Ereignisse; sie sind Hunderte Frames hintereinander wahr, und alles, was du von
|
|
dort aus abspielst, spielt hundertfach.
|
|
|
|
Gehört ein Sound wirklich zu einem Zustand, der andauert -- ein Motor, Wind, eine Sirene -- dann
|
|
starte ihn einmal mit `-1` als Schleifenzahl, behalt den Kanal und halt ihn an, wenn der Zustand
|
|
endet.
|
|
|
|
[#_the_fifth_callback]
|
|
=== Der fünfte Callback
|
|
|
|
Manchmal musst du tatsächlich wissen, wann ein Sound fertig ist. Schreib diese Funktion, und die
|
|
Engine ruft sie auf:
|
|
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
Das ist dein fünfter Callback, nach `onOverlayUpdate` in Lektion eins, den beiden
|
|
Eingabe-Callbacks in Lektion drei und `onShutdown` in Lektion neun, und er funktioniert genauso
|
|
wie sie alle: Du schreibst ihn, du
|
|
rufst ihn nie auf, die Engine ruft dich. Der `channel`, den er dir übergibt, ist der Kanal, der
|
|
gerade still geworden ist -- die Nummer, die dir `soundPlay` gegeben hat, als du ihn gestartet
|
|
hast.
|
|
|
|
Er wird ausgelöst, wenn ein Clip zu Ende ist, wenn die letzte Wiederholung einer Schleife fertig
|
|
ist, *und* wenn du einen Kanal selbst mit `soundStop` anhältst. Das Letzte überrascht Leute, die
|
|
mit dem Callback einen Sound an den nächsten hängen: Auch ein absichtlich angehaltener Kanal wird
|
|
als fertig gemeldet, und wenn du nicht aufpasst, startet der nächste Sound in der Kette trotzdem.
|
|
|
|
Das Spiel benutzt ihn gleich für etwas, das sich lohnt. Wenn das letzte Leben weg ist, spielt die
|
|
Explosion des Schiffs, und das Spiel hält still -- kein GAME OVER, kein Ausblenden der Musik --
|
|
bis diese Explosion wirklich vorbei ist. Dann hört alles auf einmal auf. Das sind eine Handvoll
|
|
Zeilen, und sie machen den Unterschied zwischen einem Ende und einem Schnitt.
|
|
|
|
[#_music_is_not_a_sound_effect]
|
|
=== Musik ist kein Soundeffekt
|
|
|
|
Du könntest ein dreiminütiges Stück mit `soundLoad` laden und mit `soundPlay` abspielen. Tu es
|
|
nicht. Musik bekommt eine eigene Familie von Aufrufen, und zwar aus Gründen, die zählen:
|
|
|
|
* Musik hat ihre eigene Lautstärke, getrennt von den Effekten, also kann ein Spieler die Musik
|
|
leiser drehen und die Schüsse trotzdem hören.
|
|
* Es gibt keine Grenze von sechzehn Kanälen und keine Kanalnummer. Ein Handle ist ein Musikstück,
|
|
und du hältst es über dieses Handle an, pausierst es und setzt es fort.
|
|
* `musicStop` kann ein Stück über ein, zwei Sekunden ausblenden, statt es abzuschneiden.
|
|
|
|
Die Aufrufe spiegeln die, die du schon kennst:
|
|
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
`musicLoad` gibt ein Handle. `musicPlay` startet es, mit derselben Schleifenzahl wie
|
|
`soundPlay`: `-1` für immer. `musicStop(theme)` beendet es, und `musicStop(theme, 1500)` nimmt
|
|
sich anderthalb Sekunden, um es auszublenden. `musicUnload` gibt es zurück.
|
|
|
|
Jetzt die Falle. *Die Musiklautstärke reicht von 0 bis 128, nicht von 0 bis 63.* Soundeffekte
|
|
benutzen eine Skala und Musik eine andere, doppelt so lange, aus historischen Gründen, die
|
|
überhaupt kein Trost sind, wenn deine Musik mit der halben Lautstärke herauskommt, die du gemeint
|
|
hast. Wenn du mit Musik arbeitest, verdopple die Zahl, die du für einen Effekt im Kopf hattest.
|
|
|
|
Das Stück, das dort geladen wird, ist die eigene Menümusik der Engine, aus dem Ordner `Singe`, den
|
|
die Engine bei deinem ersten Start in deinen Arbeitsordner entpackt hat. Es ist zehn Sekunden lang
|
|
und wurde so geschrieben, dass es am Ende ausblendet, also klingt es in Schleife genau wie eine
|
|
zehnsekündige Schleife, die immer wieder ausblendet. Zum Lernen der Aufrufe ist das in Ordnung,
|
|
und für ein fertiges Spiel taugt es überhaupt nicht, denn das liefert ein längeres eigenes Stück
|
|
mit. Jedes der üblichen Formate geht: Der Abschnitt Musik in der Referenz listet sie auf, und Ogg
|
|
Vorbis oder MP3 sind die vernünftige Wahl, weil ein paar Minuten Musik als WAV eine riesige Datei
|
|
sind.
|
|
|
|
[#_the_game_gets_a_soundtrack]
|
|
=== Das Spiel bekommt einen Soundtrack
|
|
|
|
Öffne das Spiel aus Lektion neun. Alles hier kommt dazu; nichts fliegt raus.
|
|
|
|
Vier Dinge zum Laden, oben, bei den Sprites:
|
|
|
|
[source,lua]
|
|
----
|
|
local shootClip = soundLoad(DIR .. "art/shoot.wav")
|
|
local boomClip = soundLoad(DIR .. "art/boom.wav")
|
|
local clickClip = soundLoad("Singe/click.wav")
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
----
|
|
|
|
Die ersten beiden haben ein `DIR` davor und die letzten beiden nicht, und das ist Absicht. `DIR`
|
|
bedeutet den Ordner *deines* Spiels. Ein Name, der mit `Singe/` anfängt, bedeutet stattdessen den
|
|
eigenen Ordner der Engine, wo auch immer der ist -- und darum funktioniert
|
|
`dofile("Singe/Framework.singe")` seit Lektion sieben ebenfalls ohne `DIR`. Deine eigenen Dateien
|
|
brauchen `DIR`; die der Engine nie.
|
|
|
|
Zwei neue Variablen, neben den anderen:
|
|
|
|
[source,lua]
|
|
----
|
|
local dying = false
|
|
local deathChannel = -1
|
|
----
|
|
|
|
`dying` ist der Moment zwischen dem Verlust des letzten Lebens und dem Ende des Spiels: Die
|
|
Explosion klingt noch, und auf dem Bildschirm bewegt sich nichts.
|
|
|
|
Der Schuss bekommt sein Geräusch in `onInputPressed`, genau dort, wo der Schuss selbst erzeugt
|
|
wird, und der Neustart bekommt einen Klick, damit sich das Drücken von `1` anfühlt, als hätte es
|
|
etwas getan:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if over then
|
|
if what == SWITCH_START1 then
|
|
soundPlay(clickClip)
|
|
startGame()
|
|
end
|
|
return
|
|
end
|
|
if what == SWITCH_LEFT then
|
|
goLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = true
|
|
elseif what == SWITCH_BUTTON1 then
|
|
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
Ein Felsen, der getroffen wird, knallt, und ein Felsen, der dich trifft, auch. In `updateShots`,
|
|
neben den Punkten:
|
|
|
|
[source,lua]
|
|
----
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
----
|
|
|
|
Und in `updateRocks`, wo das Leben verloren geht, der interessante Teil:
|
|
|
|
[source,lua]
|
|
----
|
|
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
local channel = soundPlay(boomClip)
|
|
|
|
newRock(rock)
|
|
lives = lives - 1
|
|
if lives <= 0 then
|
|
dying = true
|
|
deathChannel = channel
|
|
if channel < 0 then
|
|
endGame()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Wenn das letzte Leben weg ist, spielt die Explosion, und ihr Kanal wird gemerkt. Das Spiel ist
|
|
jetzt `dying`, und das hält alle Bewegung an, sagt aber noch nicht GAME OVER.
|
|
|
|
Das `if channel < 0` ist die Grenze von sechzehn Kanälen, erledigt. Falls genau in diesem Moment
|
|
zufällig alle Kanäle belegt waren, gibt es keine Explosion und keinen Kanal, auf den man warten
|
|
könnte, und ohne diese beiden Zeilen säße das Spiel für immer in `dying` fest und wartete auf einen
|
|
Sound, der nie gespielt wurde. Auf etwas zu warten, das nicht kommen kann, ist einer der
|
|
einfachsten Wege, ein Programm aufzuhängen, und die Absicherung ist immer billiger als der Fehler.
|
|
|
|
Jetzt der neue Callback und die kleine Funktion, die er sich mit dieser Absicherung teilt:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
dying = false
|
|
deathChannel = -1
|
|
over = true
|
|
musicStop(theme, 1500)
|
|
end
|
|
|
|
|
|
function onSoundCompleted(channel)
|
|
if channel == deathChannel then
|
|
endGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
Die Engine ruft `onSoundCompleted` für *jeden* Kanal auf, der aufhört, also prüft er als Erstes,
|
|
ob das der Kanal ist, der ihn interessiert. Alles andere -- jeder Schuss, jeder Felsen -- läuft
|
|
durch und wird ignoriert.
|
|
|
|
`onOverlayUpdate` muss stillhalten, solange die Explosion läuft, und das ist ein Wort:
|
|
|
|
[source,lua]
|
|
----
|
|
if not over and not dying then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
`startGame` setzt die beiden neuen Variablen zurück und startet die Musik von vorn:
|
|
|
|
[source,lua]
|
|
----
|
|
dying = false
|
|
deathChannel = -1
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
Und `onShutdown` gibt zurück, was geladen wurde:
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(shipSprite)
|
|
spriteUnload(rockSprite)
|
|
spriteUnload(shotSprite)
|
|
spriteUnload(starSprite)
|
|
soundUnload(shootClip)
|
|
soundUnload(boomClip)
|
|
soundUnload(clickClip)
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
Starte es. Schieß auf die Felsen, lass dich dreimal von einem treffen und hör dir das Ende an: Der
|
|
letzte Knall spielt vollständig aus, und erst wenn er weg ist, fängt die Musik an auszublenden, und
|
|
die Worte erscheinen.
|
|
|
|
Das fertige Skript liegt im Ordner `learn` als `10-sound.singe`, falls du vergleichen willst.
|
|
|
|
[#_try_it_10]
|
|
=== Probier es aus
|
|
|
|
. *Mach den Schuss noch leiser.* Ändere die `35` im `soundPlay` des Schusses in `10`, dann in
|
|
`63`. Finde die Zahl, bei der er unter den Explosionen liegt statt obendrauf.
|
|
. *Dreh die Musik leiser, ohne die Effekte anzufassen.* Ändere `musicSetVolume(50)` in
|
|
`musicSetVolume(15)`. Überzeug dich, dass die Knalle genauso laut sind wie vorher, und genau
|
|
deshalb ist Musik ein eigenes System.
|
|
. *Schneid das Ende ab.* Ändere in `endGame` das `musicStop(theme, 1500)` in `musicStop(theme)`.
|
|
Hör dir beide Enden zweimal an. Der Unterschied ist eine Zahl in einem Argument, und er ist kein
|
|
kleiner Unterschied.
|
|
. *Gib dem Schiff einen Motor.* Starte `soundPlay(clickClip, -1, 12)` in `startGame`, heb den
|
|
Kanal, den es dir gibt, in einer eigenen Variablen auf und halt ihn in `endGame` an. Der Klick
|
|
dauert eine Fünfzigstelsekunde, also ist er in Schleife ein Brummen, und näher kommt dieses Kit
|
|
an einen Motor nicht heran. Dann nimm das `soundStop` wieder heraus, spiel drei Spiele
|
|
hintereinander und hör dir an, was du gebaut hast.
|
|
. *Zähl die Kanäle.* Gib jedes Mal, wenn ein Felsen explodiert, die Kanalnummer aus. Dann erhöh
|
|
`ROCK_COUNT`, bis mehrere auf einmal explodieren, und erhöh weiter, bis du eine `-1`
|
|
vorbeikommen siehst.
|
|
|
|
[#_break_it_on_purpose_9]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Du hast jetzt zwei Lautstärkeskalen im Kopf, und früher oder später kommt die falsche aus deinen
|
|
Fingern. Füg diese Zeile für sich allein ein, direkt unter den vier Zeilen mit `soundLoad` und
|
|
`musicLoad`:
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(100)
|
|
----
|
|
|
|
Das Spiel weigert sich zu starten:
|
|
|
|
----
|
|
20:soundSetVolume: Invalid sound volume value: 100
|
|
----
|
|
|
|
Die Zeilennummer, die du bekommst, ist die Zeile, in die du es tatsächlich getippt hast, deine ist
|
|
also vielleicht nicht 20. Dann kommt die Funktion, die sich beschwert hat, und der Wert, über den
|
|
sie sich beschwert hat. Die Meldung sagt dir nicht, wie groß der Bereich ist -- dafür ist der
|
|
Eintrag zu `soundSetVolume` in der Referenz da -- aber sie sagt dir die Zahl, die sie nicht
|
|
angenommen hat, und sobald du weißt, dass Effekte bei 63 aufhören, ist die Korrektur eine einzige
|
|
Zahl.
|
|
|
|
`100` ist eine verdächtige Zahl für eine Lautstärke, und es lohnt sich zu wissen, warum du sie
|
|
getippt hast. Fast sicher hast du dir Lautstärke als Prozentwert gedacht, weil jeder
|
|
Lautstärkeregler, den du je benutzt hast, bis 100 geht. Der von Singe nicht. Effekte gehen von `0`
|
|
bis `63` und Musik von `0` bis `128`, und keins von beiden ist ein Prozentsatz von irgendetwas.
|
|
|
|
Das ist die allgemeine Form eines Engine-Fehlers, im Gegensatz zu den Lua-Fehlern in Lektion eins:
|
|
eine Zeilennummer, der Name der Engine-Funktion, die du aufgerufen hast, und eine Beschwerde über
|
|
die Werte, die du ihr übergeben hast. Wenn du so einen siehst, ist der Eintrag zu dieser Funktion
|
|
in der Referenz das Nächste, was du liest.
|
|
|
|
[#_what_you_learned_10]
|
|
=== Was du gelernt hast
|
|
|
|
* `soundLoad` liest einen Clip und gibt ein Handle zurück. Lade einmal, oben in der Datei.
|
|
* `soundPlay` startet einen Clip und gibt einen *Kanal* zurück, und das ist eine andere Art von
|
|
Zahl als das Handle.
|
|
* Singe mischt sechzehn Kanäle. Wenn alle belegt sind, gibt `soundPlay` `-1` zurück, und nichts
|
|
spielt.
|
|
* Kanalnummern werden wiederverwendet. Vergiss eine, sobald du ihren Kanal angehalten hast.
|
|
* Kurze Effekte lässt du in Ruhe zu Ende spielen; `soundStop` ist für Sounds, die falsch geworden
|
|
sind.
|
|
* `soundUnload` schneidet einen Clip, der noch spielt, nicht ab.
|
|
* Spiel einen Sound dort ab, wo das Ereignis passiert, nicht dort, wo eine Bedingung wahr ist,
|
|
sonst spielt er sechzigmal pro Sekunde.
|
|
* `onSoundCompleted(channel)` ist ein Callback, den die Engine jedes Mal aufruft, wenn ein Kanal
|
|
still wird, egal aus welchem Grund.
|
|
* Musik ist ein eigenes System mit eigenen Handles, eigener Lautstärke und ohne Kanalgrenze.
|
|
* Die Effektlautstärke geht von `0` bis `63`. Die Musiklautstärke geht von `0` bis `128`. Das sind
|
|
keine Prozentwerte.
|
|
|
|
[#_next_time_10]
|
|
=== Beim nächsten Mal
|
|
|
|
Deine Felsen werden getroffen, wenn sich ihre Rechtecke überlappen, und darum zählt ein Schuss auch
|
|
dann, wenn er durch eine Ecke des Himmels fliegt. In Lektion elf bekommst du die eigenen
|
|
Kollisionstests der Engine, lernst, was eine Hitbox ist, und findest heraus, warum das Rechteck um
|
|
einen runden Felsen die falsche Form hat.
|