Asteroids: los asteroides

Parece mentira, pero vamos avanzando. Ya toca meter los asteroides que, en realidad, es casi lo último (nave, disparos y asteroides). Bueno, a decir verdad quedan más cosas, porque luego están las colisiones, las animaciones (explosiones), el sonido, la pantalla de inicio, etc.

Como en el caso de la nave, lo primero es diseñar el aspecto gráfico de los asteroides. Nuevamente, usando el editor de sprites de CBM prg Studio, nos han quedado así:

Asteroids - Diseño asteroides

No hace falta utilizar otro fichero. Se puede utilizar el mismo fichero “Sprites.spt” que utilizamos para la nave, ubicando el diseño en el “frame” 9. De hecho, si utilizamos el mismo fichero ello nos facilita la exportación / importación, porque no tenemos que usar varios ficheros.

En el caso de los asteroides no vamos a necesitar varios “frames”, porque no vamos a hacer que roten. Se podría hacer si se quisiera, o si se quisiera que los asteroides tuvieran formas variadas. De momento, no es el caso.

Lo que sí vamos a hacer es pintar asteroides de diferentes tamaños, para lo que usaremos las opciones de expansión horizontal y vertical de los sprites. En el editor de sprites (ver arriba) estas opciones aparecen activadas, pero esto sólo afecta a cómo se previsualiza el sprite en el editor, con o sin expansión. Posteriormente, a la hora de programar, es cuando hay que indicar que el sprite X está expandido o no. Con los colores ocurría lo mismo.

Y hablando de colores, los asteroides también van a ser sprites multicolor. Por tanto, comparten con la nave estos colores:

  • Color multicolor 1: gris oscuro ($0b).
  • Color multicolor 2: amarillo ($07), aunque en este caso no se utiliza.

Por lo demás, los asteroides usan como color propio el gris claro ($0f). Da la casualidad de que este color también es utilizado por la nave, pero esto no tendría por qué ser así. Sólo los colores del multicolor tienen que ser compartidos.

Y poco más, una vez diseñado el sprite de los asteroides, lo exportamos junto con los “frames” de la nave. Los nueve “frames” van al fichero “Sprites.bin”, siendo los ocho primeros “frames” los de la nave, y el último “frame” el de los asteroides.

A la hora de importarlos no hay que tocar nada en el fichero “Recursos.asm”, ya que no cambia nada. Sólo cambia el contenido del fichero “Sprites.bin”, que ahora tiene un “frame” más:

Asteroids - Importar caracteres

Hay que ser conscientes, eso sí, de que el nuevo “frame” va al final del fichero y, por tanto, le corresponde el puntero 200. A la nave le siguen correspondiendo los punteros 192, 193, 194, 195, 196, 197, 198 y 199. Recordemos que 192 x 64 = 12.288 = $3000, que es precisamente la dirección de carga (*=$3000).

Todo esto lo podéis ir estudiando en la versión 09 del proyecto. Abundaremos más en todo ello en las próximas entradas.


Código del proyecto: Asteroids09

Asteroids: disparos – caracteres personalizados

Bueno, todo llega. Ya tenemos la base de los disparos: podemos crear disparos, moverlos por la pantalla, desactivarlos el llegar a los bordes de la pantalla, etc.

El siguiente paso sería dejar de usar puntos estándar (“.”) y empezar usar disparos personalizados. Como os podéis imaginar, desde el punto de vista gráfico la diferencia es poca. Aquí tenemos un punto estándar:

Asteroids - Punto estándar

Y aquí tenemos nuestro disparo personalizado:

Asteroids - Punto personalizado

La única diferencia es que nuestro disparo personalizado está centrado, mientras que el punto estándar anda algo alicaído.

Entonces… ¿por qué lo hacemos? Pues básicamente para aplicar lo aprendido y aprender a usar caracteres personalizados. Poco más.

¿Qué tenemos que hacer para usar caracteres personalizados? Pues todo esto:

  • Diseñar los caracteres personalizados, en este caso el disparo.
  • Exportar el juego de caracteres personalizados.
  • Importar el juego de caracteres en el juego, igual que se importan los sprites y las pantallas.
  • Configurar el VIC para que utilice el juego de caracteres personalizado, no el juego estándar.
  • A la hora de pintar los disparos, en vez de pintar un punto (“.”), pintar el carácter que el VIC va a sustituir por el disparo. En nuestro caso será la “@”.

Así que vamos a por ello:

Diseñar los caracteres personalizados:

Diseñar un punto es fácil. Ya lo hemos hecho arriba con el editor de caracteres de CBM prg Studio.

Pero no se trata sólo de eso. También se trata de pensar cosas como:

  • ¿Necesito diseñar un juego de caracteres completo? ¿O sólo parte?
  • Si lo rediseño completo, ¿voy a necesitar que algunos de los caracteres nuevos sean letras? ¿Voy a necesitar pintar textos? ¿O sólo gráficos?
  • Si rediseño sólo parte, ¿en qué caracteres estándar ubico mis diseños personalizados?
  • Etc.

