608 lines
22 KiB
Text
608 lines
22 KiB
Text
[#_lesson_10_sound]
|
|
== Lección 10: Sonido
|
|
|
|
image::learn/10-sound.png[La lección terminada, 480]
|
|
|
|
Un juego sin sonido parece roto de una forma difícil de señalar. Los disparos
|
|
salen y no pasa nada. Las rocas se rompen y no pasa nada. Ponle a cada una de
|
|
esas cosas un ruido de medio segundo y, de repente, el mismo juego parece hecho
|
|
de algo.
|
|
|
|
Esta lección le da a tu juego un disparo, una explosión y música de fondo. Por
|
|
el camino te presenta tu quinto callback: el que usa el motor para decirte que
|
|
un sonido ha terminado de sonar.
|
|
|
|
[#_one_noise]
|
|
=== Un ruido
|
|
|
|
El kit de arte que copiaste en la lección nueve trae dos sonidos, `shoot.wav` y
|
|
`boom.wav`, y de los dos la explosión es la que mejor se oye. Crea un archivo
|
|
nuevo junto a tu carpeta `art` y escribe esto.
|
|
|
|
[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
|
|
----
|
|
|
|
Ejecútalo y pulsa la barra espaciadora tres o cuatro veces, rápido. Los
|
|
estallidos se amontonan unos encima de otros en lugar de cortarse entre sí.
|
|
|
|
Mantener pulsada la barra espaciadora te da exactamente un estallido, no un
|
|
chorro de ellos. Es el mismo comportamiento que viste en la lección tres: una
|
|
tecla mantenida es una sola pulsación, que se avisa una vez, y el motor tira las
|
|
repeticiones que manda tu teclado. Por eso, para mover la nave, hace falta
|
|
`onInputReleased` y una variable en lugar de contar pulsaciones.
|
|
|
|
[#_what_just_happened_6]
|
|
=== Qué acaba de pasar
|
|
|
|
La forma es la misma que en la lección nueve: cargar una vez arriba, usar el
|
|
handle y devolverlo al final.
|
|
|
|
[source,lua]
|
|
----
|
|
local boom = soundLoad(DIR .. "art/boom.wav")
|
|
----
|
|
|
|
`soundLoad` lee un archivo de audio, lo guarda en memoria y te devuelve un
|
|
handle -- la misma idea que el handle de un sprite, un número que para el motor
|
|
significa «ese clip». `DIR` está ahí por la misma razón que en la lección nueve:
|
|
el sonido vive en la carpeta de tu juego, y el juego tiene que poder encontrarlo
|
|
desde dondequiera que se haya arrancado Singe.
|
|
|
|
Todo lo que dijo la lección nueve sobre cargar vale aquí palabra por palabra.
|
|
Carga arriba del todo del archivo, una vez. Un `soundLoad` dentro de
|
|
`onOverlayUpdate` carga el clip sesenta veces por segundo y no devuelve ninguno,
|
|
y el juego se muere de eso unos minutos después.
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(boom)
|
|
----
|
|
|
|
Reprodúcelo. Eso es todo lo que hace falta para reproducir un efecto de sonido,
|
|
y el motor se encarga del resto: el clip empieza, termina y nunca le estorba a
|
|
tu programa.
|
|
|
|
Pero `soundPlay` también devuelve algo, y *no* es el mismo número que le diste.
|
|
Cambia la línea y mira:
|
|
|
|
[source,lua]
|
|
----
|
|
local channel = soundPlay(boom)
|
|
debugPrint("clip " .. boom .. " on channel " .. channel)
|
|
----
|
|
|
|
Pulsa la barra espaciadora rápido media docena de veces y mira los dos números.
|
|
El número del clip no cambia nunca. El número del canal sube -- 0, luego 1,
|
|
luego 2 -- y en cuanto terminan los estallidos anteriores vuelve a bajar y
|
|
empieza a reutilizar los números bajos.
|
|
|
|
Esto es lo único del sonido que pilla a todo el mundo, así que vale la pena
|
|
decirlo despacio. *Hay dos tipos distintos de número en juego.*
|
|
|
|
El *handle del clip* que da `soundLoad` es la grabación. Solo hay uno. No
|
|
cambia, no está ni sonando ni callado: es el sonido guardado en memoria,
|
|
esperando a que lo uses.
|
|
|
|
El *canal* que da `soundPlay` es una reproducción de esa grabación. Singe tiene
|
|
dieciséis, numerados del `0` al `15`, y cada sonido que se oye en este momento
|
|
está usando uno. Por eso los estallidos se solapan: la segunda pulsación no
|
|
interrumpió la primera, sino que empezó una segunda reproducción en el siguiente
|
|
canal libre.
|
|
|
|
Todo lo que actúa sobre un *sonido que puedes oír* recibe un canal:
|
|
`soundStop`, `soundIsPlaying`, `soundPause`, `soundResume`. Todo lo que actúa
|
|
sobre la *grabación* recibe el handle: `soundPlay` y `soundUnload`. Si pasas uno
|
|
donde va el otro, no siempre te saldrá un error, porque `3` es un número de
|
|
canal válido y también un handle de clip perfectamente bueno -- te saldrá
|
|
silencio, o se parará el sonido equivocado, y eso cuesta mucho más encontrarlo.
|
|
|
|
Dieciséis son muchos, pero no infinitos. Cuando todos los canales están
|
|
ocupados, no suena nada y `soundPlay` devuelve `-1` en lugar de un número de
|
|
canal. Un juego que lanza muchísimos sonidos a la vez debería contar con ello, y
|
|
vas a escribir uno dentro de un momento.
|
|
|
|
[#_volume]
|
|
=== Volumen
|
|
|
|
Hay tres volúmenes, y se multiplican entre sí.
|
|
|
|
El volumen *general* de los efectos lo abarca todo. `soundGetVolume` lo lee y
|
|
`soundSetVolume` lo cambia, en una escala de `0` a `63`.
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(40)
|
|
----
|
|
|
|
Resiste. Ese número es del jugador, no tuyo: empieza en lo que pidiera al lanzar
|
|
el juego, y un juego que lo sobrescribe es un juego que suena demasiado alto en
|
|
la habitación tranquila de alguien. Cambia el volumen general solo cuando te lo
|
|
pida el jugador, desde una pantalla de opciones. `soundGetVolume` existe para
|
|
que una pantalla de opciones pueda mostrar cuál es ahora en lugar de adivinarlo.
|
|
|
|
Lo que deberías usar es el volumen del *canal*, que está por debajo del general.
|
|
Dale a `soundPlay` un tercer argumento y solo esa reproducción sonará más baja:
|
|
|
|
[source,lua]
|
|
----
|
|
soundPlay(shootClip, 0, 35)
|
|
----
|
|
|
|
El `0` del medio es cuántas veces repetirlo, y tienes que ponerlo porque el
|
|
volumen va detrás. `0` significa reproducirlo una vez. `-1` significa repetirlo
|
|
en bucle hasta que algo lo pare, que es como harías el zumbido de un motor o el
|
|
viento. La entrada de `soundPlay` en la referencia enumera los tres argumentos.
|
|
|
|
El tercer volumen es la distancia, para sonidos colocados en un mundo 3D. Eso
|
|
queda para la cuarta parte, cuando el juego pase a tres dimensiones.
|
|
|
|
La música tiene su propio volumen, y va en otra escala. Enseguida hablamos de
|
|
eso.
|
|
|
|
[#_stopping_and_not_stopping]
|
|
=== Parar y no parar
|
|
|
|
`soundStop` recibe un canal y lo corta allí donde esté:
|
|
|
|
[source,lua]
|
|
----
|
|
soundStop(engineChannel)
|
|
engineChannel = -1
|
|
----
|
|
|
|
Fíjate en la segunda línea. En cuanto un canal se para, el motor puede darle ese
|
|
mismo número al siguiente `soundPlay`, así que un número de canal que todavía
|
|
guardas puede pertenecer ahora a un sonido completamente distinto. Tíralo en el
|
|
momento en que lo pares. `-1` es lo útil que poner ahí, porque es el mismo valor
|
|
que te da `soundPlay` cuando no ha podido reproducir nada, así que una sola
|
|
comprobación cubre los dos casos: `if channel >= 0 then`.
|
|
|
|
`soundStop` es para un sonido que ya no debería sonar -- el ruido del motor
|
|
cuando el motor se rompe, la alarma cuando alguien la atiende. Es la herramienta
|
|
equivocada para un efecto corto. Una explosión de medio segundo cortada a la
|
|
décima de segundo suena a error, porque lo es.
|
|
|
|
Así que, casi siempre, no hagas nada. Deja que termine. Un efecto de sonido que
|
|
empiezas y en el que no vuelves a pensar termina solo y libera su canal solo,
|
|
que es exactamente lo que quieres.
|
|
|
|
Esto vale incluso al final. `soundUnload` no corta un clip que todavía está
|
|
sonando: el motor mantiene vivo el audio hasta que se para el último canal que
|
|
lo usa. Puedes descargar en `onShutdown` sin preocuparte de haber silenciado
|
|
algo.
|
|
|
|
`soundIsPlaying(channel)` responde si un canal está sonando ahora mismo. Su uso
|
|
principal no es la curiosidad sino la contención: una alarma que debe sonar una
|
|
vez, y no volver a empezar cada vez que el juego nota que el peligro sigue ahí.
|
|
|
|
[source,lua]
|
|
----
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
Lee la condición en voz alta: reproduce la alarma si no hay canal de alarma, o
|
|
si el que tienes se ha parado. Si no, déjala en paz.
|
|
|
|
[#_the_sound_that_stutters]
|
|
=== El sonido que tartamudea
|
|
|
|
Aquí está el error. Todo el mundo lo escribe, normalmente en la primera semana
|
|
después de descubrir el sonido.
|
|
|
|
Supón que quisieras que la nave siguiera disparando mientras el botón está
|
|
pulsado, en lugar de una vez por pulsación. Lo harías igual que la dirección:
|
|
una variable `firing` que se pone a `true` en `onInputPressed` y vuelve a
|
|
`false` en `onInputReleased`, y el disparo se crea en `onOverlayUpdate` mientras
|
|
sea verdadera. Es algo perfectamente razonable de querer, y el ruido parece que
|
|
va justo ahí, con el disparo:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if firing then
|
|
shots[#shots + 1] = { x = shipX, y = shipY }
|
|
soundPlay(shootClip)
|
|
end
|
|
...
|
|
----
|
|
|
|
Lo que sale no es un disparo. Es un traqueteo zumbón, o un tono plano y duro, o
|
|
un sonido como de una maquinita rompiéndose.
|
|
|
|
`onOverlayUpdate` se ejecuta unas sesenta veces por segundo. Mantén el botón un
|
|
segundo y habrás pedido sesenta reproducciones de un clip que dura más o menos
|
|
una quinta parte de segundo, cada una empezando encima de la anterior. En
|
|
cualquier momento hay unas once sonando, cada una un fotograma por detrás de la
|
|
anterior, y lo que oyes son sesenta copias del mismo ataque amontonadas unas
|
|
sobre otras.
|
|
|
|
El arreglo no es un sonido más ingenioso. Es reproducir el sonido donde ocurre
|
|
el *evento* y no donde el *estado* es verdadero:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
soundPlay(shootClip, 0, 35)
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputPressed` ocurre una vez, cuando se pulsa el botón. Una pulsación, un
|
|
disparo, un ruido.
|
|
|
|
Si de verdad quieres fuego continuo, la respuesta es la misma con otra forma:
|
|
convierte el propio disparo en un evento. Cuenta fotogramas, exactamente como
|
|
los contaba el ciclo de caminar en la lección nueve, y crea un disparo solo cada
|
|
ocho fotogramas. El ruido va con el disparo, así que suena siete u ocho veces
|
|
por segundo en lugar de sesenta, y eso es una ametralladora y no un traqueteo.
|
|
|
|
Esa es la regla, y vale la pena llevártela de esta lección: *los sonidos van con
|
|
los eventos, no con las condiciones*. Una pulsación, un impacto, una muerte,
|
|
recoger algo -- cada una de esas cosas ocurre una vez y merece un sonido. «El
|
|
botón está pulsado» y «la nave se mueve» no son eventos; son verdad durante
|
|
cientos de fotogramas seguidos, y todo lo que reproduzcas a partir de ellos se
|
|
reproduce cientos de veces.
|
|
|
|
Cuando un sonido sí pertenece a un estado que dura -- un motor, el viento, una
|
|
sirena -- empiézalo una vez con `-1` como número de repeticiones, guarda el
|
|
canal y páralo cuando termine el estado.
|
|
|
|
[#_the_fifth_callback]
|
|
=== El quinto callback
|
|
|
|
A veces sí necesitas saber cuándo ha terminado un sonido. Escribe esta función y
|
|
el motor la llamará:
|
|
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
Ese es tu quinto callback, después de `onOverlayUpdate` en la lección uno, los
|
|
dos callbacks de entrada de la lección tres y `onShutdown` en la lección nueve,
|
|
y funciona igual que todos ellos:
|
|
lo escribes tú, nunca lo llamas, y el motor te llama a ti. El `channel` que te
|
|
pasa es el canal que acaba de quedarse en silencio -- el número que te dio
|
|
`soundPlay` cuando lo empezaste.
|
|
|
|
Salta cuando un clip se acaba, cuando termina la última repetición de un bucle,
|
|
*y* cuando paras tú mismo un canal con `soundStop`. Esto último sorprende a
|
|
quien usa el callback para encadenar un sonido con el siguiente: parar un canal
|
|
a propósito también lo da por completado, y si no tienes cuidado el siguiente
|
|
sonido de la cadena empieza de todos modos.
|
|
|
|
El juego está a punto de usarlo para algo que merece la pena. Cuando se pierde
|
|
la última vida, suena la explosión de la nave y el juego se queda quieto -- sin
|
|
GAME OVER, sin que la música se desvanezca -- hasta que esa explosión ha
|
|
terminado de verdad. Entonces todo se para a la vez. Son un puñado de líneas y
|
|
es la diferencia entre un final y un corte.
|
|
|
|
[#_music_is_not_a_sound_effect]
|
|
=== La música no es un efecto de sonido
|
|
|
|
Podrías cargar una canción de tres minutos con `soundLoad` y reproducirla con
|
|
`soundPlay`. No lo hagas. La música tiene su propia familia de llamadas, por
|
|
razones que importan:
|
|
|
|
* La música tiene su propio volumen, separado del de los efectos, para que un
|
|
jugador pueda bajar la música y seguir oyendo los disparos.
|
|
* No hay límite de dieciséis canales ni número de canal. Un handle es una pieza
|
|
de música, y la paras, la pausas y la reanudas con ese handle.
|
|
* `musicStop` puede desvanecer una pieza durante un segundo o dos en lugar de
|
|
cortarla.
|
|
|
|
Las llamadas son un reflejo de las que ya conoces:
|
|
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("Singe/menuIntro.flac")
|
|
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
`musicLoad` da un handle. `musicPlay` la empieza, con el mismo número de
|
|
repeticiones que `soundPlay`: `-1` para siempre. `musicStop(theme)` la termina,
|
|
y `musicStop(theme, 1500)` tarda un segundo y medio en desvanecerla.
|
|
`musicUnload` la devuelve.
|
|
|
|
Ahora, la trampa. *El volumen de la música va de 0 a 128, no de 0 a 63.* Los
|
|
efectos de sonido usan una escala y la música otra, el doble de larga, por
|
|
razones históricas que no consuelan nada cuando tu música sale a la mitad del
|
|
volumen que querías. Cuando trabajes con música, dobla el número que tenías en
|
|
mente para un efecto.
|
|
|
|
La pieza que se carga ahí es la música del menú del propio motor, sacada de la
|
|
carpeta `Singe` que el motor desempaquetó en tu carpeta de trabajo la primera
|
|
vez que lo ejecutaste. Dura diez segundos y se compuso para desvanecerse al
|
|
final, así que en bucle suena exactamente como un bucle de diez segundos que no
|
|
para de desvanecerse. Eso está bien para aprender las llamadas y no sirve de
|
|
nada en un juego terminado, que trae una pieza propia más larga. Cualquiera de
|
|
los formatos habituales sirve: la sección Música de la referencia los enumera, y
|
|
Ogg Vorbis o MP3 son las opciones sensatas, porque unos minutos de música en WAV
|
|
son un archivo enorme.
|
|
|
|
[#_the_game_gets_a_soundtrack]
|
|
=== El juego tiene banda sonora
|
|
|
|
Abre el juego de la lección nueve. Todo lo de aquí se le añade; no se quita
|
|
nada.
|
|
|
|
Cuatro cosas que cargar, arriba, con los 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")
|
|
----
|
|
|
|
Las dos primeras llevan `DIR` y las dos últimas no, y es a propósito. `DIR`
|
|
significa la carpeta de *tu* juego. Un nombre que empieza por `Singe/` significa,
|
|
en cambio, la carpeta del propio motor, esté donde esté -- y por eso
|
|
`dofile("Singe/Framework.singe")` ha funcionado desde la lección siete sin `DIR`
|
|
tampoco. Tus propios archivos necesitan `DIR`; los del motor, nunca.
|
|
|
|
Dos variables nuevas, junto a las demás:
|
|
|
|
[source,lua]
|
|
----
|
|
local dying = false
|
|
local deathChannel = -1
|
|
----
|
|
|
|
`dying` es el momento entre que se pierde la última vida y que termina la
|
|
partida: la explosión aún suena y nada se mueve en pantalla.
|
|
|
|
El disparo recibe su ruido en `onInputPressed`, justo donde se crea el propio
|
|
disparo, y el reinicio recibe un clic para que pulsar `1` parezca que ha hecho
|
|
algo:
|
|
|
|
[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 roca alcanzada por un disparo estalla, y también una roca que te golpea. En
|
|
`updateShots`, junto a la puntuación:
|
|
|
|
[source,lua]
|
|
----
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
----
|
|
|
|
Y en `updateRocks`, donde se pierde la vida, la parte interesante:
|
|
|
|
[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
|
|
----
|
|
|
|
Cuando se pierde la última vida, suena la explosión y se guarda su canal. Ahora
|
|
el juego está en `dying`, que para todo lo que se mueve pero todavía no dice
|
|
GAME OVER.
|
|
|
|
El `if channel < 0` es el límite de dieciséis canales, resuelto. Si justo en ese
|
|
momento todos los canales estuvieran ocupados, no hay explosión ni canal que
|
|
esperar, y sin esas dos líneas el juego se quedaría en `dying` para siempre,
|
|
esperando un sonido que nunca sonó. Esperar algo que no puede llegar es una de
|
|
las formas más fáciles de colgar un programa, y la protección siempre sale más
|
|
barata que el fallo.
|
|
|
|
Ahora el callback nuevo, y la pequeña función que comparte con esa protección:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
dying = false
|
|
deathChannel = -1
|
|
over = true
|
|
musicStop(theme, 1500)
|
|
end
|
|
|
|
|
|
function onSoundCompleted(channel)
|
|
if channel == deathChannel then
|
|
endGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
El motor llama a `onSoundCompleted` por *cada* canal que se para, así que lo
|
|
primero que hace es comprobar si este es el canal que le importa. Todo lo demás
|
|
-- cada disparo, cada roca -- pasa de largo y se ignora.
|
|
|
|
`onOverlayUpdate` tiene que quedarse quieto mientras dura la explosión, y eso es
|
|
una palabra:
|
|
|
|
[source,lua]
|
|
----
|
|
if not over and not dying then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
`startGame` reinicia las dos variables nuevas y vuelve a empezar la música:
|
|
|
|
[source,lua]
|
|
----
|
|
dying = false
|
|
deathChannel = -1
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
Y `onShutdown` devuelve lo que se cargó:
|
|
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
spriteUnload(shipSprite)
|
|
spriteUnload(rockSprite)
|
|
spriteUnload(shotSprite)
|
|
spriteUnload(starSprite)
|
|
soundUnload(shootClip)
|
|
soundUnload(boomClip)
|
|
soundUnload(clickClip)
|
|
musicUnload(theme)
|
|
end
|
|
----
|
|
|
|
Ejecútalo. Dispara a las rocas, deja que una te golpee tres veces y escucha el
|
|
final: el último estallido suena entero, y solo cuando se ha ido empieza la
|
|
música a desvanecerse y aparecen las palabras.
|
|
|
|
El script terminado está en la carpeta `learn` como `10-sound.singe`, por si
|
|
quieres comparar.
|
|
|
|
[#_try_it_10]
|
|
=== Pruébalo
|
|
|
|
. *Baja todavía más el disparo.* Cambia el `35` del `soundPlay` del disparo por
|
|
`10`, y luego por `63`. Encuentra el número con el que queda por debajo de las
|
|
explosiones en lugar de por encima.
|
|
. *Baja la música sin tocar los efectos.* Cambia `musicSetVolume(50)` por
|
|
`musicSetVolume(15)`. Comprueba que los estallidos suenan exactamente igual de
|
|
fuerte que antes, que es toda la razón de que la música sea un sistema aparte.
|
|
. *Corta el final.* En `endGame`, cambia `musicStop(theme, 1500)` por
|
|
`musicStop(theme)`. Escucha los dos finales dos veces. La diferencia es un
|
|
número en un argumento, y no es una diferencia pequeña.
|
|
. *Ponle un motor a la nave.* Arranca `soundPlay(clickClip, -1, 12)` en
|
|
`startGame`, guarda el canal que te da en una variable propia y páralo en
|
|
`endGame`. El clic dura una cincuentava parte de segundo, así que en bucle es
|
|
un zumbido, que es lo más parecido a un motor que da de sí este kit. Luego
|
|
vuelve a quitar el `soundStop`, juega tres partidas seguidas y escucha lo que
|
|
has construido.
|
|
. *Cuenta los canales.* Imprime el número de canal cada vez que explota una
|
|
roca. Luego sube `ROCK_COUNT` hasta que exploten varias a la vez, y sigue
|
|
subiéndolo hasta que veas pasar un `-1`.
|
|
|
|
[#_break_it_on_purpose_9]
|
|
=== Rómpelo a propósito
|
|
|
|
Ahora tienes dos escalas de volumen en la cabeza, y tarde o temprano te saldrá
|
|
de los dedos la equivocada. Añade esta línea suelta, justo debajo de las cuatro
|
|
líneas de `soundLoad` y `musicLoad`:
|
|
|
|
[source,lua]
|
|
----
|
|
soundSetVolume(100)
|
|
----
|
|
|
|
El juego se niega a arrancar:
|
|
|
|
----
|
|
20:soundSetVolume: Invalid sound volume value: 100
|
|
----
|
|
|
|
El número de línea que te sale es la línea en la que de verdad la escribiste,
|
|
así que puede que el tuyo no sea 20. Luego viene la función que se ha quejado, y
|
|
el valor del que se ha quejado. El mensaje no te dice cuál es el rango -- para
|
|
eso está la entrada de `soundSetVolume` en la referencia -- pero sí te dice el
|
|
número que no ha aceptado, y en cuanto sabes que los efectos llegan hasta 63, el
|
|
arreglo es un número.
|
|
|
|
`100` es un número sospechoso para un volumen, y vale la pena saber por qué lo
|
|
escribiste. Casi seguro que pensaste en el volumen como un porcentaje, porque
|
|
todos los controles de volumen que has usado en tu vida llegan a 100. Los de
|
|
Singe no. Los efectos van de `0` a `63` y la música de `0` a `128`, y ninguno es
|
|
un porcentaje de nada.
|
|
|
|
Esta es la forma general de un error del motor, a diferencia de los errores de
|
|
Lua de la lección uno: un número de línea, el nombre de la función del motor que
|
|
llamaste y una queja sobre los valores que le pasaste. Cuando veas uno, lo
|
|
siguiente que hay que leer es la entrada de esa función en la referencia.
|
|
|
|
[#_what_you_learned_10]
|
|
=== Lo que has aprendido
|
|
|
|
* `soundLoad` lee un clip y devuelve un handle. Carga una vez, arriba del todo
|
|
del archivo.
|
|
* `soundPlay` empieza un clip y devuelve un *canal*, que es un tipo de número
|
|
distinto del handle.
|
|
* Singe mezcla dieciséis canales. Cuando están todos ocupados, `soundPlay`
|
|
devuelve `-1` y no suena nada.
|
|
* Los números de canal se reutilizan. Olvida uno en cuanto lo hayas parado.
|
|
* A los efectos cortos hay que dejarlos terminar en paz; `soundStop` es para
|
|
sonidos que ya no deberían sonar.
|
|
* `soundUnload` no corta un clip que todavía está sonando.
|
|
* Reproduce un sonido donde ocurre el evento, no donde una condición es
|
|
verdadera, o sonará sesenta veces por segundo.
|
|
* `onSoundCompleted(channel)` es un callback que el motor llama cada vez que un
|
|
canal se queda en silencio, por el motivo que sea.
|
|
* La música es un sistema aparte, con sus propios handles, su propio volumen y
|
|
sin límite de canales.
|
|
* El volumen de los efectos va de `0` a `63`. El de la música, de `0` a `128`.
|
|
No son porcentajes.
|
|
|
|
[#_next_time_10]
|
|
=== La próxima vez
|
|
|
|
Tus rocas reciben un impacto cuando sus rectángulos se solapan, y por eso cuenta
|
|
incluso un disparo que pasa por una esquina de cielo. En la lección once tendrás
|
|
las comprobaciones de colisión del propio motor, aprenderás qué es una zona de
|
|
impacto y descubrirás por qué el rectángulo que rodea una roca redonda tiene la
|
|
forma equivocada.
|