Asteroids: conclusiones

A lo largo de unas 50 entradas y 23 versiones del proyecto, hemos desarrollado paso a paso un juego clásico en ensamblador.

Las principales conclusiones que saco del proceso son:

  • No es suficiente con saber ensamblador del 6510. Además, hay que dominar el mapa de memoria y el hardware del C64. Un buen conocimiento de los chips especiales (VIC, SID y CIAs) es fundamental. Y también del Kernal, aunque no tanto.
  • Es muy importante que el proceso de desarrollo sea paso a paso o incremental. Hay que empezar por lo más sencillo y nuclear, e ir completándolo poco a poco. No parece viable obtener un producto completo de forma directa, especialmente si es complejo.
  • Interesa usar herramientas modernas como CMB prg Studio y VICE. Ni me quiero imaginar cómo podría ser la tarea si se intentara directamente sobre un C64.
  • Disponer de unas librerías de programación para sprites, sonido, entrada / salida, etc., también es muy importante para ser eficientes. Si el programador no dispone de ellas, siempre puede desarrollarlas y mejorarlas con cada juego.
  • Hay que plantearse retos asumibles. Y luego ir subiendo el listón. No vamos a sacar “Uridium” a la primera.
  • El código tiene que estar debidamente estructurado. No podemos tener un único fichero “asm”. Tiene que haber varios ficheros, y estos tiene que ser autocontenidos. Como decían los clásicos “alta cohesión y bajo acomplamiento”.
  • Los juegos suelen tener una inicialización y una actualización. La inicialización es el código que prepara el juego; por tanto, se ejecuta una vez. La actualización es el código que se ejecuta en cada ciclo del bucle de juego. Se utiliza para leer joystick y/o teclado, actualizar posiciones, disparar, detectar colisiones, hacer animaciones, reproducir sonido, etc.
  • El concepto de bucle de juego es muy importante también. El bucle de juego es la ejecución de las actualizaciones. Se ejecuta repetitivamente hasta que el juego termina. Hay tareas que se pueden ejecutar completas en cada ciclo, y otras tareas que hay que ejecutarlas a lo largo de varios ciclos porque, si las ejecutáramos en un único ciclo, serían imperceptibles o para hacerlas perceptibles dejarían el juego “enganchado”.
  • El ensamblador es tan rápido que hay tareas que, para que se puedan hacer bajo control, tienen que ser ralentizadas. Algunos ejemplos son los movimientos de la nave o los disparos. Esta ralentización se suele hacer mediante un “retardo”.
  • Es mejor hacer las cosas bien desde el principio, que corregir los errores en una fase tardía del proyecto, cuando el diseño ya está muy avanzado. No obstante, somos humanos y nos equivocaremos. Al menos, aprendamos de los errores.
  • Salvo que venga muy a cuento, no pongáis información en la zona derecha de la pantalla. Dejad que los sprites se muevan en libertad.
  • Tenemos sprites, caracteres personalizados y bitmaps. Cada cosa tiene su uso. Los sprites son limitados, sólo tenemos ocho. Hay que hacer un uso razonable de los recursos.
  • Las fórmulas matemáticas son un horror en ensamblador. Es mucho más sencillo usar tablas de datos. Y CBM prg Studio tiene un “data generator”.
  • Os encontraréis situaciones imprevistas como letras que desaparecen de la pantalla, asteroides que siempre se expanden, programas que se cuelgan, explosiones inesperadas, etc. Hay que estar preparado para ellas e investigarlas. Esto os consumirá mucho tiempo y esfuerzo.
  • Con frecuencia lo perfecto es enemigo de lo bueno. Quizás los disparos podrían estar más centrados respecto a la nave, pero, ¿cuánto me costaría hacerlo perfecto? Si la nave mirara siempre en la misma dirección podría resolverlo con relativa facilidad, pero admite ocho direcciones. ¿Estoy dispuesto a asumir el coste de hacerlo perfecto? ¿Hay alguna solución subóptima?
  • Es importante reutilizar ideas e intentar ser coherentes en los diseños. Si los asteroides son una mezcla entre la nave y los disparos, porque son sprites como la nave, pero los hay activos e inactivos como los disparos, pues no reinventemos la rueda.
  • Es interesante meter niveles de dificultad progresivos. Hay recursos para generar información pseudoaletoria (relojes del sistema).
  • La detección de colisiones, ya sea sprite – sprite o sprite – texto no es sencilla. Con frecuencia implicará el uso de las posiciones para averiguar con precisión quién choca con qué.
  • La animación de explosiones, igual que la reproducción del sonido, son ejemplos típicos de tareas que interesa extender a lo largo de varios ciclos del bucle de juego.
  • Los juegos no sólo tienen la pantalla de juego (la obvia). También tienen pantallas de carga, pantallas finales, pantallas de puntuación y records, etc. Diseñar las diferentes pantallas es fácil, pero la correcta transición entre ellas puede tener algo más de miga.
  • Lo normal es que los juegos se puedan jugar varias veces seguidas en una “sesión de partidas”. Cuidado con las inicializaciones en este caso, especialmente si confías en la inicialización implícita de las variables mediante directivas “byte” y similares.
  • El SID es complejillo. El manejo de librerías simplifica mucho. Y el uso de tablas para separar música o sonido de programación también.
  • A los humanos nos gustan los números en base diez. La solución obvia, que sería usar codificación y aritmética BCD, no siempre es viable. Siempre está la opción de usar codificación y aritmética binarias, y convertir a BCD antes de imprimir los datos.
  • A programar se aprende programando, y sufriendo en tus carnes los errores. Y disfrutando mucho con el proceso y el resultado, claro.