En nuestro caso, como sólo necesitamos un punto medio centrado, y además queremos pintar textos (de hecho, ya lo estamos haciendo), lo lógico es ubicar el disparo en un carácter que no moleste mucho, que no vayamos a necesitar. Y por eso hemos elegido la “@”.

Así, cuando pintemos una “@”, es decir, cuando hagamos esto:

lda #$00 ; $00 es el código de pantalla de la “@”

sta $0400 ; $0400 podría ser cualquier otra dirección de pantalla

el VIC no pintará una “@”, sino nuestro disparo. Esto supuesto que el VIC esté bien configurado para usar el juego de caracteres personalizado, claro.

Por eso en el editor de caracteres de CBM prg Studio hemos sustituido el diseño de la “@” por nuestro disparo.

Exportar el juego de caracteres personalizados:

Para exportar el diseño a un fichero binario –hay otras opciones de exportación– hay que seguir el siguiente proceso en el editor de caracteres de CBM prg Studio:

  • Character Set.
  • Export.
  • To File…

En el formulario que aparece elegimos “Export all characters” y finalmente indicamos el fichero destino (ej. “Caracteres.bin”):

Asteroids - Exportación caracteres

Si exportamos todos los caracteres, como hemos dicho, el fichero resultante ocupará 4K, puesto que el juego de caracteres completo tiene 512 caracteres (mayúsculas, mayúsculas invertidas, minúsculas, y minúsculas invertidas), y cada uno de ellos se define con 8 x 8 pixels = 64 bits = 8 bytes. Es decir, 512 x 8 bytes = 4.096 bytes.

Importar el juego de caracteres personalizados:

Inicialmente teníamos un fichero ensamblador llamado “Sprites.asm”, pero hace ya varias entradas que lo renombramos como “Recursos.asm” para importar desde él todo tipo de recursos: sprites, pantallas, caracteres, etc.

Así que está claro, hay que ampliar este fichero para importar desde ahí los nuevos caracteres en la memoria del C64:

Asteroids - Importar caracteres

Respecto a la posición de memoria en que hacer la importación, recordemos que ésta no puede ser arbitraria. Tiene que ser al comienzo de un segmento de 2K del banco de 16K direccionable por el VIC. En nuestro caso, hemos elegido la posición $3800, que efectivamente es lo mismo que $3800 = 14.336 = 7 x 2.048.

Es decir, nos estamos yendo al último segmento, o segmento 7 (%111), del banco de 16K direccionable por el VIC:

Asteroids - Juegos de caracteres

Configurar el VIC para que use el juego de caracteres personalizados:

Como ya comentamos en la entrada dedicada a los caracteres personalizados, para indicar al VIC dónde encontrar los caracteres hay que actuar sobre los bits 1, 2 y 3 del registro VMCSB = $d018. Y recordemos que los bits empiezan a numerarse con el bit 0.

En nuestro caso, que vamos a usar el último segmento, el segmento 7, tendríamos que hacer algo así:

lda #%00001110 ; recordemos que el primer bit es el bit 0

sta $d018

Alternativamente, también podemos usar la rutina “activaJuego” de la librería “LibChars”, que está programada para recibir de forma directa el número de segmento, desde 0 hasta 7, ya que internamente hace las conversiones necesarias. Obsérvese la instrucción “asl a” para desplazar los bits recibidos una posición hacia la izquierda, introduciendo un 0 en el bit 0 antes de hacer el “ora” y el “sta”:

Asteroids - Activa juego

Y esto es, precisamente, lo que hacemos desde la nueva versión de la rutina “inicializaPantalla”, le indicamos al VIC que el juego de caracteres personalizado está en el segmento 7:

Asteroids - Configuración juego chars

Pintar los caracteres personalizados:

Esto puede resultar un poco confuso. Lógicamente, hemos diseñado los caracteres personalizados para pintarlos, pero, a la hora de hacer esto, tenemos que usar los códigos de pantalla correspondientes a los caracteres estándar que se sustituyen.

Por eso, si queremos pintar nuestro disparo personalizado, lo que tenemos que ejecutar es esta secuencia de instrucciones:

lda #$00 ; $00 es el código de pantalla de la “@”

sta $0400 ; $0400 podría ser cualquier otra dirección de pantalla

Estas instrucciones cargan el valor $00 en la posición $0400 de la RAM de vídeo, pero, como el VIC ahora está configurado para usar un juego de caracteres personalizado, el valor $00 no lo pinta como “@”, sino como nuestro disparo.

Esto puede verse en la nueva versión de la rutina “actualizaDisparos”. Ya no pintamos un punto estándar (“.”), sino una “@”, lo que el VIC pintará como nuestro disparo personalizado:

Asteroids - Pintar disparo personalizado

Resultado final:

Gráficamente no se aprecia apenas diferencia entre el punto estándar y nuestro disparo personalizado, pero… ¿¿y lo muchísimo que hemos aprendido en relación con los caracteres personalizados?? 🙂

