Què és el mòdul Pawn?

El mòdul Pawn permet crear diferents personatges (també anomenats Pawn al llarg de l'article), animar-los, moure'ls, i si s'activa la part de col·lisions, detecta si xoquen amb el fondo. En aquest article aprendrem a com crear aquestes animacions però sense utilitzar la part de col·lisions. Només les animacions.

A l'article Màquina d'estats. Animant sprites ja havíem comentat com crear animacions tot tenint en compte una màquina d'estats. En aquest article utilitzarem la llibreria de la MSXgl ja creada per aqeust propòsit i que simplifica la seva creació i modificació.

Els sprites del mòdul Pawn estan dissenyats per MSX1, però amb una petit codi, podem utilitzar també els sprites multicolor que es van utilitzar a l'article Màquina d'estats. Animant sprites. Els nous sprites també han estat creats utilitzant el SEV9938 que s'explica al mencionat article, però s'ha mdodificat l'script Sev9338_a_C.py perquè ara passant-li el paràmetre --msxgl crea el fitxer .h amb la informació dels sprites per a l'estructura de la llibreria i posant --fusion-c tenim el mateix format que teníem abans.

Com funciona el mòdul Pawn?

Cada Pawn està format per aquestes estructures:

  1. Pawn_Sprite: Aquesta estructura té la posició X,Y de l'sprite, l'offset que té l'sprite en l'animació, el color (en cas de sprites MSX1) i un paràmetre Flag que pot prendre valors PAWN_SPRITE_OR és l'utilitzada en l'exemple s_game.c de la llibreria, PAWN_SPRITE_ODD salta els fotogrames parells de l'animació, només pinta els senars, i PAWN_SPRITE_EVENque fa l'oposat, mostra només els parells i no el pinta en els senars.
  2. Pawn_Frame: Definim de quants fotogrames estarà feta l'animació, la seqüència. El primer és l'identificador del patró, que coincideix amb l'índex que apareix al Sprite Debugger de l'openMSX, el segon és la durada, si tenim que el refresc del MSX és 60 Hz, si volem que duri un segon, haurem de posar que és 60; finalment el tercer paràmetre és un callback per cridar una funció que es crida a cada fotograma.
  3. Definim un enumerat a on tenim una llista de totes les animacions que pot tenir aquell Pawn. Ens serà més fàcil determinar en quin estat està.
  4. Pawn_Action: Aquesta estructura tindrà la definició de totes les animacions que té un Pawn. Té com a paràmetres, el Pawn_Frames que hem definit abans, el número de fotogrames de cada animació, si té loop i si es pot interrompre. Si té loop torna a començar i si no quan acaba tota la seqüència torna a l'estat 0 de la llista d'animacions, el primer element de Pawn_Actions. Si l'animació es pot interrompre, quan es faci un Pawn_SetAction() amb una nova animació s'executarà immediatament, interrompent l'altre, i si no es pot interrompre, serà ignorada. Així per exemple, una animació de caminar es pot interrompre per saltar o fer una altra animació, però una de morir, per exemple, no es pot interrompre.

Exemple d'un Pawn

Un cop ja hem vist totes les estructures, anem a veure com seria per exemple l'animació del Joe. Un cop carregats els patrons al VDP, tenim aquesta estructura:

Amb el que definim el codi:

 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
            

A les línies 47-51 definim de quants patrons de sprites estarà format el nostre caràcter per les animacions, que en aquest cas és de 2 sense offset en les coordenades X,Y. El primer patró es troba a 0 i el segon a 4 patrons més enllà de l'indicat a les animacions. El color és indiferent, ja que després el tornarem a pintar i les opcions també.

El moviment d'en Joe estarà format pels 4 primers patrons de l'esquema dels patrons que és el moviment de l'esquera, i pels 72, 76, 80 i 84 pel moviment a la dreta. L'animació d'inactiu l'he creada amb molts fotogrames per poder veure millor el seu comportament.

Així a les línies 64-71 definim l'animació d'inactiu que estarà feta pels fotogrames que comença el seu patró al 0, 8, 0 i 16. El primer fotograma agafarà els patrons 0 i 4 degut a la definció de la línia 48, el segon fotograma els patrons 8 i 12, després tornarà al 0 i 4 per acabar amb un moviment diferent amb els patrons 16 i 20.

Ara ens queda definir totes les animacions que tindrem. Aquí també n'he definides d'altres que hi havia en l'exemple s_game.c per poder tenir una llarga llista.

Un cop tenim la llista de les animacions creem el Pawn_Action indicant el nom de l'estructura de cada animació seguint l'ordre definit a la línia 143.

Programa per moure Pawns

Un cop ja hem vist la teoria de com crear els Pawns amb les seves animacions, anem-ho a aplicar en un programa en C, però compliquem-ho una mica més afegint-hi dos Pawns més i que siguin multicolor de 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
            

Comencem el programa posant tots els includes que utilitzarem i les constants usades en el programa. A les línies 32 a 35 definim les funcions per no haver-nos de preocupar de l'ordre en que les definim més endavant. Fem una inclusió de font/font_mgl_sample8.h perquè de pas també podem provar la part de la llibreria d'escriure en pantalla, i aquí carreguem la font, si volem un altre tipus, només cal que el canviem aquí. A la línia 42 carreguem la capçalera generada amb l'script que covnerteix el fitxer del SEV9938, del qual hem retallat els sprites, paletes i altra informació extra que no utilitzem, deixant només els sprites que no són 0, la paleta que utilitzarem i el vector index_mask amb els colors i les màscares utilitzades pels diferents patrons d'sprites.

 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
            

Passem ara a definir les diferents capes i animacions dels diferents Pawns. Tots els nostres Pawns estan formats per dues capes que sempre estaran contínues per això totes les definicions (línies 48-59) tenen com a tercer paràmetre el 0 i el 4, ja que són sprites de 16x16 i utilitza cadascun 4 patrons.

M'agradaria ressaltar que els moviments d'amunt i avall del Joe (línies 103-112) comparteixen una capa en comú, tal i com explicitem a la següent imatge (la part comuna és l'encerclada en verd):

I per tant quan definim les animacions, la que mira cap amunt comença amb la part interior i quan mira cap avall en les del voltant. D'aquesta forma estalviem 2 patrons que serien idèntics només amb una ordenació adequada.

S'ha de tenir en compte quan es defineix l'ordre de les capes que si hi ha colors generats a través del solapament de capes fent OR, el color que indica aquest bit ha d'estar en un índex superior, o equivalent a les capes superiors del Pawn_Frame (índex alts en l'array d'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};
            

Finalment queda la part de definir totes les animacions i crear el seu array corresponent de Pawn_Action.

Després definim tot de variables globals per controlar el moviment dels Pawns i inicialitzem l'aplicació a la funció State_Initialize().

Un cop ja hem definit toes les animacions, queda posar-les a la lògica del joc i inicialitzar tots els valors, això és el que farem a la següent funció:

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}
            

Comencem ocultant la pantalla perquè no hi hagi pampallugues amb tots els canvis que estem fent. La línia 261 posa el color del text i del fondo. Després netegem tota la primera pàgina de la VRAM, ja que així esborrem rastres que hi haguessin en memòria, i com que a cops no ocupem tots els patrons, d'aquesta manera no apareixeran sprites no volguts.

Després indiquem que utilitzarem sprites de 16x16 amb la comanda VDP_SetSpriteFlag(VDP_SPRITE_SIZE_16) i a la línia 269 carreguem tots els nostres patrons definits en el fitxer capçalera obtingut a través de l'script de Python dels dibuixos fets utilitzant SEV9938, Sprites és el nom de l'array que conté aquesta informació.

Entre les línies 272-282 inicialitzem els 3 Pawns que utilitzarem en aquesta aplicació. Per cadascun hem de cridar la funció Pawn_Initialize() que rep com a primer paràmetre l'estructura Pawn que hem creat com a variable global i que el sdcc ja s'encarregarà de reservar la memòria, després l'array indicant quants sprites hi ha a cada Pawn, el tercer paràmetre és el tamany d'aquest array, el quart és el pla visible que ocuparà el primer d'aquests sprites, els altres aniran a plans consecutius. Recordem que el nombre màxim de sprites visibles en MSX2 és de 32. I finalment, l'últim paràmetre que és l'estructura Pawn_Action que conté la llista de totes les animacions.

Fixem-nos que pel Sam, el número de pla visible a utilitzar hem indicat que sigui el 2, ja que en Joe comença en el 0 i està format per dos sprites i per tant ocuparan els dos primers plans, el 0 i l'1. Seguint aquesta lògica, el tercer Pawn començarà en el pla 4, ja que en Sam també utilitza dos sprites.

A la línia 285 inidquem el nom de la font que utilitzarem per escriure a la pantalla de joc. Aquest és el nom que està definit a l'include de la font. El MSXgl pot pintar les lletres amb diferents tècniques que s'especifiquen a l'apartat PRINT MODULE del fitxer msxgl_config.h i que en aquest cas hem dit que sigui bitmap (perquè funcioni per Screen 5) i que estigui en memòria, podent dibuixar línies i requadres i el format del printf.

Tot seguit creem un bucle per pintar per pantalla 14 línies horitzontals i el nom de MoltSXalats.

Carreguem la paleta dels sprites a la línia 294. Aquesta paleta és la que hem obtingut al fitxer capçalera a partir del SEV9938.

Acabem la funció posant el fondo negre i el text en blanc, activem la pantalla i indiquem a la màquina d'estats de la llibreria que estem a l'estat de la funció 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ó State_Game conté el bucle principal del joc. El primer que fa és comprovar els moviments dels diferents pawns per indicar en quina animació està. Les línies 312-325 defineixen el moviment del Joe, que és el que tenia el moviment inactiu que és el que té per defecte a no sé que s'indiqui un diferent, que és el que fa tot el conjunt if d'aquesta part. Un cop hem determinat quina és l'acció, la posem en el Pawn a la línia 321 i marquem el canvi amb Pawn_Update per finalment indicar que ja es pot tornar a dibuixar Pawn_Draw.

En el block 325-339 fem el mateix pel Sam. I finalment el més senzill de tots ja que només té dues seqüències, l'esquirol, a les línies 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); 
        

Havíem comentat que els Pawn estaven pensats per MSX1 i que no hi ha la part multicolor del MSX2, l'anterior part del codi és la que s'encarrega d'aquesta part. Primer de tot hem de saber quin Patró del Pawn estem pintant, que és el que es calcula a la línia 354 i que retorna el número del 0-255, en el cas d'en Joe mirant a l'esquerra retornaria 0, i si està mirant a la dreta, retornarà 72. Les nostres màscares de color estan formades per un codi de color per línia (del 0-15 de la paleta i si té funció OR té sumat 64) i hi ha 16 línies per 4 sprites (4 sprites és el que formaven un patró de 16x16). Per tant, hem de dividir el valor de calcPatro entre 4 i multiplicar-ho per les 16 línies per saber a on comencen els codis de colors de cada patró. Segons el nombre sprites que formin cada Pawn haurem de copiar tants codis de color, que és el que es guarda a la variable bytescopiar. Un cop ja tenim quin patró s'ha de copiar i la quantiat de bytes, hem de copiar aquests valors a la posició de la VRAM corresponent a als colors dels plans. En el cas de l'Screen 5 aquesta adreça és la 0x07400 i per passar els valors a la VRAM usem la funció VDP_WriteVRAM_128K que rep com a paràmetres la posició de la memòria a on començar a copiar, els 2 bytes baixos de l'adreça de memòria de la VRAM destí, el byte alt de l'adreça de memòria de la VRAM destí i el nombre de bytes a copiar.

A les següents línies fem el mateix per al Sam i l'esquirol, però ara el pla a pintar serà el 2 i el 4, ja que els hem posat ordenats. Repassant el codi, veig que aquesta informació es troba a Pawn_Initialize i per tant es troba dins l'estructura Pawn->SpriteID amb el que podríem escriure VDP_WriteVRAM_128K(&index_mask[indexmascara], 0x7400 + (g_SamPawn.SpriteID*16), 0, bytescopiar); .

En aquests moments estem copiant de RAM a VRAM, al principi havia pensat de fer-ho de VRAM a VRAM amb la comanda HMMM, havent fet primer una càrrega al 0x07200 i després anar copiant els blocs dels colors cap 0x07400 corresponent. Però HMMM funciona amb pixels, i també podia passar que un Pawn tingués els components en un canvi de línia, amb el que vaig decidir que potser ja tindria suficient velocitat en copiar de RAM a VRAM i no caldrien aquests càlculs més complicats.

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
        

Ara ja només queda detectar el teclat per moure els Pawns. Aixo és el que es fa a les línies 431-513. Comencem posant tots els desplaçaments i estats de moviment a 0 per poder calcular el nou estat. Després detectem les línies de teclat que corresponguin a les línies a on hi ha les tecles per moure els Pawns i actualitzem les posicions dels Pawns amb Pawn_SetPosition i retornem TRUE perquè el bucle principal del game surti i es consideri complet. Si la funció retorna FALSE el bucle continua i crida un altre cop la mateixa funció d'estat. Així doncs, quan una funció d'estat retorna FALSE significa "Ja he acabat la funció, però no acabis el fotograma encara, continar processant el següent estat (State_Game) en el mateix fotograma". Això permet que les transicions d'estat passin immediatament sense esperar al següent fotograma, la qual cosa és útil per inicialitzacions o per canvis d'estat ràpids que no consumeixen tot un fotograma.

Com funciona IS_KEY_PRESSED? El teclat del MSX està codificat en una matriu tal i com està explicat a aquest post del msx.org, segons la tecla que volem detectar hem d'esbrinar a quina filera es troba, per exemple, la tecla R es troba a la filera 4, i després hem d'esbrinar el bit que es desactiva en apretar. La filera de la matriu la llegim amb la comanda Keyboard_Read indicant-li el paràmetre el número de filera a llegir les tecles, en aquest cas seria el 4. Un cop ja tenim el byte de la matriu de teclat, és el primer paràmetre de la funció IS_KEY_PRESSED, el segon és el byte a comparar que el MSXgl ja té definit com a constants i que en aquest cas seria KEY_R que està definida a input.h. A la següent imatge hi ha un exemple d'aquesta matriu extreta del FUSION-C-Quick A4 1.2.pdf pàg 42 a on les columnes són el bit i les fileres el número que s'ha de passar a la funció 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);
        

I ja només queda la funció final, a on comencem desactivant el soroll que fan les tecles, indiquem que l'estat inicial és a la funció State_Initialize i que comenci el bucle principal amb Game_Start i els paràmetres del mode de pantalla a executar i si el refresc és de 60Hz. Quan s'acabi el bucle dels estats, sortim de l'aplicació amb Bios_Exit(0).

Conclusió

En aquest article hem vist com animar diferents personatges utilitzant el mòdul de game/pawn i controlant el bucle principal amb game/state. No hem utilitzat la detecció de col·lisions que també es troba en aquest mòdul.

Com sempre podeu trobar el codi al GitLab de MoltSXalats. I provar el programa directament al WebMSX.


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