610 lines
22 KiB
Text
610 lines
22 KiB
Text
[#_lesson_10_sound]
|
|
== Lezione 10: Suono
|
|
|
|
image::learn/10-sound.png[La lezione finita, 480]
|
|
|
|
Un gioco senza suono sembra rotto, in un modo su cui è difficile mettere il
|
|
dito. I colpi partono e non succede niente. Le rocce si spezzano e non succede
|
|
niente. Metti un rumore di mezzo secondo su ognuna di queste cose e lo stesso
|
|
gioco all'improvviso sembra fatto di qualcosa.
|
|
|
|
Questa lezione dà al tuo gioco uno sparo, un'esplosione e una musica sotto.
|
|
Strada facendo ti presenta la tua quinta callback: quella che il motore usa per
|
|
dirti che un suono ha finito di suonare.
|
|
|
|
[#_one_noise]
|
|
=== Un solo rumore
|
|
|
|
Il kit di grafica che hai copiato nella nona lezione contiene due suoni,
|
|
`shoot.wav` e `boom.wav`, e dei due l'esplosione è quello che si sente meglio.
|
|
Crea un nuovo file accanto alla tua cartella `art` e scrivi questo.
|
|
|
|
[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
|
|
----
|
|
|
|
Eseguilo e premi la barra spaziatrice tre o quattro volte, in fretta. I botti
|
|
si ammucchiano uno sull'altro invece di interrompersi a vicenda.
|
|
|
|
Tenere premuta la barra spaziatrice ti dà esattamente un botto, non una
|
|
raffica. È lo stesso comportamento che hai incontrato nella terza lezione: un
|
|
tasto tenuto premuto è una sola pressione, segnalata una volta, e il motore
|
|
butta via le ripetizioni che manda la tastiera. È per questo che per sterzare
|
|
servono `onInputReleased` e una variabile, invece di contare le pressioni.
|
|
|
|
[#_what_just_happened_6]
|
|
=== Cos'è appena successo
|
|
|
|
La forma è la stessa della nona lezione: carica una volta in cima, usa
|
|
l'handle, restituiscilo alla fine.
|
|
|
|
[source,lua]
|
|
----
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
----
|
|
|
|
`soundLoad` legge un file audio e lo tiene in memoria, e ti restituisce un
|
|
handle -- la stessa idea dell'handle di uno sprite, un numero che per il
|
|
motore vuol dire «quella clip». `DIR` c'è per lo stesso motivo per cui c'era
|
|
nella nona lezione: il suono sta nella cartella del tuo gioco, e il gioco deve
|
|
riuscire a trovarlo da qualunque posto sia stato avviato Singe.
|
|
|
|
Tutto quello che la nona lezione diceva sul caricamento vale qui parola per
|
|
parola. Carica in cima al file, una volta. Un `soundLoad` dentro
|
|
`onOverlayUpdate` carica la clip sessanta volte al secondo e non ne restituisce
|
|
mai nessuna, e qualche minuto dopo il gioco ne muore.
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(boom)
|
|
----
|
|
|
|
Riproducila. Riprodurre un effetto sonoro è tutto qui, e al resto pensa il
|
|
motore: la clip parte, finisce, e non intralcia mai il tuo programma.
|
|
|
|
Ma anche `soundPlay` restituisce qualcosa, e *non* è lo stesso numero che gli
|
|
hai dato. Cambia la riga e guarda:
|
|
|
|
[source,lua]
|
|
----
|
|
local channel = soundPlay(boom)
|
|
debugPrint("clip " .. boom .. " on channel " .. channel)
|
|
----
|
|
|
|
Premi in fretta la barra spaziatrice una mezza dozzina di volte e osserva i due
|
|
numeri. Il numero della clip non cambia mai. Il numero del canale sale -- 0,
|
|
poi 1, poi 2 -- e una volta finiti i botti precedenti torna giù e ricomincia a
|
|
riusare i numeri bassi.
|
|
|
|
Questa è l'unica cosa del suono che frega tutti, quindi vale la pena dirla
|
|
lentamente. *In gioco ci sono due tipi diversi di numero.*
|
|
|
|
L'*handle della clip* che viene da `soundLoad` è la registrazione. Ce n'è uno
|
|
solo. Non cambia, non sta né suonando né non suonando: è il suono che se ne sta
|
|
in memoria in attesa di essere usato.
|
|
|
|
Il *canale* che viene da `soundPlay` è un'esecuzione di quella registrazione.
|
|
Singe ne ha sedici, numerati da `0` a `15`, e ogni suono che si sente in questo
|
|
momento ne usa uno. Ecco perché i botti si sovrappongono: la seconda pressione
|
|
non ha interrotto la prima, ha avviato una seconda esecuzione sul canale libero
|
|
successivo.
|
|
|
|
Tutto ciò che agisce su un *suono che senti* prende un canale: `soundStop`,
|
|
`soundIsPlaying`, `soundPause`, `soundResume`. Tutto ciò che agisce sulla
|
|
*registrazione* prende l'handle: `soundPlay` e `soundUnload`. Passa l'uno dove
|
|
va l'altro e non sempre otterrai un errore, perché `3` è un numero di canale
|
|
valido e anche un handle di clip perfettamente buono -- otterrai silenzio, o il
|
|
suono sbagliato che si ferma, che è molto più difficile da trovare.
|
|
|
|
Sedici sono tanti, ma non sono infiniti. Quando tutti i canali sono occupati
|
|
non suona niente, e `soundPlay` restituisce `-1` invece di un numero di canale.
|
|
Un gioco che fa partire moltissimi suoni insieme dovrebbe aspettarselo, e tra
|
|
un minuto ne scriverai uno.
|
|
|
|
[#_volume]
|
|
=== Volume
|
|
|
|
Ci sono tre volumi, e si moltiplicano tra loro.
|
|
|
|
Il volume *generale* degli effetti copre tutto. `soundGetVolume` lo legge e
|
|
`soundSetVolume` lo imposta, su una scala da `0` a `63`.
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(40)
|
|
----
|
|
|
|
Resisti. Quel numero è del giocatore, non tuo: parte da quello che ha chiesto
|
|
quando ha avviato il gioco, e un gioco che lo sovrascrive è un gioco troppo
|
|
forte nella stanza silenziosa di qualcuno. Cambia il volume generale solo
|
|
quando te lo chiede il giocatore, da una schermata delle opzioni.
|
|
`soundGetVolume` c'è perché una schermata delle opzioni possa mostrare quanto
|
|
vale adesso invece di tirare a indovinare.
|
|
|
|
Quello che dovresti usare è il volume del *canale*, che sta sotto quello
|
|
generale. Dai a `soundPlay` un terzo argomento e questa singola esecuzione
|
|
suona più piano:
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(shootClip, 0, 35)
|
|
----
|
|
|
|
Lo `0` in mezzo è quante volte ripetere, e devi darlo perché il volume viene
|
|
dopo. `0` vuol dire suonala una volta. `-1` vuol dire ripetila finché qualcosa
|
|
non la ferma, ed è così che faresti il rombo di un motore o il vento. La voce
|
|
del Riferimento per `soundPlay` elenca tutti e tre gli argomenti.
|
|
|
|
Il terzo volume è la distanza, per i suoni collocati in un mondo 3D. Quello è
|
|
per la quarta parte, quando il gioco passa alle tre dimensioni.
|
|
|
|
La musica ha un volume tutto suo, ed è su una scala diversa. Ne riparliamo tra
|
|
un momento.
|
|
|
|
[#_stopping_and_not_stopping]
|
|
=== Fermare, e non fermare
|
|
|
|
`soundStop` prende un canale e lo interrompe lì dov'è:
|
|
|
|
[source,lua]
|
|
----
|
|
soundStop(engineChannel)
|
|
engineChannel = -1
|
|
----
|
|
|
|
Nota la seconda riga. Appena un canale si ferma, il motore è libero di dare
|
|
quello stesso numero al `soundPlay` successivo, quindi un numero di canale che
|
|
tieni ancora potrebbe ora appartenere a un suono completamente diverso. Buttalo
|
|
via nel momento stesso in cui lo fermi. `-1` è la cosa utile da metterci,
|
|
perché è lo stesso valore che `soundPlay` ti dà quando non è riuscito a suonare
|
|
affatto, quindi un solo controllo copre tutti e due i casi:
|
|
`if channel >= 0 then`.
|
|
|
|
`soundStop` è per un suono che adesso è sbagliato -- il rumore del motore
|
|
quando il motore muore, l'allarme quando qualcuno ha risposto all'allarme. È lo
|
|
strumento sbagliato per un effetto breve. Un'esplosione di mezzo secondo
|
|
troncata dopo un decimo di secondo sembra un errore, perché lo è.
|
|
|
|
Quindi la maggior parte delle volte non fare proprio niente. Lascialo finire.
|
|
Un effetto sonoro che avvii e a cui non pensi più finisce da solo e libera il
|
|
suo canale da solo, che è esattamente quello che vuoi.
|
|
|
|
Questo vale anche alla fine. `soundUnload` non interrompe una clip che sta
|
|
ancora suonando: il motore tiene vivo l'audio finché non si è fermato l'ultimo
|
|
canale che lo usa. Puoi scaricare in `onShutdown` senza preoccuparti di aver
|
|
zittito qualcosa.
|
|
|
|
`soundIsPlaying(channel)` risponde se un canale sta facendo rumore in questo
|
|
momento. Il suo uso principale non è la curiosità ma la moderazione: un allarme
|
|
che deve suonare una volta, e non ripartire ogni volta che il gioco si accorge
|
|
che il pericolo è ancora lì.
|
|
|
|
[source,lua]
|
|
----
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
Leggi la condizione ad alta voce: suona l'allarme se non c'è un canale
|
|
dell'allarme, o se quello che hai si è fermato. Altrimenti lascialo stare.
|
|
|
|
[#_the_sound_that_stutters]
|
|
=== Il suono che balbetta
|
|
|
|
Ecco l'errore. Lo scrivono tutti, di solito entro una settimana da quando
|
|
scoprono il suono.
|
|
|
|
Supponi di volere che la nave continui a sparare finché il pulsante resta
|
|
premuto, invece di sparare una volta per pressione. Lo faresti come sterzi:
|
|
una variabile `firing` messa a `true` in `onInputPressed` e di nuovo a `false`
|
|
in `onInputReleased`, e il colpo creato in `onOverlayUpdate` finché è vera. È
|
|
una cosa perfettamente sensata da volere, e il rumore sembra stare proprio lì,
|
|
insieme al colpo:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if firing then
|
|
shots[#shots + 1] = { x = shipX, y = shipY }
|
|
soundPlay(shootClip)
|
|
end
|
|
...
|
|
----
|
|
|
|
Quello che esce non è uno sparo. È un ronzio gracchiante, o un tono duro e
|
|
piatto, o un suono come di una piccola macchina che si rompe.
|
|
|
|
`onOverlayUpdate` viene eseguita circa sessanta volte al secondo. Tieni premuto
|
|
il pulsante per un secondo e avrai chiesto sessanta esecuzioni di una clip che
|
|
dura circa un quinto di secondo, ognuna che parte sopra la precedente. In ogni
|
|
momento ne stanno suonando circa undici, ognuna un fotogramma dietro quella
|
|
prima, e quello che senti sono sessanta copie dello stesso attacco ammucchiate l'una sull'altra.
|
|
|
|
La soluzione non è un suono più furbo. È riprodurre il suono dove avviene
|
|
l'*evento* invece che dove lo *stato* è vero:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputPressed` avviene una volta, quando il pulsante va giù. Una pressione,
|
|
un colpo, un rumore.
|
|
|
|
Se vuoi davvero il fuoco continuo, la risposta è la stessa in un'altra forma:
|
|
fai diventare un evento lo sparo stesso. Conta i fotogrammi, esattamente come
|
|
li contava il ciclo della camminata nella nona lezione, e crea un colpo solo
|
|
ogni otto fotogrammi. Il rumore va con il colpo, quindi suona sette o otto
|
|
volte al secondo invece di sessanta, e quella è una mitragliatrice e non un
|
|
gracchiare.
|
|
|
|
Questa è la regola, e vale la pena portarsela via da questa lezione: *i suoni
|
|
vanno con gli eventi, non con le condizioni*. Una pressione, un colpo a segno,
|
|
una morte, un oggetto raccolto -- ognuna di queste cose avviene una volta e
|
|
merita un suono. «Il pulsante è premuto» e «la nave si sta muovendo» non sono
|
|
eventi; sono veri per centinaia di fotogrammi di fila, e qualunque cosa
|
|
riproduci a partire da loro viene riprodotta centinaia di volte.
|
|
|
|
Quando un suono appartiene davvero a uno stato che dura -- un motore, il vento,
|
|
una sirena -- avvialo una volta con `-1` come numero di ripetizioni, tieni il
|
|
canale e fermalo quando lo stato finisce.
|
|
|
|
[#_the_fifth_callback]
|
|
=== La quinta callback
|
|
|
|
A volte ti serve proprio sapere quando un suono è finito. Scrivi questa
|
|
funzione e il motore la chiamerà:
|
|
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
È la tua quinta callback, dopo `onOverlayUpdate` nella prima lezione, le due
|
|
callback dei comandi nella terza e `onShutdown` nella nona, e funziona come tutte le altre: tu la scrivi,
|
|
non la chiami mai, il motore chiama te. Il `channel` che ti passa è il canale
|
|
che si è appena zittito -- il numero che `soundPlay` ti ha dato quando l'hai
|
|
avviato.
|
|
|
|
Scatta quando una clip finisce, quando termina l'ultima ripetizione di un loop,
|
|
*e* quando fermi tu stesso un canale con `soundStop`. Quest'ultimo caso
|
|
sorprende chi usa la callback per concatenare un suono al successivo: fermare
|
|
un canale apposta lo segnala comunque come completato, e se non stai attento il
|
|
suono successivo della catena parte lo stesso.
|
|
|
|
Il gioco sta per usarla per qualcosa che vale la pena fare. Quando se ne va
|
|
l'ultima vita, suona l'esplosione della nave e il gioco resta immobile --
|
|
niente GAME OVER, niente musica che sfuma -- finché quell'esplosione non è
|
|
davvero finita. Poi si ferma tutto insieme. Bastano poche righe, ed è la
|
|
differenza tra un finale e un taglio.
|
|
|
|
[#_music_is_not_a_sound_effect]
|
|
=== La musica non è un effetto sonoro
|
|
|
|
Potresti caricare un motivo di tre minuti con `soundLoad` e riprodurlo con
|
|
`soundPlay`. Non farlo. La musica ha una famiglia di chiamate tutta sua, per
|
|
motivi che contano:
|
|
|
|
* La musica ha un volume suo, separato da quello degli effetti, così un
|
|
giocatore può abbassare la musica e sentire ancora i colpi.
|
|
* Non c'è il limite dei sedici canali e non c'è un numero di canale. Un handle
|
|
è un brano, e lo fermi, lo metti in pausa e lo riprendi tramite quell'handle.
|
|
* `musicStop` può far sfumare un brano in un secondo o due invece di troncarlo.
|
|
|
|
Le chiamate rispecchiano quelle che conosci già:
|
|
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
`musicLoad` dà un handle. `musicPlay` lo avvia, con lo stesso numero di
|
|
ripetizioni di `soundPlay`: `-1` per sempre. `musicStop(theme)` lo termina, e
|
|
`musicStop(theme, 1500)` impiega un secondo e mezzo a farlo sfumare.
|
|
`musicUnload` lo restituisce.
|
|
|
|
Ora la trappola. *Il volume della musica va da 0 a 128, non da 0 a 63.* Gli
|
|
effetti sonori usano una scala e la musica un'altra, lunga il doppio, per
|
|
motivi storici che non ti consolano affatto quando la tua musica esce a metà
|
|
del volume che volevi. Quando lavori con la musica, raddoppia il numero che
|
|
avevi in mente per un effetto.
|
|
|
|
Il brano caricato lì è la musica del menu del motore stesso, presa dalla
|
|
cartella `Singe` che il motore ha estratto nella tua cartella di lavoro al primo
|
|
avvio. Dura dieci secondi ed è stato scritto per sfumare alla fine, quindi
|
|
ripeterlo in loop suona esattamente come un loop di dieci secondi che continua
|
|
a sfumare. Va bene per imparare le chiamate e non va bene per niente in un
|
|
gioco finito, che porta con sé un brano più lungo tutto suo. Va bene uno
|
|
qualunque dei formati soliti: la sezione Musica del Riferimento li elenca, e
|
|
Ogg Vorbis o MP3 sono le scelte sensate, perché qualche minuto di musica in WAV
|
|
è un file enorme.
|
|
|
|
[#_the_game_gets_a_soundtrack]
|
|
=== Il gioco ha una colonna sonora
|
|
|
|
Apri il gioco della nona lezione. Tutto quello che c'è qui gli si aggiunge; non
|
|
si toglie niente.
|
|
|
|
Quattro cose da caricare, in cima, insieme agli sprite:
|
|
|
|
[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")
|
|
----
|
|
|
|
I primi due hanno `DIR` e gli ultimi due no, ed è voluto. `DIR` vuol dire la
|
|
cartella del *tuo* gioco. Un nome che comincia con `Singe/` vuol dire invece la
|
|
cartella del motore, dovunque si trovi -- ed è per questo che
|
|
`dofile("Singe/Framework.singe")` funziona dalla settima lezione anche lui
|
|
senza `DIR`. I tuoi file hanno bisogno di `DIR`; quelli del motore mai.
|
|
|
|
Due nuove variabili, accanto alle altre:
|
|
|
|
[source,lua]
|
|
----
|
|
local dying = false
|
|
local deathChannel = -1
|
|
----
|
|
|
|
`dying` è il momento tra la perdita dell'ultima vita e la fine della partita:
|
|
l'esplosione si sente ancora, e sullo schermo non si muove niente.
|
|
|
|
Il colpo prende il suo rumore in `onInputPressed`, proprio dove viene creato il
|
|
colpo stesso, e la ripartenza prende un clic, così premere `1` dà l'impressione
|
|
di aver fatto qualcosa:
|
|
|
|
[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
|
|
----
|
|
|
|
Una roccia colpita fa un botto, e così una roccia che colpisce te. In
|
|
`updateShots`, accanto al punteggio:
|
|
|
|
[source,lua]
|
|
----
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
----
|
|
|
|
E in `updateRocks`, dove si perde la vita, la parte interessante:
|
|
|
|
[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
|
|
----
|
|
|
|
Quando se ne va l'ultima vita, l'esplosione suona e il suo canale viene
|
|
ricordato. Il gioco ora è `dying`, cosa che ferma ogni movimento ma non dice
|
|
ancora GAME OVER.
|
|
|
|
Quel `if channel < 0` è il limite dei sedici canali, gestito. Se proprio in
|
|
quell'istante tutti i canali fossero occupati, non ci sarebbero né
|
|
un'esplosione né un canale da aspettare, e senza quelle due righe il gioco
|
|
resterebbe in `dying` per sempre, ad aspettare un suono mai partito. Aspettare
|
|
qualcosa che non può arrivare è uno dei modi più facili per bloccare un
|
|
programma, e il controllo costa sempre meno del bug.
|
|
|
|
Ora la nuova callback, e la piccola funzione che condivide con quel controllo:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
dying = false
|
|
deathChannel = -1
|
|
over = true
|
|
musicStop(theme, 1500)
|
|
end
|
|
|
|
|
|
function onSoundCompleted(channel)
|
|
if channel == deathChannel then
|
|
endGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
Il motore chiama `onSoundCompleted` per *ogni* canale che si ferma, quindi la
|
|
prima cosa che fa è controllare se questo è il canale che le interessa. Tutto
|
|
il resto -- ogni colpo, ogni roccia -- passa oltre e viene ignorato.
|
|
|
|
`onOverlayUpdate` deve restare immobile mentre l'esplosione va avanti, e basta
|
|
una parola:
|
|
|
|
[source,lua]
|
|
----
|
|
if not over and not dying then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
`startGame` azzera le due nuove variabili e fa ripartire la musica da capo:
|
|
|
|
[source,lua]
|
|
----
|
|
dying = false
|
|
deathChannel = -1
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
E `onShutdown` restituisce quello che era stato caricato:
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(shipSprite)
|
|
spriteUnload(rockSprite)
|
|
spriteUnload(shotSprite)
|
|
spriteUnload(starSprite)
|
|
soundUnload(shootClip)
|
|
soundUnload(boomClip)
|
|
soundUnload(clickClip)
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
Eseguilo. Spara alle rocce, fatti colpire tre volte e ascolta la fine: l'ultimo
|
|
botto suona fino in fondo, e solo quando è finito la musica comincia a sfumare
|
|
e compaiono le parole.
|
|
|
|
Lo script finito si trova nella cartella `learn` come `10-sound.singe`, se vuoi
|
|
fare un confronto.
|
|
|
|
[#_try_it_10]
|
|
=== Prova tu
|
|
|
|
. *Rendi il colpo ancora più piano.* Cambia il `35` nel `soundPlay` del colpo
|
|
in `10`, poi in `63`. Trova il numero in cui sta sotto le esplosioni invece
|
|
che sopra.
|
|
. *Abbassa la musica senza toccare gli effetti.* Cambia `musicSetVolume(50)` in
|
|
`musicSetVolume(15)`. Verifica che i botti siano forti esattamente quanto
|
|
prima, che è tutto il motivo per cui la musica è un sistema a parte.
|
|
. *Taglia il finale.* In `endGame`, cambia `musicStop(theme, 1500)` in
|
|
`musicStop(theme)`. Ascolta entrambi i finali due volte. La differenza è un
|
|
numero in un argomento, e non è una piccola differenza.
|
|
. *Dai un motore alla nave.* Avvia `soundPlay(clickClip, -1, 12)` in
|
|
`startGame`, tieni il canale che ti dà in una variabile tutta sua e fermalo
|
|
in `endGame`. Il clic dura un cinquantesimo di secondo, quindi in loop
|
|
diventa un ronzio, che è quanto di più vicino a un motore offra questo kit.
|
|
Poi togli di nuovo il `soundStop`, gioca tre partite di fila e ascolta che
|
|
cosa hai costruito.
|
|
. *Conta i canali.* Stampa il numero del canale ogni volta che una roccia
|
|
esplode. Poi aumenta `ROCK_COUNT` finché non ne esplodono parecchie insieme,
|
|
e continua ad aumentarlo finché non vedi passare un `-1`.
|
|
|
|
[#_break_it_on_purpose_9]
|
|
=== Rompilo apposta
|
|
|
|
Ormai hai in testa due scale di volume, e prima o poi dalle dita esce quella
|
|
sbagliata. Aggiungi questa riga da sola, subito sotto le quattro righe
|
|
`soundLoad` e `musicLoad`:
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(100)
|
|
----
|
|
|
|
Il gioco si rifiuta di partire:
|
|
|
|
----
|
|
20:soundSetVolume: Invalid sound volume value: 100
|
|
----
|
|
|
|
Il numero di riga che ottieni è la riga in cui l'hai scritta davvero, quindi il
|
|
tuo potrebbe non essere 20. Poi la funzione che ha protestato, e il valore
|
|
contro cui ha protestato. Il messaggio non ti dice qual è l'intervallo -- a
|
|
quello serve la voce di `soundSetVolume` nel Riferimento -- ma ti dice il
|
|
numero che non ha voluto accettare, e una volta che sai che gli effetti si
|
|
fermano a 63 la correzione è un numero solo.
|
|
|
|
`100` è un numero sospetto da trovare in un volume, e vale la pena sapere
|
|
perché l'hai scritto. Quasi di sicuro hai pensato al volume come a una
|
|
percentuale, perché ogni cursore del volume che tu abbia mai usato arriva a
|
|
100. Quello di Singe no. Gli effetti vanno da `0` a `63` e la musica da `0` a
|
|
`128`, e nessuno dei due è una percentuale di qualcosa.
|
|
|
|
Questa è la forma generale di un errore del motore, a differenza degli errori
|
|
di Lua della prima lezione: un numero di riga, il nome della funzione del
|
|
motore che hai chiamato e una lamentela sui valori che le hai passato. Quando ne
|
|
vedi uno, la voce di quella funzione nel Riferimento è la prossima cosa da
|
|
leggere.
|
|
|
|
[#_what_you_learned_10]
|
|
=== Cosa hai imparato
|
|
|
|
* `soundLoad` legge una clip e restituisce un handle. Carica una volta, in cima
|
|
al file.
|
|
* `soundPlay` avvia una clip e restituisce un *canale*, che è un tipo di numero
|
|
diverso dall'handle.
|
|
* Singe mescola sedici canali. Quando sono tutti occupati, `soundPlay`
|
|
restituisce `-1` e non suona niente.
|
|
* I numeri dei canali vengono riusati. Dimenticane uno appena l'hai fermato.
|
|
* Gli effetti brevi vanno lasciati finire da soli; `soundStop` è per i suoni
|
|
che sono diventati sbagliati.
|
|
* `soundUnload` non interrompe una clip che sta ancora suonando.
|
|
* Riproduci un suono dove avviene l'evento, non dove una condizione è vera,
|
|
altrimenti suona sessanta volte al secondo.
|
|
* `onSoundCompleted(channel)` è una callback che il motore chiama ogni volta
|
|
che un canale si zittisce, per qualunque motivo.
|
|
* La musica è un sistema separato, con i suoi handle, il suo volume e nessun
|
|
limite di canali.
|
|
* Il volume degli effetti va da `0` a `63`. Il volume della musica va da `0` a
|
|
`128`. Non sono percentuali.
|
|
|
|
[#_next_time_10]
|
|
=== La prossima volta
|
|
|
|
Le tue rocce vengono colpite quando i loro rettangoli si sovrappongono, ed è
|
|
per questo che un colpo che passa per un angolo di cielo conta lo stesso.
|
|
Nell'undicesima lezione ottieni i controlli di collisione del motore, impari
|
|
che cos'è una hitbox e scopri perché il rettangolo attorno a una roccia
|
|
rotonda ha la forma sbagliata.
|