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

Asteroids: niveles de dificultad progresivos

Si habéis jugado a la versión 18 del proyecto, habréis visto que el juego se pone extremadamente difícil nada más empezar. Hay demasiados asteroides (aunque el máximo es sólo siete) y se mueven muy deprisa. Total, enseguida se agotan las vidas del jugador, aunque de momento todavía no estamos controlando si han llegado a cero.

Además, lo anterior ocurre a pesar de que la rutina que activa los asteroides (“actualizaAsteroidesActivos”) no los activa todos de golpe, sino que sólo activa uno en cada ciclo de juego. Pero el código máquina es tan rápido que, sin darnos cuenta, enseguida tenemos todos los asteroides activos.

Lo normal es que, según va avanzando un juego, éste se vaya poniendo paulatinamente más difícil. La mayor dificultad puede consistir en tener más enemigos, tener enemigos más listos o más rápidos, recibir retos más difíciles de resolver, etc.

Por otro lado, el “avance” del juego suele determinarse por el mero paso del tiempo, por el número de enemigos eliminados, por los puntos conseguidos, por las pantallas superadas, etc.

En nuestro caso particular, la mayor dificultad va a consistir en que aparezcan más asteroides (hasta siete), pero de una forma verdaderamente progresiva. Además, también podríamos hacer que éstos se movieran más rápido, de modo que fuera más difícil esquivarlos y destruirlos.

Y el “avance” del juego lo determinaremos en función de un nivel del jugador, que irá subiendo según éste vaya destruyendo asteroides.

La definición del nivel podemos verla en las variables del jugador. Obsérvese que ahora definimos dos variables nuevas, “jugadorNivel” y “jugadorRetardoNivel”:

Asteroids - Nivel del jugador

La variable “jugadorNivel” es el nivel del jugador. Irá subiendo de valor según el jugador vaya matando asteroides. Y la segunda variable, “jugadorRetardoNivel”, la usaremos para lo mismo que el resto de retardos del juego (giros de la nave, disparos, animaciones, etc.), para no incrementar el nivel con cada asteroide destruido, sino con cada N.

De este modo, al destruir un asteroide seguimos haciendo lo mismo que hasta ahora:

  • En la rutina “actualizaColisionesAsteroides”, detectamos la colisión entre el disparo y un asteroide (registro SPBGCL). Señalizamos la colisión cambiando a $02 el estado del asteroide afectado.
  • En la rutina “animaExplosionAsteroide”, hacemos la animación de la explosión para el asteroide afectado.
  • Siguiendo con la rutina “animaExplosionAsteroide”, cuando termina la animación de la explosión, incrementamos en diez los puntos del jugador, restauramos la animación, y ponemos el asteroide inactivo ($00), de modo que la rutina “actualizaAsteroidesActivos” puede volver a activarlo cuando estime oportuno.

Pero ahora tenemos una novedad. Además de incrementar en diez los puntos del jugador, tenemos que incrementar su nivel (cuando proceda). Esto lo hacemos con este trocito de código (etiqueta “aeaFinAnimacion” de la rutina “animaExplosionAsteroide”):

Asteroids - Incremento de nivel

Es decir, además de incrementar los puntos:

  • Decrementamos el retardo del nivel.
  • Sólo si el retardo ha llegado a cero, volvemos a ponerlo en cinco e incrementamos el nivel.
  • El nivel tiene un tope de seis. De ahí no dejamos que pase.

Por tanto, el jugador ya tiene un nivel (desde 0 hasta 6) que va ligado al número de asteroides que ha matado. Cuantos más asteroides destruidos, más nivel.

Por último, nos queda vincular la activación de nuevos asteroides al nivel del jugador. Cuanto mayor sea el nivel, más asteroides activos permitiremos.

Esto lo hacemos en una nueva versión de la rutina “actualizaAsteroidesActivos”:

  • Recorremos la tabla “asteroidesActivos”.
  • Si el asteroide está activo, pasamos al siguiente. Hasta un tope, claro.
  • Si el asteroide no está activo, lo activamos y determinamos aleatoriamente a partir del Jiffy Clock su coordenada X (dos bytes), su coordenada Y, su velocidad, su ángulo y su expansión. Lo posicionamos con “posicionaSprite” y lo configuramos con “configuraBasica” y “configuraAvanzada”.

Hasta aquí, todo básicamente igual que en versiones anteriores. La principal novedad es que ahora el bucle no recorre los siete asteroides posibles, sino que sólo los recorre hasta el nivel actual del jugador (ver «cpy jugadorNivel»):

Asteroids - Activa asteroides nivel jugador

Esto hace que el número de asteroides activos en un momento dado esté limitado superiormente por el nivel del jugador.

Efectivamente, ese es el objetivo: que según el jugador va matando asteroides y ganando puntos, su nivel va subiendo, y el juego, a su vez, va respondiendo con más asteroides activos, es decir, se lo va poniendo más difícil.

Adicionalmente, podríamos meter otro factor de complejidad: la velocidad de los asteroides. Hasta ahora, esa velocidad la hemos venido determinando aleatoriamente a partir del Jiffy Clock:

Asteroids - Velocidad aleatoria

Sin embargo, también podemos ligarla al nivel del jugador con un código como éste (velocidad del asteroide = nivel del jugador + 1):

Asteroids - Velocidad por nivel

