dilluns, 11 de maig de 2009

Cas d'estudi: Super Mario Bros - Part II: Estructura de joc i scroll

Be, açò es el que vaig fer el divendres, pero me vaig quedar sense temps de escriure.

Per a començar, he afegit algunes funcions a les APIs. Anem a repasar els canvis:
JDraw
- He afegit funcions per a carregar una font: JD_Font JD_LoadFont( char *file, int width, int height); carregarà una font a partir d'un bitmap i li especificarà que els caracters son de widthxheight. El bitmap que espera tindrà, en fila hitzontal, els caracters a partir del #32, que es el espai, en avant, seguint la codificació ASCII. Jo, en el meu bitmap, nomes he ficat els numeros (#48-#57) i les lletres majuscules (#65-#90), pero si es vol es poden ficar tots, seguint inclus més avant.
- Altra funció per a pintar text: void JD_DrawText( int x, int y, JD_Font *source, char *text); pintarà el text especificat en "text" en la posició (x,y) usant la font "source" que haurem carregat amb la funció d'abans. Més avant ho prepararé per a que les fonts puguen ser multicolor.
- Una funció que ens torna els FPS. Aquesta funció torna un text, ja que normalment vull els FPS per a pintarlos en pantalla.

JGame
- Normalment, en un bucle de joc, hi ha dos parts ben definides: El pintat i l'actualització interna. El pintat solem deixar que vaja el més rapid posible, així que es pinta a cada pas del bucle. No obstant, l'actualització de vegades no la volem el més rapid posible, per dos raons: La primera perque potser es massa rapid, i la segona perque el joc aniría a diferents velocitats depenent de la potencia de la màquina. Així que l'actualització lo normal es només executar-la cada x milisegons. Per a estalviar-se ficar la lògica de control del temps pel mig del bucle, ho he traslladat al mòdul JGame amb dos funcions:
- void JG_SetUpdateTicks(Uint32 milliseconds);. Amb aquesta funció fixem el nombre de milisegons que deu passar entre actualització i actualització.
- bool JG_ShouldUpdate();. En cada bucle podem consultar aquesta funció per a saber si ja toca actualitzar o no. Així, en compte de controlar tot el tema del temps, nomes tenim que fer un "if" d'aquesta funció. Ara després ho vorem en el codí del Super Mario Bros.


Ale, ara anem a començar per fi amb el Super Mario. Com el titol endica, vorem la estructura bàsica del joc i la càrrega i pintat del mapa.


Per a començar, anem a vore com es la funció "main" del joc. Repasaré un mínim conceptes de C, per a qui faça anys que no ho toca:


#include "jgame.h"
#include "jdraw.h"
#include "modulegame.h"

Comencem per inclure les capçaleres que ens faràn falta. Ací nomes s'inluou el que fa falta ací, no tot el que fa falta durant el joc.


int main( int argc, char* args[] ) {

JG_Init("Super Mario Bros");
JD_Init(256, 240, false);

Per a començar, inicialitzem JGame i JDraw. Com podeu vorem he ficat la resolució de la NES, per a que siga igual que l'original.


int gameState = 0;

La variable gameState mantindrà l'estat en que es troba el joc. Aquest estat es correspondrà amb els mòduls del joc, com per exemple: intro, menu, joc, sequència... i eixes coses.


while (gameState != -1) {
switch (gameState) {
case 0:
gameState = ModuleGame_Go();
break;
}
}

Aquest es el bucle principal. L'unic que fà es executar-se mentres el estat de joc no siga "-1", que voldrà dir que havem decidit eixir del joc. En la pròxima actualització canviarè aquestos numerets per constants, així es veu tot més clar.

En cas de que gameState no siga "-1" executarà el bucle, dins el qual el que tenim es un switch del propi gameState, que el que fa es que segons el estat executa un mòdul concret. Per ara nomes està el mòdul del propi joc, així que es simple.

Dins de cada mòdul hi ha un altre bucle que s'executarà fins que ocorrega el que siga per a eixir d'eixe mòdul, tornant a aquest bucle principal.


JG_Finalize();

return 0;
}

Quan s'haja decidit eixir del joc, es finlaitza JGame. I ja està, aquesta es la funció main.

Si vos haveu fixat, havem inclos un tal "modulegame.h" i, a més, estem cridant a una tal "ModuleGame_Go()", que no sabem per on para. Anem a crear tot aixó ara. Comencem per l'arxiu "modulegame.h":


#pragma once

int ModuleGame_Go();

Ale, ha sigut fàcil. Com a recordatori: els arxius de capçalera (.h) mostren la interficie externa d'un mòdul. Lo normal es que els ".h" vagen acompanyats d'un ".c" o ".cpp". Per a entendren's sería com si el ".h" continguera l'interficie d'un objecte i els ".cpp" contingueren l'implementació. Així, els demés moduls enllaçen amb el ".h", i nomes veuen el que havem declarat ahí dins. Després en el ".cpp" tenim que implementar el que havem declarat en el ".h", però també podem declarar més variables i funcions, que no es podràn vore desde fora del mòdul.