Así que, lo dicho, espero que hayáis disfrutado mucho del proceso y, sobre todo, que os sirva de inspiración para vuestros propios proyectos.

Asteroids: mapa de memoria y librerías mejoradas

La versión final del proyecto, que es la versión 23, tiene esta distribución en memoria. Nos la da CBM prg Studio al ensamblar:

Asteroids - Asteroids mapa memoria.PNG

Es decir, el juego consta de cinco segmentos de memoria:

  • $0801 – $080c, 12 bytes: Este es el cargador BASIC.
  • $0810 – $1be2, 5.075 bytes: Este es el grueso del juego, librerías incluidas.
  • $3000 – $32ff, 768 bytes: Es la definición de los 12 “frames” para los sprites.
  • $3800 – $4fcf, 6.096 bytes: Son los caracteres personalizados (4 KB) y las pantallas (2 x 1 KB).
  • $c000 – $c094, 149 bytes: Es un programa de prueba para probar la conversión de codificación binaria a BCD. Se podría quitar del proyecto.

En definitiva, vemos como un juego sencillo ya casi ocupa 12 KB. Por supuesto, se podría reducir un poco, por ejemplo, sustituyendo la pantalla de juego (1 KB) por la impresión de una simple línea, quitando librerías o rutinas de librería que no se utilizan, o replanteando el uso de los caracteres personalizados (4 KB).

Pero la idea subyacente es la misma: el C64 tiene muy poca memoria (64 KB, con partes de RAM y ROM solapadas), y para hacer virguerías hay que echarle mucho ingenio, y jugar con los diferentes mapas de memoria posibles. Aquí hemos utilizado el mapa estándar.

Por lo demás, durante el proceso no sólo hemos desarrollado un juego. También hemos mejorado unas librerías. Partíamos de las librerías disponibles aquí. Y, como parte del proceso, hemos mejorado las siguientes librerías:

Librería Rutina / variable Mejora
LibChars.asm configuraColores Admite color del borde
LibSprites.asm posicionaSprite Admite coordenada X de 9 bits
  configuraAvanzada Permite expandir y “desexpandir”
LibText.asm pintaHex2 Permite pintar un número hexadecimal en una posición de pantalla concreta
  pintaCadena3 Permite pintar una cadena de caracteres en una posición de pantalla concreta
  biteToBCD Permite convertir un byte en binario a su codificación BCD
  bitesToBCD Idem con dos bytes
LibSonido.asm offsetVoces Renumera las voces como 0, 1 y 2
LibMath.asm maximo Nueva rutina para calcular el máximo de dos números
  minimo Nueva rutina para calcular el mínimo de dos numeros

Las nuevas librerías quedan disponibles aquí.

Como se habrá podido ver, el uso de unas librerías de rutinas (o macros) simplifica mucho el desarrollo de aplicaciones. Hacerlo desde cero hubiera sido mucho más complejo. Cada programador debe buscar las librerías que sean de su agrado, o desarrollar unas propias.


Nuevas librerías: Lib – V2

Asteroids: el reto de los asteroides que siempre se expanden (solución)

No podemos rematar la faena sin resolver el gran misterio. ¿Por qué cada vez había más asteroides expandidos y menos sin expandir?

La respuesta no estaba en la rutina “actualizaAsteroidesActivos”, como podría parecer, sino que estaba en la rutina “configuraAvanzada” de la librería “LibSprites.asm”. La versión original de esta rutina era así:

Asteroids - Rutina configuración avanzada

