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

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

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

Tras pintar la pantalla inicial / final, y en su caso las puntuaciones, hay que pedirle al usuario que dé algún tipo de indicación de que quiere comenzar una partida. Típicamente se trata de pulsar una tecla o el disparo del joystick.

Dado que ya estábamos usando el joystick 2 para mover la nave y disparar, inicialmente pensé en usar el disparo para dar paso a la partida. Y llegué a implementar el código de este modo:

Asteroids - Inicio partida con disparo

Es decir:

  • Leemos el joystick 2 y cargamos lo leído en la variable “joy2”.
  • Hacemos un AND de la máscara Disparo = %00010000 y la variable “joy2”, es decir, nos quedamos con el bit 4 de “joy2”. Ese bit corresponde al disparo del joystick 2.
  • Si el bit 4 vale 1, volvemos a empezar. Recordemos que la señal para marcar un disparo (o un movimiento del joystick) no es 1, sino 0.
  • Si el bit 4 vale 0 es que se ha disparado. Por tanto, continuamos la ejecución con lo que sigue, que es la inicialización del juego.

El código funciona, pero tiene un problema. Cuando uno está afanado matando asteroides, y viene a visitarle “La Parca” porque ya no le quedan vidas, lo más normal es que le pille a uno pulsando el disparo frenéticamente. Al menos, morir matando, ¿no? Y en circunstancias como éstas es muy fácil morir, salir a la pantalla inicial, seguir disparando todo loco, y volver e empezar –sin querer– otra partida.

Total, hay que buscarle solución. ¿Qué tal el teclado? El juego de momento no lo usa, así que puede ser una buena solución al problema anterior, aparte de una buena oportunidad para aprender y practicar cosas nuevas.

Para leer del teclado una opción es usar la rutina “GETIN” del Kernal. Hay otras rutinas (ej. “SCNKEY”), pero “GETIN” es fácil de usar. Su definición es así:

Asteroids - Getin.PNG

Es decir, “GETIN” lee un byte de la entrada estándar, que por defecto es el teclado, y lo deposita en el acumulador. Más fácil imposible.

Total, nos queda este código sencillito, si lo que queremos es que el usuario pulse el espacio para jugar:

Asteroids - Inicio partida con espacio.PNG

Es decir:

  • Leemos el teclado con “jsr getin”.
  • Si el byte leído es 0, no se ha pulsado una tecla. Por tanto, volvemos a leer el teclado.
  • Si el byte leído no es 0, se ha pulsado una tecla. Si no es el espacio (32), volvemos a leer el teclado. Si sí es el espacio, continuamos con la inicialización del juego, es decir, iniciamos la partida.

En realidad, el salto condicional “beq menuBucle” se podría considerar subsumido en “cmp #32” y “bne menuBucle”, ya que si el acumulador vale 0 está claro que tampoco vale 32. Vamos, que se podría simplificar. Pero está bien saber cómo comprobar si ha habido pulsación o no en general, sin comprobar una tecla específica. Esto se hace comparando con 0.

Y no tiene mucho más misterio… Pero nos ha servido para aprender a usar el teclado, que lógicamente es algo de lo más interesante.

Asteroids - Pantalla inicial sin puntos

El código de aplicación sigue siendo el de la versión 20. Y en la próxima entrada controlaremos el final de la partida, la visita de “La Parca” …


Código del proyecto: Asteroids20

Asteroids: pantalla inicial y pantalla de juego – nueva pantalla

Hasta ahora nos hemos centrado en la pantalla de juego, que es la principal. Ahora bien, los juegos suelen tener una pantalla inicial o de carga, la pantalla o pantallas de juego, y una pantalla final que típicamente muestra las puntuaciones y los records.

Esto es lo que vamos a hacer en esta entrada y las que siguen: añadir una pantalla inicial y una pantalla final. En realidad, si la pantalla inicial la reutilizamos tanto para el comienzo del juego como para el final del juego (añadiendo las puntuaciones en este caso), entonces sólo necesitamos una pantalla nueva: la pantalla inicial / final.

Añadir una pantalla inicial a la pantalla de juego implica hacer todo esto:

  • Por supuesto, diseñar la pantalla inicial y exportarla a un fichero.
  • Cargar la pantalla inicial igual que el resto de los recursos.
  • Al empezar el juego, pintar la pantalla inicial.
  • Pedir al usuario que pulse una tecla o el disparo del joystick. Mientras no lo haga, esperar.
  • A partir del momento en que el usuario pulsa la tecla o el disparo, inicializar el juego como hasta ahora (inicializar la pantalla de juego, el jugador y los asteroides), y entrar en el bucle de juego también como hasta ahora (actualizar la pantalla de juego, el jugador, los disparos y los asteroides).

