Asteroids: presentación de números en decimal – posible uso de BCD

Un byte codificado en binario es muy fácil de presentar en pantalla en binario. Llega con hacer “rol”, ir pasando sus bits al acarreo, empezando por el bit 7 y terminando por el bit 0, e ir pintando un 1 o un 0 según el bit / acarreo valga 1 o 0, respectivamente. Puede verse una implementación en la rutina “pintaBin” de la librería “LibText.asm”:

Asteroids - Rutina pintaBin.PNG

Sumar $30 = 48 permite convertir un 0 en el carácter PETSCII ‘0’, y permite convertir un 1 en el carácter PETSCII ‘1’, porque en este caso se suma también el acarreo, es decir, $31 = 49.

Igualmente, un byte codificado en binario también es muy fácil de presentar en pantalla en hexadecimal. Llega con quedarse con el nibble alto (bits 4 – 7), pintar el dígito hexadecimal ($0 – $f) correspondiente a los 16 posibles valores del nibble, y repetir el proceso con el nibble bajo (bits 0 – 3). Pueden verse las rutinas “pintaHex” y “pintaHex2” de la librería “LibText.asm”:

Asteroids - Rutina PintaHex.PNG

Sin embargo, lo que no es tan fácil es presentar en decimal un byte codificado en binario. Para esto es para lo que está pensada la codificación BCD. BCD, al guardar un dígito decimal en cada nibble, es fácil de presentar en decimal. Podemos ver la rutina “pintaBCD” de librería “LibText.asm”:

Asteroids - Rutina pintaBCD.PNG

La idea básica es quedarse con el nibble alto, acceder a una tabla que convierte en el dígito decimal correspondiente (0 – 9) los diez posibles valores del nibble (recordemos que lo que hay en el nibble es un dígito decimal), y repetir el proceso con el nibble bajo. Fácil, porque BCD es esencialmente una codificación decimal.

Por tanto, podríamos pensar que la solución a todos nuestros problemas es usar la codificación BCD para las coordenadas (X, Y), la velocidad, el ángulo, las vidas y los puntos. Sin embargo, no es exactamente así. Iremos viendo por qué…

Asteroids: presentación de números en decimal – repaso de conceptos

El juego está esencialmente terminado. Por supuesto, se le pueden añadir muchas cosas, pero hemos alcanzado el 99% de los objetivos: una nave que se mueve y dispara, unos asteroides que explotan contra la nave y con los disparos, unos sprites que se mueven por toda la pantalla, caracteres personalizados, pantallas, uso del joystick y del teclado, niveles de dificultad progresivos, sonido, etc.

Si acaso, hay una cosa que queda pendiente para considerar que el juego está terminado: los números correspondientes a las coordenadas (X, Y), la velocidad, el ángulo, las vidas y los puntos actualmente se presentan en hexadecimal. Esto es adecuado para un programador, pero no para un usuario normal.

Por tanto, tenemos que hacer que estos números se presenten en decimal. En principio, esto parece una tontería, pero no lo es. El ensamblador es un lenguaje tan simple, en el sentido de que sus instrucciones son tan sencillas, que incluso una tarea como presentar los números en decimal es más compleja de lo que parece.

Vamos a resolverlo, pero primero repasemos algunos conceptos:

Bits, bytes y palabras:

Los ordenadores clásicos almacenan bits y bytes. Hoy en día empieza a haber otros ordenadores, los cuánticos, que almacenan “qubits”.

Un bit es un dígito binario, es decir, un 0 o un 1. Un byte es un paquete de ocho bits. Y luego hay palabras (16 bits), dobles palabras (32 bits), etc. Pero el C64 es una máquina de ocho bits; por tanto, almacena bytes.

Y ya está. Al nivel de la electrónica eso es lo que se almacena. Se podría decir que se almacenan números, caracteres, sonidos, etc., pero eso requiere de un proceso adicional que es la codificación digital de la información.

Números, caracteres, sonido y gráficos:

Los números (sin signo o con él), los caracteres, los sonidos, los gráficos, etc., se pueden codificar digitalmente, es decir, mediante secuencias de bits. Pero dado un byte, por ejemplo, no me parecería correcto decir que eso ES un número con signo, o un número sin signo, u otra cosa, porque todo depende de lo que haya codificado o cómo lo interprete el programador o la aplicación.

Por tanto, es una buena idea distinguir entre la información (números, caracteres, …), el proceso de codificación, y el resultado de ese proceso (bits y bytes).

Números: codificación binaria y BCD