Todo esto puede verse en la versión 08 del proyecto. En la próxima entrada ya metemos los asteroides.


Código del proyecto: Asteroids08

Asteroids: disparos – algunas mejoras necesarias

Los que probéis un poco la versión 06 del proyecto enseguida os daréis cuenta de una cosa: en ocasiones los disparos borran partes del texto que tenemos a la derecha.

Asteroids - Caracteres borrados

¿Cómo es posible esto si los disparos se desactivan al llegar a los bordes? El problema no está tanto en los disparos que nacen en la zona admitida de la pantalla, y avanzan hacia el texto de la derecha. Estos disparos se desactivan correctamente.

El problema está, más bien, en los disparos que nacen cuando la nave está justo atravesando la zona oculta de la derecha. En estos casos, dado que ahora mismo no tenemos ningún control especial en el momento de la creación del disparo, éste nace, se mueve (borrando alguna letra), y enseguida se desactiva.

El efecto resultante es el ya comentado: algunas letras se borran.

Todavía más, con un poco de habilidad es posible mover la nave justo por la zona superior o inferior de la pantalla, disparar, que el disparo nazca en una posición de memoria inferior o superior a la RAM de vídeo ($0400 – $07e7), y que, al moverse, sobrescriba alguna posición de memoria delicada, haciendo que el programa vuelva a BASIC. Lo que en un PC llamaríamos un “cuelgue”.

Asteroids - Cuelgue

Todo esto tiene fácil solución. Lo más cómodo es verificar en “creaDisparo” que la posición charX, charY en la que nace el disparo está dentro de la zona permitida y, en caso contrario, retornar de la rutina sin llegar a crear el disparo (instrucción “rts”).

De este modo, en la nueva rutina “creaDisparo” aparecen estos controles que ya deberían resultar familiares:

Asteroids - Evitar borrados

Y con ellos, evitamos el problema de que se borren las letras o que la aplicación se cuelgue.

Obsérvese el método de desarrollo incremental. Cuando me puse a diseñar y programar los disparos de ninguna manera podía anticipar que esto ocurriría. Pero, una vez detectado el problema, se corrige con un pequeño “incremento” y listo.

Los cambios pueden verse en la versión 07 del proyecto. Yo con esta versión ya no he conseguido borrar letras ni tirar el ciberespacio 🙂 .


Código del proyecto: Asteroids07

Asteroids: disparos – rutinas accesorias

En la entrada anterior vimos cómo introducir disparos en el juego de Asteroids. Y una característica importante de los disparos es que los hemos implementado como caracteres, no como sprites (que son muy preciados).

Los caracteres, como ya sabemos, tienen características diferentes a las de los sprites. Entre ellas:

  • Se posicionan en una pantalla de 25 filas por 40 columnas. Es decir, su coordenada X va de 0 a 39 (o menos si se sacrifica la parte derecha), y su coordenada Y va de 0 a 24.
  • En el fondo, esas coordenadas (X, Y) o (columna, fila) dan lugar a una posición de memoria en la RAM de vídeo, es decir, en el rango $0400 – $07e7. Es en esa posición de memoria donde hay que hacer un “sta” para pintar el carácter, o pintar un espacio para borrarlo.
  • Los caracteres no se mueven (los sprites sí). Para simular el movimiento de un carácter hay que borrarlo en su posición actual y volver a pintarlo en su nueva posición.

Debido a estas características especiales o diferentes de los caracteres, viene bien tener las dos rutinas accesorias que ya presentamos en la entrada anterior:

Rutina “pixel2Char”:

Cuando la nave dispara, tenemos que quedarnos con sus coordenadas (X, Y) y su ángulo en la tabla de disparos activos. Pero la nave es un sprite, y sus coordenadas se expresan en pixels. Sin embargo, los disparos son caracteres, y sus coordenadas se expresan en (columna, fila).

Sabiendo que un sprite colocado en la esquina superior izquierda de la pantalla ocupa la posición (24, 50), que esto equivale al carácter (0, 0), y que los caracteres ocupan 8 x 8 pixels, ya tenemos las fórmulas que hay que usar:

  • charX = (pixelX – 24) / 8
  • charY = (pixelY – 50) / 8

De este modo, efectivamente, si la nave ocupa la posición (pixelX = 24, pixelY = 50), entonces su disparo nacería en la posición (charX = 0, charY = 0). Correcto.

Ahora bien, esto admite varios matices y correcciones. Por ejemplo, la posición de un sprite se expresa respecto de su esquina superior izquierda. Pero un sprite ocupa una superficie de 24 x 21 pixels, es decir, de casi 3 x 3 caracteres. Y lo suyo sería que el disparo naciera del centro de la nave, no de una esquina. Por tanto:

  • charX = (pixelX – 24) / 8 + 1
  • charY = (pixelY – 50) / 8 + 1

Y esto da lugar a la rutina que hemos diseñado:

Asteroids - Rutina pixel2Char