Si estudiamos la parte de la rutina dedicada a la expansión (líneas 148 – 164) veremos que la rutina es capaz de pasar de una situación inicial en que el sprite no está expandido, a otra situación final en que sí lo está. Esto se consigue haciendo “ora XXPAND” y “sta XXPAND”, para la expansión horizontal, y “ora YXPAND” y “sta YXPAND”, para la expansión horizontal.

Pero la rutina no es capaz de deshacer esa situación. Es decir, la rutina no es capaz de “desexpandir” un sprite que previamente ya está expandido.

Es decir, la rutina se diseñó para, partiendo de la situación inicial o de arranque del C64 en que ningún sprite está expandido, expandir los sprites que se desee. Pero no se diseñó para hacer también el camino contrario.

Por ello, según avanzaba el juego, algunos asteroides se expandían y otros no. Los expandidos nunca se “desexpandían”. Y los no expandidos, por el mero paso del tiempo y los juegos de probabilidades, antes o después acababan expandidos, sin poder salir de ahí. Al final, todos expandidos.

Sin embargo, a partir de la versión 19 del proyecto, esta rutina se mejora y pasa a ser así (sólo parte dedicada a la expansión):

Asteroids - Rutina configuración avanzada mejorada

Es decir, cuando “caExpansionH” vale 0:

  • Con “lda tablaSpr,x” se obtiene un byte con todos los bits a 0 menos el correspondiente al sprite, que estará a 1.
  • Con “eor #%11111111” se cambian todos los bits de ese byte. Es decir, quedan a 1 todos los bits, menos el correspondiente al sprite, que queda a 0.
  • Con “and XXPAND”y “sta XXPAND” lo que se consigue es desactivar la expansión horizontal del sprite en cuestión.

Y lo mismo para la expansión vertical cuando “caExpansionV” vale 0.

Espero que esto aclare el misterio. A mí me tuvo loco mucho tiempo 🙂 .

Asteroids: algunas sugerencias para el lector

Como hemos dicho, damos el juego por terminado en su versión 23. Eso no quiere decir que el juego sea perfecto, ni mucho menos. De hecho, es un juego relativamente sencillo.

Pero, viendo todo el proceso que nos ha llevado, y las dificultades que han surgido, sirve muy bien para ejemplificar lo complejo que puede llegar a ser desarrollar un juego en ensamblador.

Pensad ahora en vuestros clásicos favoritos: Uridium, Raid Over Moscow, Entombed, Commando, Green Beret, Ant Attack, Booga-Boo, etc. Eran meses de trabajo de equipos con varios especialistas: programadores, diseñadores gráficos, músicos, etc. ¡¡Y con las herramientas de los 80!! Vamos… unos héroes…

Asteroids - Uridium.PNG

Y aprovechando que el juego no es perfecto, aquí quedan algunas sugerencias para el lector con interés:

  • Guardar el record en un fichero y leerlo al empezar el juego.
  • Incluir en la pantalla inicial un bonito bitmap, o incluso un bitmap y una zona de texto.
  • Incluir música más allá del sonido de disparos y explosiones. Intentar reproducir la música mediante interrupciones.
  • Desarrollar una tecla de “hiperespacio” que haga que la nave aparezca en un punto aleatorio de la pantalla. Puede usarse cuando haya riesgo inminente de colisión.
  • Que al explotar los asteroides den lugar a otros asteroides más pequeños.
  • Que aparezcan ovnis en pantalla. Estos ovnis disparan a la nave y pueden colisionar con ella.
  • Etc.

En definitiva, a programar se aprende programando. Y equivocándose muchas veces. Así que… ¡¡mucho ánimo!!


Código del proyecto: Asteroids23

Asteroids: presentación de números en decimal – puntos y record

En primer lugar, los puntos y el record adolecen de un problema: sólo estamos usando un byte para cada uno. Por tanto, dado que cada asteroide destruido nos reporta diez puntos, esto nos da para un máximo de 25 asteroides (255 / 10 = 25,5). Parece poco.

Por tanto, lo primero es ampliar ambas variables para que ocupen dos bytes, el byte “lo” y el byte “hi”. Este cambio es bastante rutinario, y básicamente sólo hay que tener en cuenta dos cosas especiales:

  • Al sumar diez puntos por destruir un asteroide en la rutina “animaExplosionAsteroide”, hay que acordarse de sumarlos a la parte “lo”, y luego sumar el acarreo a la parte “hi”.
  • Al comparar los puntos con el record en la rutina “animaExplosionJugador”, dado que ahora ambos tienen parte “lo” y parte “hi”, hay que empezar por comparar los bytes más significativos (parte “hi”) y, en función del resultado de esa comparación, comparar o no las partes menos significativas (“lo”).

