Objetivo
En un artículo anterior (Moviéndonos a través de una pantalla de casillas), exploramos cómo crear una pantalla con un mapa de mosaicos y cómo mover un sprite que detecta los mosaicos intransitables. Pero ¿qué pasa si nuestro mapa de mosaicos es más grande que el espacio de pantalla disponible? Una solución común en el MSX es borrar la pantalla y volver a cargar la nueva sección visible. Otro método es desplazar la pantalla. Este artículo se centrará en desarrollar esta última técnica, que implica el desplazamiento. En este vídeo se puede ver el resultado final.
Tipos de scroll en MSX
Las computadoras MSX1 utilizan el VDP (Procesador de Datos de Vídeo) de Texas Instruments TMS9918 (o un clon de este). Este VDP no tiene capacidades de scroll incorporadas, por lo que el scroll debía hacerse mediante software. La siguiente generación utiliza el VDP de Yamaha V9938, que incluye desplazamiento vertical, y en la tercera generación, el MSX2+, introduce el desplazamiento horizontal con el V9958.
Entonces, ¿cómo lograban las computadoras MSX1 el desplazamiento, como en Knightmare? Tenían que implementar el desplazamiento mediante software. Cada actualización de pantalla requería volver a dibujarla rápidamente con el desplazamiento deseado, y en la mayoría de los juegos, el desplazamiento parece ser algo entrecortado, como se puede ver en este vídeo de Knightmare de Araubi. Este artículo de Grauw explica cómo funciona el desplazamiento por software en diferentes sistemas MSX. Exploraremos las características proporcionadas por el MSX2+ para implementar nuestro desplazamiento.
Scroll hardware V9958 (MSX2+)
El chip de vídeo del MSX2+ (y Turbo-R) añade 3 registros nuevos para controlar el desplazamiento horizontal, que son #25, #26, #27, según se explica en la documentación de Yamaha.
El registro #25 configura el tipo de desplazamiento en los bits más bajos. El bit 0, llamado SP2, se establece en 0 si el desplazamiento es de una sola página o de doble página. En un desplazamiento de una sola página, la parte izquierda de la pantalla que no es visible aparece en el lado derecho. Si SP2 se establece en 1, el desplazamiento es de doble página, y a medida que la primera página desaparece, se llena desde el otro lado con la segunda página.
El otro bit de configuración, el bit 1, llamado MSK, controla si se enmascaran los 8 píxeles más a la izquierda. Cuando se establece en 0, los 8 píxeles a la izquierda no están enmascarados y son visibles. Cuando se establece en 1, los 8 píxeles están ocultos. Esto se utiliza porque durante el desplazamiento, los 8 píxeles a la izquierda tienen un valor incierto, por lo que enmascararlos oculta cualquier artefacto.
En Fusion-C, los comandos para realizar el desplazamiento son SetScrollDouble(char n), SetScrollMask(char n), SetScrollH(int n), SetScrollV(char n). Los dos primeros se utilizan para configurar el registro #25. Pasar el valor 1 a SetScrollDouble coloca la siguiente página de VRAM después de la página activa. Con un valor de 0, el desplazamiento se realiza solo en una página. Con SetScrollMask, cuando se le pasa un 1 oculta los 8 píxeles más a la izquierda, y pasar un 0 los muestra. Los otros dos comandos mueven la pantalla, desplazando la cantidad especificada de píxeles desde la imagen original.
Para tener un primer contacto con estas funciones, veamos un pequeño programa que carga 4 imágenes en las 4 páginas de la pantalla 5 y prueba las funciones mencionadas anteriormente. Puedes encontrar este programa en el GitLab de MoltsXalats.
Primero que todo, comenzamos escribiendo los includes y definiendo las variables para cargar las imágenes y las paletas según se explica en el artículo "Cargar imágenes al MSX".
A continuación, continuamos definiendo las funciones para leer archivos desde el disco:
Y comenzamos a definir la función principal (main) del programa:
Hasta la línea 156, cambiamos el color del texto y cambiamos a la pantalla 5 para cargar las diferentes imágenes y paletas. Las imágenes que tenemos en cada página, según lo indicado por el depurador, son:
Lo primero que intentaremos es desplazar la página 2 de la VRAM 125 píxeles hacia la derecha y 15 píxeles hacia abajo, con el bloque de 8 píxeles más a la izquierda oculto, y esperaremos a que se presione una tecla. En la pantalla, obtenemos:
Ahora haremos lo mismo, pero con el bloque de 8 píxeles visible para la página 0:
El hecho de ocultar o no los 8 píxeles en un movimiento espontáneo, sin que varíe, simplemente significa que tenemos 8 píxeles menos de imagen.
Continuemos con el programa:
Ahora realizamos un desplazamiento concatenado (SetScrollDouble(1)) de la página 2, en las líneas 198-209:
Y ¿qué pasa si la página visible es la 3? ¿Qué se engancha en la parte posterior? Pues se engancha la página 0 como se puede ver en el siguiente ejemplo de código, resultando en esta imagen:
La única diferencia que hemos notado al ocultar o no el bloque de los primeros 8 píxeles es que perdemos parte de la imagen que queda oscura. Pero ¿qué pasa si hacemos un desplazamiento de uno en uno? ¿Qué vemos en este bloque de 8 píxeles? Esto es lo que hemos intentado resolver con las líneas de código de la 228 a la 243.
Lo que sucede en este bloque de 8 píxeles es que parpadean cada 8 píxeles, se desplazan, pero cuando llegan al final vuelven a ser negros.
Mientras que si tenemos el bloque oculto, como se hace en las líneas 246-254, vemos que la parte visible se desplaza suavemente, sin parpadear en el lado izquierdo.
Finalmente, nos queda preguntarnos qué sucede si le indicamos que haga un desplazamiento de más de 256 píxeles. Esto es lo que intentamos responder en la última actividad (línea 256), y como se puede ver, vuelve a comenzar desde el principio. Entiendo que el hardware acepta valores de hasta 512, por ejemplo, si hacemos un desplazamiento en la pantalla 7, pero cuando se aplica a 256, simplemente vuelve a empezar.
Finalmente, regresamos a la pantalla 0, restauramos la paleta y salimos.
Si abres el depurador de openMSX durante alguno de los ejemplos, verás las imágenes tal como están en la memoria, sin ningún desplazamiento. Este desplazamiento de los registros V9958 solo afecta al renderizado de la pantalla; las posiciones en la VRAM permanecen inalteradas. Por lo tanto, si necesitamos copiar segmentos de memoria usando coordenadas, estas coordenadas deben hacer referencia a la página original, la que está en el depurador, no a las coordenadas que vemos en la pantalla.
Una última cosa que me gustaría mencionar es que SpaceManbow utiliza el V9958. En este video de YouTube, puedes ver la diferencia entre el V9958 y el V9938 y apreciar el suave desplazamiento del V9958.
Scroll en pantalla de casillas
Una vez que hemos experimentado con lo que puede hacer el V9958, intentemos aplicarlo al mapa de casillas que teníamos en el artículo anterior. Lo que queremos lograr es que cuando el personaje principal se mueva hacia el borde de la pantalla, aparezca el siguiente bloque de casillas desplazando todas las demás. Al mismo tiempo, cuando se mueva dentro de la zona permitida y encuentre una casilla por la que no pueda pasar, no debería cruzarla como lo hacía en el mapa de casillas.
Para lograr esto, hemos optado por desplazarnos dentro de una sola pantalla y dibujar nuevas casillas a medida que nos movemos, utilizando los 8 píxeles del bloque oculto y los píxeles más allá de la línea 212.
Explicaremos el código en detalle:
Comencemos con los includes que utilizaremos y las variables para cargar las imágenes y la paleta. Luego, definimos dos variables que indican la casilla actual del mapa que se está pintando en la esquina superior izquierda de la pantalla, map_tile_x y map_tile_y. Las variables x e y representan las coordenadas del personaje, el sprite que controlamos y movemos por la pantalla.
A continuación, tenemos constantes para las teclas y las coordenadas de la página de memoria VRAM. Después de eso, definimos NOMBRE_RAJOLES_HOR y NOMBRE_RAJOLES_VER, que representan las dimensiones del mapa de casillas definido en map1. Las constantes NOMBRE_RAJOLES_HOR_ORIGEN_PATRONS representan el número de casillas que caben en una pantalla horizontal, y NOMBRE_RAJOLES_PANTALLA_HOR contiene el número de casillas que pintaremos, excluyendo la oculta que estamos revelando gradualmente. Las siguientes constantes son las equivalentes para las dimensiones verticales: NOMBRE_RAJOLES_PANTALLA_VER representa las casillas visibles, y NOMBRE_RAJOLES_PANTALLA_VER_SCROLL representa las casillas que usamos para el desplazamiento, aquellas que caben en una página de rotación completa.
Finalmente, tenemos la variable constante map1, que contiene la información sobre el tipo de cada casilla, similar al artículo del mapa de casillas, pero esta vez mucho más grande que lo que cabe en una pantalla. Aquí, la he definido directamente en el código, pero si pagináramos y quisiéramos que fuera independiente del código, podríamos definir la variable en una dirección de memoria como char map __at 0x????, y luego cargar el archivo en esa dirección de memoria durante la carga. Dado que la variable es tan larga, ocupa muchas capturas de pantalla.
Una vez que hemos definido todo el mapa, definimos un sprite que moveremos por la pantalla, seguido de todas las funciones para cargar las imágenes desde el disco.
A continuación, tenemos la interrupción del VDP, que, al igual que en el artículo del mapa de casillas, utilizaremos para activar una bandera que indique que necesitamos analizar toda la lógica de movimiento.
Ahora pasamos a pintar la pantalla del juego, tal como lo hicimos en el artículo anterior: stamp_x y stamp_y tendrán las coordenadas donde tenemos las imágenes de las casillas que formarán el mapa. Dependiendo del tipo de casilla, devolverán ciertas coordenadas para ser pintadas.
La función obtenir_coordenades_rajola(map_x, map_y) devuelve el tipo de casilla que debe colocarse en las coordenadas dadas.
Luego, pintamos la pantalla del juego en init_pantalla_joc() cargando la imagen con los patrones y pintándola completamente. Creamos el sprite y establecemos el resto en la coordenada 255 para que no aparezcan. Leí este artículo en msx.org mientras escribía este post y pensé que en lugar de la línea 255, que tiene el problema de que cuando el desplazamiento hacia arriba aparecen, es mejor colocarlos en los 8 píxeles ocultos del desplazamiento horizontal, que no se mueven.
Ahora tenemos las variables que usaremos para controlar el desplazamiento. pos_scroll_x y pos_scroll_y almacenarán la posición del scroll en los ejes horizontal y vertical, respectivamente. Usaremos banderas para determinar si aún necesitamos hacer scroll o si ya estamos en el límite del mapa. Estas banderas son fer_scroll_lateral y fer_scroll_vertical. En la línea 341, tenemos las variables desti_x y desti_y, que indican la posición donde necesitamos pintar la nueva línea de casillas de 8x8. Estas variables están definidas como char para que podamos realizar la operación siempre con módulo 256, ya que si superamos el límite, siempre toma los 8 bits menos significativos. Dado que tanto el desplazamiento vertical como horizontal (ya que no estamos usando las dos páginas vinculadas) también son de 256.
Ahora, comencemos con la primera función para pintar el scroll, que es la de movimiento hacia arriba, scroll_amunt(). Lo primero que hacemos es verificar si hemos alcanzado el límite del mapa; si no es así, podemos activar el scroll vertical estableciendo la bandera fer_scroll_vertical.
En la línea 348, tenemos el código para el desplazamiento. Primero, decrementamos la variable pos_scroll_y para mover una línea hacia arriba y comprobamos si estamos en la posición para volver a pintar una línea en la pantalla, que es cada 8 píxeles. Por lo tanto, comprobamos si los 3 bits menos significativos son 0. Si necesitamos pintar una nueva línea, tenemos que informar al mapa de que hemos cambiado de posición, así que decrementamos map_tile_y. A continuación, necesitamos determinar la posición del scroll para saber qué coordenadas de la página estamos viendo. Esto se almacena en desti_y, que es el valor del scroll menos los 8 píxeles que vamos a pintar. El bucle de pintura debe hacerse horizontalmente, así que desti_x dependerá de la variable del bucle (n) y de la posición del scroll. Lo primero que hacemos es convertir las coordenadas de píxeles a coordenadas de mapa (bloques de 8) dividiendo por 8, tomando la parte entera, que es lo mismo que desplazar 3 bits hacia la derecha. Luego, sumamos la variable del bucle que está en casillas y la convertimos nuevamente en coordenadas, ahora multiplicando por 8, pero sería más eficiente desplazar 3 bits hacia la izquierda. Obtenemos las coordenadas de las casillas del mapa y copiamos la casilla de patrón en las posiciones calculadas de desti_x y desti_y.
En la línea 362, restamos una posición al scroll vertical. Encontré esta línea empíricamente. Si el personaje principal se detenía justo en esta línea y no continuaba hacia arriba, sino que cambiaba de dirección, el siguiente mapa que se pintaba no se hacía correctamente.
Después de manejar estos casos, procedemos a hacer scroll vertical llamando a la función SetScrollV().
Como última parte del código, comprobamos si con las nuevas coordenadas de scroll hemos excedido el mapa. Si es así, indicamos a la bandera que el scroll ya no está activo y reducimos la posición de la pantalla para volver a la anterior.
Ahora pasamos a hacer lo mismo para el desplazamiento hacia abajo. Primero, detectamos si no estamos ya al final del mapa en la línea 371. Si es el momento de pintar (líneas 375-390), solo cambiamos la parte de obtener las coordenadas, ya que si usamos la parte superior izquierda como indicadores de dónde estamos (map_x y map_y), necesitamos agregar un desplazamiento para saber cuál es la parte inferior, lo cual hacemos con NOMBRE_RAJOLES_PANTALLA_VER - 1. Finalmente, en la línea 391, comprobamos si necesitamos establecer la bandera de desplazamiento a 0 (sin desplazamiento).
Ahora comenzamos con el primer desplazamiento lateral, el de la izquierda, que sigue la misma estructura que el anterior pero con nuevas condiciones para detectar límites y dónde pintar para el desplazamiento lateral. Además, esta vez el bucle no se hace sobre las coordenadas X, sino que pintamos una columna entera.
En la línea 400, detectamos si podemos hacer un desplazamiento lateral y activamos la bandera. Luego, actualizamos la posición del desplazamiento y en la línea 406, detectamos si necesitamos pintar una nueva casilla del mapa. Dado que son casillas de 8x8, también hacemos lo mismo que en el desplazamiento vertical y detectamos los 3 bits menos significativos cuando son 0 (cada 8 veces). Si es momento de pintar una nueva columna, actualizamos la posición horizontal del mapa (map_tile_x) y calculamos dónde necesitamos pintar. desti_x está determinado por el desplazamiento, y desti_y se obtiene dividiendo los píxeles por 8 (desplazando 3 bits hacia la derecha).
Entre las líneas 411 y 422, tenemos el bucle para pintar verticalmente. Aquí lo hemos dividido en dos partes: desde la posición actual hacia abajo, y luego la parte restante que va al principio de la pantalla.
La línea 424 es la misma que en el desplazamiento vertical, que se encontró empíricamente que si no pintaba correctamente justo cuando el personaje dejaba este punto e iba a otro lugar en el desplazamiento.
En la línea 426, realizamos el desplazamiento horizontal con la función SetScrollH().
Finalmente, comprobamos si estamos al principio del mapa, y si es así, revertimos el cambio en la posición del desplazamiento y activamos la bandera para evitar más movimientos hacia la izquierda.
¿Pero no podemos usar un solo bucle para pintar verticalmente como hicimos en los desplazamientos verticales? Sí, podemos hacerlo. Podemos aprovechar el hecho de que el desbordamiento del carácter no nos afecta y comienza a contar de nuevo. El bucle sería así:
for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL ; m++) {
obtenir_coordenades_rajola(map_tile_x, m + map_tile_y);
desti_y = (m + (pos_scroll_y>>3)) * 8;
HMMM(stamp_x, stamp_y, desti_x,
OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
}
Ahora pasemos a implementar el equivalente para el desplazamiento hacia la derecha. Los únicos cambios serán en las comprobaciones de límites del mapa en las líneas 437 y 463; en lugar de decrementar map_tile_x y pos_scroll_x, se incrementarán; y la posición para obtener las coordenadas a pintar tendrá un desplazamiento de 31 casillas.
Esta función también se podría simplificar eliminando los dos bucles de pintura y reemplazándolos por:
for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL; m++) {
obtenir_coordenades_rajola(map_tile_x + 31, m + map_tile_y);
desti_y = (m + (pos_scroll_y>>3)) * 8;
HMMM(stamp_x, stamp_y, desti_x,
OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
}
Una vez que hemos definido las nuevas funciones de desplazamiento, ahora tenemos las funciones para determinar si la casilla a la que nos dirigimos es una casilla que se puede atravesar o no. Descubrimos esto con la función es_casellaParet(), que devuelve un 1 si la casilla no se puede atravesar, según el tipo de casilla.
La siguiente función, calculem_tiles(), nos indica en qué casilla de la pantalla nos encontramos según el desplazamiento. Convierte las coordenadas en casillas. La coordenada del personaje solo se ve afectada por los 3 bits menos significativos del desplazamiento para determinar si ha cambiado de casilla. Por lo tanto, usamos una operación AND a nivel de bits (&) para aislar esos bits, los sumamos a la coordenada y luego los dividimos por 8 para obtener la coordenada de la casilla (desplazando los 3 bits menos significativos hacia la derecha >>3).
La función anterior es la que utilizaremos en las diferentes funciones de movimiento para determinar dónde estamos en el mapa de casillas. La primera función de movimiento que tenemos es la función moviment_amunt(), donde decrementamos la coordenada vertical, determinamos la casilla en la que nos encontramos y comprobamos si la casilla actual o la anterior (debido a problemas de redondeo, como se explica en el artículo Moviéndonos a través de una pantalla de casillas, necesitamos comprobar dos casillas) se puede atravesar o no. En la línea 506, comparamos si las casillas que hemos comprobado son bloqueantes y, si es así, revertimos la posición del personaje principal. Luego, en la línea 509, comprobamos si el personaje está por encima de un umbral (en este caso, 14 píxeles), más allá del cual activaremos el desplazamiento y moveremos al personaje hacia atrás, ya que la pantalla avanza.
La función moviment_avall() hace lo mismo pero suma a la coordenada Y en lugar de restar. La posición de las casillas también varía y se ha ajustado empíricamente, siguiendo el método de prueba y error.
Ahora necesitamos seguir los mismos pasos para el eje horizontal en las funciones moviment_dreta() y moviment_esquerra(). Luego tenemos la función moviment_sprite() que maneja si presionamos la tecla ESC para salir o si estamos moviendo el joystick (o los cursores). Dependiendo de la tecla presionada, ejecutará ciertas funciones de movimiento u otras:
Y finalmente, la función main() comienza configurando la pantalla y las posiciones iniciales dentro del mapa de la cuadrícula. Luego, en la línea 608, llama a la función para inicializar la pantalla (cargar imágenes y crear el mapa de la cuadrícula). Terminamos inicializando variables utilizadas para controlar el desplazamiento e inicializamos el control de interrupciones para realizar movimientos del personaje principal. En la línea 623, dibujamos el sprite del personaje principal y simplemente escuchamos la pulsación de la tecla de salida mientras procesamos el movimiento del personaje principal con la función moviment_sprite().
Si salimos con la tecla ESC, detenemos las interrupciones y regresamos al estado inicial. Luego, entre las líneas 628 y 645, imprimimos información de depuración. Finalmente, regresamos a DOS con Exit(0).
Conclusiones
Hemos visto cómo implementar el desplazamiento utilizando las funciones de hardware del V9958. Las funciones de desplazamiento son todas muy similares y podrían combinarse en una sola función. Esto es lo que intenté hacer en scrfusfons.c. Desafortunadamente, no pude hacer que funcionara correctamente. Si alguien es capaz de arreglarlo, por favor avísenos.
A continuación, podríamos explorar cómo mantener la parte superior de la pantalla quieta para mostrar información del juego como vidas, puntos, armas, etc. Esto se podría hacer copiando un bloque o utilizando interrupciones de línea. Exploraremos esto en otra publicación del blog.
Clica aquí para ver los ejemplos funcionando.
Comments