Introducció
En aquest post, veurem el que és una màquina d'estats i com ens serveix per animar els sprites. També farem una introducció al SEV9938, una eina que ens permet dissenyar sprites i en el cas del MSX2 fer l'OR quan es sol·lapen dos sprites.
Què és una màquina d'estats?
Una màquina d'estats és un model computacional que consisteix en un conjunt finit d'estats, transicions entre aquests estats, i accions associades a cada transició o estat. S'utilitza per representar i gestionar comportaments que depenen d'estats particulars i dels esdeveniments que els fan canviar.
En el nostre cas ho aplicarem a l'animació d'sprites. Així, la màquina d'estats ens permetrà controlar les diferents posicions o moviments del caràcter principal o d'un personatge depenent de l'entrada de l'usuari. Cada estat representarà una postura o moviment concret del sprite (com "mirant amunt", "caminant a la dreta", etc.), i les transicions s'activaran en resposta a accions com moure el joystick o una tecla.
Així, en comptes d'haver de gestionar manualment cada possible estat del personatge, la màquina d'estats facilita el codi fent que el canvi d'estat automàtic activi la corresponent animació o comportament.
En el joc Bricks ja es feia servir una màquina d'estats controlada amb sentències if i les tecles, però aquí utilitzarem els typedef enum i un array per definir les transicions.
El nostre caràcter principal tindrà 8 estats, amunt, avall, dreta i esquerra. Cadascun serà doble per poder animar quan estan dins de la mateixa direcció. Aquest diagrama el podeu trobar a la imatge Imatge 1. Les fletxes del diagrama indiquen el canvi d'estat segons la tecla apretada. Així per exemple si estem a l'estat Mirant amunt 2 i apretem la tecla dreta anirem a l'estat Mirant dreta 2. A cada estat també hi he indicat els patrons dels sprites que corresponen.
Codi
Aquest cop tenim un fitxer capçalera que hem creat a partir de les dades del SEV9938. Aquest fitxer capçalera, Sprites_Joe.h, conté l'array Sprites a on hi ha la definició dels sprites de 16x16, cada línia defineix 2 bytes i estan posats en format binari perquè així a simple vista, puguis veure la forma que tindran al MSX, i perquè és el format que s'usa a la comanda SetSpritePattern (char pattern_n, char patternData, char Size) del Fusion-C, a on primer indiquem el número de patró, després indiquem l'adreça a on comença el patró a dibuixar i despreś el tamany, 16 en aquest cas. Així si volem posar en memòria al patró 3, l'sprite definit com a 8 faríem, SetSpritePattern(3, Sprites[16*8],16).
A part dels sprites, també tenim els arrays dels índex de colors de cada línia a index_mask i la paleta que utilitzarem a paleta_bricks. Si indiquem index_mask[18] ens estem referint a la línia 3 del Sprite1. Ambdós arrays segueixen el format per ser utilitzats directament amb les funcions SetSpriteColors (char spriteNumber, char *data) i SetPalette ((Palette *) mypalette) del Fusion-c respectivament.
Un cop explicat el fitxer capçalera, anem a explicar el fitxer principal MaqEstatJoe.c. Primer comencem incloent les parts de la llibreria Fusion-C que utilitzarem. I aquest cop també hem d'incloure el fitxer Sprites_Joe.h explicat anteriorment. A la línia 12 creem la variable global processar_mov que ens servirà de semàfor per saber quan hem de processar el moviment en el joc.
Des de la línia 14 fins a la 82 definim les variables i funcions per controlar el Hook de la interrupció vertical del VDP. Això permetrà que el joc pugui executar tasques periòdiques, com verificar si ha passat suficient temps per actualitzar el moviment de l'sprite.
A la línia 84 definim la variable global compta_tics que ens servirà per portar diferents timings, per exemple podríem actualitzar el caràcter principal cada 2 tics i els enemics cada 3. Aquesta variable i l'anterior processa_mov, són les que utilitzarem a la funció que s'executa a cada interrupció del VDP i que consisteix només en actualitzar aquestes variables.
Tot seguit comencem a crear els enumerats que ens ajudaran a que el codi sigui fàcilment interpretable. Primer de tot els diferents estats que tenim a la nostra màquina d'estats amb els enumarats State. Cal destacar l'últim enumerat NUM_STATES que indica el nombre màxim d'estats i que ens servirà per marcar el límit dels bucles, per exemple.
El següent enumerat és el Key que es correspon als successos que hem definit en el diagrama que ocasionaven el canvi d'estat.
Finalment l'enumerat Joy conté els diferents valors possibles del joystick, començant pel valor 0, JOY_REPOS, i acabant amb el valor de dalt a l'esquerra, JOY_AMUNT_ESQUERRA. Aquest el farem servir per controlar els moviments llegits del Joystick o del teclat.
A la línia 128 definim el que serà l'estructura de l'sprite, les agrupacions que en el SEV9938 són anomenades SpriteGFX i que en el nostres cas només l'hem fet de dos patrons, ja que tots els nostres sprites són com a màxim el solapament de dos. En aquesta estructura guardarem l'índex dels patróns utiltizats i dels índexos dels colors corresponents. Més endavant, a la línia 265 és a on els inicialitzem i ja quedarà més clar el seu us. Finalment amb aquesta estructura creem l'array state_sprites.
La següent estructura és la del caràcter principal, ens servirà per tenir la posició i l'estat d'aquest. Creem la variable Joe amb aquesta estructura.
A la línia 148 tenim la funció pinta_sprite() que s'encarrega de dibuixar l'sprite del caràcter principal. Com que el caràcter principal sempre és la superposició de dos sprites, utilitzarem el pla 0 i 1 que són els de màxima prioritat, així passaran per davant de tota la resta de sprites. Cada pla té el seu color amb el SetSpriteColors(), i tant els patrons com els colors venen determinats per l'estat. Els índexs que hem utilitzat per guardar el color són del número de la paleta, per tant els hem de multiplicar per 16 per trobar la paleta corresponent a aquell patró (línia 152), però multiplicar per 16 és el mateix que desplaçar a l'esquerra 4 llocs el byte, que és el que s'ha fet a la línia 153. Desplaçar bytes és més ràpid que fer la multiplicació. Una altra manera que haguéssim guanyat velocitat és guardar directament la posició en comptes del número de paleta, així per exemple a la línia 283, hauríem de posar state_sprites[MIRANT_AMUNT_1].color_1 = 128 en lloc del 8 que hi ha ara i la línia 152 quedaria com SetSpriteColors(0, &index_mask[state_sprites[Joe.estat].color_1]);, sense multiplicador ni desplaçament.
El punt clau arriba ara, la línia 157, a on definim una matriu d'estats a on cada estat (primera dimensió) segons la tecla enviada (NUM_KEY) retorna l'estat a on es trobarà. Per exemple, si estic a l'estat MIRANT_DRETA_2 i arribo amb la tecla KEY_AVALL, que és la posició quarta segons l'enumerat Key i si mirem la línia 161 que és on es tracta l'estat MIRANT_DRETA_2 veiem que l'estat a on estarà, el quart, el del fnal, serà estat MIRANT_AVALL_2. Amb aquesta fórmula senzilla passem de l'estat actual al següent.
Després tenim les funcions que calculen el moviment del caràcter principal segons el moviment de les tecles que hem apretat: moviment_esquerra(), moviment_dreta(), moviment_avall() i moviment_amunt(). He posat el mateix nom que les funcions que fan la mateixa funció a "Desplaçant pantalla (scroll) en una pantalla de caselles" perquè sigui més fàcil fusionar els dos programes. Aquestes funcions miren si ens podem desplaçar, en aquest cas només que no sortim de la pantalla, i calcula el nou estat de l'animació de l'sprite.
La següent funció que tenim és la que calcula segons l'entrada de stick com processar el moviment. Els valors amunt, dreta, avall i esquerra són senzills perquè són directamente els Key que tenim, però no hem fet estats per calcular la diagonal. En aquest cas jo he descompost el moviment en dos, en un primer vertical i un segon horitzontal, això feia que sempre es veiés l'animació lateral, per això he provat en el JOY_AMUNT_ESQUERRA d'anar-les alternant, però queda molt brusca. El millor potser és anar alternant l'animació lateral o vertical.
Al final de la funció process_input, pintem l'sprite del caràcter principal amb el nou estat i posició que hem calculat.
I ja tenim el main! Comencem configurant que estarem a l'Screen 5 i que podem processar el moviment del personatge activant la variable globals processar_mov. Activem els sprites de tamany 16x16, canviem el color del fondo i carreguem la paleta amb els colors que utilitzarem.
A la línia 257 iniciem un bucle que anirà carregant els sprites que tenim a la RAM a la VRAM per poder ser pintats a la pantalla. A les línies 260-263 creem els sprites simètrics del moviment lateral. També els haguéssim pogut crear directament en el programa, però així estalviem memòria RAM i la canviem per unes poques línies de codi. La funció Pattern16FlipVram (char SrcPatternNum, char DestPatternNum, char direction) rep com a primer paràmetre l'índex de l'sprite que volem copiar, com que estem treballant en 16*16 els índexs van de 4 en 4, el segon paràmetre és el número de patró destí i l'últim si el volem copiar amb flip horitzontal o flip vertical, en el nostre cas és l'horitzontal (0).
Tot seguit tenim la definició de quins patrons i colors tenim a cada sprite. Hi havia la notació interpretada per diferents compiladors en C, que és molt més compacta quan s'inicialitza l'array, però que amb el sdcc no vaig aconseguir que la interpretés i vaig decidir fer el mètode amb més escriptura.
Definim la posició i estat inicial del caràcter principal a les línies 301-304 i el pintem amb la funció pinta_sprite().
A les línies 309-347 dibuixo els sprites en els diferents estats. Això és perquè al primer cop que vaig executar el programa tots els estats estaven mal dibuixats, i per intentar investigar què succeïa vaig decidir imprimir cada estat de forma independent en diferents posicions per poder esbrinar quin era l'error. El primer error és que tenia patrons mal definits per l'estat, no es corresponia aquell estat i vaig corregir les línies 266-298 amb els patrons correctes.
L'altre problema que tenia és que hi havia línies que desapareixien, després de consultar al grup de telegram de MSX-Lab, en JamQue em va indicar que potser era les línies que feine el color OR. Ho vaig comprovar. Tenia raó, les línies que desapareixien eren les que tenien l'OR definit per generar un nou color quan es solapaven. Vaig repassar la documentació i vaig veure que en el "Technical Handbook" el bit CC del color ha d'estar a 1 només en el pla que té un número més alt, així si hem de fer un OR dels patrons que estan al pla (capa) 0 i 4, el bit CC ha d'estar activat pel pla 4. Això es pot veure en el fitxer Sprites_Joe.h a on els colors que estan per sobre del valor 15 que és el màxim de l'índex, tenen un offset de 64 que és per activar el bit CC.
Després iniciem la variable compta_tics i inicialitzem la interrupció del VDP que executarà el temps del joc 50 vegades (ó 60 si és norma japonesa) cada segon.
Tot seguit entrem en un bucle a on només podem sortir si apretem la tecla ESC (27 en ASCII) i a on anem llegint la tecla del cursor o del joystick apretada i si és el moment de processar-la ho fem.
Finalment parem les interrupcions, restablim la paleta i tornem al mode 0 del DOS.
SEV9938 - Editor d'sprites
Per fer el disseny d'aquests sprites, he utilitzat la utilitat SEV9938 que té la interfície tal i com es pot veure a 'Imatge 2'. A la zona 1 és on anem dibuixant el nostre sprite, clicant o desclicant els pixels que seran actius. Al costat dret d'aquesta zona hi ha una columna a on indiquem l'índex de color de la línia, si utilitzarà el color 11 o el 12 de la paleta definida a la zona 4. A la part de sota, dins encara de la zona 1, trobem una utilitat per copiar els patrons, i per poder-los invertir i/o fer el simètric. El botó Show OR mostrarà una taula amb les combinacions de les capes.
A la zona 2 és a on definime el SpriteGFX que és a on associem els diferents patrons junts, per saber quins són els que formen el dibuix. En ressaltat, tenim el patró que estem veient a la zona 1, i a la zona 3 tenim el resultat de tot el solapament. El nom de "Sprite GFX 2" es pot canviar per noms que tinguin més sentit com "lateral-esquerra" per exemple. A dalta de tot de la zona 1 tenim una barra per marcar l'opacitat de la capa, així podem mentre estem dibuixant el nou patró, veure a sota el que ja teníem dins l'SpriteGFX. A baix de tot de la zona 2 podem canviar l'ordre dels patrons i fer un offset d'un respecte l'altre.
A la zona 3 veiem el resultat de l'SpriteGFX aplicant l'OR si s'ha activat. En l'exemple tenim OR en el puny creat i en els peus. Podem actiar si volem veure el resultat de l'OR, el tamany de la imatge i el color de fons.
A la zona 4 escollim la paleta amb les fletxes esquerra i dreta als extrems de l'array de colors i clicant el botó "PALETTES" creem una nova o podem esborrar les altres.
A la zona 5 tenim els diferents patrons que hem creat. Quan cliquem un, matxaca el que està actiu a la zona 2. Estigueu alerta que molts cops he esborrat el patró que no tocava.
Finalment a la zona 6 tenim per gravar el projecte. En aquests moments no hi ha per exportar a C, però sí que podem gravar en el format genèric .scumsx2. El fitxer és um json en format text a on tenim tota la informació. Per crear el fitxer Sprites_Joe.h he creat un script en python que transforma el fitxer en un fitxer .h. A aquest fitxer resultant l'he editat manualment per acabar de donar-li el format definitiu que apareix en el gitlab. Si voleu utilitzar aquest script el podeu trobar aquí. Si teniu algun suggeriment o el modifiqueu, no dubteu en comunicar-m'ho.
Per cert, si teniu els SpriteGFX ordenats per ordre d'animació, amb les fletxes de la secció 2 podreu anar canviant-los i obtenir una primera idea de com seria l'animació.
Conclusió
Aquest és un exemple bàsic de com crear una animació basada en sprites en MSX utilitzant una màquina d'estats. Una possible millora seria el moviment en diagonal, és podria afegir un nou estat i controlar-ho a través de la màquina d'estats o es podria fer que sempre mirés cap al costat però amb una animació, tenim una variable que la va canviant cada vegada que entra. Una altra millora podria ser ajuntar els Num_keys que ens fan canviar els estats de l'sprite i la seva posició amb les entrades rebudes del joystick. O potser és millor mantenir-ho separat com està ara, per si utilitzes altres combinacions de tecles poder-les mapejar als Num_keys corresponents.
També hem fet una breu introducció al funcionament del SEV9938 per dissenyar els sprites aprofitant l'OR del V9938 i V9958. Al graphics9000 no li cal ja que els sprites són totalment bitmaps.
Clica aquí per a veure l'exemple funcionant.
Comments