El módulo Pawn permite crear diferentes personajes (también llamados Pawns a lo largo del artículo), animarlos, moverlos, y si se activa la parte de colisiones, detectar si colisionan con el fondo. En este artículo aprenderemos a crear estas animaciones pero sin usar la parte de colisiones. Solo las animaciones.
En el artículo State Machine. Animating Sprites ya habíamos hablado de cómo crear animaciones teniendo en cuenta una máquina de estados. En este artículo usaremos la librería MSXgl ya creada para este propósito y que simplifica su creación y modificación.
Los sprites del módulo Pawn están diseñados para MSX1, pero con un pequeño trozo de código, también podemos usar los sprites multicolor que se usaron en el artículo State Machine. Animating Sprites. Los nuevos sprites también han sido creados usando SEV9938 que se explica en el artículo mencionado, pero el script Sev9338_a_C.py ha sido modificado para que ahora pasando el parámetro --msxgl cree el archivo .h con la información de sprites para la estructura de la librería y poniendo --fusion-c tenemos el mismo formato que teníamos antes.
Cada Pawn está compuesto por estas estructuras:
Pawn_Sprite: Esta estructura tiene la posición X,Y del sprite, el offset que tiene el sprite en la animación, el color (en caso de sprites MSX1) y un parámetro Flag que puede tomar valores PAWN_SPRITE_OR que se usa en el ejemplo s_game.c de la librería, PAWN_SPRITE_ODD se salta los frames pares de la animación, pintando solo los impares, y PAWN_SPRITE_EVEN que hace lo contrario, mostrando solo los pares y no pintándolos en los impares.Pawn_Frame: Definimos de cuántos frames estará compuesta la animación, la secuencia. El primero es el identificador de patrón, que coincide con el índice que aparece en el openMSX Sprite Debugger, el segundo es la duración, si tenemos que el refresco del MSX es de 60 Hz, si queremos que dure un segundo, tendremos que ponerlo a 60; finalmente el tercer parámetro es un callback para llamar a una función que se llama en cada frame.Pawn_Action: Esta estructura tendrá la definición de todas las animaciones que tiene un Pawn. Tiene como parámetros, los Pawn_Frames que definimos antes, el número de frames de cada animación, si tiene loop y si se puede interrumpir. Si tiene loop vuelve a empezar y si no cuando acaba toda la secuencia vuelve al estado 0 de la lista de animaciones, el primer elemento de Pawn_Actions. Si la animación se puede interrumpir, cuando se hace un Pawn_SetAction() con una nueva animación se ejecutará inmediatamente, interrumpiendo la otra, y si no se puede interrumpir, se ignorará. Así por ejemplo, una animación de caminar se puede interrumpir para saltar o hacer otra animación, pero una de morir, por ejemplo, no se puede interrumpir.Una vez hemos visto todas las estructuras, veamos cómo sería la animación de Joe por ejemplo. Una vez cargados los patrones en el VDP, tenemos esta estructura:
![]()
Con la cual definimos el código:
47// Pawn sprite layers 48const Pawn_Sprite g_SpriteLayers[] = 49{// X Y Pattern Color Option 50 { 0, 0, 0, COLOR_BLACK, 0 }, 51 { 0, 0, 4, COLOR_WHITE, 0 }, 52}; 53 … 64// Idle animation frames 65const Pawn_Frame g_FramesIdle[] = 66{ // Pattern Time Function 67 { 0*8, 60, NULL }, // Standing pose 1 // El 8 és perquè l'sprite és de 2 elements? En el s_game utilitza múltiples de 16 i és de 3 capes 68 { 1*8, 60, NULL }, // Standing pose 2 (slight variation) 69 { 0*8, 60, NULL }, // Back to pose 1 70 { 2*8, 30, NULL }, // Blink or small movement 71}; 72 73// Move animation frames 74const Pawn_Frame g_FramesMove[] = { 75 // ID, Duration, callback event 76 // Aquest Id és el número de l'sprite de 0-256 dins els aptrons 77 { 0*8, 8, NULL }, // Walk frame 1 78 { 1*8, 8, NULL }, // Walk frame 2 79}; 80 81// Move animation frames 82const Pawn_Frame g_FramesMove_Right[] = { 83 {72, 8, NULL}, // Walk frame 1 84 {80, 8, NULL}, // Walk frame 2 85}; 86 … 142// Actions id 143enum ANIM_ACTION_ID { 144 ACTION_IDLE = 0, 145 ACTION_MOVE, 146 ACTION_MOVE_RIGHT, 147 ACTION_JUMP, 148 ACTION_FALL, 149 ACTION_DOWN, 150 ACTION_UP, 151}; 152 153// List of all player actions 154const Pawn_Action g_AnimActions[] = { 155 // Frames Number Loop? Interrupt? 156 {g_FramesIdle, numberof(g_FramesIdle), TRUE, TRUE}, 157 {g_FramesMove, numberof(g_FramesMove), TRUE, TRUE}, 158 {g_FramesMove_Right, numberof(g_FramesMove_Right), TRUE, TRUE}, 159 {g_FramesJump, numberof(g_FramesJump), TRUE, TRUE}, 160 {g_FramesFall, numberof(g_FramesFall), TRUE, TRUE}, 161 {g_FramesDown, numberof(g_FramesDown), TRUE, TRUE}, 162 {g_FramesUp, numberof(g_FramesUp), TRUE, TRUE}, 163}; 164
En las líneas 47-51 definimos de cuántos patrones de sprite estará compuesto nuestro personaje para las animaciones, que en este caso son 2 sin offset en las coordenadas X,Y. El primer patrón está ubicado en 0 y el segundo en 4 patrones más allá de lo que se indica en las animaciones. El color es irrelevante, ya que lo repintaremos después y las opciones también.
El movimiento de Joe estará compuesto por los primeros 4 patrones del esquema de patrones que es el movimiento hacia la izquierda, y por 72, 76, 80 y 84 para el movimiento hacia la derecha. He creado la animación idle con muchos frames para poder ver mejor su comportamiento.
Así que en las líneas 64-71 definimos la animación idle que estará compuesta por frames que empiezan su patrón en 0, 8, 0 y 16. El primer frame tomará los patrones 0 y 4 debido a la definición en la línea 48, el segundo frame los patrones 8 y 12, luego volverá a 0 y 4 para terminar con un movimiento diferente con los patrones 16 y 20.
Ahora necesitamos definir todas las animaciones que tendremos. Aquí también he definido otras que estaban en el ejemplo s_game.c para poder tener una lista larga.
Una vez tenemos la lista de animaciones creamos el Pawn_Action indicando el nombre de la estructura de cada animación siguiendo el orden definido en la línea 143.
Una vez hemos visto la teoría de cómo crear Pawns con sus animaciones, vamos a aplicarlo en un programa en C, pero vamos a complicarlo un poco más añadiendo dos Pawns más y haciéndolos multicolor MSX2.
9//============================================================================= 10// INCLUDES 11//============================================================================= 12#include "color.h" 13#include "core.h" 14#include "input.h" 15#include "game/state.h" 16#include "game/pawn.h" 17#include "print.h" 18#include "vdp.h" 19//============================================================================= 20// DEFINES 21//============================================================================= 22 23// Physics values 24#define FORCE 24 25#define GRAVITY 1 26#define GROUND 192 27 28// Library's logo 29#define MSX_GL "\x02\x03\x04\x05" 30 31// Function prototypes 32bool State_Initialize(); 33bool State_Game(); 34bool State_Pause(); 35 36//============================================================================= 37// READ-ONLY DATA 38//============================================================================= 39 40// Font 41#include "font/font_mgl_sample8.h" 42#include "Bricks_sprites_msxgl.h" 43
Empezamos el programa poniendo todos los includes que usaremos y las constantes utilizadas en el programa. En las líneas 32 a 35 definimos las funciones para no tener que preocuparnos por el orden en que las definimos después. Incluimos font/font_mgl_sample8.h para poder probar también la parte de escritura en pantalla de la librería, y aquí cargamos la fuente, si queremos otro tipo, solo necesitamos cambiarla aquí. En la línea 42 cargamos el header generado con el script que convierte el archivo SEV9938, del cual hemos recortado los sprites, paletas y otra información extra que no usamos, dejando solo los sprites que no son 0, la paleta que usaremos y el vector index_mask con los colores y máscaras utilizados por los diferentes patrones de sprite.
47// Pawn sprite layers 48const Pawn_Sprite g_SpriteLayers[] = 49{// X Y Pattern Color Option 50 { 0, 0, 0, COLOR_BLACK, 0 }, 51 { 0, 0, 4, COLOR_WHITE, 0 }, 52}; 53 54const Pawn_Sprite g_SamLayers[] = { 55 {0, 0, 0, COLOR_BLACK, 0}, // El tercer paràmetre és l'offset que després s'utilitza quan fas les animacions 56 {0, 0, 4, COLOR_BLACK, 0}, 57}; 58 59const Pawn_Sprite g_SquirrelLayers[] = { 60 {0, 0, 0, COLOR_BLACK, 0}, 61 {0, 0, 4, COLOR_BLACK, 0}, 62}; 63 64// Idle animation frames 65const Pawn_Frame g_FramesIdle[] = 66{ // Pattern Time Function 67 { 0*8, 60, NULL }, // Standing pose 1 // El 8 és perquè l'sprite és de 2 elements? En el s_game utilitza múltiples de 16 i és de 3 capes 68 { 1*8, 60, NULL }, // Standing pose 2 (slight variation) 69 { 0*8, 60, NULL }, // Back to pose 1 70 { 2*8, 30, NULL }, // Blink or small movement 71}; 72 73// Move animation frames 74const Pawn_Frame g_FramesMove[] = { 75 // ID, Duration, callback event 76 // Aquest Id és el número de l'sprite de 0-256 dins els aptrons 77 { 0*8, 8, NULL }, // Walk frame 1 78 { 1*8, 8, NULL }, // Walk frame 2 79}; 80 81// Move animation frames 82const Pawn_Frame g_FramesMove_Right[] = { 83 {72, 8, NULL}, // Walk frame 1 84 {80, 8, NULL}, // Walk frame 2 85}; 86 87// Jump animation frames 88const Pawn_Frame g_FramesJump[] = 89{ 90 { 4*8, 12, NULL }, // Jump preparation 91 { 5*8, 8, NULL }, // Jump peak 92 { 6*8, 8, NULL }, // Jump extension 93}; 94 95// Fall animation frames 96const Pawn_Frame g_FramesFall[] = 97{ 98 { 5*8, 8, NULL }, // Fall pose 1 99 { 6*8, 8, NULL }, // Fall pose 2 100}; 101 102// Down animation frames 103const Pawn_Frame g_FramesDown[] = { 104 {44, 8, NULL}, 105 {32, 8, NULL}, 106}; 107 108// Up animation frames 109const Pawn_Frame g_FramesUp[] = { 110 {36, 8, NULL}, 111 {48, 8, NULL}, 112}; 113 114// Animacions Sam 115const Pawn_Frame g_FramesSamLeft[] = { 116 {88, 8, NULL}, 117 {96, 8, NULL}, 118}; 119const Pawn_Frame g_FramesSamRight[] = { 120 {24, 8, NULL}, 121 {56, 8, NULL}, 122}; 123const Pawn_Frame g_FramesSamUp[] = { 124 {104, 8, NULL}, 125 {108, 8, NULL}, 126}; 127const Pawn_Frame g_FramesSamDown[] = { 128 {116, 8, NULL}, 129 {120, 8, NULL}, 130}; 131 132// Animacions Squirrel 133const Pawn_Frame g_FramesSquirrelLeft[] = { 134 {136, 8, NULL}, 135 {128, 8, NULL}, 136}; 137const Pawn_Frame g_FramesSquirrelRight[] = { 138 {64, 8, NULL}, 139 {144, 8, NULL}, 140}; 141
Ahora pasamos a definir las diferentes capas y animaciones de los diferentes Pawns. Todos nuestros Pawns están compuestos por dos capas que siempre serán continuas, por eso todas las definiciones (líneas 48-59) tienen 0 y 4 como tercer parámetro, ya que son sprites de 16x16 y cada uno usa 4 patrones.
Me gustaría destacar que los movimientos hacia arriba y hacia abajo de Joe (líneas 103-112) comparten una capa común, como explicamos en la siguiente imagen (la parte común está marcada en verde):

