singe/docs/de/lessons/10-sound.adoc

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.