Asteroids: sonido y música – tablas de definición de los sonidos

Hemos dicho que, para separar la labor del compositor de la labor del programador, es habitual registrar en una tabla la definición musical de la melodía, es decir, las octavas, las notas, las duraciones y los volúmenes.

Lógicamente, esta recomendación tiene más sentido en el caso de música propiamente dicha que en el caso de sonidos. El sonido de un disparo viene a ser básicamente un pitido, y el sonido de una explosión un ruido. Por tanto, en estos casos no hay mucha “melodía” que registrar en una tabla.

Por tanto, aunque vamos a seguir aplicando la filosofía de usar tablas para definir los sonidos, la vamos a simplificar.

En primer lugar, no vamos a usar una única tabla, sino que vamos a usar dos, una para los disparos y otra para las explosiones. Esto es así porque disparos y explosiones no son dos voces de una misma melodía, sino que son dos sonidos que no tienen nada que ver el uno con el otro. Por tanto, dos tablas.

Estas dos tablas de definición del sonido de disparos y explosiones son así (fichero “Sonidos.asm”):

Asteroids - Tablas sonidos

El formato de las tablas no es exactamente el que hemos recomendado hasta ahora para melodías o piezas musicales (octava – nota – duración – volumen), sino que es más sencillo: frecuencia low – frecuencia high – duración. Esto es así por varios motivos:

  • Si fuera una pieza musical, podría interesar hacer efectos con el volumen. Como estamos hablando de sonidos básicos, un volumen fijo es más que suficiente. Ese volumen fijo ya se configuró en la inicialización.
  • Nuevamente, si fuera una pieza musical, tendría sentido registrar e interpretar octavas y notas (porque el lenguaje musical es ese), pero tratándose de pitidos y ruidos, con tal de registrar la frecuencia o frecuencias (en sus partes low y high), es más que suficiente. Así nos ahorramos la complejidad que supone pasar de octavas y notas a frecuencias.

Por lo demás, se puede observar que el disparo tiene dos entradas en la tabla, es decir, dos frecuencias, siendo la primera (18, 209) = ($12, $d1) = $12d1 = 4.817 y la segunda (22, 96) = ($16, $60) = $1660 = 5.728. Ambas entradas o frecuencias tienen una duración de cinco. Como se verá más adelante, esta duración se mide en ciclos del bucle de juego.

Por su parte, la explosión tiene una única entrada o frecuencia, de valor (21, 31) = ($15, $1f) = $151f = 5.407. La duración es de 20 ciclos del bucle de juego.

El final de las tablas viene marcado por la terna (255, 255, 255), puesto que $ffff no es una frecuencia válida para el C64 (ver página 384 y siguientes del libro “Commodore 64 Programmer’s Reference Guide”).

Con esto ya hemos definido cómo son nuestros sonidos, y estamos listos para reproducirlos como respuesta a determinados eventos. Y esto será lo que veamos en la siguiente entrada: los eventos y cómo registrarlos.

De momento, seguimos con la versión 22 del proyecto.

Asteroids: sonido y música – inicialización del SID y las voces

El SID tiene 29 registros de los cuales 25 son de sólo escritura y 4 de sólo lectura. Por ello, es habitual trabajar sobre una imagen del SID (posiciones de memoria convencionales sobre las que sí se puede leer y escribir) y, luego, transferir o copiar esta imagen a los 25 registros del SID sobre los que sí se puede escribir.

Además, lo habitual en toda aplicación antes de usar el SID es inicializarlo que, por lo comentado en el párrafo anterior, suele realizarse inicializando la imagen y transfiriéndola al SID. En el caso de Asteroids, la inicialización del SID la hacemos en el fichero “Asteroids.asm”, justo antes de inicializar o pintar la pantalla inicial / final:

Asteroids - Inicialización del SID.PNG

Lo hacemos aquí porque, como puede haber varias partidas sucesivas, interesa reinicializar el SID al final de cada partida. De este modo evitamos que los últimos sonidos de la partida (disparos, explosiones, …) sigan sonando en la pantalla final.

Por otro lado, está la inicialización de las voces, es decir, la configuración de sus formas de onda y sus envolventes ADSR. Esto lo llamamos desde la sección de inicialización del juego:

Asteroids - Inicialización voces.PNG

La rutina “inicializaSonido”, y todo lo que tiene que ver con el sonido en general, lo metemos en un nuevo fichero “Sonido.asm”, que viene a ser análogo a los ficheros “Jugador.asm”, “Disparos.asm”, “Asteroides.asm”, etc., ya conocidos.

