623 lines
24 KiB
Text
623 lines
24 KiB
Text
[#_lesson_10_sound]
|
||
== Leçon 10 : Le son
|
||
|
||
image::learn/10-sound.png[La leçon terminée, 480]
|
||
|
||
Un jeu sans son a l'air cassé, d'une façon difficile à cerner. Les tirs partent
|
||
et rien ne se passe. Les rochers éclatent et rien ne se passe. Mettez un bruit
|
||
d'une demi-seconde sur chacun de ces moments et, tout à coup, le même jeu semble
|
||
fait de quelque chose.
|
||
|
||
Cette leçon donne à votre jeu un tir, une explosion et de la musique en dessous.
|
||
En chemin, elle vous présente votre cinquième callback : celui dont le moteur se
|
||
sert pour vous dire qu'un son a fini de jouer.
|
||
|
||
[#_one_noise]
|
||
=== Un bruit
|
||
|
||
Le kit d'illustrations que vous avez copié à la leçon neuf contient deux sons,
|
||
`shoot.wav` et `boom.wav`, et l'explosion est le plus facile des deux à
|
||
entendre. Créez un nouveau fichier à côté de votre dossier `art` et tapez ceci.
|
||
|
||
[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
|
||
----
|
||
|
||
Lancez-le et tapez rapidement trois ou quatre fois sur la barre d'espace. Les
|
||
détonations s'empilent les unes sur les autres au lieu de se couper la parole.
|
||
|
||
Garder la barre d'espace enfoncée vous donne exactement une détonation, pas une
|
||
rafale. C'est le même comportement que celui que vous avez rencontré à la leçon
|
||
trois : une touche tenue, c'est un seul appui, signalé une fois, et le moteur
|
||
jette les répétitions que votre clavier envoie. C'est pour ça que diriger le
|
||
vaisseau demande `onInputReleased` et une variable, plutôt que de compter les
|
||
appuis.
|
||
|
||
[#_what_just_happened_6]
|
||
=== Ce qui vient de se passer
|
||
|
||
La forme est la même qu'à la leçon neuf : chargez une fois en haut, servez-vous
|
||
du handle, rendez-le à la fin.
|
||
|
||
[source,lua]
|
||
----
|
||
local boom = soundLoad(DIR .. "art/boom.wav")
|
||
----
|
||
|
||
`soundLoad` lit un fichier audio, le garde en mémoire et vous renvoie un
|
||
handle -- la même idée qu'un handle de sprite, un nombre qui veut dire « ce
|
||
son-là » pour le moteur. `DIR` est là pour la même raison qu'à la leçon neuf :
|
||
le son se trouve dans le dossier de votre jeu, et le jeu doit pouvoir le
|
||
trouver, d'où que Singe ait été lancé.
|
||
|
||
Tout ce que la leçon neuf a dit du chargement s'applique ici mot pour mot.
|
||
Chargez en haut du fichier, une fois. Un `soundLoad` dans `onOverlayUpdate`
|
||
charge le son soixante fois par seconde sans jamais en rendre un seul, et le
|
||
jeu en meurt quelques minutes plus tard.
|
||
|
||
[source,lua]
|
||
----
|
||
soundPlay(boom)
|
||
----
|
||
|
||
Jouez-le. C'est tout ce qu'il faut pour jouer un effet sonore, et le moteur
|
||
s'occupe du reste : le son démarre, il se termine, et il ne gêne jamais votre
|
||
programme.
|
||
|
||
Mais `soundPlay` renvoie quelque chose, lui aussi, et ce n'est *pas* le nombre
|
||
que vous lui avez donné. Changez la ligne et regardez :
|
||
|
||
[source,lua]
|
||
----
|
||
local channel = soundPlay(boom)
|
||
debugPrint("clip " .. boom .. " on channel " .. channel)
|
||
----
|
||
|
||
Tapez rapidement une demi-douzaine de fois sur la barre d'espace et observez
|
||
les deux nombres. Le numéro du son ne change jamais. Le numéro de canal
|
||
grimpe -- 0, puis 1, puis 2 -- et une fois que les premières détonations sont
|
||
finies, il redescend et se remet à réutiliser les petits numéros.
|
||
|
||
C'est la chose qui piège tout le monde avec le son, alors elle mérite d'être
|
||
dite lentement. *Deux sortes de nombres différentes sont en jeu.*
|
||
|
||
Le *handle du son*, celui que donne `soundLoad`, c'est l'enregistrement. Il n'y
|
||
en a qu'un. Il ne change pas, il n'est ni en train de jouer ni arrêté, c'est le
|
||
son posé en mémoire, qui attend qu'on s'en serve.
|
||
|
||
Le *canal* que donne `soundPlay`, c'est une lecture de cet enregistrement.
|
||
Singe en a seize, numérotés de `0` à `15`, et chaque son qu'on entend en ce
|
||
moment en utilise un. Voilà pourquoi les détonations se chevauchent : le
|
||
deuxième appui n'a pas interrompu le premier, il a lancé une deuxième lecture
|
||
sur le canal libre suivant.
|
||
|
||
Tout ce qui agit sur un *son que vous entendez* prend un canal : `soundStop`,
|
||
`soundIsPlaying`, `soundPause`, `soundResume`. Tout ce qui agit sur
|
||
l'*enregistrement* prend le handle : `soundPlay` et `soundUnload`. Donnez l'un
|
||
là où l'autre est attendu et vous n'aurez pas toujours une erreur, parce que `3`
|
||
est un numéro de canal valable et aussi un handle de son tout à fait
|
||
correct -- vous aurez du silence, ou le mauvais son qui s'arrête, ce qui est
|
||
bien plus difficile à trouver.
|
||
|
||
Seize, c'est beaucoup, mais ce n'est pas infini. Quand tous les canaux sont
|
||
occupés, rien ne joue et `soundPlay` renvoie `-1` au lieu d'un numéro de canal.
|
||
Un jeu qui lance énormément de sons à la fois doit s'y attendre, et vous allez
|
||
en écrire un dans une minute.
|
||
|
||
[#_volume]
|
||
=== Le volume
|
||
|
||
Il y a trois volumes, et ils se multiplient entre eux.
|
||
|
||
Le volume *général* des effets couvre tout. `soundGetVolume` le lit et
|
||
`soundSetVolume` le règle, sur une échelle de `0` à `63`.
|
||
|
||
[source,lua]
|
||
----
|
||
soundSetVolume(40)
|
||
----
|
||
|
||
Résistez. Ce nombre appartient au joueur, pas à vous : il part de ce que le
|
||
joueur a demandé en lançant le jeu, et un jeu qui l'écrase est un jeu trop fort
|
||
dans la pièce tranquille de quelqu'un. Ne changez le volume général que si le
|
||
joueur vous le demande, depuis un écran d'options. `soundGetVolume` est là pour
|
||
qu'un écran d'options puisse afficher sa valeur actuelle au lieu de la deviner.
|
||
|
||
Ce dont vous devriez vous servir, c'est le volume du *canal*, qui se place sous
|
||
le volume général. Donnez un troisième argument à `soundPlay` et cette
|
||
lecture-là est plus douce :
|
||
|
||
[source,lua]
|
||
----
|
||
soundPlay(shootClip, 0, 35)
|
||
----
|
||
|
||
Le `0` du milieu est le nombre de répétitions, que vous devez donner parce que
|
||
le volume vient après. `0` veut dire jouer une fois. `-1` veut dire boucler
|
||
jusqu'à ce que quelque chose l'arrête, et c'est comme ça que vous feriez le
|
||
ronronnement d'un moteur ou le vent. L'entrée de la Référence pour `soundPlay`
|
||
donne les trois arguments.
|
||
|
||
Le troisième volume, c'est la distance, pour les sons placés dans un monde en
|
||
3D. Ce sera pour la quatrième partie, quand le jeu passera en trois dimensions.
|
||
|
||
La musique a son propre volume, et il est sur une autre échelle. On y revient
|
||
dans un instant.
|
||
|
||
[#_stopping_and_not_stopping]
|
||
=== Arrêter, et ne pas arrêter
|
||
|
||
`soundStop` prend un canal et le coupe là où il en est :
|
||
|
||
[source,lua]
|
||
----
|
||
soundStop(engineChannel)
|
||
engineChannel = -1
|
||
----
|
||
|
||
Remarquez la deuxième ligne. Dès qu'un canal s'arrête, le moteur est libre de
|
||
donner ce même numéro au `soundPlay` suivant, si bien qu'un numéro de canal que
|
||
vous gardez encore peut maintenant appartenir à un tout autre son. Jetez-le dès
|
||
que vous l'arrêtez. `-1` est la bonne chose à mettre à sa place, parce que
|
||
c'est la même valeur que celle que vous donne `soundPlay` quand il n'a rien pu
|
||
jouer du tout, et un seul test couvre les deux cas : `if channel >= 0 then`.
|
||
|
||
`soundStop`, c'est pour un son qui n'a plus lieu d'être -- le bruit du moteur
|
||
quand le moteur meurt, l'alarme quand quelqu'un a répondu à l'alarme. Ce n'est
|
||
pas le bon outil pour un effet court. Une explosion d'une demi-seconde coupée au
|
||
bout d'un dixième de seconde sonne comme une erreur, parce que c'en est une.
|
||
|
||
Alors, la plupart du temps, ne faites rien du tout. Laissez-le finir. Un effet
|
||
sonore que vous lancez et auquel vous ne pensez plus se termine tout seul et
|
||
libère son canal tout seul, et c'est exactement ce que vous voulez.
|
||
|
||
C'est vrai même à la fin. `soundUnload` ne coupe pas un son qui est encore en
|
||
train de jouer : le moteur garde l'audio en vie jusqu'à ce que le dernier canal
|
||
qui s'en sert se soit arrêté. Vous pouvez décharger dans `onShutdown` sans
|
||
craindre d'avoir fait taire quelque chose.
|
||
|
||
`soundIsPlaying(channel)` répond à la question : ce canal fait-il du bruit en
|
||
ce moment ? Son usage principal n'est pas la curiosité mais la retenue : une
|
||
alarme qui doit sonner une fois, et ne pas redémarrer chaque fois que le jeu
|
||
remarque que le danger est toujours là.
|
||
|
||
[source,lua]
|
||
----
|
||
function raiseAlarm()
|
||
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
||
alarmChannel = soundPlay(alarmClip)
|
||
end
|
||
end
|
||
----
|
||
|
||
Lisez la condition à voix haute : jouez l'alarme s'il n'y a pas de canal
|
||
d'alarme, ou si celui que vous avez s'est arrêté. Sinon, laissez-la tranquille.
|
||
|
||
[#_the_sound_that_stutters]
|
||
=== Le son qui bégaie
|
||
|
||
Voici l'erreur. Tout le monde l'écrit, en général dans la semaine qui suit sa
|
||
découverte du son.
|
||
|
||
Supposons que vous vouliez que le vaisseau continue de tirer tant que le bouton
|
||
est enfoncé, plutôt qu'une fois par appui. Vous le feriez comme pour diriger :
|
||
une variable `firing` mise à `true` dans `onInputPressed` et remise à `false`
|
||
dans `onInputReleased`, et le tir créé dans `onOverlayUpdate` tant qu'elle est
|
||
vraie. C'est une envie parfaitement sensée, et le bruit a tout l'air d'avoir sa
|
||
place juste là, avec le tir :
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
if firing then
|
||
shots[#shots + 1] = { x = shipX, y = shipY }
|
||
soundPlay(shootClip)
|
||
end
|
||
...
|
||
----
|
||
|
||
Ce qui en sort n'est pas un tir. C'est un crépitement qui bourdonne, ou une
|
||
note dure et plate, ou le bruit d'une petite machine en train de casser.
|
||
|
||
`onOverlayUpdate` s'exécute environ soixante fois par seconde. Tenez le bouton
|
||
une seconde et vous avez demandé soixante lectures d'un son qui dure environ un
|
||
cinquième de seconde, chacune démarrant par-dessus la précédente. Environ onze
|
||
d'entre elles jouent à chaque instant, chacune une image derrière la précédente,
|
||
et ce que vous entendez, ce sont soixante copies de la même attaque empilées les
|
||
unes sur les autres.
|
||
|
||
La solution n'est pas un son plus malin. C'est de jouer le son là où
|
||
l'*événement* se produit plutôt que là où l'*état* est vrai :
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_BUTTON1 then
|
||
soundPlay(shootClip, 0, 35)
|
||
end
|
||
end
|
||
----
|
||
|
||
`onInputPressed` se produit une fois, quand le bouton s'enfonce. Un appui, un
|
||
tir, un bruit.
|
||
|
||
Si vous voulez vraiment un tir continu, la réponse est la même sous une autre
|
||
forme : faites du tir lui-même un événement. Comptez les images, exactement
|
||
comme le cycle de marche les comptait à la leçon neuf, et ne créez un tir
|
||
qu'une image sur huit. Le bruit va avec le tir, donc il joue sept ou huit fois
|
||
par seconde au lieu de soixante, et ça, c'est une mitrailleuse plutôt qu'un
|
||
crépitement.
|
||
|
||
Voilà la règle, et elle mérite de sortir de cette leçon avec vous : *les sons
|
||
vont avec les événements, pas avec les conditions*. Un appui, un coup, une
|
||
mort, un objet ramassé -- chacun arrive une fois et mérite un son. « Le bouton
|
||
est enfoncé » et « le vaisseau bouge » ne sont pas des événements ; ils sont
|
||
vrais pendant des centaines d'images d'affilée, et tout ce que vous jouez à
|
||
partir d'eux joue des centaines de fois.
|
||
|
||
Quand un son appartient vraiment à un état qui dure -- un moteur, le vent, une
|
||
sirène -- lancez-le une fois avec `-1` comme nombre de répétitions, gardez le
|
||
canal, et arrêtez-le quand l'état prend fin.
|
||
|
||
[#_the_fifth_callback]
|
||
=== Le cinquième callback
|
||
|
||
Parfois, vous avez vraiment besoin de savoir quand un son est fini. Écrivez
|
||
cette fonction et le moteur l'appellera :
|
||
|
||
[source,lua]
|
||
----
|
||
function onSoundCompleted(channel)
|
||
end
|
||
----
|
||
|
||
C'est votre cinquième callback, après `onOverlayUpdate` à la leçon un, les deux
|
||
callbacks des commandes à la leçon trois et `onShutdown` à la leçon neuf, et il
|
||
marche comme tous les autres : vous l'écrivez, vous ne l'appelez jamais, c'est le moteur qui vous
|
||
appelle. Le `channel` qu'il vous passe est le canal qui vient de se taire -- le
|
||
numéro que `soundPlay` vous a donné quand vous l'avez lancé.
|
||
|
||
Il est appelé quand un son arrive au bout, quand la dernière répétition d'une
|
||
boucle se termine, *et* quand vous arrêtez un canal vous-même avec `soundStop`.
|
||
Ce dernier cas surprend ceux qui se servent du callback pour enchaîner un son
|
||
sur le suivant : arrêter un canal exprès le signale quand même comme terminé,
|
||
et si vous n'y prenez pas garde, le son suivant de la chaîne démarre malgré
|
||
tout.
|
||
|
||
Le jeu va s'en servir pour quelque chose qui en vaut la peine. Quand la
|
||
dernière vie s'en va, l'explosion du vaisseau joue, et le jeu reste immobile --
|
||
pas de GAME OVER, pas de musique qui s'éteint -- jusqu'à ce que cette explosion
|
||
soit vraiment finie. Ensuite, tout s'arrête d'un coup. Ça tient en une poignée
|
||
de lignes, et c'est la différence entre une fin et une coupure.
|
||
|
||
[#_music_is_not_a_sound_effect]
|
||
=== La musique n'est pas un effet sonore
|
||
|
||
Vous pourriez charger un air de trois minutes avec `soundLoad` et le jouer avec
|
||
`soundPlay`. Ne le faites pas. La musique a sa propre famille d'appels, pour des
|
||
raisons qui comptent :
|
||
|
||
* La musique a son propre volume, séparé de celui des effets, si bien qu'un
|
||
joueur peut baisser la musique et entendre encore les tirs.
|
||
* Il n'y a pas de limite de seize canaux, ni de numéro de canal. Un handle,
|
||
c'est un morceau de musique, et c'est par ce handle que vous l'arrêtez, le
|
||
mettez en pause et le reprenez.
|
||
* `musicStop` peut faire disparaître un morceau en fondu sur une seconde ou deux
|
||
au lieu de le couper.
|
||
|
||
Les appels sont le reflet de ceux que vous connaissez déjà :
|
||
|
||
[source,lua]
|
||
----
|
||
local theme = musicLoad("Singe/menuIntro.flac")
|
||
|
||
musicSetVolume(50)
|
||
musicPlay(theme, -1)
|
||
----
|
||
|
||
`musicLoad` donne un handle. `musicPlay` le lance, avec le même nombre de
|
||
répétitions que `soundPlay` : `-1` pour toujours. `musicStop(theme)` l'arrête,
|
||
et `musicStop(theme, 1500)` prend une seconde et demie pour le faire disparaître
|
||
en fondu. `musicUnload` le rend.
|
||
|
||
Maintenant, le piège. *Le volume de la musique va de 0 à 128, pas de 0 à 63.*
|
||
Les effets sonores utilisent une échelle et la musique une autre, deux fois plus
|
||
longue, pour des raisons historiques qui ne consolent de rien quand votre
|
||
musique sort à la moitié du volume que vous vouliez. Quand vous travaillez avec
|
||
la musique, doublez le nombre que vous auriez choisi pour un effet.
|
||
|
||
Le morceau chargé ici est la musique de menu du moteur lui-même, tirée du
|
||
dossier `Singe` que le moteur a déballé dans votre dossier de travail lors de
|
||
votre premier lancement. Il dure dix secondes et il a été écrit pour s'éteindre
|
||
en fondu à la fin, donc en boucle il sonne exactement comme une boucle de dix
|
||
secondes qui n'arrête pas de s'éteindre. C'est très bien pour apprendre les
|
||
appels et ça ne vaut rien du tout pour un jeu fini, qui embarque un morceau plus
|
||
long bien à lui. N'importe lequel des formats habituels fera l'affaire : la
|
||
section Musique de la Référence les donne, et Ogg Vorbis ou MP3 sont les choix
|
||
raisonnables, parce que quelques minutes de musique en WAV font un fichier
|
||
énorme.
|
||
|
||
[#_the_game_gets_a_soundtrack]
|
||
=== Le jeu reçoit une bande-son
|
||
|
||
Ouvrez le jeu de la leçon neuf. Tout ce qui suit s'y ajoute ; rien n'en est
|
||
retiré.
|
||
|
||
Quatre choses à charger, en haut, avec les 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")
|
||
----
|
||
|
||
Les deux premières portent `DIR` et les deux dernières non, et c'est voulu.
|
||
`DIR` veut dire le dossier de *votre* jeu. Un nom qui commence par `Singe/` veut
|
||
dire à la place le dossier du moteur lui-même, où qu'il soit -- c'est pourquoi
|
||
`dofile("Singe/Framework.singe")` marche depuis la leçon sept sans `DIR` lui
|
||
non plus. Vos propres fichiers ont besoin de `DIR` ; ceux du moteur, jamais.
|
||
|
||
Deux nouvelles variables, à côté des autres :
|
||
|
||
[source,lua]
|
||
----
|
||
local dying = false
|
||
local deathChannel = -1
|
||
----
|
||
|
||
`dying`, c'est le moment entre la perte de la dernière vie et la fin de la
|
||
partie : l'explosion résonne encore, et rien ne bouge à l'écran.
|
||
|
||
Le tir reçoit son bruit dans `onInputPressed`, à l'endroit même où le tir est
|
||
créé, et le redémarrage reçoit un clic, pour qu'appuyer sur `1` donne
|
||
l'impression d'avoir fait quelque chose :
|
||
|
||
[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
|
||
----
|
||
|
||
Un rocher abattu fait boum, et un rocher qui vous percute aussi. Dans
|
||
`updateShots`, à côté du score :
|
||
|
||
[source,lua]
|
||
----
|
||
newRock(rock)
|
||
score = score + HIT_SCORE
|
||
gone = true
|
||
soundPlay(boomClip, 0, 45)
|
||
----
|
||
|
||
Et dans `updateRocks`, là où la vie est perdue, la partie intéressante :
|
||
|
||
[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
|
||
----
|
||
|
||
Quand la dernière vie s'en va, l'explosion joue et son canal est mémorisé. Le
|
||
jeu est maintenant `dying`, ce qui arrête tout mouvement mais n'affiche pas
|
||
encore GAME OVER.
|
||
|
||
Le `if channel < 0`, c'est la limite des seize canaux, prise en compte. Si tous
|
||
les canaux se trouvaient occupés à cet instant précis, il n'y a ni explosion ni
|
||
canal à attendre, et sans ces deux lignes le jeu resterait dans `dying` pour
|
||
toujours, à attendre un son qui n'a jamais joué. Attendre quelque chose qui ne
|
||
peut pas arriver est l'une des façons les plus simples de bloquer un programme,
|
||
et le garde-fou coûte toujours moins cher que le bogue.
|
||
|
||
Maintenant, le nouveau callback, et la petite fonction qu'il partage avec ce
|
||
garde-fou :
|
||
|
||
[source,lua]
|
||
----
|
||
function endGame()
|
||
dying = false
|
||
deathChannel = -1
|
||
over = true
|
||
musicStop(theme, 1500)
|
||
end
|
||
|
||
|
||
function onSoundCompleted(channel)
|
||
if channel == deathChannel then
|
||
endGame()
|
||
end
|
||
end
|
||
----
|
||
|
||
Le moteur appelle `onSoundCompleted` pour *chaque* canal qui s'arrête, alors la
|
||
première chose que fait la fonction, c'est vérifier si celui-ci est le canal qui
|
||
l'intéresse. Tout le reste -- chaque tir, chaque rocher -- passe et est ignoré.
|
||
|
||
`onOverlayUpdate` doit rester immobile pendant l'explosion, et ça tient en un
|
||
mot :
|
||
|
||
[source,lua]
|
||
----
|
||
if not over and not dying then
|
||
updateShip()
|
||
updateShots()
|
||
updateRocks()
|
||
end
|
||
----
|
||
|
||
`startGame` remet à zéro les deux nouvelles variables et relance la musique
|
||
depuis le début :
|
||
|
||
[source,lua]
|
||
----
|
||
dying = false
|
||
deathChannel = -1
|
||
musicSetVolume(50)
|
||
musicPlay(theme, -1)
|
||
----
|
||
|
||
Et `onShutdown` rend ce qui a été chargé :
|
||
|
||
[source,lua]
|
||
----
|
||
function onShutdown()
|
||
spriteUnload(shipSprite)
|
||
spriteUnload(rockSprite)
|
||
spriteUnload(shotSprite)
|
||
spriteUnload(starSprite)
|
||
soundUnload(shootClip)
|
||
soundUnload(boomClip)
|
||
soundUnload(clickClip)
|
||
musicUnload(theme)
|
||
end
|
||
----
|
||
|
||
Lancez-le. Tirez sur les rochers, laissez-vous toucher trois fois, et écoutez
|
||
la fin : la dernière détonation joue jusqu'au bout, et c'est seulement quand
|
||
elle s'est tue que la musique commence à s'éteindre et que les mots apparaissent.
|
||
|
||
Le script terminé se trouve dans le dossier `learn` sous le nom
|
||
`10-sound.singe`, si vous voulez comparer.
|
||
|
||
[#_try_it_10]
|
||
=== Essayez
|
||
|
||
. *Rendez le tir encore plus discret.* Changez le `35` du `soundPlay` du tir en
|
||
`10`, puis en `63`. Trouvez le nombre où il passe sous les explosions au lieu
|
||
de passer par-dessus.
|
||
. *Baissez la musique sans toucher aux effets.* Changez `musicSetVolume(50)` en
|
||
`musicSetVolume(15)`. Vérifiez que les détonations sont exactement aussi
|
||
fortes qu'avant, ce qui est toute la raison pour laquelle la musique est un
|
||
système à part.
|
||
. *Coupez la fin.* Dans `endGame`, changez `musicStop(theme, 1500)` en
|
||
`musicStop(theme)`. Écoutez les deux fins deux fois. La différence tient à un
|
||
nombre dans un argument, et ce n'est pas une petite différence.
|
||
. *Donnez un moteur au vaisseau.* Lancez `soundPlay(clickClip, -1, 12)` dans
|
||
`startGame`, gardez le canal qu'il vous donne dans une variable à lui, et
|
||
arrêtez-le dans `endGame`. Le clic dure un cinquantième de seconde, donc en
|
||
boucle c'est un bourdonnement, ce qui est ce que ce kit a de plus proche d'un
|
||
moteur. Puis retirez de nouveau le `soundStop`, jouez trois parties
|
||
d'affilée, et écoutez ce que vous avez construit.
|
||
. *Comptez les canaux.* Affichez le numéro de canal chaque fois qu'un rocher
|
||
explose. Puis augmentez `ROCK_COUNT` jusqu'à ce que plusieurs explosent en
|
||
même temps, et continuez de l'augmenter jusqu'à voir passer un `-1`.
|
||
|
||
[#_break_it_on_purpose_9]
|
||
=== Cassez-le exprès
|
||
|
||
Vous avez maintenant deux échelles de volume en tête, et tôt ou tard la mauvaise
|
||
sortira de vos doigts. Ajoutez cette ligne toute seule, juste sous les quatre
|
||
lignes `soundLoad` et `musicLoad` :
|
||
|
||
[source,lua]
|
||
----
|
||
soundSetVolume(100)
|
||
----
|
||
|
||
Le jeu refuse de démarrer :
|
||
|
||
----
|
||
20:soundSetVolume: Invalid sound volume value: 100
|
||
----
|
||
|
||
Le numéro de ligne que vous obtenez est celui de la ligne où vous l'avez
|
||
vraiment tapée, donc le vôtre n'est peut-être pas 20. Ensuite vient la fonction
|
||
qui a protesté, puis la valeur contre laquelle elle a protesté. Le message ne
|
||
vous dit pas quelle est la plage permise -- c'est à ça que sert l'entrée de la
|
||
Référence pour `soundSetVolume` -- mais il vous dit bien le nombre qu'elle a
|
||
refusé, et une fois que vous savez que les effets s'arrêtent à 63, la
|
||
correction tient en un nombre.
|
||
|
||
`100` est un nombre suspect à trouver dans un volume, et ça vaut la peine de
|
||
savoir pourquoi vous l'avez tapé. Presque à coup sûr, vous avez pensé au volume
|
||
comme à un pourcentage, parce que tous les curseurs de volume dont vous vous
|
||
êtes jamais servi vont jusqu'à 100. Celui de Singe, non. Les effets vont de `0`
|
||
à `63` et la musique de `0` à `128`, et ni l'un ni l'autre n'est un pourcentage
|
||
de quoi que ce soit.
|
||
|
||
Voilà la forme générale d'une erreur du moteur, par opposition aux erreurs Lua
|
||
de la leçon un : un numéro de ligne, le nom de la fonction du moteur que vous
|
||
avez appelée, et une plainte au sujet des valeurs que vous lui avez passées.
|
||
Quand vous en voyez une, l'entrée de la Référence pour cette fonction est la
|
||
prochaine chose à lire.
|
||
|
||
[#_what_you_learned_10]
|
||
=== Ce que vous avez appris
|
||
|
||
* `soundLoad` lit un son et renvoie un handle. Chargez une fois, en haut du
|
||
fichier.
|
||
* `soundPlay` lance un son et renvoie un *canal*, qui est une autre sorte de
|
||
nombre que le handle.
|
||
* Singe mélange seize canaux. Quand ils sont tous occupés, `soundPlay` renvoie
|
||
`-1` et rien ne joue.
|
||
* Les numéros de canal sont réutilisés. Oubliez-en un dès que vous l'avez
|
||
arrêté.
|
||
* Laissez les effets courts finir tranquillement ; `soundStop` est pour les sons
|
||
qui n'ont plus lieu d'être.
|
||
* `soundUnload` ne coupe pas un son qui est encore en train de jouer.
|
||
* Jouez un son là où l'événement se produit, pas là où une condition est vraie,
|
||
sinon il joue soixante fois par seconde.
|
||
* `onSoundCompleted(channel)` est un callback que le moteur appelle chaque fois
|
||
qu'un canal se tait, quelle qu'en soit la raison.
|
||
* La musique est un système à part, avec ses propres handles, son propre volume,
|
||
et pas de limite de canaux.
|
||
* Le volume des effets va de `0` à `63`. Celui de la musique va de `0` à `128`.
|
||
Ce ne sont pas des pourcentages.
|
||
|
||
[#_next_time_10]
|
||
=== La prochaine fois
|
||
|
||
Vos rochers sont touchés quand leurs rectangles se chevauchent, et c'est
|
||
pourquoi un tir qui passe à travers un coin de ciel compte quand même. À la
|
||
leçon onze, vous aurez les tests de collision du moteur lui-même, vous
|
||
apprendrez ce qu'est une boîte de collision, et vous découvrirez pourquoi le
|
||
rectangle autour d'un rocher rond n'a pas la bonne forme.
|