Obsérvese cómo se activa el acarreo con “sec” antes de las restas (“sbc”) y cómo se desactiva con “clc” antes de las sumas (“adc”). Obsérvese, también, cómo se consigue la división entre 8 mediante tres desplazamientos a la derecha (“lsr”).

Rutina “char2Mem”:

Una vez que tenemos la posición del disparo expresada en caracteres (charX, charY), el siguiente paso es pintar algo en la RAM de vídeo. Puede ser pintar un punto, es decir, propiamente un disparo, o puede ser pintar un espacio para borrarlo.

En cualquier caso, necesitamos pasar de las coordenadas (charX, charY) a la posición equivalente de la RAM de vídeo. Y esto es lo que hace la rutina “char2Mem”.

Nuevamente, sabiendo que la posición (0, 0) equivale a $0400 = 1024, y que las filas son de 40 columnas, la fórmula es sencilla:

  • Posición memoria = 40 x charY + charX + 1024.

De este modo, efectivamente, la posición (0, 0) equivale a $0400 = 1024, y la posición (39, 24) equivale a $07e7 = 2023. Todo correcto.

Sabemos que en ensamblador es fácil multiplicar y dividir por dos. Para ello pueden usarse las instrucciones “asl” y “lsr”, respectivamente. Por ende, también es fácil multiplicar y dividir por ocho (8 = 2^3). Basta con repetir estas instrucciones tres veces.

Y como 40 = 8 x 5, podríamos multiplicar por ocho y sumar cinco veces: 40 x charY = (8 x 5) x charY = (8 x charY) x 5 = (8 x charY) + … + (8 x charY).

Y todavía quedaría sumar charX y 1024, que encima tiene dos bytes. Total, muy farragoso. Tiene que haber algo más fácil.

Y efectivamente lo hay: las tablas de datos. Podemos guardar en una tabla de memoria la posición inicial de cada fila de pantalla:

  • $0400 = 1024.
  • $0428 = 1064.
  • $0450 = 1104.
  • $07c0 = 1984

De este modo, si cargamos charY en un registro índice, por ejemplo, en el registro Y, y lo usamos como índice para acceder a la tabla, ya tenemos el comienzo de la fila. Basta sumar charX y ya tenemos la posición final que estamos buscando.

Hay que tener cuidado con un detalle: las posiciones de memoria del C64 ocupan dos bytes. Por tanto, en realidad necesitamos dos tablas que se complementan, una para el byte menos significativo o “lo” (de low) y otra para el byte más significativo o “hi” (de high). Pero conceptualmente es como una tabla única.

Y esto es, precisamente, lo que utiliza la rutina “char2Mem”:

Asteroids - Rutina char2Mem

Al sumar charX (llamado charX2 en la rutina) no hay que olvidarse del acarreo. Por eso sumamos charX a la parte “lo” de la posición de memoria y, por si hubiera acarreo, sumamos $00 a la parte “hi”. De este modo, si no hay acarreo no se suma nada, y si lo hay, se suma el acarreo (es decir, “me llevo una”) a la parte “hi”.

Por último, CBM prg Studio nos da facilidades para generar tablas de datos. Se hace con la opción Tools > Data Generator:

Asteroids - Data generator

Cuidado, porque Data Generator llama “x” de una forma genérica a la variable que se quiera usar para la fórmula, que en nuestro caso es la coordenada Y o fila del carácter. Que esto no produzca confusión.

Cuando los valores generados son de 16 bits (words), puede ser necesario reorganizar los valores generados en una tabla “lo” y otra “hi”, como hemos hecho en este caso.

Resultado final:

Y todo esto para obtener un resultado final, que es una nave que puede moverse y generar disparos, hasta diez a la vez, que pueden avanzar en direcciones variadas, y que desaparecen al llegar a los límites de la pantalla.

Esto puede verse aquí:

Asteroids - Nave disparando2


Código del proyecto: Asteroids06

Asteroids: disparos

El tema de los disparos es una novedad sustancial. Hay muchas cosas que decidir. ¿Cómo se van a implementar los disparos? ¿Con sprites o con caracteres? ¿Con caracteres estándar o con caracteres personalizados? ¿Cuántos disparos puede haber a la vez? ¿Hasta dónde van a llegar los disparos? Etc.

Respecto a la primera pregunta, sprites o caracteres, dado que la nave ya consume un sprite, y se supone que queremos tener muchos asteroides (el máximo sería siete), la pregunta casi se responde sola: caracteres. Además, un disparo básicamente es un punto o una raya, así que tampoco necesitamos un gráfico muy especial.

Respecto a la segunda pregunta, caracteres estándar o personalizados, en esta entrada vamos a conformar la base de los disparos utilizando caracteres estándar. Más adelante, cuando la base ya esté funcionando, cambiaremos a caracteres personalizados, casi más por aquello de aprender a usarlos que por lo que aportan gráficamente al proyecto.

Admitiremos un máximo de diez disparos a la vez, y estos no tendrán un alcance limitado (esta sería otra opción posible), sino que llegarán hasta los límites de la pantalla o, en su caso, hasta colisionar con un asteroide.