Desde un punto de vista matemático, el mismo número, por ejemplo, el 17, se puede representar con muchas bases: 17 en decimal, $11 en hexadecimal y %10001 en binario. Pero el número es el mismo: el 17. Y lo que se usa para almacenar el número 17 en el ordenador (en principio) es su codificación binaria, es decir, el byte %00010001.

La codificación binaria es la que el microprocesador entiende de forma nativa, y con la que sabe operar fácilmente. Por tanto, es muy adecuada para el ordenador. Pero no es tan adecuada para los humanos, que estamos acostumbrados a la codificación decimal.

Por ello, el C64 soporta una codificación alternativa llamada BCD – Binary Coded Decimal. Esta codificación consiste en codificar un dígito decimal (del 0 al 9) en un nibble (del %0000 al %1001). Por tanto, con un byte (dos nibbles) se pueden codificar los números decimales desde el 00 hasta el 99. Y encadenando varios bytes se pueden codificar números mayores.

No sólo eso, además, el C64 permite hacer operaciones aritméticas (“adc” y “sbc”) usando la codificación BCD. Para ello hay que activar la aritmética decimal con “sed”. También se puede desactivar con “cld”.

Lógicamente, para que todo sea correcto el programador debe asegurarse de que la codificación que utiliza (binaria o BCD) y la aritmética que utiliza (binaria o BCD) son coherentes. No se pueden mezclar. Si se mezclan los resultados no son correctos.

Por último, el ordenador no utiliza la base 16 como tal. La base 16 o hexadecimal la usa el programador, por ejemplo, al programar en ensamblador, porque es muy cómoda y muy compacta para trabajar con bytes.

Repasados todos estos conceptos, en las entradas que siguen veremos qué hacemos con las coordenadas (X, Y), la velocidad, el ángulo, las vidas y los puntos para presentarlos en decimal.

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

Vamos con la última pieza del puzle: la reproducción de los sonidos.

El fichero “Sonido.asm”, como todos sus homólogos, tiene una declaración de variables (ya vista), una inicialización (ya vista), y una actualización. Lo que nos falta es, precisamente, la actualización.

La rutina “actualizaSonido” es así:

Asteroids - Rutina actualiza sonido.PNG

Es decir, utilizando el registro Y como índice:

  • Recorre la tabla “vocesActivas”.
  • Si la entrada / voz toma el valor $01, reproduce el sonido para esa voz. Esto lo hace llamando a la rutina “reproduceSonido”, que recibe el número de la voz (0 ó 1) mediante el parámetro “rsVoz”.
  • Si la entrada / voz no toma el valor $01, pasa a la siguiente entrada / voz. Así hasta haber revisado las dos voces que contemplamos actualmente.

En definitiva, aquí comprobamos que la voz o el sonido se va a reproducir siempre y cuando se haya producido el evento asociado (disparo o colisión). Recordemos que son los eventos los que apuntan el valor $01 en la tabla “vocesActivas” llamando a las rutinas “activaSonidoDisparo” o “activaSonidoColision”.

Y poco a poco nos vamos acercando a la rutina nuclear del asunto, que es la rutina “reproduceSonido”. Esta rutina tiene dos aspectos que es importante entender muy bien:

  • La rutina funciona con cualquier voz (0 ó 1). Recibe la voz como parámetro de entrada (“rsVoz”) y, en función del valor de este parámetro, localiza y actualiza los datos correctos en las tablas “vocesActivas”, “vocesEntradas”, “vocesRetardos” y “vocesSonidos”.
  • La rutina no reproduce el sonido de comienzo a fin, sino un poquito en cada ciclo del bucle de juego. Los sonidos están definidos mediante varias entradas en las tablas de definición de sonidos, y cada entrada tiene una frecuencia y una duración (en ciclos). Pues bien, en cada ciclo del bucle de juego se va descontando un ciclo del “retardo”, hasta que el retardo llega a cero. En ese momento hay que pasar a la siguiente entrada, o terminar la reproducción del sonido.

Como la rutina es larga y compleja, y como además una ejecución guarda relación con las ejecuciones previas (porque el sonido se reproduce a lo largo de varios ciclos), lo mejor para entenderla es “despiezarla”. La variable clave en la ejecución es el retardo:

Si el retardo actual en “vocesRetardos” es cero:

Si el retardo es cero, es porque todavía no se ha reproducido ese sonido, o porque ya se venía reproduciendo de antes, pero la entrada es nueva, a estrenar.

Por tanto, la rutina tiene que hacer todo esto:

Asteroids - Reproduce sonido - retardo cero.PNG

