Doble buffer, capas y FPS in-constantes

La técnica del doble buffer es un una vieja conocida en el arte del desarrollo de videojuegos. Hay explicaciones muy detalladas por internet así que no voy a detenerme en hablar del doble buffer aquí. Lo que nos interesa es saber que el VDP de los MSX2 con 128KB de VRAM  podemos aplicar esta técnica fácilmente:

El VDP organiza toda la su memoria en cuatro “páginas” de 32KB. Cada una de esas páginas puede (y debe) almacenar toda la información necesaria para mostrar en nuestra tele o monitor una imagen, incluyendo la tabla de patrones, de colores de sprites, etc. El VDP además permite hacer el intercambio rápido de la página que se está mostrando y, esto es lo mejor, permite estar modificando una página mientras se muestra otra. Este intercambio solo es posible hacerlo entre las páginas 0 y 1, y las páginas 2 y 3, pero esta limitación no es para nada problemática.

Yo estoy usando estas facilidades del VDP junto con sus operaciones de movimiento de bits de VRAM a VRAM para componer la pantalla del juego “por capas”. Veamos cómo lo hago.

Utilizo las páginas 0 y 1 para aplicar el doble buffer: Mientras dibujo en la página 0, muestro la página 1. Y viceversa, claro. En la página 2 guardo el “sprite sheet” necesario para la fase que el jugador esté jugando en ese momento. En la página 3 se encuentra el HUD que se superpone a toda “la acción”. Ya puestos en situación del contenido de la VRAM, paso a explicar cómo muevo una nave por la pantalla como si fuera un sprite que podemos situar tras un foreground.

Supongamos que estamos en un instante del juego que llamaremos t. En ese instante se está mostrando la página 1 al jugador mientras se va a modificar el contenido de la página 0 para que sea el mostrado en el instante t+1. Antes de modificar la página 0, el contenido de esta es lo que se estaba mostrando al jugador en el instante t-1. La nave se está moviendo hacia abajo:

Movimiento de la nave

Paso 1

Lo primero que hacemos es “borrar” la nave que sigue dibujada en la posición antigua (t-1). Para conseguirlo sustituimos el contenido del recuadro que ocupa la nave con el contenido de ese mismo recuadro (mismos x, y, ancho, alto) desde la página 2, que es donde se encuentra el HUD limpio y reluciente.

Borrado de la nave anterior

Paso 2

A continuación copio la imagen de la nave en la nueva posición que esta tendrá en el instante t+1. Hay que explicar que se aplica “transparencia”: los pixeles  pintados de color 0 (transparente) no son copiados en el destino, respetando el color que ya tuviera. Sin embargo el resultado que obtenemos no es el deseado, puesto que la nave queda por encima del HUD y queremos que quede por debajo.

Copiamos la nave en la nueva posición

Paso 3

Para solucionarlo lo que  hacemos es copiar la porción de HUD que queremos que quede sobre la nave aplicando el mismo operador de “transparencia”:

Superponemos el HUD

Paso 4

Una vez realizadas todos estas operaciones para todos los objetos que queramos mover por la pantalla, podemos intercambiar las funciones de las páginas 0 y 1: La primera se convertirá en la página visible y la segunda en la que modificaremos para ser mostrada más adelante en el instante t+2.

Mejora del algoritmo

Existe una forma de reducir en un tercio el tiempo necesario para montar una escena (si solo dependiera de estas operaciones). Consiste en el uso inteligente de la definición de paletas de colores y las operaciones lógicas.

Si obviamos los colores que no necesitamos para la explicación, la paleta que estoy usando podría ser algo como esto:

Color Índice de paleta Binario
Transparente 0 0000
Blanco 6 0110
Verde 15 1111

Si volvemos al paso 2, vemos que copiábamos la nave de la página 2 a la 0 sustituyendo los píxeles de esta por los de la nave siempre que estos no sean 0. Esta operación en los manuales del VDP recibe el nombre de TIMP. Si en lugar de usar esa operación utilizamos la operación OR, en lugar de sustituir los píxeles lo que hacemos es realizar la operación OR entre la página de origen y de destino. El resultado de todas las combinaciones posibles de colores de pixel de nave con los posible de HUD quedan reflejados en esta tabla:

Pixel nave Pixel destino Pixel resultante
Transparente (0000) OR Transparente (0000)
= Transparente (0000)
Blanco (0110) OR Transparente (0000)
= Blanco (0110)
Transparente (0000) OR Verde (1111) = Verde (1111)
Blanco (0110) OR Verde (1111)
= Verde (1111)

El resultado es que cuando en el HUD haya un pixel transparente se respetará el color de la nave, ya sea blanco u otro color, puesto que todos los números de índices OR 0000 darán como resultado el mismo número. Y cuando haya un píxel verde del HUD prevalecerá este, puesto cualquier número de índice OR 1111 tendrá como resultado 1111, que es el índice del color verde. De esta forma nos ahorramos el paso 3.

FPS inconstantes

El algoritmo final se complica algo más en realidad. Si la nave está en los bordes de la pantalla, con parte de ella fuera de ella, no es necesario (ni recomendable) copiar todos los pixeles de origen,  Lo que se hace es calcular qué parte de la nave es realmente visible en la pantalla, copiando solo esa porción. Eso provoca que el tiempo que se tarda en aplicar el algoritmo para una sola nave en pantalla no sea siempre el mismo, dependiendo del número de píxeles a copiar. Si además estamos realizando operaciones adicionales que puedan ralentizar la construcción de la página (como por ejemplo dibujar un puñado de estrellas) hace que la velocidad de refresco de la escena no tenga un ratio de FPS constante. Si además no tenemos esto en cuenta para el resto de operaciones del juego, tenemos como resultado que la velocidad de los elementos percibida por parte del jugador no es constante. Se puede apreciar estos efectos en video que ya publiqué por aquí:

En eso y el disparo de la nave es en lo que me encuentro trabajando en este momento. Cuando lo tenga solucionado, gracias a la variable del sistema llamada JIFFY, intentaré hacer otra entrada.

Ahí queda mi explicación. Ni que decir tiene que si alguien encuentra una forma más eficiente de hacerlo soy todo oídos.

Dibujando láseres

He pasado la mañana de hoy escribiendo el código para dibujar los disparos de los láseres sin mucho éxito. Primero he intentado dibujar las líneas simplemente con una operación OR para conservar el HUD (ya explicaré cómo en otro post), pero no se me ha ocurrido la forma de borrarlas después si borrar también el HUD. El siguiente intento ha consistido en hacer la operación XOR tanto para el pintado como para el borrado. Esta vez funciona pero produces ciertos artefactos. Imagino que el algoritmo de pintado de líneas no está comprobando si el VDP está ocupado moviendo bits y entra en conflicto con el movimiento de objetos por pantalla. Toca descansar, comer y repensar el asunto.

 

Un cielo lleno de estrellas

Hace tiempo que no escribo por aquí. Tengo en preparación un artículo algo más técnico que me está dando más trabajo del que pensaba. A veces tengo que elegir entre programar y escribir por el poco tiempo que tengo. Normalmente gana programar. Dejo subido un vídeo de lo que he avanzado hoy con el movimiento del cielo, aprovechando que estoy de vacaciones. Aún no tiene un FPS constante, pero estoy en ello:

Aspect ratio

Una cosa que siempre me resultó molesta de los MSX era que si dibujabas un cuadrado o un circulo, en pantalla aparecía un rectángulo o una elipse achatados respectivamente. Había que aplicar un factor de 1,4 aproximadamente en el eje Y para que visualmente tuvieran el mismo ancho que alto. Además, por lo que sé, esa relación entre los ejes X e Y es diferente si el ordenador es NTCS en lugar del PAL europeo. No quiero que mi juego  tenga ese aspecto achatado característico así que estoy diseñando los gráficos con la Y multiplicada por ese 1’4:

Imagen creada en Pixen con las Y “estiradas”