Desde el punto de vista de los programas, las principales novedades son:

  • Fichero “Asteroids.asm”: Como la partida nacerá sin disparos, no es imprescindible una rutina “inicializaDisparos”. Ahora bien, para mover los disparos sí hará falta llamar a una rutina “actualizaDisparos” en el bucle de juego.
  • Fichero “Jugador.asm”: Igual que los movimientos del joystick 2 actualizan la posición del jugador, el pulsado del botón de disparo provocará la creación de un nuevo disparo. Para ello se dota la nueva rutina “actualizaDisparosJugador”.
  • Nuevo fichero “Disparos.asm”: Tiene las estructuras de datos (tablas) para guardar la información sobre los disparos (si están activos o no, su coordenada X, su coordenada Y, su ángulo, etc.). Podría tener una rutina “inicializaDisparos”, aunque ya hemos dicho que no es imprescindible. Tiene una rutina “creaNuevaDisparo” para actualizar esas estructuras de datos con disparos nuevos, así como una rutina “actualizaDisparos” para actualizarlos, lo que básicamente es moverlos.
  • El fichero “Disparos.asm” también tendrá otras rutinas de carácter accesorio, como una rutina “pixel2Char” para convertir posiciones X, Y expresadas en pixels a posiciones X, Y expresadas en caracteres (40 columnas x 25 filas), otra rutina “char2Mem” para convertir posiciones expresadas en caracteres a posición de memoria (en plan dirección de memoria de la RAM de vídeo, es decir, entre $0400 y $07e7), y una rutina “calculaNuevaPosicionDisparo” para calcular la nueva posición de un disparo a partir de su posición actual y su ángulo.

Todo esto lo iremos viendo detalladamente a continuación:

Novedades en el fichero “Asteroids.asm”:

La principal novedad en el fichero “Asteroids.asm” es que ahora tenemos disparos y, por tanto, tenemos que actualizarlos, es decir, moverlos. Esto se hace mediante la llamada a la nueva rutina “actalizaDisparos”, que aparece en el bucle de juego:

Asteroids - Actualiza disparos - llamada

Igualmente, podría haber una inicialización de los disparos, pero en este caso, como la partida nace sin disparos, no se inicializa nada.

Novedades en el fichero “Jugador.asm”:

Ahora actualizar el jugador no sólo es moverlo. También hay que reaccionar ante el disparo del joystick 2. Por tanto, se complica la rutina “actualizaJugador”, ya que aparece una llamada a la nueva rutina “actualizaDisparosJugador”:

Asteroids - Actualiza jugador con disparos

La nueva rutina “actualizaDisparosJugador” detecta la pulsación del botón de disparo del joystick 2 y, en tal caso, crea un nuevo disparo llamando a la rutina “creaDisparo”. La detección de que se ha pulsado el botón de disparo es totalmente análoga a cómo se detectaba el movimiento hacia arriba/abajo/izquierda/derecha (uso de una máscara y la instrucción “bit”), por lo que no abundaremos más aquí:

Asteroids - Actualiza disparos jugador

Sí merece la pena aclarar dos cuestiones:

  • La creación del disparo no es instantánea. En realidad, lo que se hace al detectar el botón es decrementar un retardo o contador (nueva variable “jugadorRetardoDisparo”) y, cuando este contador llega a cero, se crea el disparo y se vuelve a inicializar el contador para el siguiente disparo. De este modo se consigue el mismo efecto que con el ángulo de la nave (variable “jugadorRetardoAngulo”): que la nave no esté disparando a tontas y locas constantemente. Hacen falta varias pulsaciones del disparo seguidas, concretamente cinco, para que tenga lugar un disparo.
  • Cuando por fin tiene lugar el disparo, la rutina “creaDisparo” guarda la posición X, Y de la nave, y también su ángulo. De este modo, el disparo puede nacer en esa posición X, Y, y moverse en la dirección de la nave. Ahora bien, como los disparos van a ser caracteres, y no sprites, no interesa guardar esa posición expresada en pixels, sino expresada en columna y fila. Por ello, se llama antes a la rutina “pixel2Char”, que hace la conversión de unidades.

Nuevo fichero “Disparos.asm”:

El nuevo fichero “Disparos.asm” es análogo a “Jugador.asm”, “Pantalla.asm” y otros similares que añadiremos en el futuro (ej. “Asteroides.asm”). Tiene unas estructuras de datos para guardar información, una rutina de inicialización (que en este caso particular no es necesaria), y una rutina de actualización.

Las estructuras de datos son estas:

Asteroids - Disparos - datos.PNG

Hablamos de “estructuras de datos”, pero igualmente podríamos hablar de “variables”. Usamos el primer término porque, al permitirse hasta diez disparos activos, hacen falta unas tablas en vez de variables simples. Pero en el fondo son lo mismo; la principal diferencia es que hay que manejar un índice para usarlas.

