Objetivo
En este artículo, explicamos cómo crear una pantalla utilizando mosaicos prefabricados y cómo mover un sprite dentro de este mapa detectando colisiones con casillas que no son transitables.
Solo utilizaremos dos tipos de mosaicos, transitables o no, inspirados en los patrones de Usas, un juego de plataformas de Konami para MSX2 y uno de mis favoritos. Extraje los patrones cargando el juego con openMSX, haciendo una pausa con el depurador, abriendo el visor de VRAM y volcando la imagen de la página de patrones a un archivo. Este archivo en formato BMP se convirtió luego a formato MSX, como se explica en el artículo Carga de Imágenes en MSX.
El programa solo utiliza dos tipos de casillas para una introducción simple. Para crear un mapa más grande, recomiendo usar el software Tiled, que es multiplataforma y permite exportar el mapa a CSV, fácilmente copiable en un archivo C como un array.
¿Qué es un mapa de casillas?
Un mapa de casillas es la creación de un mapa donde cada casilla tiene un código en memoria que indica qué tipo de casilla es y cómo se debe mostrar. Utilizaremos casillas de 8x8 en la pantalla 5, por lo que nuestra pantalla estará formada por 32x26.5 casillas, perdiendo los últimos 4 píxeles de la última fila. Al conocer el contenido de cada casilla en memoria, podemos determinar si un sprite está en una posición restringida o no.
Para detectar si hay una colisión entre el sprite y el mapa, utilizaremos la técnica explicada en este artículo, que consiste en verificar si la siguiente casilla a la que se supone que el sprite se moverá es accesible o no.
Código
En este programa, cargamos una imagen de casillas y, basándonos en un mapa, pintamos estas casillas. Luego, creamos un sprite y controlamos su movimiento en el mapa sin permitir que pase sobre casillas no transitables.
Para el movimiento del sprite, utilizamos una rutina de interrupción más avanzada que la que se encuentra en Fusion-C 1.3. La antigua función era `InitVDPInterruptHandler` (puedes ver su uso en el ejemplo `Interrupt-vdp.c`), pero tenía la desventaja de que si la función ejecutada durante la interrupción estaba en la página 2 de la memoria, se quedaba colgado. Oduvaldo Pavan amablemente desarrolló funciones para que la interrupción funcione en cualquier página, y estas funciones son las que encontrarás en este código.
Hemos hecho esto en el siguiente programa, que se puede encontrar en el Gitlab de MoltSXalats:
La primera parte del código llama a los includes que se utilizarán en el programa, define las variables para cargar las imágenes (file, BUFFER_SIZE_SC5, LDbuffer, mypalette) y la variable `map1` que contendrá información sobre si una casilla puede ser cruzada o no. Este será el esquema que transformaremos posteriormente en la pantalla pintando píxeles de 8x8 de manera diferente según si la casilla map1 es un 0 o un 1.
En la variable `sprite` definimos un sprite sencillo que utilizaremos para movernos por la pantalla.
A continuación vienen las funciones para cargar archivos desde MSX-DOS: `FT_SetName`, `FT_errorHandler`, `FT_LoadSc5Image`, `FT_LoadPalette` y `FT_LoadPaletteMSXViewer`, que ya se han explicado en las publicaciones sobre Carga de Imágenes en MSX, y que utilizaremos para cargar las casillas de 8x8 en la memoria de video del MSX.
Después de las funciones para cargar datos en la VRAM, comenzamos a definir las variables que usaremos para controlar el gancho de las interrupciones de video. La dirección 0xFD9F es la que lee el Z80 cada vez que hay una interrupción de video, y la reescribimos para que ejecute nuestra función.
La función `InterruptHandlerHelper` es una pequeña función en ensamblador que se utilizará para saltar a nuestra función.
`InitializeMyInterruptHandler` se encarga de reemplazar la dirección del gancho de la interrupción por la dirección de nuestra función, que pasamos como primer parámetro, `myInterruptHandlerFunction`. El segundo parámetro, `isVdpInterrupt`, se establece en 1 para indicar que es la interrupción del VDP.
¿Por qué usamos una interrupción del VDP? La utilizamos para establecer un tempo en el juego, un ritmo, donde en cada interrupción actualizamos todos los datos del juego: posición del jugador, posición del enemigo, música, efectos de sonido, etc. Utilizando la interrupción del VDP, se pierde la opción de detectar colisiones de sprites. Pero tienes la ventaja de llevar un ritmo, por ejemplo, para mantener el ritmo de la música de fondo.
En este caso, utilizaremos la interrupción para controlar el movimiento del sprite.
La función `EndMyInterruptHandler` se utiliza para devolver la interrupción del VDP a su rutina anterior antes de poner la nuestra.
La variable `copsVsync` es un contador de las veces que entramos en la interrupción. Sirve para manejar diferentes ritmos; tal vez los enemigos se actualizan cada 2 interrupciones, la música cada interrupción, etc. La variable `processar_moviment` es una bandera para indicar que ahora puedo mover el sprite. Esta bandera me permite separar las tareas realizadas durante la interrupción porque indicará a otra función en el bucle principal que ahora puede trabajar en procesar el movimiento del sprite. De esta manera, si el cálculo del movimiento fuera demasiado pesado y tardara más que el tiempo entre interrupciones, no interferiríamos con otras tareas que también se actualizarían.
La función `main_loop` se ejecuta en cada interrupción del VDP. En este momento, solo tenemos un contador de veces que se produce la interrupción, y cada dos interrupciones, actualizamos la bandera para procesar la posición del sprite. Puedes probar con otros valores; valores más altos harán que el sprite sea menos manejable, y valores más bajos harán que se mueva muy rápido.
La siguiente función inicializa la pantalla del juego. Entre las líneas 283 y 286, carga la imagen de los patrones en una página oculta de la VRAM. Luego, tiene dos bucles para recorrer la matriz de mapas de casillas, comenzando por las filas y luego las columnas. Para cada posición en la matriz, verifica qué tipo de casilla es. En este caso, solo tenemos 2 tipos; según el número, copia la casilla de 8x8 de la página donde cargamos la imagen y la coloca en la posición correspondiente. De esta manera, se dibuja la pantalla. Las funciones HMMM tienen los dos primeros parámetros como las coordenadas de origen de la casilla de 8x8 a copiar. Si cambiamos estos dos parámetros, tendremos un dibujo diferente para las casillas prohibidas y permitidas.
En las líneas siguientes (299 a 303), creamos un sprite simple para moverse por la pantalla. Las líneas siguientes son variables que utilizaremos como coordenadas del sprite y para convertir estas coordenadas a texto y mostrarlas en la pantalla.
Aquí tenemos la función `moviment_sprite`, que es un bucle infinito hasta que se presiona la tecla ESC (línea 311). Dentro de este bucle, primero verificamos si estamos en una interrupción donde necesitamos calcular el movimiento (activada por la función `main_loop` de la interrupción VDP). Si necesitamos encontrar las posiciones del sprite, primero verificamos si se presiona alguna flecha (o joystick en el primer puerto, ya que lee lo mismo al pasar el parámetro 0 a `JoystickRead`) y lo guardamos en la variable `stick`. Las líneas 317 a 321 escriben estas coordenadas en la pantalla; primero convertimos el número a una cadena de caracteres (`sprintf`) y luego lo mostramos en la pantalla con `PutText`. Esto facilita la depuración ya que estamos visualizando las coordenadas del sprite.
Luego hacemos todo el control de teclas para mover el sprite. Comenzamos con la flecha hacia arriba (`stick`==1). Primero establecemos la posición donde iría el sprite un píxel hacia arriba y verificamos en qué casilla estaríamos con esta nueva ubicación. Dado que son bloques de 8x8, necesito convertir las coordenadas a casillas. Para hacer esto, dividimos por 8; para simplificar, desplazamos 3 bits hacia la derecha (`>>` operación a nivel de bits), lo que aproxima a una división por 8, redondeando hacia abajo al entero más cercano. Ahora necesitamos verificar qué tipo de casilla es esta nueva posición, así que buscamos el valor en nuestra variable que contiene el mapa, traduciendo las casillas a un array. Dado que está organizado por filas, la posición de la casilla X es tal como está, y debemos aumentar cada posición de las Y en 32 porque el mapa tiene 32 casillas horizontales. Esto es la línea 331.
Además, también debemos mirar la casilla que está una posición a la derecha del mapa, que es la línea 332. Agregué esta segunda línea de manera empírica; teóricamente, solo sería necesario revisar la casilla futura a la que vamos a ver si se puede acceder o no. Sin embargo, si no la agregaba, accedía a una parte de la casilla prohibida. Sin embargo, mientras escribía esto, creo que el problema puede venir cuando se redondea a un entero, que siempre redondea hacia abajo, y por eso se necesita la siguiente casilla. Bueno, si alguien tiene una mejor explicación que no dude en hacérnoslo saber.
Dado que es un caso simple y solo tenemos 2 tipos de casillas, verificamos si alguna de las dos casillas inspeccionadas está prohibida (de tipo 1 en nuestro caso). Si está prohibida, revertimos la posición del sprite que habíamos cambiado; en este caso, y=y+1. Si hubiera más tipos de casillas prohibidas, deberíamos agregarlas también en este if.
Finalmente, reubicamos el sprite con el nuevo valor de las coordenadas.
El resto de los casos tienen la misma estructura; la posición del sprite cambia y verifica si la nueva casilla (y la siguiente) está permitida o no.
Finalmente, tenemos la función principal que comienza configurando la pantalla (líneas 378 y 379). Luego, cambia el color 15, que en la paleta de la imagen cargada tenía un color oscuro, a blanco para que se pueda leer en la pantalla.
Inicializamos las variables y configuramos la función `main_loop` en el gancho de la interrupción VDP (línea 385). Indicamos la posición donde comenzará el sprite y llamamos a la función con un bucle infinito hasta que se presiona la tecla ESC. Esta función es `moviment_sprite` en la línea 389.
Cuando salimos de este bucle, restauramos el gancho de la interrupción VDP con `EndMyInterruptHandler` y volvemos a la pantalla 0 con los colores originales. Finalmente, terminamos la función y volvemos a DOS con `Exit`.
Conclusión
En este artículo, hemos construido un mapa de cuadrícula simple y movido un sprite por la pantalla sin permitir que el sprite se desplace sobre casillas prohibidas. El mapa solo tiene dos tipos de casillas, y si queremos un mapa más colorido y variado, necesitaríamos crear diferentes tipos de casillas. También tendríamos que controlar estos tipos al pintar, agregando más condicionales en las líneas 291. Conocer estos tipos también requeriría saber sus coordenadas en VRAM, donde se almacenan, para copiarlos correctamente utilizando la función HMMM.
El MSX2 tiene desplazamiento vertical, y el MSX2+ también tiene desplazamiento horizontal. ¿Qué pasa si tenemos un mapa de casillas más grande de lo que se muestra en la pantalla? ¿Podemos usar funciones de desplazamiento para mover toda la pantalla y seguir detectando colisiones? Estos temas se abordarán en futuros artículos.
Ahora que también hemos visto la interrupción de video, podríamos intentar generar un reproductor de MSX-Music (OPLL) que actualiza las notas que se están reproduciendo con cada interrupción. Trabajaremos en esta funcionalidad en artículos futuros.
Haz clic aquí para ver el ejemplo en funcionamiento.
Comments