Asteroids: disparos #Programación retro del Commodore 64

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 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:

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":

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í:

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:

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:

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"):

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

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í:

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:

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:

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, las dejamos para la entrada siguiente.


Editar

Josepzin

No hay comentarios:

Publicar un comentario