Se utilizan las siguientes tablas de diez posiciones cada una:

  • “disparosActivos”: En cada posición, se marca con $01 que el disparo i-ésimo está activo, y con $00 que está inactivo. Un disparo se pone activo al nacer, y permanece activo hasta que se choca con un asteroide o se sale de pantalla. Una posición inactiva indica que puede usarse para guardar la información de un nuevo disparo que pudiera surgir. Si las diez posiciones estuvieran activas en un momento dado, no se podrían generar más disparos de momento.
  • “disparosCharX”: En cada posición, guarda la columna actual del disparo i-ésimo. Esta información se va actualizando según se mueve el disparo.
  • “disparosCharY”: En cada posición, guarda la fila actual del disparo i-ésimo. Esta información se va actualizando según se mueve el disparo.
  • “disparosAngulo”: En cada posición, guarda el ángulo del disparo i-ésimo. Esta información no cambia, ya que se determina a partir del ángulo de la nave cuando nace el disparo, y a partir de ahí permanece constante hasta que el disparo desaparece. Eso sí, al moverse el disparo el ángulo influye en si sólo cambia su X, sólo su Y, o ambas.

En definitiva, son datos similares a los que ya tenía el jugador (posición X, posición Y, ángulo, …), con las diferencias principales de que puede haber hasta diez disparos a la vez y, por tanto, la información se multiplica por diez (tablas) y, además, hay que controlar qué disparos están activos y cuáles no en un momento dado. Por lo demás, bastante parecido.

Yendo al terreno de las rutinas, la rutina “creaDisparo” es muy sencilla:

Asteroids - Crea disparo.PNG

Es decir, usando el registro X como índice, la rutina recorre la tabla “disparosActivos”. Si el disparo x-ésimo está activo ($01) continúa con el siguiente (hasta un máximo de diez, claro). Si el disparo x-éximo está inactivo ($00), guarda la información de posición y ángulo en él. En resumen, esta rutina crea un disparo guardando su información inicial, si es que hay hueco libre para ello.

Por su parte, la rutina “actualizaDisparos” se encarga de mover los disparos. Esto se hace en bucle para cada uno de los diez disparos posibles, verificando primero si está activo o inactivo. Lógicamente, sólo los disparos activos se mueven.

Y como ahora estamos hablando de caracteres, y no de sprites, “mover” un carácter implica:

  • Borrarlo de su posición actual.
  • Calcular su nueva posición.
  • Comprobar si se ha salido de pantalla.
  • Si no se ha salido, pintarlo en su nueva posición.

Los sprites se pueden mover; los caracteres no. Para simular el movimiento de un carácter hay que borrarlo y volver a pintarlo, como ya se ha indicado.

Para borrar un carácter, lo que hacemos es pintar un espacio en su posición actual (ver nueva rutina “pintaCaracter” de “Pantalla.asm”):

Asteroids - Borrar carácter

Para calcular la nueva posición del disparo, utilizamos la rutina “calculaNuevaPosicionDisparo”:

Asteroids - Disparo nueva posición

Esta rutina es básicamente equivalente a la ya vista “calculaNuevaPosicionJugador”. Es decir, en función del ángulo, incrementa o decrementa la X, la Y, o ambas.

La principal diferencia es que ahora estamos trabajando con caracteres, no con sprites y, por tanto, sumar o restar uno equivale a mover el carácter ocho pixels, frente a lo que ocurría con “calculaNuevaPosicionJugador”, donde el incremento/decremento de pixels se hacía en función de la velocidad de la nave, y con un máximo de tres pixels por incremento/decremento. Por tanto, los disparos se mueven más rápido que la nave, como es lógico.

Para comprobar si un disparo se ha salido de los límites de la pantalla, lo hacemos de forma similar a como eliminamos las zonas ocultas de los sprites, es decir:

  • Nueva columna X: Se compara contra el intervalo válido (0-31). Si es menor que 0 o mayor que 31, se desactiva el disparo. Si está en el intervalo permitido, se acepta el valor de la nueva columna X.
  • Nueva fila Y: Se compara contra el intervalo válido (0-25). Si es menor que 0 o mayor que 25, se desactiva el disparo. Si está en el intervalo permitido, se acepta el valor de la nueva fila Y.

Nótese que para la X de momento no usamos el intervalo 0-39, que sería el lógico teniendo en cuenta que la pantalla del C64 tiene 40 columnas, sino el 0-31 porque la zona más a la derecha es de momento inaccesible para los sprites y además tiene información de posición, velocidad, ángulo, etc.

Estos límites para los disparos pueden verse aquí:

Asteroids - Límites disparos

Por último, queda pintar el disparo en su nueva posición, lo cual viene a hacerse de forma totalmente análoga a como se borró antes, pero pintando un punto (de momento un punto estándar, sin personalizar) y haciéndolo en la nueva posición:

Asteroids - Pintar carácter

Y todo lo anterior (borrar, calcular nueva posición, limitar y repintar) en bucle de 0 a 9, haciéndolo sólo para los disparos activos:

Asteroids - Bucle actualiza disparos

Rutinas accesorias en “Disparos.asm”:

