Introducción
En este post, veremos qué es una máquina de estados y cómo nos ayuda a animar sprites. También presentaremos el SEV9938, una herramienta que nos permite diseñar sprites y, en el caso del MSX2, realizar la operación OR cuando dos sprites se superponen.
Qué es una máquina de estados
Una máquina de estados es un modelo computacional que consiste en un conjunto finito de estados, transiciones entre estos estados y acciones asociadas con cada transición o estado. Se utiliza para representar y gestionar comportamientos que dependen de estados específicos y los eventos que provocan cambios en ellos.
En nuestro caso, la aplicaremos a la animación de sprites. La máquina de estados nos permitirá controlar las diferentes posiciones o movimientos del personaje principal o cualquier otro personaje, según la entrada del usuario. Cada estado representará una postura o movimiento específico del sprite (como "mirando hacia arriba", "caminando hacia la derecha", etc.), y las transiciones se activarán en respuesta a acciones como mover el joystick o presionar una tecla.
De esta manera, en lugar de manejar manualmente todos los posibles estados del personaje, la máquina de estados simplifica el código al hacer que los cambios automáticos de estado activen la animación o el comportamiento correspondiente.
En el juego Bricks, ya se utilizaba una máquina de estados controlada por sentencias if y entradas de teclado, pero aquí utilizaremos `typedef enum` y un array para definir las transiciones.
Nuestro personaje principal tendrá ocho estados: arriba, abajo, derecha e izquierda, cada uno duplicado para animar el movimiento en la misma dirección. Puedes encontrar este diagrama en Imatge 1. Las flechas en el diagrama indican el cambio de estado según la tecla presionada. Por ejemplo, si estamos en el estado "Mirant amunt 2" y presionamos la tecla derecha, pasaremos al estado "Mirant dreta 2". Para cada estado, también he especificado los patrones de sprite correspondientes.
Código
Esta vez, tenemos un archivo de cabecera creado a partir de los datos del SEV9938. Este archivo de cabecera, Sprites_Joe.h, contiene el array Sprites, que define los sprites de 16x16. Cada línea especifica 2 bytes y está formateada en binario para que se pueda ver fácilmente la forma que tendrán en el MSX, además de que coincide con el formato utilizado en el comando SetSpritePattern (char pattern_n, char patternData, char Size) de Fusion-C. En este comando, primero indicamos el número del patrón, luego la dirección donde comienza el patrón y, finalmente, el tamaño, que en este caso es 16. Por lo tanto, si queremos cargar el sprite definido como número 8 en el patrón 3, usaríamos SetSpritePattern(3, Sprites[16*8], 16).
Además de los sprites, también tenemos arrays para los índices de color de cada línea en index_mask y la paleta que utilizaremos en paleta_bricks. Si nos referimos a index_mask[18], estamos señalando la línea 3 del Sprite1. Ambos arrays siguen el formato para uso directo con las funciones de Fusion-C SetSpriteColors (char spriteNumber, char *data) y SetPalette ((Palette *) mypalette), respectivamente.
Habiendo explicado el archivo de cabecera, pasemos a hablar del archivo principal, MaqEstatJoe.c. Primero, comenzamos incluyendo las partes de la biblioteca Fusion-C que utilizaremos. Esta vez, también necesitamos incluir el archivo Sprites_Joe.h que hemos explicado anteriormente. En la línea 12, creamos la variable global processar_mov, que actuará como un semáforo para indicar cuándo procesar el movimiento en el juego.
Desde la línea 14 hasta la 82, definimos las variables y funciones para controlar el Hook de la interrupción vertical del VDP. Esto permitirá que el juego ejecute tareas periódicas, como comprobar si ha pasado suficiente tiempo para actualizar el movimiento del sprite.
En la línea 84, definimos la variable global compta_tics, que ayudará a gestionar diferentes tiempos. Por ejemplo, podríamos actualizar al personaje principal cada 2 tics y a los enemigos cada 3. Esta variable, junto con processa_mov, se usará en la función que se ejecuta en cada interrupción del VDP, la cual simplemente actualiza estas variables.
A continuación, empezamos a crear las enumeraciones que harán que el código sea fácilmente legible. Primero, definimos los diferentes estados de nuestra máquina de estados con la enumeración State. Es importante destacar el último enumerador, NUM_STATES, que indica el número máximo de estados y se utilizará para establecer los límites de bucles, por ejemplo.
La siguiente enumeración es Key, que corresponde a los eventos definidos en el diagrama que desencadenan cambios de estado.
Finalmente, la enumeración Joy contiene los diferentes valores posibles del joystick, comenzando desde 0 (JOY_REPOS) y terminando con el valor para arriba-izquierda (JOY_AMUNT_ESQUERRA). Esto se usará para gestionar los movimientos leídos del joystick o del teclado.
En la línea 128, definimos la estructura para el sprite, similar a los grupos llamados SpriteGFX en el SEV9938. En nuestro caso, solo comprende dos patrones, ya que todos nuestros sprites implican como máximo la superposición de dos. Esta estructura almacenará los índices de los patrones utilizados y los índices de color correspondientes. Más adelante, en la línea 265, inicializamos esta estructura, lo que aclarará su uso. Por último, creamos el array state_sprites con esta estructura.
La siguiente estructura es para el personaje principal, que usaremos para almacenar su posición y estado. Creamos la variable Joe utilizando esta estructura.
En la línea 148, tenemos la función pinta_sprite(), responsable de dibujar el sprite del personaje principal. Dado que el personaje principal siempre consiste en dos sprites superpuestos, usamos los planos 0 y 1, que tienen la prioridad más alta y aparecerán frente a todos los demás sprites. El color de cada plano se establece usando SetSpriteColors(), y tanto los patrones como los colores se determinan según el estado. Los índices utilizados para almacenar el color se refieren al número de la paleta, por lo que necesitamos multiplicar por 16 para localizar la paleta correspondiente a ese patrón (línea 152). Sin embargo, multiplicar por 16 es equivalente a desplazar el byte 4 lugares a la izquierda, como se hace en la línea 153. El desplazamiento de bytes es más rápido que la multiplicación. Otra forma en la que podríamos haber mejorado la velocidad es almacenando directamente la posición en lugar del número de paleta. Por ejemplo, en la línea 283, configuraríamos state_sprites[MIRANT_AMUNT_1].color_1 = 128 en lugar del actual 8, y la línea 152 se convertiría en SetSpriteColors(0, &index_mask[state_sprites[Joe.estat].color_1]);, sin multiplicación ni desplazamiento.
El punto clave llega en la línea 157, donde definimos una matriz de estados. Cada estado (primera dimensión) devuelve el siguiente estado en función de la tecla presionada (NUM_KEY). Por ejemplo, si estamos en el estado MIRANT_DRETA_2 y presionamos la tecla KEY_AVALL (la cuarta posición según la enumeración Key), al observar la línea 161, que maneja MIRANT_DRETA_2, vemos que el cuarto estado en el resultado es MIRANT_AVALL_2. Con esta simple estructura, podemos pasar del estado actual al siguiente.
A continuación, tenemos las funciones que calculan el movimiento del personaje principal basándose en las teclas presionadas: moviment_esquerra(), moviment_dreta(), moviment_avall() y moviment_amunt(). He mantenido los mismos nombres que las funciones que realizan tareas similares en "Scroll en una pantalla de casillas" para facilitar la integración de ambos programas. Estas funciones verifican si el movimiento es posible (en este caso, asegurándose de que no salgamos de la pantalla) y calculan el nuevo estado de animación para el sprite.
La siguiente función procesa el movimiento basado en la entrada del joystick. Los valores para arriba, derecha, abajo e izquierda son sencillos, ya que corresponden directamente a la enumeración Key que tenemos. Sin embargo, no hemos creado estados específicos para el movimiento diagonal. En este caso, divido el movimiento en dos fases: primero un movimiento vertical y luego uno horizontal. Este enfoque hacía que la animación lateral se mostrara siempre. Para abordar esto, intenté alternar entre animaciones en JOY_AMUNT_ESQUERRA, pero el resultado fue una apariencia entrecortada. Un mejor enfoque podría ser alternar suavemente entre las animaciones laterales o verticales.
Al final de la función process_input, dibujamos el sprite del personaje principal con el estado y la posición recién calculados.
¡Y ahora tenemos la función principal! Comenzamos configurando la pantalla en Screen 5 y habilitando el procesamiento del movimiento del personaje activando la variable global processar_mov. Activamos el tamaño de sprites de 16x16, cambiamos el color de fondo y cargamos la paleta con los colores que vamos a usar.
En la línea 257, iniciamos un bucle para cargar los sprites de la RAM a la VRAM, de modo que puedan mostrarse en pantalla. En las líneas 260-263, creamos sprites espejados para el movimiento lateral. Aunque podríamos haberlos creado directamente en el programa, este método ahorra RAM a cambio de unas pocas líneas adicionales de código. La función Pattern16FlipVram(char SrcPatternNum, char DestPatternNum, char direction) toma como primer parámetro el índice del sprite que queremos copiar (los índices se incrementan en 4 porque trabajamos con tamaños de 16x16). El segundo parámetro es el número del patrón de destino, y el último parámetro especifica si queremos un volteo horizontal o vertical; en nuestro caso, es horizontal (0).
A continuación, definimos qué patrones y colores utiliza cada sprite. Existe una notación que es interpretada por distintos compiladores de C y que hace la inicialización de arrays más compacta, pero no pude hacer que funcionara con sdcc, así que opté por el método con más escritura.
Definimos la posición y el estado inicial del personaje principal en las líneas 301-304 y lo dibujamos usando la función pinta_sprite().
En las líneas 309-347, dibujo los sprites en sus diferentes estados. Inicialmente, cuando ejecuté el programa, todos los estados se renderizaban incorrectamente, así que para diagnosticar el problema, decidí imprimir cada estado por separado en diferentes posiciones para identificar la causa. El primer problema era que algunos patrones de estado estaban asignados incorrectamente, así que corregí los patrones en las líneas 266-298 para asegurarme de que coincidieran con sus estados.
El otro problema era que algunas líneas desaparecían. Después de consultar en el grupo de Telegram de MSX-Lab, JamQue sugirió que las líneas que desaparecían podrían deberse a la configuración de OR de los colores. Confirmé esta hipótesis y descubrí que era cierto: las líneas que desaparecían eran aquellas en las que se configuraba la operación OR para mezclar colores al superponerse. Al revisar la documentación, encontré que, tal como se indica en el "Technical Handbook", el bit CC del color solo debe estar configurado en 1 en el plano con el número más alto. Así, si necesitamos aplicar una operación OR a patrones en los planos 0 y 4, el bit CC debe activarse para el plano 4. Esto se puede observar en el archivo Sprites_Joe.h, donde los índices de color superiores a 15 tienen un desplazamiento de 64 para activar el bit CC.
Después de eso, inicializamos la variable compta_tics y configuramos la interrupción del VDP, que ejecutará el temporizador del juego 50 veces por segundo (o 60 veces en el estándar japonés).
A continuación, entramos en un bucle que solo se interrumpe cuando se presiona la tecla ESC (ASCII 27). En este bucle, leemos la entrada de las teclas de cursor o del joystick y la procesamos en el momento adecuado.
Finalmente, detenemos las interrupciones, restablecemos la paleta y volvemos al modo DOS 0.
SEV9938 - Sprite Editor
Para diseñar estos sprites, utilicé la aplicación SEV9938, que tiene la interfaz tal y como en 'Imatge 2'. En la zona 1, dibujamos el sprite haciendo clic para activar o desactivar los píxeles. A la derecha de esta área, hay una columna donde indicamos el índice de color para cada línea, especificando si usará el color 11 o 12 de la paleta definida en la zona 4. Debajo de esto, aún en la zona 1, hay una herramienta para copiar patrones y voltearlos o espejarlos. El botón "Show OR" muestra una tabla con las combinaciones de las capas.
En la zona 2, definimos el SpriteGFX, que asocia diferentes patrones para formar un sprite completo. El patrón resaltado es el que se muestra actualmente en la zona 1, mientras que la zona 3 muestra el resultado de todas las superposiciones. El nombre "Sprite GFX 2" se puede cambiar por etiquetas más descriptivas, como "lado izquierdo" para mayor claridad. En la parte superior de la zona 1, hay una barra para ajustar la opacidad de la capa, lo que permite ver el contenido de las capas inferiores de SpriteGFX mientras se dibuja un nuevo patrón. En la parte inferior de la zona 2, podemos reorganizar el orden de los patrones y aplicar un desplazamiento de uno en relación con otro.
La zona 3 muestra el resultado final de SpriteGFX, con la operación OR aplicada si está habilitada. En el ejemplo, el efecto OR es visible en el puño y los pies creados. Podemos elegir mostrar el resultado de la operación OR, ajustar el tamaño de la imagen y cambiar el color de fondo.
En la zona 4, seleccionamos la paleta usando las flechas izquierda y derecha en los extremos del arreglo de colores, y al hacer clic en el botón "PALETTES", podemos crear una nueva paleta o eliminar otras.
La zona 5 contiene los diferentes patrones que hemos creado. Al hacer clic en uno, reemplazará el patrón activo actualmente en la zona 2. Ten cuidado, ya que a menudo he sobrescrito el patrón equivocado por error.
Finalmente, en la **zona 6**, tenemos la opción de guardar el proyecto. Actualmente, no hay una opción incorporada para exportar a C, pero podemos guardar en el formato genérico **.scumsx2**. Este archivo es un archivo de texto en formato JSON que contiene toda la información necesaria. Para crear el archivo **Sprites_Joe.h**, desarrollé un script en Python que convierte este archivo en un archivo de cabecera .h. Luego edité manualmente el archivo resultante para aplicar el formato final que se ve en GitLab. Si deseas utilizar este script, puedes encontrarlo aquí. No dudes en compartir cualquier sugerencia o modificación.
Por cierto, si tienes tus SpriteGFX organizados en orden de animación, puedes usar las flechas en la sección 2 para alternar entre ellos y obtener una vista previa de la animación.
Conclusión
Este es un ejemplo básico de cómo crear animación basada en sprites en MSX utilizando una máquina de estados. Una posible mejora sería manejar el movimiento en diagonal; esto podría lograrse añadiendo un nuevo estado y gestionándolo a través de la máquina de estados, o asegurándose de que el personaje siempre mire hacia los lados con una animación, utilizando una variable que cambie cada vez que se active. Otra mejora podría consistir en fusionar los Num_keys que cambian el estado y la posición del sprite con las entradas del joystick. Alternativamente, mantenerlos separados, como están ahora, podría ser beneficioso para asignar otras combinaciones de teclas a los Num_keys adecuados.
También proporcionamos una breve introducción al uso de SEV9938 para diseñar sprites, aprovechando la funcionalidad OR de los chips V9938 y V9958. Esto no es necesario para el graphics9000, ya que sus sprites están completamente en mapas de bits.
Haz clic aquí para ver el ejemplo en funcionamiento.
Comments