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:
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.
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.
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”:
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í:
https://vimeo.com/245771849
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.