Es decir, la rutina tiene que:

  • Ver por qué entrada iba en “vocesEntradas”.
  • Preparar un puntero para acceder a la tabla de definición del sonido (puntero $fb-$fc).
  • Leer la frecuencia de la entrada.
  • Fijar la frecuencia en la imagen del SID.
  • Leer la duración en ciclos de la entrada.
  • Activar la voz en la imagen del SID.
  • Transferir la imagen al SID.

A partir de aquí la entrada está sonando. Y va a sonar tantos ciclos como pone la entrada, porque será cuando el retardo llegue a cero cuando pasaremos a la siguiente entrada. Hasta entonces la voz seguirá igual, sin cambios.

Si el retardo actual en “vocesRetardos” no es cero:

Si el retardo no es cero, es porque el sonido / entrada se viene reproduciendo de antes. La entrada no es nueva.

Por tanto, en general, sólo hay que decrementar el retardo. De este modo, la entrada seguirá sonando:

Asteroids - Reproducir sonido - decrementa retardo.PNG

Ahora bien, si al decrementar el retardo éste llega a cero, es porque la entrada ha terminado y hay que pasar a la siguiente o dar por terminado el sonido. Veamos el apartado que sigue.

Al decrementar el retardo actual en “vocesRetardos” ha llegado a cero:

Efectivamente, si al decrementar el retardo éste ha llegado a cero, es porque la entrada ha terminado y hay que pasar a la siguiente entrada o dar por terminado el sonido.

Por tanto, la rutina tiene que hacer todo esto:

Asteroids - Reproduce sonido - siguiente entrada

Es decir, la rutina tiene que:

  • Ver por qué entrada iba en “vocesEntradas”.
  • Sumarle tres a la entrada. Las entradas son triples (frecuencia low, frecuencia high y duración). Más que entradas propiamente dichas, estamos contando octetos en la tabla de definición del sonido.
  • Preparar un puntero para acceder a la tabla de definición del sonido (puntero $fb-$fc).
  • Leer el primer octeto de la siguiente entrada.
  • Si el octeto no es $ff, es que el sonido tiene más entradas. En el siguiente ciclo la cargaremos. Ver apartado “Si el retardo actual es cero” más arriba.

Ahora bien, si el primer octeto de la siguiente entrada es $ff, es porque ya no hay más entradas y el sonido ha terminado. Veamos el apartado que sigue.

El primer octeto de la siguiente entrada es $ff:

Efectivamente, si el primer octeto de la siguiente entrada es $ff, es porque ya no hay más entradas. Hay que dar el sonido por terminado.

Por tanto, la rutina tiene que hacer todo esto:

Asteroids - Reproduce sonido - volver entrada 0.PNG

Es decir, la rutina tiene que:

  • Volver a indicar que va por la entrada cero. Esto es para preparar futuras reproducciones del mismo sonido.
  • Marcar la voz como que ya no está activa. De este modo no se intentará volver a reproducir mientras no se produzca otro evento (disparo o explosión).
  • Desactivar la voz en la imagen del SID.
  • Transferir la imagen al SID.

A partir de este momento, el sonido deja de sonar. Quedamos preparados para futuras reproducciones.

Como se puede observar, los sonidos se reproducen a lo largo de varios ciclos del bucle de juego, tantos como indique la tabla de definición del sonido. No hay ningún problema en que el SID reproduzca los sonidos, mientras el 6510 continúa con el juego. De hecho, el SID puede reproducir hasta tres voces a la vez.

Y con esto, hemos completado el puzle del sonido, que no era sencillo. Se puede repasar el puzle completo en la versión 22 del proyecto.


Código del proyecto: Asteroids22

Asteroids: sonido y música – eventos con sonido

Los eventos que queremos que tengan sonidos son los disparos y la explosiones, tanto las explosiones de la nave como las de los asteroides.

Y lo que hemos dicho es que, ante estos eventos, no vamos a reproducir los sonidos de forma directa o inmediata, dado que ello dejaría “enganchado” el juego, sino que vamos a tomar nota de ellos en unas estructuras de datos (tablas) y luego vamos a reproducir los sonidos (a lo largo de varios ciclos) igual que actualizamos el jugador, la pantalla, los disparos o los asteroides.

Vamos primero con las tablas para tomar nota de los eventos. Son así (fichero “Sonidos.asm”):

Asteroids - Tablas tomar notas eventos.PNG