En este nuevo fichero “Sonido.asm” puede verse la rutina “inicializaSonido”:

Asteroids - InicializacionVolumen.PNG

En esta rutina puede verse:

  • La inicialización del SID (comentada). Inicialmente estaba ahí, pero hubo que moverla al lugar previamente descrito (“Asteroids.asm”) por los motivos ya comentados (posibilidad de varias partidas sucesivas).
  • La configuración del volumen. Éste se configura a su valor máximo ($0f = 15). Si estuviéramos hablando de música podría tener sentido modificar el volumen con la melodía, pero como estamos hablando de sonido (disparos y explosiones) un volumen fijo es suficiente.
  • La configuración de la voz 0 y de la voz 1.

La voz 0 la usamos para los disparos, y tiene esta configuración (fichero “Sonido.asm”):

Asteroids - ConfiguraVoz0.PNG

Es decir, estamos configurando una forma de onda en rampa y un ADSR así (ver https://programacion-retro-c64.blog/2019/04/19/envolvente-adsr/):

  • Attack = $00 = 2 ms.
  • Decay = $00 = 6 ms.
  • Sustain = $0a = 10 (de un máximo de 15).
  • Release = $00 = 6 ms.

Por otro lado, la voz 1 la dedicamos a las explosiones, tanto de la nave como de los asteroides, y tiene esta otra configuración (fichero “Sonido.asm”):

Asteroids - ConfiguraVoz1.PNG

Es decir, estamos configurando una forma de onda de tipo ruido y un ADSR igual que el de la voz 0:

  • Attack = $00 = 2 ms.
  • Decay = $00 = 6 ms.
  • Sustain = $0a = 10 (de un máximo de 15).
  • Release = $00 = 6 ms.

Obsérvese que en todo momento venimos utilizando las rutinas de la librería “LibSonido.asm”, análogamente a lo que ya hemos hecho en el pasado con sprites (“LibSprites.asm”), joystick (“LibJoy.asm”), etc.

Con esto hemos dado un paso más en nuestro camino hacia reproducir los sonidos. Pero todavía nos falta describir los sonidos en sendas tablas (octavas, notas, duraciones y volúmenes), detectar los eventos que dan lugar a los sonidos (disparos y explosiones), tomar nota de esos eventos, y reproducir los sonidos.

De todo esto nos encargaremos en las entradas que siguen. De momento, pueden verse los avances en la versión 22 de proyecto.


Código del proyecto: Asteroids22

Asteroids: sonido y música – visión global

Hemos dicho que lo recomendable es recoger en una tabla la información de octavas, notas, duraciones y volúmenes de los diferentes sonidos o melodías. Pues ya está, hagámoslo así: ante los eventos de interés (disparo o colisión), reproducimos el sonido descrito en esa tabla y listo.

¿Qué nos va a pasar? Lo mismo que con los giros de la nave, los disparos o las explosiones: si el sonido es muy breve, apenas vamos a percibirlo; y si el sonido es más largo, dejará el juego “enganchado” en ese punto.

Y la solución también es la misma que con la nave, los disparos y las explosiones: conseguir que la reproducción del sonido se extienda durante varios ciclos del bucle de juego. De este modo el sonido será claramente perceptible (porque dura varios ciclos), pero a la vez no impedirá que se ejecuten el resto de actualizaciones (nave, disparos, asteroides, etc.).

Además, una vez que se activa una nota, ésta suena hasta que se desactive, puesto que de esta labor –la reproducción– se encarga el SID, no el 6510. Por tanto, el 6510 puede estar ejecutando otras cosas en paralelo (ej. actualizaciones de la nave, los disparos o los asteroides). El 6510 sí se encarga de activar las voces, desactivarlas, cambiar las frecuencias, etc., pero no de la reproducción del sonido propiamente dicha. De esto se encarga el SID.

Todavía más, puesto que las tres voces son independientes, pueden sonar varios sonidos a la vez, por ejemplo, si hay disparos y explosiones al mismo tiempo.

Por tanto, la cuestión de reproducir los sonidos la vamos a resolver así:

  • Igual que inicializamos la pantalla, el jugador y los asteroides, vamos a inicializar el sonido. En esta inicialización haremos la configuración inicial de las voces (forma de onda, ADSR, …).
  • Ante determinados eventos (disparos o explosiones), en vez de reproducir el sonido de forma directa, vamos a tomar nota en una tabla de que hay que reproducir un sonido. La tabla tendrá una entrada por cada voz (0, 1 y 2), aunque sólo vamos a usar dos voces.
  • En cada ciclo del bucle de juego, igual que actualizamos el jugador, la pantalla, los disparos y los asteroides, vamos a “actualizar el sonido”.
  • Actualizar el sonido va a consistir en recorrer la tabla arriba indicada y, por cada voz activa, reproducir su sonido.
  • Reproducir el sonido va a consistir en mantener la nota / frecuencia anterior durante un tiempo (varios ciclos del bucle de juego). Cuando termine la duración prevista, ir a la tabla de definición del sonido (típica tabla de octavas / frecuencias / duraciones / volúmenes, pero algo más simple en este caso) y configurar el SID con la nueva frecuencia ahí indicada. Así hasta que termine el sonido.

Iremos viendo todo esto paso a paso en las siguientes entradas. Pero era importante tener la visión global primero para entender bien cómo encajan las piezas.

Asteroids: sonido y música – repaso del SID

Todo juego que se precie tiene que tener sonido y, a poder ser, también música. En el caso de Asteroids, vamos a meter sonido en los disparos y en las colisiones o explosiones, tanto en las colisiones de la nave con los asteroides, como en las colisiones de los asteroides con los disparos.

Como el SID tiene tres voces podemos hacer este reparto:

  • Voz 0 para los disparos.
  • Voz 1 para las colisiones o explosiones.
  • Voz 2 para la música, aunque, al menos de momento, no vamos a incluir música.

De este modo, es perfectamente posible reproducir a la vez el sonido de los disparos, el sonido de las explosiones e, hipotéticamente, la música.

Pero antes de entrar en los detalles de cómo hemos programado el sonido, hagamos un breve repaso del SID:

El SID consta de tres voces. Cada voz se configura con una forma de onda, una envolvente ADSR y una frecuencia. La frecuencia, lógicamente, suele cambiar con el paso del tiempo para conformar una melodía o sonido. Si la forma de onda es cuadrada, entonces también hay que configurar el ancho de pulso.

Cada voz se puede activar y desactivar (“gating”) independientemente de las demás. El tiempo que una voz esté activa con la misma frecuencia, es una nota. Una nota (incluida su octava) es, por tanto, una frecuencia y una duración.

Las tres voces comparten el mismo volumen. El volumen también puede cambiar con el paso del tiempo.

Para separar lo mejor posible lo que es la labor del músico (componer la melodía) de lo que es la labor de programación del SID, es habitual, aunque no obligatorio, llevar a una tabla la información de las octavas y notas (frecuencias) de las tres voces, así como la información de duración y volumen, que es a compartir entre las tres.

El SID también tiene otras opciones, como filtros y resonancia, pero en esta ocasión no vamos a utilizarlas.

En las entradas que siguen veremos cómo aplicamos todo esto a Asteroids.

Asteroids: una pantalla inicial un poco más sofisticada

La pantalla inicial / final nos ha salido un poco sosa. Tiene un título (“ASTEROIDS”), el autor del juego, la puntuación y el record (sólo si venimos de terminar una partida), y la petición de pulsar espacio para jugar. Todo ello con caracteres estándar.

Para hacer esta pantalla un poco más atractiva tenemos varias opciones:

Las opciones anteriores no son excluyentes, se pueden combinar.

Recordemos que los bitmap pueden ser estándar (también llamados hi-res) o multicolor. Como en el caso de los sprites o los caracteres multicolor, los bitmap multicolor tienen la mitad de resolución horizontal (200 x 160 pixels frente a los 200 x 320 de los bitmap estándar) a cambio de tener más color.

Para incluir un bitmap, habría que hacer todo esto:

  • Diseñar el bitmap o convertirlo desde un gráfico de PC (JPG, PNG, …). Una herramienta interesante para hacer esto es Multipaint 2019 (ver http://multipaint.kameli.net/). Hay versiones para PC, Linux y Mac. Soporta los modos bitmap estándar y multicolor.
  • Generar un fichero con la información gráfica. Este fichero ocupará 200 x 320 = 64.000 bits = 8.000 bytes para los pixels activos / inactivos, más otros 1.000 bytes para la información del color a ubicar en la RAM de pantalla. Adicionalmente, si el bitmap fuera multicolor, harían falta otros 1.000 bytes de color a ubicar en la RAM de color.
  • Importar la información gráfica en un programa en ensamblador. Se puede utilizar la directiva “incbin” del CBM prg Studio, igual que hemos hecho con los sprites, los caracteres personalizados, o las pantallas.
  • Configurar el VIC para localizar el bitmap en memoria (bit 3 del registro VMCSB = $d018) y para activar el modo bitmap (activar el bit 5 del registro SCROLY = $d011). Además, si se quiere el modo bitmap multicolor, hará falta activar el bit 4 del registro SCROLX = $d016.

Como se puede observar, los bitmaps ocupan mucha memoria (9K ó 10K) y son complejos de manejar. Pero pueden llegar a ser muy vistosos. 

Asteroids - Multipaint multicolor.PNG

Por todo ello, y en aras de la simplicidad, en nuestro caso sólo hemos añadido algunos sprites / asteroides a la pantalla inicial / final. Concretamente estos tres:

Asteroids - Pantalla con asteroides

Esto es fácil de conseguir. Llega con incluir en la rutina que inicializa esa pantalla, la rutina “inicializaPantallaMenu”, una llamada con “jsr” a la nueva rutina “pinta3Asteroides”:

Asteroids - Pinta 3 asteroides.PNG

Esta nueva rutina:

  • Configura el multicolor. Esto pasamos a configurarlo aquí, y dejamos de hacerlo en “incializaJugador”, porque “pinta3Asteroides” pasa a ser la primera rutina relativa a sprites que se ejecuta en el programa.
  • Para cada uno de los tres sprites / asteroides que pintamos, lo posiciona, hace la configuración básica (puntero, color y activación) y la configuración avanzada (multicolor, expansión y prioridad respecto al fondo).

Una curiosidad es que los tres asteroides deben ubicarse de modo que no toquen ninguna letra. Si lo hacen, al empezar el juego con la tecla espacio, el programa detecta la colisión inmediatamente y anima una explosión. Esto no es más que un ejemplo de los muchos problemas inesperados que uno se encuentra al hacer un juego.

Adicionalmente, ahora se vuelve necesario desactivar todos los sprites antes de inicializar la pantalla de juego, el jugador y los asteroides. Si no lo hacemos así, los tres sprites de la pantalla inicial aparecen activos al empezar el juego:

Asteroids - Desactivar sprites.PNG

Igualmente, antes de pintar la pantalla inicial / final interesa desactivar todos los sprites porque, caso de no hacerlo, los sprites activos del final de la partida aparecerían en esa pantalla:

Asteroids - Desactivar sprites 2.PNG

En definitiva, dado que ahora el juego puede constar de muchas partidas, no podemos dar por hecho que la pantalla inicial / final se pintará partiendo de una situación sin sprites activos, salvo que expresamente los desactivemos.

Todo esto podéis verlo en la versión 21 del proyecto. En las próximas entradas ya nos dedicaremos al sonido.


Código del proyecto: Asteroids21

Asteroids: pantalla inicial y pantalla de juego – inicializaciones

Con el cambio que hemos hecho de meter una pantalla inicial, volver a esa pantalla al final de la partida, y permitir sucesivas partidas, hay un cambio fundamental: el juego ya no se ejecuta una única vez; puede ejecutarse varias veces sucesivas. Por eso ahora hablamos de “partidas” y de “sesión de partidas”.

Esto tiene sus implicaciones en cuanto a la inicialización del jugador, los asteroides y la pantalla. Hasta ahora, estaba previsto que esas inicializaciones se ejecutaran sólo una vez. A partir de ahora, tendrán que ejecutarse varias veces, una vez al comienzo de cada partida.

Pongamos como ejemplo el jugador. Su inicialización hasta ahora era así:

Asteroids - Inicialización jugador anterior

Pero esto no es del todo cierto, porque ese código va precedido de una declaración de variables (posiciones de memoria) del jugador que no es sólo eso, una mera declaración, sino que también se aprovecha para dar valores iniciales a algunas variables (estado, posición, velocidad, ángulo, retardos, vidas, puntos, nivel, etc.):

Asteroids - Variables jugador anterior

Al cargar el juego, esas variables se cargan con su valor inicial. Al jugar, su valor se modifica. Por tanto, si al terminar la partida y empezar otra nos limitamos a ejecutar la inicialización tradicional hasta ahora (“jsr inicializaPantallaJuego”, “jsr incializaJugador” y “jsr inicializaAsteroides”), esas variables no recuperarán su valor inicial. Heredarán los valores de la partida anterior.

Por tanto, tenemos que hacer algo para asegurarnos de que cada partida empieza con la situación inicial que queremos.

Una primera posibilidad sería convertir la declaración de variables en una mera declaración o reserva de posiciones de memoria, pero sin aprovechar para dar valores iniciales. Por tanto, todo se declararía como byte $00. Y esos valores iniciales podrían fijarse mediante instrucciones “lda <valor>” y “sta <variable>” en el cuerpo de las inicializaciones (rutinas “inicializaPantallaJuego”, “incializaJugador” y “inicializaAsteroides”). Por ejemplo:

Asteroids - Inicialización por sta.PNG

Otra posibilidad, que es por la que hemos optado al final, es no tener los valores iniciales “por código”, es decir, en instrucciones “lda <valor>” y “sta <variable>”, sino tenerlos en secciones de memoria, y copiar esas secciones a las variables al comienzo de las rutinas de inicialización. Lo mejor es verlo con un ejemplo:

Asteroids - Variables jugador con sección inicialización

Como se puede observar, las variables van precedidas por una sección de memoria delimitada por las etiquetas “jugadorConfComienzo” y “jugadorConfFin”. Esta sección de memoria incluye, a modo de plantilla, los valores iniciales de las variables. No hace falta incluir ahí lo que sean constantes que no se modifican (“jugadorSprite”, “jugadorFrames”, “explosionFrames”, …), ni tampoco variables como “jugadorMaxPuntos” (el record), que sí deben heredarse o mantenerse de una partida a la siguiente.

Y ahora, en la rutina de inicialización “incializaJugador”, empezamos por copiar esos valores a las variables. Para ello, llamamos a la nueva subrutina “inicializaVariablesJugador”, que se basa la rutina “copiaBloque” de la librería “LibChars”:

Asteroids - Copia de la plantilla

De este modo, en cada partida, al ejecutarse la inicialización del jugador, se empieza por inicializar sus variables.

Con los asteroides hacemos exactamente lo mismo para inicializar sus variables: la rutina “inicializaAsteroides” empieza llamando a la nueva subrutina “inicializaVariablesAsteroides”.

Por último, en el caso de las pantallas, en teoría haría falta lo mismo. Sin embargo, aunque las pantallas tienen inicializaciones, las pantallas no utilizan variables y, por tanto, lo anterior no es necesario.

En definitiva, una lección importante que hemos aprendido con esta entrada es que, si nuestro juego va a tener partidas sucesivas (lo cual es muy habitual), no debemos apoyarnos en las inicializaciones que suelen acompañar a las declaraciones de variables. Mejor hacer esas inicializaciones por código, porque así podremos repetirlas con comodidad.

Todo esto puede verse, también, en la versión 20 de proyecto.


Código del proyecto: Asteroids20

Asteroids: pantalla inicial y pantalla de juego – final de partida

Hemos dicho que tenemos que empezar a detectar el final de las partidas, es decir, tenemos que identificar que las vidas del jugador han llegado a cero y actuar en consecuencia (presentar la pantalla de fin).

El lugar idóneo para hacer esto es cuando se decrementan las vidas, es decir, cuando se detecta una colisión nave – asteroide y se anima una explosión. Si recordáis, el final de la explosión había este código (ver final de la rutina “animaExplosionJugador”):

Asteroids - Decrementar vidas anterior

Ahora hay que mejorar es código para, además de decrementar las vidas, detectar si éstas han llegado a cero. Esto lo hacemos en la versión 20 del proyecto:

Asteroids - Decrementar vidas nuevo

Como se puede observar, ahora, además de decrementar las vidas e inicializar la animación para el futuro:

  • Analizamos si las vidas han llegado a cero (“lda jugadorVidas” y “bneFinAnimacion2”).
  • En caso negativo, seguimos jugando la partida (“rts”).
  • Y en caso afirmativo, comparamos si los puntos (“jugadorPuntos”) superan el record (“jugadorMaxPuntos”), para quedarnos en ese caso con el nuevo record, y volvemos a la pantalla inicial / final con “jsr menuInit”.

En esta pantalla inicial / final, dado que los puntos no serán cero, pintaremos la puntuación y el record. Y volveremos a esperar a que el usuario pulse el espacio para jugar otra partida o decida apagar el C64.

Asteroids - Pantalla inicial con puntos

En definitiva, ya estamos identificando el final de la partida y, además, estamos gestionando el record de la sesión de partidas. Este record se podría grabar en un fichero y cargarlo en futuras sesiones de partidas.


Código del proyecto: Asteroids20