Además, habrá que detectar una condición de finalización de la partida, que en nuestro caso será que las vidas del jugador lleguen a cero. Hasta ahora no estábamos controlando esta situación, pero tenemos que empezar a hacerlo.

A partir de ahora, cada vez que la nave choque con un asteroide, además de restar una vida al jugador, controlaremos si las vidas han llegado a cero. En caso negativo, seguiremos jugando. Y en caso afirmativo, terminaremos la partida volviendo a la pantalla inicial que, ahora sí, mostrará los puntos conseguidos y el record de la sesión de partidas.

Vamos con los primeros pasos:

Diseño y exportación de la pantalla inicial:

Aquí hay pocas novedades, porque ya hemos diseñado varias pantallas. Se trata de diseñar una pantalla nueva con el editor de pantallas de CBM prg Studio:

Asteroids - Pantalla inicial.PNG

No es necesario utilizar un fichero nuevo. Se puede reutilizar el fichero “Pantallas.sdd” añadiendo una nueva pantalla.

Hay que exportar y cargar la nueva pantalla. Es perfectamente posible hacerlo conjuntamente con la pantalla de juego, en un único fichero de exportación / carga, pero en mi caso he hecho dos exportaciones a ficheros independientes: “Pantallas.bin” para la pantalla de juego y “PantallaMenu.bin” para la pantalla inicial / final.

Por cierto, otra alternativa perfectamente posible, y muy usada, sería diseñar y cargar un bonito bitmap.

Carga de la pantalla inicial:

El siguiente paso es hacer la carga desde “Recursos.asm”.

Al hacerlo, si mantenéis el orden previo de carga (sprites, pantallas y caracteres) veréis que las dos pantallas que tenemos ahora, y que ocupan 1.000 caracteres cada una (total 2.000), no caben en el hueco de memoria entre sprites (que empiezan en $3000) y caracteres (que empezaban en $3800). Esto es lógico, porque $3800 – $3000 = $800 = 2.048, pero de ahí hay que descontar lo que ocupan los sprites (12 sprites x 64 bytes = $300 = 768).

Total, hay que rediseñar la carga en memoria. Esto se ve en el nuevo fichero “Recursos.asm”:

Asteroids - Nueva carga recursos.PNG

La idea básica es cargar las pantallas a partir de los caracteres personalizados, no antes.

Pintado de la pantalla inicial:

Antes, al empezar al juego nos íbamos directos a pintar / inicializar la pantalla de juego (ver llamada “jsr inicializaPantalla”):

Asteroids - Pintado pantalla juego

Ahora, al empezar el juego tenemos que pintar / inicializar la pantalla inicial (ver llamada “jsr inicializaPantallaMenu”):

Asteroids - Pintado pantalla inicial.PNG

Además, y sólo cuando la variable “jugadorPuntos” sea distinta de cero, es decir, cuando vengamos de terminar una partida previa, también se pintarán los puntos con la rutina “pintaPuntos”. Por tanto, con el juego recién cargado, no se pintarán los puntos; tras jugar una partida sí se pintarán.

La nueva rutina “inicializaPantallaMenu” tiene esta definición:

Asteroids - Rutina pantalla inicial.PNG

Todo lo que tiene que ver con la configuración de los colores del borde, del fondo y de los caracteres se ha movido de la rutina “inicializaPantallaJuego” a la nueva, puesto que no tiene sentido ejecutar esa configuración varias veces (salvo que hubiera cambios de color, lo cual no es el caso).

Pintado de los puntos:

La rutina “pintaPuntos”, que se invoca tras pintar la pantalla inicial cuando los puntos del jugador no son cero, tiene esta otra definición:

Asteroids - Pinta puntos.PNG

Es decir, esta rutina pinta el literal “PUNTOS:” y los puntos del jugador, así como el literal “MÁXIMO:” y el máximo de puntos de la sesión.

En todos los casos, el pintado se hace en posiciones específicas de la RAM de vídeo, que se indican como parámetros de entrada a las rutinas. Esto ya era posible con los números (ver rutina “pintaHex2” de la librería “LibText”), pero todavía no era posible con las cadenas. Para ello ha sido necesario mejorar la librería “LibText” con una nueva rutina “pintaCadena3”.

Resultado:

El resultado con el juego recién cargado es éste:

Asteroids - Pantalla inicial sin puntos.PNG

Y tras terminar una partida es este otro:

Asteroids - Pantalla inicial con puntos.PNG

Todo esto puede verse en la versión 20 del proyecto.

En las entradas que siguen completaremos las explicaciones que faltan (pulsación de una tecla o del disparo del joystick, control del final de la partida, etc.).


Código del proyecto: Asteroids20