En segundo lugar, los puntos y el record serían candidatos ideales para la codificación BCD. Sus valores no se almacenan en ningún registro especial (VIC, SID, …), y la aritmética que hacemos con ellos es relativamente sencilla (básicamente sumar puntos de diez en diez). Esta medida facilitaría su presentación en pantalla en formato decimal.

Sin embargo, dado que en la entrada anterior ya hemos desarrollado una rutina que es capaz de convertir números codificados en binario a números codificados en BCD, ni siquiera el uso de BCD es necesario. Podemos seguir usando la codificación y la aritmética binarias, convertir a BCD antes de imprimir, e imprimir en decimal con “pintaBCD”.

Aplicando la misma solución que a las coordenadas X e Y (convertir a BCD antes de imprimir), vemos cómo los puntos ya aparecen en decimal, tanto en la pantalla de juego:

Asteroids - Puntos decimal.PNG

como en la pantalla final:

Asteroids - Puntos y record decimal.PNG

Y con esto podemos dar el juego por terminado en su versión 23 aunque, por supuesto, caben muchas mejoras, como comentaremos más adelante.


Código del proyecto: Asteroids23

Asteroids: presentación de números en decimal – coordenadas X e Y

La coordenada Y ocupa un byte y puede tomar los valores 0 a 255, aunque no todos caen dentro de la zona visible de la pantalla. La coordenada X ocupa dos bytes, aunque sólo se usan 9 bits. Por tanto, puede tomar los valores 0 a 511, aunque tampoco todos caen dentro de la zona visible de la pantalla.

En este caso, al superarse el intervalo 0 – 9, podríamos plantearnos dejar de usar la codificación binaria y pasar a usar la codificación y la aritmética BCD, todo con el objeto de facilitar la presentación de estas variables en pantalla en formato decimal.

Sin embargo, hay un problema. El VIC requiere que las coordenadas X e Y que se informan a los registros SP0X, SP0Y y siguientes ($d000 – $d00f) estén codificados en binario, no en BCD.

Por tanto, tenemos que seguir usando la codificación y la aritmética binarias. Podemos convertir a BCD antes de pintar en pantalla o desarrollar alguna rutina que, directamente desde la codificación binaria, imprima los números en decimal.

Y esto es lo que vamos a hacer: una rutina que partiendo de un byte en codificación binaria nos dé el número equivalente en codificación BCD. Y luego imprimiremos ese segundo número BCD en decimal.

Un byte puede tomar los valores decimales 0 – 255. Por tanto, para la codificación BCD necesitamos al menos tres nibbles o tres dígitos decimales (para almacenar el 2, el 5 y el otro 5). Y como en el C64 la unidad de memoria mínima que se puede asignar o reservar es el byte, reservaremos para el retorno de la rutina dos bytes o, lo que es lo mismo, cuatro nibbles o cuatro dígitos decimales. Por tanto, nos sobrará un nibble o dígito decimal.

Empecemos por lo sencillo: el nibble bajo del byte codificado en binario. El nibble bajo puede tomar los valores %0000 – %1111, que en decimal son los valores 0 – 15. El 0 sólo tiene un dígito decimal; por tanto, nos llegaría con un nibble BCD. Sin embargo, el 15 tiene dos dígitos decimales; por tanto, necesitamos dos nibbles BCD para codificarlo (un nibble para el 1 y otro nibble para el 5).

La conversión de lo primero (los 16 posibles valores del nibble bajo) a lo segundo (los nibbles o dígitos BCD) es fácil de conseguir mediante una tabla de conversión:

Nibble bajo Valor decimal Byte BCD Hi Byte BCD Lo
%0000 0 00 00
%0001 1 00 01
%0010 2 00 02
%0011 3 00 03
%0100 4 00 04
%0101 5 00 05
%0110 6 00 06
%0111 7 00 07
%1000 8 00 08
%1001 9 00 09
%1010 10 00 10
%1011 11 00 11
%1100 12 00 12
%1101 13 00 13
%1110 14 00 14
%1111 15 00 15

En definitiva, si el valor del nibble bajo lo pasamos al registro X o Y, y usamos ese registro como índice para acceder a la tabla, obtenemos fácilmente los nibbles o dígitos en BCD.

La conversión de binario a BCD ha sido más fácil de lo que cabía esperar. De hecho, me declaro un absoluto forofo de las tablas de datos. Son mucho más fáciles de usar, y seguro que incluso más rápidas, que las fórmulas matemáticas, que son un horror para implementar en ensamblador en cuanto tienen algo más complejo que sumas, restas, multiplicaciones por dos o divisiones por dos.