Eso complica un poco las cosas porque si bien es fácil crear una elipse o un rectángulo con las dimensiones correctas, hacer que todas esas rayitas oblicuas queden en su sitio y correctamente orientadas tiene su dificultad. Al final el resultado ha sido más que satisfactorio (para mí):

Resultado en un MSX2 real.

Con la imagen del HUD de momento finalizada me he puesto por fin a codificar: carga de imágenes en VRAM, composición de la pantalla por capas, doble buffer y movimiento del puntero. En la película el punto de mira se mueve por el interior del circulo central del HUD con un mando y la posición de la cabina con otro (según he podido deducir). Yo por simplificar estoy haciendo que cuando el puntero llega al borde del círculo empuja a la cabina en esa dirección:

En este punto me he encontrado con otro problema. Limitar el movimiento dentro del círculo es relativamente sencillo en un computador con un procesador de 8 bits a 4MHz: no hay más que comprobar que la distancia al cuadrado del puntero al centro es menor que el cuadrado del radio del circulo (uso el cuadrado para evitar hacer la raíz cuadrada). Sooooolo queeee en realidad no es un círculo sino una elipse porque como soy tan guay quería que mis círculos no estuvieran achatados. Recuerda que aunque la imagen se muestre como la captura del MSX2 real, internamente el dibujo es la elipse de la primera imagen de esta entrada. Comprobar que un punto está dentro de una elipse no es tan trivial en un procesador como el que hemos mencionado. En este punto he tenido que ser imaginativo y calcular los límites de la elipse de forma off-line en una hoja de cálculo.

Como cada uno de los cuatro sectores en los que podemos dividir una elipse son simétricos dos a dos en los ejes vertical y horizontal, en realidad solo he tenido que calcular los límites del sector superior derecho. Para cada X he calculado la Y máxima y las he guardado en un array.

static TINY y[50] = {
 69,69,69,69,69,69,68,68,68,68,
 68,67,67,67,66,66,65,65,64,64,
 63,62,62,61,60,59,58,58,57,56,
 55,53,52,51,50,48,47,45,44,42,
 40,38,36,33,30,27,24,20,14,0
 };

Para comprobar que el punto de mira está dentro de los limites de la elipse, suponiendo que el centro de la elipse es el origen de coordenadas:

  1. Compruebo que el valor absoluto de la X del punto de mira es menor que la longitud de ese array.
  2. Si se cumple lo anterior, compruebo que el valor de la Y del punto de mira es menor que el valor guardado en el array para esa X.
 x = abs(x);
 y = abs(y);

 if (x<50 && y<y[x]) {
  /* Mover punto de mira */
 } else {
  /* Mover cabina */
 }

Funciona, como se puede ver en el vídeo.

Continuará…

Un año más tarde…

Al final las cosas nunca salen como uno planea. Después de un año sólo puedo decir que he aprendido mucho y que mis proyectos de juegos están en su mayoría parados. Por el camino ha caído un tercer premio en la Imagin Bank Challenge 2016 (Games Edition) y una vuelta a mis orígenes.

Me explico: Yo empecé en esto de la programación con unos 13 ó 14 años con un ordenador MSX de primera generación, concretamente con un Panasonic CF-2700. Por esos tiempos ya intentaba hacer mis primeros juegos y soñaba con tener un MSX2. Hace poco adquirí en una subasta de eBay un Philips NSM 8250 con una disquetera adicional (como un NMS 8255), que era uno de los modelos que más deseaba tener. Nunca llegué a publicar ningún juego, ni siquiera en la escena amateur que se creó casi al final de la vida de ese estándar de ordenadores de 8 bits.

 

MSX2 game concept test from Onikami on Vimeo.

 

He decidido quitarme esa espinita viendo que aun hoy en día se siguen publicando títulos para esas máquinas. Después de escribir en BASIC una prueba de concepto para comprobar si un MSX2 tiene la potencia necesaria, he decidido que mi primer juego para MSX2 (y espero que primero que publique para el estándar) sea un juego de combate espacial inspirado en la película The Last Starfighter. Espero que la próxima vez que escriba en este blog sea en breve para contar mis avances, y no dentro de un año.