En este trozo de código nos encontramos la declaración de cuatro tablas:

  • Tabla “vocesActivas”: Sirve para llevar cuenta de qué voces están activas (porque ha ocurrido un evento) y, por tanto, deben dar lugar a reproducción de sonido. La tabla tiene tres entradas porque el SID tiene tres voces, pero de momento sólo usamos las entradas / voces 0 y 1. La 0 para los disparos; la 1 para las explosiones.
  • Tabla “vocesEntradas”: Como cada sonido puede tener varias entradas en su tabla de definición del sonido (recordar que el disparo tenía dos entradas, mientras que la explosión sólo tenía una), debemos llevar cuenta de por qué entrada vamos en la reproducción. Lo hacemos con esta tabla.
  • Tabla “vocesRetardos”: Cada entrada en la tabla de definición del sonido tenía asociada una duración (5 y 5 para los disparos; 20 para las explosiones). Esta duración se medía en ciclos del bucle de juego. Debemos llegar cuenta de por qué ciclo vamos para detectar cuándo acaba una entrada y debemos pasar a la siguiente o terminar la reproducción.
  • Tabla “vocesSonidos” (partes lo y hi): Esta tabla contiene punteros a las tablas de definición de los sonidos. Estos punteros nos facilitan el acceso a estas tablas usando el modo de direccionamiento indirecto – indexado.

En realidad, de las cuatro tablas, sólo la primera sirve para llevar cuenta de qué eventos han ocurrido y qué sonidos deben reproducirse. Las otras tres tablas son tablas de apoyo que facilitan la reproducción de los sonidos (la entrada por la que voy, el número de ciclos que quedan para esa entrada, y el puntero a la tabla con la definición del sonido).

Ahora que ya conocemos la tabla “vocesActivas”, vamos a ver cómo tomar nota de que se ha producido un disparo o una explosión. Esto se hace con las rutinas “activaSonidoDisparo” y “activaSonidoColision” del fichero “Sonidos.asm”. La primera es así:

Asteroids - Activa sonido disparo.PNG

Y la segunda es así:

Asteroids - Activa sonido colisión.PNG

Ambas rutinas son muy parecidas, y básicamente lo que hacen en grabar el valor $01 en la entrada correspondiente a la voz del disparo (voz 0) o a la voz de la colisión (voz 1). Este valor sirve de señal a la rutina que “actualiza” o reproduce el sonido (la veremos más adelante), para que sepa que tiene que reproducir o continuar reproduciendo ese sonido.

Pero luego hay un matiz. Debemos pensar en la situación en que el usuario dispara constantemente, o se producen varias colisiones muy seguidas. ¿Qué queremos que ocurra en este caso? ¿Qué debe ocurrir ante los sucesivos disparos o colisiones? Una opción es volver a empezar la reproducción del sonido desde el comienzo; otra opción es continuar por donde íbamos y no darnos por enterados.

Lo mejor es probar ambas soluciones y ver qué queda mejor en cada caso. En el caso de los disparos, que constan de dos entradas / frecuencias en la tabla de definición, si el usuario dispara de forma continuada, nunca llega a completarse la reproducción del sonido. Por ello, en este caso mejor si continuamos por dónde íbamos. Por eso no tocamos “vocesEntradas” ni “vocesRetardos”.

En el caso de las explosiones o colisiones, queda bien si volvemos a empezar la reproducción del sonido desde el comienzo. Hace un efecto como de acumulación de explosiones. Por eso en este caso, además de guardar $01 en “vocesActivas”, guardamos $00 en “vocesEntradas” y “vocesRetardos”. A todos los efectos, lo que estamos haciendo es volver al comienzo de la reproducción si ya hubiera una explosión sonando.

Estas rutinas que “toman nota” de los disparos y las explosiones lógicamente hay que llamarlas desde algún sitio. Y esos sitios son los evidentes:

  • La rutina “actualizaColisionesJugador” de “Jugador.asm”.
  • La rutina “creaDisparo” de “Disparos.asm”.
  • La rutina “actualizaColisionesAsteroides” de “Asteroides.asm”.

A modo de ejemplo, reproducimos aquí cómo se llama a “activaSonidoColision” desde “actualizaColisionesJugador” de “Jugador.asm”:

Asteroids - Activa sonido colisión - Llamada.PNG

Con esto ya hemos encajado tres de las cuatro piezas del puzle (inicialización del SID y las voces, definición de los sonidos, y eventos que dan lugar a sonidos). Nos queda la última pieza, que es la reproducción a lo largo de varios ciclos de juego.

Seguimos con la versión 22 del proyecto.


Código del proyecto: Asteroids22

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