Y por tanto cuando definimos las animaciones, la que mira hacia arriba empieza con la parte interior y cuando mira hacia abajo con las circundantes. De esta manera ahorramos 2 patrones que serían idénticos con solo una ordenación adecuada.
Hay que tener en cuenta al definir el orden de las capas que si hay colores generados a través del solapamiento de capas usando OR, el color que indica este bit debe estar en un índice más alto, o equivalente a las capas superiores del Pawn_Frame (índices altos en el array de estructura).
142// Actions id 143enum ANIM_ACTION_ID { 144 ACTION_IDLE = 0, 145 ACTION_MOVE, 146 ACTION_MOVE_RIGHT, 147 ACTION_JUMP, 148 ACTION_FALL, 149 ACTION_DOWN, 150 ACTION_UP, 151}; 152 153// List of all player actions 154const Pawn_Action g_AnimActions[] = { 155 // Frames Number Loop? Interrupt? 156 {g_FramesIdle, numberof(g_FramesIdle), TRUE, TRUE}, 157 {g_FramesMove, numberof(g_FramesMove), TRUE, TRUE}, 158 {g_FramesMove_Right, numberof(g_FramesMove_Right), TRUE, TRUE}, 159 {g_FramesJump, numberof(g_FramesJump), TRUE, TRUE}, 160 {g_FramesFall, numberof(g_FramesFall), TRUE, TRUE}, 161 {g_FramesDown, numberof(g_FramesDown), TRUE, TRUE}, 162 {g_FramesUp, numberof(g_FramesUp), TRUE, TRUE}, 163}; 164 165// Accions Sam 166// Actions id 167enum ANIM_SAM_ACTION_ID { 168 ACTION_SAM_LEFT = 0, 169 ACTION_SAM_RIGHT, 170 ACTION_SAM_DOWN, 171 ACTION_SAM_UP, 172}; 173 174// List of all player actions 175const Pawn_Action g_AnimSamActions[] = { 176 // Frames Number Loop? Interrupt? 177 {g_FramesSamLeft, numberof(g_FramesSamLeft), TRUE, TRUE}, 178 {g_FramesSamRight, numberof(g_FramesSamRight), TRUE, TRUE}, 179 {g_FramesSamDown, numberof(g_FramesSamDown), TRUE, TRUE}, 180 {g_FramesSamUp, numberof(g_FramesSamUp), TRUE, TRUE}, 181}; 182 183// Accions Squirrel 184// Actions id 185enum ANIM_SQUIRREL_ACTION_ID { 186 ACTION_SQUIRREL_LEFT = 0, 187 ACTION_SQUIRREL_RIGHT, 188}; 189 190// List of all squirrel actions 191const Pawn_Action g_AnimSquirrelActions[] = { 192 // Frames Number Loop? Interrupt? 193 {g_FramesSquirrelLeft, numberof(g_FramesSquirrelLeft), TRUE, TRUE}, 194 {g_FramesSquirrelRight, numberof(g_FramesSquirrelRight), TRUE, TRUE}, 195};
Finalmente, queda la parte de definir todas las animaciones y crear su correspondiente array Pawn_Action.
Luego definimos todas las variables globales para controlar el movimiento de los Pawns e inicializamos la aplicación en la función State_Initialize().
Una vez hemos definido todas las animaciones, queda ponerlas en la lógica del juego e inicializar todos los valores, esto es lo que haremos en la siguiente función:
257bool State_Initialize() 258{ 259 // Initialize display 260 VDP_EnableDisplay(FALSE); 261 VDP_SetColor(COLOR_BLACK); 262 263 // Netegem la primera pàgina, tota, ja que hi ha restes de dades de sprites 264 VDP_FillVRAM_16K(0, 0, 0x7FFF); 265 266 // Initialize sprite 267 VDP_SetSpriteFlag(VDP_SPRITE_SIZE_16); 268 // VDP_LoadSpritePattern(g_DataSprtLayer, 0, 13*4*4); 269 VDP_LoadSpritePattern(Sprites, 0, sizeof(Sprites) / 8); 270 VDP_DisableSpritesFrom(3); 271 272 // Init player pawn 273 Pawn_Initialize(&g_PlayerPawn, g_SpriteLayers, numberof(g_SpriteLayers), 0, g_AnimActions); 274 Pawn_SetPosition(&g_PlayerPawn, 16, 16); 275 276 // Init Sam pawn 277 Pawn_Initialize(&g_SamPawn, g_SamLayers, numberof(g_SamLayers), 2, g_AnimSamActions); // L'sprite ID és el que fa que es solapin, abans de 2 hi havia un 1 i és el que col·lapsava 278 Pawn_SetPosition(&g_SamPawn, 64, 64); 279 280 // Init Squirrel pawn 281 Pawn_Initialize(&g_SquirrelPawn, g_SquirrelLayers, numberof(g_SquirrelLayers), 4, g_AnimSquirrelActions); 282 Pawn_SetPosition(&g_SquirrelPawn, 112, 112); 283 284 /* // Initialize text */ 285 Print_SetBitmapFont(g_Font_MGL_Sample8); 286 287 for (u8 k=0;k<13;k++) { 288 Print_DrawLineH(k * 10, 15 + k * 15, 20); 289 Print_SetPosition(k * 10, k * 16); 290 Print_DrawText("MoltSXalats"); 291 } 292 293 // Set Palette 294 VDP_SetPalette(BRICKS); 295 296 VDP_SetColor2(0, 15); 297 VDP_EnableDisplay(TRUE); 298 299 Game_SetState(State_Game); 300 return FALSE; // Frame finished 301}
Empezamos ocultando la pantalla para que no haya parpadeos con todos los cambios que estamos haciendo. La línea 261 establece el color del texto y del fondo. Luego limpiamos toda la primera página de VRAM, borrando así cualquier rastro que pueda estar en memoria, y como a veces no usamos todos los patrones, de esta manera no aparecerán sprites no deseados.
Luego indicamos que usaremos sprites de 16x16 con el comando VDP_SetSpriteFlag(VDP_SPRITE_SIZE_16) y en la línea 269 cargamos todos nuestros patrones definidos en el archivo header obtenido a través del script de Python de los dibujos hechos usando SEV9938, Sprites es el nombre del array que contiene esta información.
Entre las líneas 272-282 inicializamos los 3 Pawns que usaremos en esta aplicación. Para cada uno tenemos que llamar a la función Pawn_Initialize() que recibe como primer parámetro la estructura Pawn que hemos creado como variable global y de la que sdcc se encargará de reservar la memoria, luego el array indicando cuántos sprites hay en cada Pawn, el tercer parámetro es el tamaño de este array, el cuarto es el plano visible que ocupará el primero de estos sprites, los otros irán a planos consecutivos. Recordad que el número máximo de sprites visibles en MSX2 es 32. Y finalmente, el último parámetro que es la estructura Pawn_Action que contiene la lista de todas las animaciones.
Notad que para Sam, el número de plano visible a usar hemos indicado que sea 2, ya que Joe empieza en 0 y está compuesto por dos sprites y por tanto ocupará los dos primeros planos, 0 y 1. Siguiendo esta lógica, el tercer Pawn empezará en el plano 4, ya que Sam también usa dos sprites.
En la línea 285 indicamos el nombre de la fuente que usaremos para escribir en la pantalla del juego. Este es el nombre que está definido en el include de la fuente. MSXgl puede pintar letras con diferentes técnicas que se especifican en la sección PRINT MODULE del archivo msxgl_config.h y que en este caso hemos dicho que sea bitmap (para que funcione en Screen 5) y que esté en memoria, pudiendo dibujar líneas y cuadrados y el formato printf.
A continuación creamos un bucle para pintar 14 líneas horizontales y el nombre MoltSXalats en la pantalla.
Cargamos la paleta de sprites en la línea 294. Esta paleta es la que obtuvimos en el archivo header de SEV9938.
Terminamos la función estableciendo el fondo negro y el texto blanco, activamos la pantalla e indicamos a la máquina de estados de la librería que estamos en el estado de la función State_Game.
308bool State_Game() 309{ 310// VDP_SetColor(COLOR_DARK_GREEN); 311 // Update player animation & physics 312 u8 act = ACTION_IDLE; 313 if (g_bMoving) 314 act = ACTION_MOVE; 315 else if (g_bMoving_Right) 316 act = ACTION_MOVE_RIGHT; 317 else if (g_bMoving_Down) 318 act = ACTION_DOWN; 319 else if (g_bMoving_Up) 320 act = ACTION_UP; 321 Pawn_SetAction(&g_PlayerPawn, act); 322 // Pawn_SetMovement(&g_PlayerPawn, g_DX, g_DY); 323 Pawn_Update(&g_PlayerPawn); 324 Pawn_Draw(&g_PlayerPawn); 325 326 // Update Sam animation & physics 327 u8 samAct = ACTION_SAM_LEFT; // Default action 328 if (g_bSamMoving_Left) 329 samAct = ACTION_SAM_LEFT; 330 else if (g_bSamMoving_Right) 331 samAct = ACTION_SAM_RIGHT; 332 else if (g_bSamMoving_Down) 333 samAct = ACTION_SAM_DOWN; 334 else if (g_bSamMoving_Up) 335 samAct = ACTION_SAM_UP; 336 Pawn_SetAction(&g_SamPawn, samAct); 337 Pawn_SetAction(&g_SamPawn, samAct); 338 Pawn_Update(&g_SamPawn); 339 Pawn_Draw(&g_SamPawn); 340 341 // Update Squirrel animation & physics 342 u8 squirrelAct = ACTION_SQUIRREL_LEFT; // Default action 343 if (g_bSquirrelMoving_Left) 344 squirrelAct = ACTION_SQUIRREL_LEFT; 345 else if (g_bSquirrelMoving_Right) 346 squirrelAct = ACTION_SQUIRREL_RIGHT; 347 Pawn_SetAction(&g_SquirrelPawn, squirrelAct); 348 Pawn_Update(&g_SquirrelPawn); 349 Pawn_Draw(&g_SquirrelPawn);
La función State_Game contiene el bucle principal del juego. Lo primero que hace es comprobar los movimientos de los diferentes pawns para indicar en qué animación están. Las líneas 312-325 definen el movimiento de Joe, que tiene el movimiento idle como predeterminado a menos que se especifique uno diferente, que es lo que hace todo el bloque if en esta parte. Una vez hemos determinado cuál es la acción, la establecemos en el Pawn en la línea 321 y marcamos el cambio con Pawn_Update para finalmente indicar que se puede redibujar con Pawn_Draw.
En el bloque 325-339 hacemos lo mismo para Sam. Y finalmente el más simple de todos ya que solo tiene dos secuencias, la ardilla, en las líneas 342-349.
358 indexmascara = calcPatro / 4 * 16; 359 bytescopiar = numberof(g_SpriteLayers) * 16; 360 VDP_WriteVRAM_128K(&index_mask[indexmascara], 0x7400, 0, bytescopiar); // He d'afinar aquest número 361 // VDP_CommandHMMM(calcPatro * 4 * 2, 228, 0, 232, 32 * 2, 1); 362 // Quan va a la dreta no pinta bé els colors, ja que no els dec haver definit 363 // en el buffer quan els he copiat, potser també hauria d'allargar aquella 364 // zona de memòria. 365 // Si utilitzo HMMM hauré de tenir en compte cada vegada el 366 // final de línia i a on està l'original. Per això tindré en memòria tots els 367 // patrons (fins i tot els rotats) i copiar-los cada vegada al seu pla actiu. 368 369 // Ara els patrons del Sam 370 calcPatro = g_AnimSamActions[g_SamPawn.ActionId].FrameList[g_SamPawn.AnimStep].Id; 371 indexmascara = calcPatro / 4 * 16; 372 bytescopiar = numberof(g_SamLayers) * 16; 373 VDP_WriteVRAM_128K(&index_mask[indexmascara], 0x7400 + (2*16), 0, bytescopiar); 374 375 // Ara els patrons del Squirrel 376 calcPatro = g_AnimSquirrelActions[g_SquirrelPawn.ActionId].FrameList[g_SquirrelPawn.AnimStep].Id; 377 indexmascara = calcPatro / 4 * 16; 378 bytescopiar = numberof(g_SquirrelLayers) * 16; 379 VDP_WriteVRAM_128K(&index_mask[indexmascara], 0x7400 + (4*16), 0, bytescopiar);
Habíamos mencionado que los Pawns fueron diseñados para MSX1 y que no hay parte multicolor para MSX2, la parte anterior del código es lo que maneja esta parte. Primero necesitamos saber qué Patrón de Pawn estamos pintando, que es lo que se calcula en la línea 354 y devuelve el número de 0-255, en el caso de Joe mirando a la izquierda devolvería 0, y si está mirando a la derecha, devolvería 72. Nuestras máscaras de color están compuestas por un código de color por línea (de 0-15 de la paleta y si tiene función OR tiene 64 añadido) y hay 16 líneas por 4 sprites (4 sprites es lo que formaba un patrón de 16x16). Por tanto, tenemos que dividir el valor de calcPatro por 4 y multiplicarlo por las 16 líneas para saber dónde empiezan los códigos de color de cada patrón. Según el número de sprites que forman cada Pawn tendremos que copiar tantos códigos de color, que es lo que se almacena en la variable bytescopiar. Una vez tenemos qué patrón copiar y la cantidad de bytes, tenemos que copiar estos valores a la posición VRAM correspondiente a los colores del plano. En el caso de Screen 5 esta dirección es 0x07400 y para pasar los valores a VRAM usamos la función VDP_WriteVRAM_128K que recibe como parámetros la posición de memoria donde empezar a copiar, los 2 bytes bajos de la dirección de memoria VRAM de destino, el byte alto de la dirección de memoria VRAM de destino y el número de bytes a copiar.
En las siguientes líneas hacemos lo mismo para Sam y la ardilla, pero ahora el plano a pintar será 2 y 4, ya que los hemos puesto en orden. Revisando el código, veo que esta información se encuentra en Pawn_Initialize y por tanto se encuentra dentro de la estructura Pawn->SpriteID así que podríamos escribir VDP_WriteVRAM_128K(&index_mask[indexmascara], 0x7400 + (g_SamPawn.SpriteID*16), 0, bytescopiar); .
De momento estamos copiando de RAM a VRAM, al principio había pensado en hacerlo de VRAM a VRAM con el comando HMMM, habiendo hecho primero una carga a 0x07200 y luego copiando los bloques de color al 0x07400 correspondiente. Pero HMMM funciona con píxeles, y también podría pasar que un Pawn tuviera los componentes en un cambio de línea, así que decidí que quizás ya tendría suficiente velocidad copiando de RAM a VRAM y estos cálculos más complicados no serían necesarios.
385 // Update movement 386 g_DX = 0; 387 g_DY = 0; 388 g_SamDX = 0; 389 g_SamDY = 0; 390 g_SquirrelDX = 0; 391 g_SquirrelDY = 0; 392 393 // Reset both movement flags first 394 g_bMoving = FALSE; 395 g_bMoving_Right = FALSE; 396 g_bMoving_Down = FALSE; 397 g_bMoving_Up = FALSE; 398 399 // Reset Sam movement flags 400 g_bSamMoving_Left = FALSE; 401 g_bSamMoving_Right = FALSE; 402 g_bSamMoving_Down = FALSE; 403 g_bSamMoving_Up = FALSE; 404 405 // Reset Squirrel movement flags 406 g_bSquirrelMoving_Left = FALSE; 407 g_bSquirrelMoving_Right = FALSE; 408 g_bSquirrelMoving_Down = FALSE; 409 g_bSquirrelMoving_Up = FALSE; 410 411 u8 row8 = Keyboard_Read(8); 412 if (IS_KEY_PRESSED(row8, KEY_RIGHT)) 413 { 414 g_DX++; 415 g_bMoving_Right = TRUE; 416 } 417 else if (IS_KEY_PRESSED(row8, KEY_LEFT)) 418 { 419 g_DX--; 420 g_bMoving = TRUE; 421 } 422 if (IS_KEY_PRESSED(row8, KEY_DOWN)) { 423 g_DY++; 424 g_bMoving_Down = TRUE; 425 } else if (IS_KEY_PRESSED(row8, KEY_UP)) { 426 g_DY--; 427 g_bMoving_Up = TRUE; 428 } 429 430 // Sam movement with E-S-D-F keys 431 u8 row3 = Keyboard_Read(3); // Row 3 contains E, D, F keys 432 u8 row5 = Keyboard_Read(5); // S 433 if (IS_KEY_PRESSED(row3, KEY_E)) // E = Up 434 { 435 g_SamDY--; 436 g_bSamMoving_Up = TRUE; 437 } 438 if (IS_KEY_PRESSED(row5, KEY_S)) // S = Left 439 { 440 g_SamDX--; 441 g_bSamMoving_Left = TRUE; 442 } 443 if (IS_KEY_PRESSED(row3, KEY_D)) // D = Down 444 { 445 g_SamDY++; 446 g_bSamMoving_Down = TRUE; 447 } 448 if (IS_KEY_PRESSED(row3, KEY_F)) // F = Right 449 { 450 g_SamDX++; 451 g_bSamMoving_Right = TRUE; 452 } 453 454 // Squirrel movement with I-J-K-L keys 455 u8 row4 = Keyboard_Read(4); // Row 2 contains I, J, K, L keys 456 if (IS_KEY_PRESSED(row3, KEY_I)) // I = Up 457 { 458 g_SquirrelDY--; 459 g_bSquirrelMoving_Up = TRUE; 460 } 461 if (IS_KEY_PRESSED(row3, KEY_J)) // J = Left 462 { 463 g_SquirrelDX--; 464 g_bSquirrelMoving_Left = TRUE; 465 } 466 if (IS_KEY_PRESSED(row4, KEY_K)) // K = Down 467 { 468 g_SquirrelDY++; 469 g_bSquirrelMoving_Down = TRUE; 470 } 471 if (IS_KEY_PRESSED(row4, KEY_L)) // L = Right 472 { 473 g_SquirrelDX++; 474 g_bSquirrelMoving_Right = TRUE; 475 } 476 if (g_bJumping) 477 { 478 g_DY -= g_VelocityY / 4; 479 480 g_VelocityY -= GRAVITY; 481 if (g_VelocityY < -FORCE) 482 g_VelocityY = -FORCE; 483 484 } 485 else if (IS_KEY_PRESSED(row8, KEY_SPACE) ) 486 { 487 g_bJumping = TRUE; 488 g_VelocityY = FORCE; 489 } 490 491 if (IS_KEY_PUSHED(row8, g_PrevRow8, KEY_DEL)) 492 { 493 g_bEnable = !g_bEnable; 494 Pawn_SetEnable(&g_PlayerPawn, g_bEnable); 495 } 496 497 g_PrevRow8 = row8; 498 499 if (Keyboard_IsKeyPressed(KEY_ESC)) 500 Game_Exit(); 501 502 Pawn_SetPosition(&g_PlayerPawn, g_PlayerPawn.PositionX + g_DX, 503 g_PlayerPawn.PositionY + g_DY); 504 505 // Update Sam position 506 Pawn_SetPosition(&g_SamPawn, g_SamPawn.PositionX + g_SamDX, 507 g_SamPawn.PositionY + g_SamDY); 508 509 // Update Squirrel position 510 Pawn_SetPosition(&g_SquirrelPawn, g_SquirrelPawn.PositionX + g_SquirrelDX, 511 g_SquirrelPawn.PositionY + g_SquirrelDY); 512 513 return TRUE; // Frame finished
Ahora solo queda detectar el teclado para mover los Pawns. Esto es lo que se hace en las líneas 431-513. Empezamos poniendo todos los desplazamientos y estados de movimiento a 0 para poder calcular el nuevo estado. Luego detectamos las líneas del teclado que corresponden a las líneas donde están las teclas para mover los Pawns y actualizamos las posiciones de los Pawns con Pawn_SetPosition y devolvemos TRUE para que el bucle principal del juego salga y se considere completo. Si la función devuelve FALSE el bucle continúa y llama a la misma función de estado otra vez. Así, cuando una función de estado devuelve FALSE significa "he terminado la función, pero no termines el frame todavía, continúa procesando el siguiente estado (State_Game) en el mismo frame". Esto permite que las transiciones de estado ocurran inmediatamente sin esperar al siguiente frame, lo cual es útil para inicializaciones o para cambios de estado rápidos que no consumen un frame entero.
¿Cómo funciona IS_KEY_PRESSED? El teclado del MSX está codificado en una matriz como se explica en este post en msx.org, según la tecla que queramos detectar tenemos que averiguar en qué fila está, por ejemplo, la tecla R está en la fila 4, y luego tenemos que averiguar el bit que se desactiva cuando se presiona. Leemos la fila de la matriz con el comando Keyboard_Read indicando como parámetro el número de fila para leer las teclas, en este caso sería 4. Una vez tenemos el byte de la matriz del teclado, es el primer parámetro de la función IS_KEY_PRESSED, el segundo es el byte a comparar que MSXgl ya tiene definido como constantes y que en este caso sería KEY_R que está definido en input.h. La siguiente imagen tiene un ejemplo de esta matriz extraída de FUSION-C-Quick A4 1.2.pdf página 42 donde las columnas son el bit y las filas son el número que debe pasarse a la función Keyboard_Read:

529void main() 530{ 531 Bios_SetKeyClick(FALSE); 532 533 Game_SetState(State_Initialize); 534 Game_Start(VDP_MODE_GRAPHIC4, FALSE); 535 536 Bios_Exit(0);
Y finalmente solo queda la función final, donde empezamos desactivando el ruido que hacen las teclas, indicamos que el estado inicial está en la función State_Initialize y que el bucle principal empieza con Game_Start y los parámetros del modo de pantalla a ejecutar y si el refresco es de 60Hz. Cuando el bucle de estados termina, salimos de la aplicación con Bios_Exit(0).

En este artículo hemos visto cómo animar diferentes personajes usando el módulo game/pawn y controlando el bucle principal con game/state. No hemos usado la detección de colisiones que también se encuentra en este módulo.
Como siempre puedes encontrar el código en MoltSXalats GitLab. Y probar el programa directamente en WebMSX.
![]() |
| This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. |