Para que todo lo descrito sea posible hacen falta dos rutinas accesorias:

  • La rutina “pixel2Char”. Esta rutina convierte la posición X, Y (en pixels) de la nave a la posición columna, fila (en caracteres) equivalente para los disparos.
  • La rutina “char2Mem”. Esta rutina convierte la posición columna, fila (en caracteres) a la posición de RAM de vídeo equivalente, de modo que se pueda pintar un espacio (borrar) o un carácter.

Pero para no complicar más esta entrada, que ha sido larga, dejamos las rutinas accesorias para la entrada siguiente.


Código del proyecto: Asteroids06

Asteroids: información en la zona derecha de la pantalla

Los sprites pueden utilizar hasta nueve bits para moverse en la dirección X. En la dirección Y sólo pueden utilizar ocho bits. La pantalla del C64 es apaisada.

El uso de nueve bits en la dirección X no es obligatorio. Muchos juegos del C64 presentaban en la zona derecha de la pantalla información de posición, vidas, tiempo, puntos, etc. Esta información hay que ponerla en algún sitio y, puesto que llevar los sprites hasta la derecha implica complicar un poco los programas, una solución aceptable es pintarla ahí.

Y ese fue el enfoque inicial que le di al proyecto Asteroids05 y siguientes. Luego me he arrepentido, porque me ha parecido que el juego queda mejor con la información abajo, aprovechando la pantalla en toda su amplitud.

No obstante, como hemos dicho que de lo que más se aprende es de los errores, y estoy firmemente convencido de esto, ahí vamos: vamos a meter en la zona derecha de la pantalla información de posición, vidas, puntos, etc.

Luego cambiaremos de enfoque y llevaremos la información abajo, lo que nos dejará una lección importante: es más fácil hacer las cosas bien desde el comienzo, que cambiarlas más adelante. Esto es tanto más cierto cuanto más importante sea el requerimiento o la decisión de diseño que se rectifica.

Por el camino aprenderemos o recordaremos otra cosa: el uso de pantallas. Pero… ¿no dijimos en la primera entrada que Asteroids no usaba pantallas? A ver, no es el típico juego de pantallas que hay que ir superando. Básicamente tiene dos pantallas, la de juego, que es la que nos ocupa ahora, y la inicial, que meteremos más adelante.

Y si empezamos a hacer varias cosas con pantallas, lo suyo es tener un nuevo fichero “Pantalla.asm” (análogo a “Jugador.asm”, “Disparos.asm”, etc.):

Asteroids - Fichero pantallas

En “Pantalla.asm” tenemos:

  • La inicialización de la pantalla.
  • La actualización de la pantalla.

La inicialización de la pantalla es la rutina “inicializaPantalla” que antes estaba directamente en el fichero “Asteroids.asm”:

Asteroids - Rutina inicializa pantalla

En “inicializaPantalla” seguimos teniendo la configuración del color del borde y los colores de fondo en negro, y el relleno de la RAM de color con el color blanco.

Ha desaparecido, en cambio, el relleno de la RAM de vídeo con espacios. Esto es así porque ahora esta información sale de una pantalla diseñada con CBM prg Studio.

La pantalla está diseñada en el fichero “Pantallas.sdd”, y tiene esta apariencia:

Asteroids - Diseño pantalla

La forma de incluirla en el programa es mediante la directiva “incbin Pantallas.bin”. Esta directiva se encuentra en el nuevo fichero “Recursos.asm”, que es un sucesor más general que el anterior “Sprites.asm”:

Asteroids - Fichero recursos

Y, por último, la forma de cargar la pantalla en la inicialización es usando la rutina “copiaBloque” de la librería “LibChars”:

Asteroids - Cargar pantalla

Por otro lado, si pintamos información en pantalla lógicamente es para actualizarla durante el juego. Esto lo hacemos con la rutina “actualizaPantalla”, que se llama desde el bucle de juego:

Asteroids - Rutina actualiza pantalla - llamada

Y lo que hace la rutina “actualizaPantalla” es pintar las coordenadas X, Y, la velocidad y el ángulo. Más adelante añadiremos vidas, puntos, y demás historias.

Asteroids - Rutina actualiza pantalla

Hasta ahora, para pintar un número hexadecimal usábamos la rutina “pintaHex” de la librería “LibText”. Pero esta rutina pinta el número en la posición del cursor, y ahora queremos usar posiciones de memoria concretas. Por ello, ha sido necesario mejorar “libText” con nuevas rutinas que usan posiciones de memoria concretas en vez de la posición del cursor (ej. “pintaHex2”).

En resumen, las novedades que tenemos son:

  • Un nuevo fichero “Pantalla.asm”.
  • Una pantalla diseñada en el fichero “Pantallas.sdd” y exportada al fichero “Pantallas.bin”.
  • El anterior fichero “Sprites.asm” se convierte en el nuevo fichero “Recursos.asm”. Este fichero importa los recursos (sprites, pantallas, etc.).
  • Se inicializa la pantalla en la inicialización, y se actualiza en el bucle de juego.
  • Para la actualización de la pantalla, se ha mejorado la librería “LibText”.