Per tant, com del mòdul de joc només ens interessa que es puga cridar al propi mòdul i au, nomes publiquem aquesta funció. Ara dins del ".cpp" implementarem aquesta funció i altres funcions satel·lit.

Una última cosa: "#pragma once" es una directiva de compilació que el que diu es que aquesta capçalera nomes s'ha d'incloure una vegada. Es equivalent al tipic "#IFNDEF _MODULEGAME_H_ ....". El cas es que en C, cada vagada que inclous una llibreria, esta va i se torna a incloure, provocant errors després de simbols duplicats. Amb aquestes directives el que fem es dir-li que, per moltes vegades que referenciem al arxiu, nomes s'incloga una.

Ara anem a vore l'implementació de "modulegame.cpp":


#include "modulegame.h"
#include "jgame.h"
#include "jdraw.h"
#include "jinput.h"
#include "jfile.h"

Lo primer es incloure les APIs que ens fan falta. A més, sempre incloguem el ".h" que anem a implementar.


JD_Surface *tiles;
JD_Font fuente;
Uint8 tileMap[256][15];
Uint8 blocMap[256][15];
int scrollX = 0;

Ara declarem les variables que necessitarem a nivell global en el mòdul. "tiles" mantindrà el bitmap de tiles; "fuente" mantindrà la font que usarem per a escriure text; "tileMap" mantindrà el array de tiles a pintar; "blocMap" mantidrà els tiles de bloc; per últim, "scrollX" mantindrà la posició horitzontal del mapa. Aquesta variable es tempòral, fins que tingam sprites i tal.

Ara ve la definició de les funcions. Les funcions, quan no s'han definit en el ".h", han de ser definides en el ordre correcte: Si una funció A crida a una funció B, la funció B ha de ser definida abans. Com aixó es un poc lio per a explicar, jo vaig a anar parlant de les funcions de més externa a més interna. Després, en el codi que vos podeu baixar, ja estàn ordenades correctament.


int ModuleGame_Go() {

Init();

while (!JG_Quitting()) {

ModuleGame_Draw();
ModuleGame_Update();
}

Finalize();

return ( JG_Quitting() ? -1 : 0 );
}

Aquesta es la funció principal, a la que se crida desde fora. En ella, el primer que es fa es cridar a una funció "Init()" que s'encarregarà d'inicialitzar tot el necessari. Després comença el bucle. Per ara, nomes controla si volem eixir de l'aplicació, pero més avant controlarà que eixim nomes del mòdul (per a tornar al menú, per exemple). Dins del bucle, es crida a una funció per a pintar l'escena i altra per a actualitzar-la. En quan eixim del bucle, cridem a una funció de finalització que allibere la memòria i tot això.

Per últim, tornem un valor, que serà el que adquirirà la variable gameState per aanar a altre mòdul o eixir de la aplicació. Ara mateixa nomes es pot eixir de la aplicació i au. En el return el que he ficat es un "if reduït" d'eixos: Si s'acompleix la condició que hi ha abans de l'interrogant, torna el que hi ha després de l'interrogant. Sinó, torna el que hi ha després dels dos punts.

Anem a vore les funcions a les que havem cridat:


void Init() {
LoadMap();
tiles = JD_LoadSurface("tiles.gif");
fuente = JD_LoadFont("fuente.gif", 8, 8);
JG_SetUpdateTicks(1);
}

La funció d'inicialització crida a altra funció que carregarà el mapa. Després carrega el bitmap de tiles i la font que usarem per a pintar text. Per últim, fixa el temps d'actualizació cada 1 milisegons.


void LoadMap() {
int fileSize;
char *buffer = JF_GetBufferFromResource("mapa1_1.map", &fileSize);
int filePos = 1;

int filenameSize = buffer[filePos++];
char *fileName = (char *)malloc(filenameSize+1);
for (int i=0;i fileName[i] = buffer[filePos+i];
}
fileName[filenameSize] = '\0';

filePos = filePos + filenameSize;

for (int y=0; y<15; y++) {
for (int x=0; x<256; x++) {
tileMap[x][y] = buffer[filePos++];
}
}

for (int y=0; y<15; y++) {
for (int x=0; x<256; x++) {
blocMap[x][y] = buffer[filePos++];
}
}

delete fileName;
delete buffer;
}

Aquesta es la funció que carrega el mapa desde l'arxiu. Aquesta funció la refaré més avant, ja que vull incloure en JFile funcions per a que llegir d'un arxiu siga més simple, sense buffers i chars (si no es vol, clar).