La versión en rutina del algoritmo anterior es así de fácil:

Asteroids - Rutina nibble0ToBCD.PNG

El problema es que el byte codificado en binario no sólo tiene el nibble bajo, también tiene el nibble alto. Y con él hay que hacer lo propio. Pero, cuidado, porque ahora el nibble alto “pesa” más que el nibble bajo. Es decir, la tabla de conversión es así:

Nibble alto Valor decimal Byte BCD Hi Byte BCD Lo
%0000 0 x 16 = 0 00 00
%0001 1 x 16 = 16 00 16
%0010 2 x 16 = 32 00 32
%0011 3 x 16 = 48 00 48
%0100 4 x 16 = 64 00 64
%0101 5 x 16 = 80 00 80
%0110 6 x 16 = 96 00 96
%0111 7 x 16 = 112 01 12
%1000 8 x 16 = 128 01 28
%1001 9 x 19 = 144 01 44
%1010 10 x 16 = 160 01 60
%1011 11 x 16 = 176 01 76
%1100 12 x 16 = 192 01 92
%1101 13 x 16 = 208 02 08
%1110 14 x 16 = 224 02 24
%1111 15 x 16 = 240 02 40

Nuevamente, la versión en rutina del algoritmo anterior es así (sólo cambian las tablas de datos):

Asteroids - Rutina nibble1ToBCD

Ahora, llamando a las dos rutinas anteriores y sumando sus resultados (suma decimal con “sed”), ya somos capaces de pasar de un byte codificado en binario (con su nibble bajo y su nibble alto) a dos bytes equivalentes con la codificación BCD:

Asteroids - Rutina byteToBCD

Y los dos bytes BCD resultantes ya se pueden imprimir en formato XXXX (cuatro dígitos decimales) con la rutina “pintaBCD”. Por ejemplo, para la coordenada Y:

Asteroids - Pinta Y en BCD

Por tanto, ya hemos resuelto el problema de imprimir en decimal (no en binario, ni en hexadecimal) un byte que contiene un número en codificación binaria.

Si alguna variable, como la coordenada X, no ocupa un byte, sino que ocupa dos bytes, es posible extender el razonamiento anterior tanto como sea necesario.

El resultado podemos verlo en la versión 23 del proyecto y aquí. Obsérvese cómo las coordenadas X e Y ya aparecen en decimal:

Asteroids - XY en decimal.PNG


Código del proyecto: Asteroids23

Asteroids: presentación de números en decimal – velocidad, ángulo y vidas

De todas las variables que tenemos que presentar en decimal, velocidad, ángulo y vidas son las más sencillas. Y son las más sencillas porque, por el rango de valores que toman actualmente, sus codificaciones binarias y BCD coinciden. Por tanto, podemos actuar como si ya estuvieran codificadas en BCD.

Efectivamente, la velocidad toma valores entre 0 y 3 y, en ese rango de valores, las codificaciones binaria y BCD son iguales. Lo mismo ocurre con el ángulo (0 – 7) y con las vidas (0 – 5). Las dos codificaciones empiezan a ser diferentes a partir del valor 10, que en binario se codifica como %0000-1010 y en BCD se codifica como %0001-0000. Pero hasta ahí son idénticas:

Número Codificación binaria Codificación BCD
0 0000-0000 0000-0000
1 0000-0001 0000-0001
2 0000-0010 0000-0010
3 0000-0011 0000-0011
4 0000-0100 0000-0100
5 0000-0101 0000-0101
6 0000-0110 0000-0110
7 0000-0111 0000-0111
8 0000-1000 0000-1000
9 0000-1001 0000-1001
10 0000-1010 0001-0000

Por tanto, en el caso de la velocidad, el ángulo y las vidas, podemos hacer cualquiera de estas cosas:

  • Cambiar para usar la codificación y la aritmética BCD. No merece la pena.
  • O seguir usando la codificación y la aritmética binaria.

Ni siquiera es necesario pasar usar “pintaBCD”. Se puede seguir usando “pintaHex2”, porque los números en el rango 0 – 9, además de codificarse igual en binario y BCD, también se pintan igual en decimal (“pintaBCD”) y en hexadecimal (“pintaHex2”):

Número Representación decimal Representación hexadecimal
0 0 0
1 1 1
2 2 2
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 10 a

En definitiva, para velocidad, ángulo y vidas no hace falta hacer nada, siempre y cuando sigan tomando valores entre 0 y 9. Ahora bien, si algún día se cambia el juego para, por ejemplo, meter 15 vidas, habrá que revisar esta decisión.

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