Todo esto puede verse en el proyecto Asteroids05, y el resultado aquí:

Asteroids - Pantalla con info derecha


Código del proyecto: Asteroids05

Asteroids: eliminación de zonas ocultas

La nave ya puede acelerar, frenar, y girar bajo control. Si subimos muy arriba la nave aparece por abajo (y al revés). Y si vamos muy a la izquierda, la nave aparece por la derecha (y al revés). Esto es normal; el ciberespacio es así. Ahora estás aquí y luego estás allí.

Además, hay un trozo de pantalla a la derecha que no conseguimos alcanzar. Esto también es normal, puesto que los sprites utilizan nueve bits para su coordenada X, y de momento nosotros estamos usando sólo ocho. Todo lo que sea X > 255 nos lo perdemos. Esto lo mejoraremos más adelante (librería “LibSprites”).

Por último, tanto arriba, como abajo, como a la izquierda (y más adelante cuando metamos los nueve bits, también a la derecha), hay zonas de pantalla que están ocultas. Los sprites pueden meterse ahí y que no se vean. Esto estaría bien para esquivar los asteroides, si no fuera porque ellos también pueden circular por ahí si no tomamos ninguna medida.

Asteroids - Nave entrando zona oculta

Y esto es precisamente lo que vamos a hacer en esta entrada: eliminar las zonas ocultas.

Para ello, recurrimos a un truco. Temporalmente, vamos a poner el borde en blanco, de modo que se vea mejor la frontera de lo que queremos eliminar:

Asteroids - Borde blanco

Ya con el borde en blanco, vamos a jugar con la posición inicial de la nave, dando valores diferentes a las variables “posicionX” y “posicionY”:

Asteroids - Jugar posicion inicial

Incluso si queremos ser muy precisos en la determinación de los límites, podemos cambiar a mano el “frame” inicial de la nave, de modo que usemos el frame 0 para el borde superior, el frame 4 para el borde inferior, y el frame 6 para el borde izquierdo:

Asteroids - Jugar frame inicial

De este modo, cambiando la posición y el frame iniciales, reensamblando y ejecutando, podemos determinar que los límites son:

  • Por la izquierda: Si jugadorX=1 el sprite todavía se ve; si jugadorX=0 el sprite ya no se ve.
  • Por la derecha: De momento esperamos, porque hay que mejorar el movimiento por la derecha.
  • Por arriba: Si jugadorY=30 el sprite todavía se ve; si jugadorY=29 el sprite ya no se ve.
  • Por abajo: Si jugadorY=249 el sprite todavía se ve; si jugadorY=250 el sprite ya no se ve.

Podemos tomarnos nuestro trabajo muy en serio, e implantar límites por arriba, abajo, izquierda y derecha. O también podemos observar que los límites más importantes son los verticales (Y), especialmente el de abajo, porque tiene un margen de 29 o 30 pixels que se tarda su tiempo en atravesar, y donde pueden ocultarse los sprites.

¿De verdad alguien piensa que una nave espacial, que viaja casi a la velocidad de la luz, tarda en atravesar un pixel (izquierda)? Los límites horizontales los podemos obviar, al menos de momento.

Y así nos disponemos a eliminar las zonas ocultas de arriba y abajo, cosa que logramos con este código nuevo en la rutina “actualizaPosicionJugador”:

Asteroids - Zona oculta inferior

Es decir, la nueva coordenada X que nos devuelve la rutina “calculaNuevaPosicion” la aceptamos tal cual (“sta jugadorX”), y la nueva coordenada Y la sometemos a un tratamiento que consiste en:

  • Si la nueva Y es menor que 29, la convierte en 249.
  • Si la nueva Y es mayor que 250, la convierte en 30.

Es decir, si la nave acaba de entrar en la zona no visible de arriba (29), nos pasa a la zona visible de abajo (249). Y si la nave acaba de entrar en la zona no visible de abajo (250), nos pasa a la zona visible de arriba (30).

El tema de la comparación de números en ensamblador es más complejo de lo que parece, lo que me da pie para recordar esta tabla que resulta muy útil: (donde “A” es el acumulador y “M” es un valor inmediato o el contenido de una posición de memoria que se comparar con el acumulador mediante la instrucción “cmp”):

Condición Instrucción que salta
A = M beq
A M (distinto) bne
A > M bcs
A < M bcc
A >= M bcs
A <= M bcc + beq

Más adelante tendremos que mejorar el control de límites en dos sentidos:

  • Eliminar también las zonas ocultas horizontales, cuando ampliemos la coordenada X a nueve bits.
  • Encapsular este código en una nueva rutina independiente, o en dos rutinas (una para la X y otra para la Y), de modo que no se complique mucho la rutina “actualizaPosicionJugador”.

Y ya podemos navegar por el ciberespacio sin zonas ocultas… Se pueden ver los cambios en el proyecto Asteroids04.


Código del proyecto: Asteroids04