¿Qué es el Módulo Pawn?

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.

¿Cómo funciona el Módulo Pawn?

Cada Pawn está compuesto por estas estructuras:

  1. 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.
  2. 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.
  3. Definimos una enumeración donde tenemos una lista de todas las animaciones que puede tener el Pawn. Nos será más fácil determinar en qué estado está.
  4. 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.

Ejemplo de un Pawn

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.

Programa para Mover Pawns

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).

Conclusión

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.


Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.