El primer que fa es obtindre un punter a un array de char amb el contingut de l'arxiu. Ara el processarem char a char (char = 8 bits sense signe). Primer fiquem el punter a 1 (passem del char[0], que es el de la versió). Llegim el tamany del nom del bitmap. Sabent el tamany del nom del bitmap, declarem un punter a array de char (que es el que s'usa per a les cadenes en C) del tamany
que havem llegit + 1 (ja que les cadenes en C acaven amb el caracter 0). Aleshores llegim cada char. Per últim, fiquem l'ultim a zero i ja tenim la cadena del nom del bitmap. Per ara no la use, pero més avant carregaré els tiles d'aquest arxiu, en compte de a pel com havem vist en la funció Init().

Després augmente el punter tantes posicions com caracters tenia el nom, i ja estic en posició de llegir l'informació del mapa de tiles i del mapa de blocs. Per últim allibere la memòria dels punters.


void Finalize() {
JD_FadeOut();

JD_FreeSurface(tiles);
JD_FreeSurface(fuente.surface);
}

Quan tot haja acavat farem un fade out, i alliberarem la memòria que tenen pillada els bitmaps de tiles i el bitmap de la font.


void ModuleGame_Draw() {
for (int y=0; y<15; y++) {
for (int x=0; x<17; x++) {
Uint8 tile = tileMap[x+(scrollX >> 4)][y] & 127;
JD_Blit((x << 4)-(scrollX & 15), y << 4, tiles, (tile & 15) << 4, (tile >> 4) << 4, 16, 16);
}
}

JD_DrawText(0, 0, &fuente, JD_GetFPS());
JD_Flip();
}

Aquesta es la funció que pinta el mapa a cada pas del bucle. Recorre tots els tiles en l'eix Y i 17 tiles en l'eix X (16 que en caben en pantalla + 1, com ja vaig explicar amb el Gauntlet). Per a cada un, mire en el array de tiles, sumant el scrollX per a saber el desplaçament en el mapa. A més, faig un AND de 127, amb lo qual lleve el bit 8 (que es el que m'especifica si el tile es animat) per a obtindre el número de tile correcte. Més avant comprovaré eixe bit per a saber si té animació o no. Per últim pinte el tile. Si algú no s'aclareix amb la lògica del scroll, que ho diga i fique un post especific.

Per a finalitzar, pinte els FPS per a vore que tal va, i faig el flip.


void ModuleGame_Update() {
if (JG_ShouldUpdate()) {
JI_Update();

if (JI_KeyPressed(SDLK_RIGHT)) {
scrollX++;
if (scrollX > 3776) scrollX = 3776;
}

if (JI_KeyPressed(SDLK_LEFT)) {
scrollX--;
if (scrollX < 0) scrollX = 0;
}

if (JI_KeyPressed(SDLK_ESCAPE)) {
JG_QuitSignal();
}
}
}

Aquesta es la funció d'actualització. Ara mateixa nomes comprove si s'ha d'actualitzar o no. En cas afirmatiu, actualizte JInput i comprove si s'ha pulsat dreta o esquerra, per a augmentar o disminuir el offset del scroll. També mire si s'ha pulsat ESC, per a enviar una senyal d'eixir de l'aplicació.


I açò es tot per ara. Per cert, també he actualitzat l'editor, perque era molt lento i xicotet, i se feia incómode usar-lo. Podeu descarregar el codi d'ací. Si hui me dona temps igual ja fique un inici als sprites. Comentaris, sugerencies, dubtes... seràn gràtament apreciats.

PD: Hui es el meu aniversari. Demostra interés per els JailGames per a felicitar-me ;-)

3 comentaris:

  1. FELICITATS! Gonnna apuntar la data a la agenda que si no mai me'n recorde :)

    Com he posat al Twitter, vaig llegir mig post a la feina, pero com era tot tan 'codi' ho vaig deixar per a quan estiguera 'en el ajo', sentat al comp i codificant.

    La veritat es que se me fa prou dificil tornar a programar, tinc més en el cap les idees i disseny del que sería el remake de cert joc per a celebrar altre anniversari que la idea del codi en si, pero be, ja vorem que fem.

    Que en fas, 29? o ja has arribat a canviar de decena entrant en els crueltosos 30? (crec que açò últim) :P

    ResponElimina
  2. Estic ací per a tots i cada un dels dubtes que tingau.

    Per cert, jo era molt de intentar dur els projectets en secret per a que foren sorpresa. D'eixa forma tots els projectets s'anaven quedant pel camí. Per aixó vaig optar per fer el blog. Ara, intentant compartir-ho tot, pareix que tot va avant millor.

    PD: gracies ;-) En son 30.

    ResponElimina
  3. Pos ja em posaré en contacte amb tu per a xarrar-ho, si no serà com sempre: pensat i mai fet.

    ResponElimina