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.
Cada Pawn està format per aquestes estructures:
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.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.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.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.
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).

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.
![]() |
| This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. |