Sin embargo, con un código así, cuando el usuario llega al nivel seis, los asteroides se mueven de siete en siete pixels, lo cual es una locura. Por ello, se hace necesario volver al esquema inicial (velocidad aleatoria) o reducir un poco la velocidad utilizando alguna otra fórmula que la vincule con el nivel. En nuestro caso, volvemos al esquema aleatorio.

Por lo demás, la nueva rutina “actualizaAsteroidesActivos” incluye alguna otra novedad, por ejemplo, sendas llamadas a “verificaLimitesXAsteroides” y “verificaLimitesYAsteroides”, de modo que, si las coordenadas aleatorias arrojadas por el Jiffy Clock no caen dentro de los límites permitidos, éstas son corregidas:

Asteroids - Verificación límites nuevos asteroides

Todo esto podéis verlo en la versión 19 del proyecto que, además, incluye una corrección para que los asteroides puedan ser expandidos o sin expandir. Esta corrección está un poco oculta en el código, así que dejaremos un poco más de tiempo para quien quiera responder al reto planteado en la entrada anterior.

Asteroids - Niveles progresivos

En la siguiente entrada dotaremos al juego de una pantalla inicial. Hasta ahora sólo hemos trabajado con la pantalla de juego propiamente dicha.


Código del proyecto: Asteroids19

Asteroids: el reto de los asteroides que siempre se expanden

Una curiosidad que me llamó la atención al probar la versión 18 del proyecto es que, al comienzo del juego, había asteroides grandes y pequeños. Sin embargo, según iba pasando el tiempo parecía que cada vez había más asteroides grandes (expandidos) y menos pequeños (sin expandir). Al final sólo los había grandes.

¿Cómo es posible esto, si la expansión la determino aleatoriamente a partir del Jiffy Clock?

Asteroids - Expansión aleatoria

No os imagináis la de vueltas que le habré dado. Y la de pruebas que habré hecho… ¿Os ha pasado lo mismo?

El gazapo ya está descubierto, y también corregido en la versión 19 del proyecto, que no publicaré hasta la siguiente entrada, dedicada a meter niveles de dificultad progresivos.

Mientras tanto, un reto para el que quiera animarse: el primero que dé con el gazapo y lo documente como un comentario en el blog, recibirá gratis un ejemplar del libro “Programación Retro del Commodore 64”. Y si ya lo tiene, ya veremos lo que hacemos; como poco se llevará el honor de haber pillado el gazapo 🙂 .


Código del proyecto: Asteroids18

Asteroids: movimiento de los sprites por toda la pantalla – asteroides

Los asteroides son sprites, igual que la nave. Por tanto, los cambios que hay que hacer en los asteroides para moverse por toda la pantalla son muy similares a los ya realizados para la nave.

Como en casos anteriores, los cambios ya conocidos o que no merezcan mucha explicación, sólo serán enumerados. Nos detendremos sólo en aquellos que merezcan un poco más de detalle.

Los cambios para los asteroides son:

  • Las coordenadas X ahora pasan a tener dos bytes, el byte Lo y el byte Hi. Recordemos que hay siete asteroides y, por tanto, estamos hablando de tablas. Lo que antes era la tabla “asteroidesX” ahora pasa a ser las tablas “asteroidesXLo” y “asteroidesXHi”.
  • Al inicializar los asteroides activos, los posicionamos en pantalla con “posicionaSprite”. Dado que esta rutina ahora toma dos bytes para la coordenada X, hay que tenerlo en cuenta.
  • Al actualizar la posición de los asteroides, hay que calcular su nueva posición con la rutina “calculaNuevaPosicionJugador” (sí, es correcto). Nuevamente, esta rutina toma ahora dos bytes para la coordenada X. Por tanto, hay que tenerlo en cuenta. También al reposicionar los sprites tras calcular su nueva posición.
  • La verificación de límites cambia. Primero porque ahora verificamos los límites verticales (no es nuevo) y horizontales (novedad). Pero es que además la verificación de límites la sacamos a las nuevas rutinas “verificaLimiteXAsteroides” y “verificaLimiteYAsteroides”. Estas nuevas rutinas son totalmente análogas a las ya vistas para la nave.

En definitiva, son los mismos cambios que ya tuvimos que considerar para la nave. Pero además hay otro cambio que no teníamos con la nave: la rutina “actualizaAsteroidesActivos”.

Esta rutina se encarga de ir activando nuevos asteroides. Y, para hacerlo, utiliza el Jiffy Clock como fuente de información aleatoria para dar valor a la coordenada X, la coordenada Y, la velocidad, el ángulo, y la expansión de los asteroides. Y como ahora la coordenada X de los asteroides consta de dos bytes, hay que tenerlo en cuenta (observad el “and #%00000001” para quedarnos sólo con el bit 0):

Asteroids - Activar asteroides con X doble

En definitiva, mover los sprites por toda la pantalla implica usar dos bytes (nueve bits) para su coordenada X. Lo más básico es que la rutina que posiciona los sprites (“posicionaSprite” en nuestro caso) acepte esos dos bytes. A partir de ahí se derivan un montón de cambios adicionales en declaraciones de variables, inicializaciones, actualizaciones varias, verificaciones de límites, etc., que, afortunadamente, son muy repetitivos.

El resultado podéis verlo aquí y en la ya famosa versión 18 del proyecto:

Asteroids - Sprites por la derecha


Código del proyecto: Asteroids18