how to build
a simple
2D
multiplatform
game
(1)
Primi Passi
creazione di una applicazione openGL da template Code-Blocks
analisi struttura del codice
Screenshot
Lavorare in modo strutturato
(2)
Separiamo cosa da come
strutturiamo il codice secondo una logica multiplatform
entry point device dependant
codice gioco
libreria astratta
libreria specifica grafica (openGL)
libreria specifica audio (openAL)
Struttura
Project
OS
Win
win_main.c // codice specifico per Windows
Game
g_main.c // codice di gioco
Libraries
l_module.c // librerie e funzioni astratte
GFX
l_openGL.c // implementazione funzioni grafica
SND
l_openAL.c // implementazione funzioni audio
Immagini
concetto di texture
uso dei texture in openGL
due funzioni sufficienti (quasi) per tutto
gfx_drawBOX
gfx_drawSPRITE
Codice Texture
// ---------------------------------------------
// - TEXTURE
// ---------------------------------------------
int tex_core_create(int width,int height,void*data,int mode);
// ---------------------------------------------
int tex_readTGA(const char*s,int mode);
void tex_delete(int*textID);
// ---------------------------------------------
Codice GFX
// ---------------------------------------------
// - GRAPHIC
// ---------------------------------------------
int gfx_init();
int gfx_reset();
void gfx_enable2D(int enable);
void gfx_cleanSCREEN();
void gfx_drawBOX(float x,float y,float w,float h,float r,float g,float b,float alpha);
void gfx_drawSPRITE(float x,float y,float w,float h,int textID,float tx,float ty,float tw,float th,float alpha,float scale,int dir);
// ---------------------------------------------
Suoni
concetto di wave vs sounds
uso dei wave
Codice SND
// ---------------------------------------------
// - SOUND
// ---------------------------------------------
int wav_read(const char*s,int mode);
int wav_delete(int*waveID);
// ---------------------------------------------
int snd_init();
int snd_reset();
int snd_play(int waveID,int loop,int mode);
int snd_stop(int playwaveID);
int snd_pause(int playwaveID);
// ---------------------------------------------
Usiamo quanto imparato
if(game_init)
{
game_loop
game_reset
}
if(game_init)
{
game_loop
game_reset
}
Codice GAME (1)
int GAME_init(WORLD*w,const char*res_basepath,float width,float height)
{
strcpy(os_szResPath,res_basepath);
os_video_w=width;
os_video_h=height;
gfx_init();
snd_init();
gfx_enable2D(1);
return 1;
}
Codice GAME (2)
int GAME_reset(WORLD*w)
{
gfx_enable2D(0);
snd_reset();
gfx_reset();
return 1;
}
void GAME_loop(WORLD*w)
{
// to do!
}
Considerazioni
classi con derivazione virtuale vs callbacks
lavoriamo a raffinamenti successivi
il concetto di pixel perfect non esiste (quasi) più
dimensione schermo
Screenshot
Dal programma al gioco
(3)
Dove vogliamo giocare
impostiamo il gioco
strutturiamo il labirinto
disegnamo il labirinto
Codice definizione Maze
char orig_maze[11][20]= { "11111111111111111111",
"1.o..1........1..o.1",
"1.11.1.111111.1.11.1",
"1.1..............1.1",
"1.1.11.11 11.11.1.1",
"1......1ABCD1......1",
"1.1.11.111111.11.1.1",
"1.1.......*......1.1",
"1.11.1.111111.1.11.1",
"1.o..1........1..o.1",
"11111111111111111111"
};
Based on http://www-inst.eecs.berkeley.edu/~cs188/pacman/projects/multiagent/pacman_multi_agent.png
Codice disegno Maze
void drawMAZE(WORLD*w)
{
int msx=20,msy=11;
float x,y,sx=os_video_w/msx,float sy=os_video_h/msy;
for(y=0; y<sy; y++)
for(x=0; x<sx; x++)
{
float px=x*sx,py=y*sy;
switch(orig_maze[(int)y][(int)x])
{
case '1': gfx_drawBOX(px,py,sx,sy,0,0,0.5f,1); break;
case '.': gfx_drawBOX(px+sx/2-sx/10,py+sy/2-sx/10,sx/5,sx/5,1,1,1,1); break;
case 'o': gfx_drawBOX(px+sx/2-sx/4,py+sy/2-sx/4,sx/2,sx/2,1,1,0,1); break;
}
}
}
game loop
handleUI
action
draw
Codice GAME_loop
void GAME_loop(WORLD*w)
{
INGAME_handleUI(w);
INGAME_action(w);
INGAME_draw(w);
}
Codice INGAME
int INGAME_handleUI(WORLD*w)
{
return 1;
}
int INGAME_action(WORLD*w)
{
return 1;
}
int INGAME_draw(WORLD*w)
{
gfx_cleanSCREEN();
drawMAZE(w);
return 1;
}
Screenshot
Mettiamo tutto in gioco
(4)
Area di gioco e gioco
labirinto come parte del mondo
concetto di livello di gioco / di partita
Codice GAME_init
int GAME_init(WORLD*w,const char*res_basepath,float width,float height)
{
strcpy(os_szResPath,res_basepath);
os_video_w=width;
os_video_h=height;
gfx_init();
snd_init();
gfx_enable2D(1);
GFXRES_init();
MATCH_init(w,1);
return 1;
}
Codice Strutture
// ---------------------------------------------
// - STRUCTURES
// ---------------------------------------------
typedef struct tagMAZE
{
char maze[11][20];
int sx,sy;
int home_x,home_y;
int coins;
} MAZE;
typedef struct tagWORLD{
MAZE M;
}WORLD;
Con chi giochiamo
actors
caricamento risorse
animazione personaggi
Risorse
http://www.javaonthebrain.com/java/msmidpman/spritecloseup.gif
Codice
typedef struct tagACTOR
{
float x,y;
int baseFrame,nFrames,curFrame,dir;
} ACTOR;
int g_nActors=5;
ACTOR g_actors[5],*g_player;
int g_textID;
int GFXRES_init()
{
g_textID=tex_readTGA("sprite.tga",0);
return (g_textID!=-1);
}
int GFXRES_reset()
{
tex_delete(&g_textID);
return 1;
}
Codice MATCH_init
int MATCH_init(WORLD*w,int way)
{
int x,y;
memset(&g_actors,0,sizeof(g_actors));
g_player=&g_actors[0];
if(way)
{
memset(&w->M,0,sizeof(w->M));
w->M.coins=0;
w->M.sx=20;
w->M.sy=11;
memcpy(w->M.maze,orig_maze,sizeof(orig_maze));
}
for(y=0; y<w->M.sy; y++)
for(x=0; x<w->M.sx; x++)
{
char ch=w->M.maze[y][x];
if((ch=='.')||(ch=='o'))
{
if(way)
w->M.coins++;
}
else if(ch=='*')
{
int wh=0;
ACTOR*a=&g_actors[wh];
a->x=x;
a->y=y;
a->baseFrame=0;
a->nFrames=3;
a->dir=dirLEFT;
}
else if((ch>='A')&&(ch<='D'))
{
int wh=1+(ch-'A');
ACTOR*a=&g_actors[wh];
if(ch=='B')
{
w->M.home_x=x;
w->M.home_y=y;
}
a->x=x;
a->y=y;
a->baseFrame=8+wh*8;
a->nFrames=2;
a->dir=wh;
}
}
return 1;
}
Codice draw Actors
void drawACTORS(WORLD*w)
{
int i;
float sx=os_video_w/w->M.sx;
float sy=os_video_h/w->M.sy;
for(i=0; i<g_nActors; i++)
{
ACTOR*a=&g_actors[i];
float px=a->x*sx,py=a->y*sy;
int curframe=a->baseFrame+(a->dir-1)*a->nFrames+a->curFrame;
float fy=(curframe/8)*32.0f,fx=(curframe%8)*32.0f;
gfx_drawSPRITE(px,py,sx,sy,g_textID,fx,fy,32,32,1,1,0);
a->curFrame++;
if(a->curFrame>=a->nFrames)
a->curFrame=0;
}
}
Codice INGAME_draw
int INGAME_draw(WORLD*w)
{
gfx_cleanSCREEN();
drawMAZE(w);
drawACTORS(w);
return 1;
}
Screenshot
Muoversi e agire
(5)
Input, azioni e conseguenze
movimento player (handleUI)
mangiare le pillole (coins)
discorso punteggio
Codice Movimento (1)
#define dirUP 1
#define dirDOWN 2
#define dirLEFT 3
#define dirRIGHT 4
typedef struct tagWORLD{
int keys;
int score;
MAZE M;
}WORLD;
float l_dirX[5]= {0,0,0,-1,1};
float l_dirY[5]= {0,-1,1,0,0};
Codice Movimento (2)
int INGAME_handleUI(WORLD*w)
{
w->keys=0;
#if defined(OS_WIN32)
if(GetKeyState(VK_UP) & 0x80 )
w->keys=dirUP;
else if(GetKeyState(VK_DOWN) & 0x80 )
w->keys=dirDOWN;
else if(GetKeyState(VK_LEFT) & 0x80 )
w->keys=dirLEFT;
else if(GetKeyState(VK_RIGHT) & 0x80 )
w->keys=dirRIGHT;
#endif
return 1;
}
Codice Movimento (3)
int getavailableDIRS(MAZE*M,ACTOR*m,int*dir,int*mask,int check)
{
int x=(int)m->x,y=(int)m->y,ndirs=0;
if(mask) *mask=0;
if(x&&M->maze[y][x-1]!='1')
{
if((check==0)||(m->dir!=dirRIGHT))
{
if(dir) dir[ndirs++]=dirLEFT;
if(mask) *mask|=1;
}
}
if((x+1<M->sx)&&(M->maze[y][x+1]!='1'))
{
if((check==0)||(m->dir!=dirLEFT))
{
if(dir) dir[ndirs++]=dirRIGHT;
if(mask) *mask|=2;
}
}
if(y&&M->maze[y-1][x]!='1')
{
if((check==0)||(m->dir!=dirDOWN))
{
if(dir) dir[ndirs++]=dirUP;
if(mask) *mask|=4;
}
}
if((y+1<M->sy)&&(M->maze[y+1][x]!='1'))
{
if((check==0)||(m->dir!=dirUP))
{
if(dir) dir[ndirs++]=dirDOWN;
if(mask) *mask|=8;
}
}
return ndirs;
}
int chooseDIR(MAZE*M,ACTOR*m)
{
int dirs[4],ndirs=getavailableDIRS(M,m,dirs,NULL,1);
if(ndirs)
return dirs[rand()%ndirs];
else
return 0;
}
Codice Movimento con UI (1)
int INGAME_action(WORLD*w)
{
...
if(w->keys)
if(g_player->moving==0)
{
int i,dirs[4],ndirs=getavailableDIRS(&w->M,g_player,dirs,NULL,0);
for(i=0; i<ndirs; i++)
if(dirs[i]==w->keys)
{
g_player->dir_x=l_dirX[w->keys];
g_player->dir_y=l_dirY[w->keys];
g_player->dir=w->keys;
break;
}
}
...
}
Codice Movimento con UI (2)
...
if(g_actor->dir_x||g_actor->dir_y)
{
g_actor->x+=g_actor->dir_x*0.20f;
g_actor->y+=g_actor->dir_y*0.20f;
g_actor->moving++;
if(
(fabs(g_actor->x-g_actor->nx)<0.11f)&&
(fabs(g_actor->y-g_actor->ny)<0.11f)
)
{
g_actor->moving=0;
g_actor->x=g_actor->nx;
g_actor->y=g_actor->ny;
}
}
else
g_actor->moving=0;
...
Codice Movimento/Collisione
if(g_actor->moving==0) { int x,y; x=(int)g_actor->x+g_actor->dir_x; y=(int)g_actor->y+g_actor->dir_y; if(w->M.maze[y][x]=='1') g_actor->dir_x=g_actor->dir_y=0; else { g_actor->nx=(int)g_actor->x+g_actor->dir_x; g_actor->ny=(int)g_actor->y+g_actor->dir_y;
} }
Codice Collisione Coins
...
else
{
g_actor->nx=(int)g_actor->x+g_actor->dir_x;
g_actor->ny=(int)g_actor->y+g_actor->dir_y;
if(i==0) // solo il player può mangiare i coins
{
x=(int)g_actor->x;
y=(int)g_actor->y;
if(w->M.maze[y][x]=='.')
{
w->M.maze[y][x]=' ';
w->M.coins--;
w->score+=10;
}
}
}
Considerazioni
Virtualizzare l'input
Screenshot
Il gioco è quasi concluso
ma il lavoro non è finito
(6)
Completiamo il gioco
cambio di livello
restart del gioco a fine vite
Codice Fine Livello/Vite
int INGAME_postaction(WORLD*w)
{
...
if(w->M.coins==0)
{
MATCH_init(w,2); // WIN!
}
...
if(w->lifes>1)
{
w->lifes--;
MATCH_init(w,0);
}
else
{
... // GAME OVER
}
GUI/HUD
Aggiungere la GUI/HUD (Graphical User Interface / heads-up display)
(caricamento e uso di un font bitmapped)
Risorse (2)
http://www.bigmessowires.com/atarifont.png
Fantasmi!
movimento dei nemici (callback)
morte in caso di contatto
gestione delle pillole più grosse
Codice Struttura Actor
struct tagACTOR;
typedef int (*actionproc)(MAZE*M,struct tagACTOR*m);
typedef struct tagACTOR
{
float x,y;
float nx,ny;
float dir_x,dir_y;
int baseFrame,nFrames,curFrame;
int dir,moving,blink;
int movementmask;
int status;
actionproc act;
} ACTOR;
Codice Callback Player
int PLAYER_act(MAZE*M,struct tagACTOR*m)
{
if(g_W.keys)
{
if(m->moving==0)
{
int i,dirs[4],ndirs=getavailableDIRS(M,m,dirs,NULL,0);
for(i=0; i<ndirs; i++)
if(dirs[i]==g_W.keys)
{
m->dir_x=l_dirX[g_W.keys];
m->dir_y=l_dirY[g_W.keys];
m->dir=g_W.keys;
return 1;
}
}
}
return 0;
}
Codice Callback Fantasmi
int GHOST_act(MAZE*M,struct tagACTOR*m)
{
if(m->moving==0)
if(m->dir_x||m->dir_y)
{
int mask;
getavailableDIRS(M,m,NULL,&mask,1);
if(mask!=m->movementmask)
{
m->movementmask=mask;
if((rand()%10)>=5)
{
m->dir=chooseDIR(M,m);
m->dir_x=l_dirX[m->dir];
m->dir_y=l_dirY[m->dir];
}
}
}
else
{
m->dir=chooseDIR(M,m);
m->dir_x=l_dirX[m->dir];
m->dir_y=l_dirY[m->dir];
}
return 1;
}
Codice Post Action
int INGAME_postaction(WORLD*w)
{
{
int i;
for(i=1; i<g_nActors; i++)
{
ACTOR*g_actor=&g_actors[i];
if((fabs(g_actor->x-g_player->x)<0.5f)&&(fabs(g_actor->y-g_player->y)<0.5f))
if(g_actor->status&3)
{
g_actor->x=g_actor->nx=w->M.home_x;
g_actor->y=g_actor->ny=w->M.home_y;
g_actor->dir=dirUP;
g_actor->moving=0;
g_actor->movementmask=0;
g_actor->status=0;
w->score+=w->M.hunt_score;
w->M.hunt_score*=2;
}
else
{
if(w->lifes>1)
{
w->lifes--;
MATCH_init(w,0);
}
else
{
INGAME_set(w);
}
}
}
}
return 1;
}
Codice PowerUp (1)
...
else if(w->M.maze[y][x]=='o')
{
int j;
w->M.maze[y][x]=' ';
w->M.coins--;
w->score+=50;
w->M.hunt_timer=os_getMilliseconds()+5000;
w->M.hunt_score=200;
for(j=1; j<g_nActors; j++)
g_actor[j].status=1;
}
Codice PowerUp (2)
...
if(w->M.hunt_timer)
if(os_getMilliseconds()>w->M.hunt_timer)
{
int j;
w->M.hunt_timer=0;
for(j=1; j<g_nActors; j++)
g_actors[j].status=0;
}
else if(os_getMilliseconds()+2500>w->M.hunt_timer)
{
int j;
for(j=1; j<g_nActors; j++)
if(g_actors[j].status&1)
g_actors[j].status|=2;
}
game loop
uso delle callback
Codice Callback
typedef int (*gameloopproc)(WORLD*W);
typedef struct tagGAMELOOP{
gameloopproc handleUI;
gameloopproc action;
gameloopproc postaction;
gameloopproc draw;
}GAMELOOP;
void GAME_loop(WORLD*w)
{
if(g_G.handleUI)
g_G.handleUI(w);
if(g_G.action)
g_G.action(w);
if(g_G.postaction)
g_G.postaction(w);
if(g_G.draw)
g_G.draw(w);
}
Screenshot
Aggiungiamo
sub-scene
(7)
Eventi
Uso dei timer
Codice Eventi
typedef struct tagWORLD{
...
int event,event_timer;
...
}WORLD;
void EVENT_set(WORLD*w,int event)
{
w->event=event;
w->event_timer=os_getMilliseconds()+1000;
}
Screenshot
Scene e input mobile
(8)
Home e Game
Un gioco ha almeno due scene
Screenshot
Input touch
Un semplice Virtual Pad
Screenshot
Altro
Gestione hiscores
Piccolo ritocco all'estetica del labirinto
Audio
(9)
Musica e suoni
caricamento dell'audio
uso di musica e effetti sonori
Codice SNDRES (1)
int g_wav_intro;
int g_wav_eat,g_wav_eatG;
int g_wav_chomp,g_wav_die;
int g_intoMUSIC,g_chomp;
int SNDRES_init()
{
snd_init();
g_wav_intro=wav_read("pacman_beginning.wav",0);
g_wav_eat=wav_read("pacman_eatfruit.wav",0);
g_wav_eatG=wav_read("pacman_eatghost",0);
g_wav_die=wav_read("pacman_death.wav",0);
g_wav_chomp=wav_read("pacman_chomp.wav",0);
return (g_wav_intro!=-1)&&(g_wav_eat!=-1)&&(g_wav_eatG!=-1)&&(g_wav_die!=-1)&&(g_wav_chomp!=-1);
}
Codice SNDRES (2)
int SNDRES_reset()
{
wav_deleteall();
snd_reset();
return 1;
}
Codice INGAME_set
void INGAME_set(WORLD*w)
{
if(g_intoMUSIC!=-1)
{
snd_stop(g_intoMUSIC);g_intoMUSIC=-1;
}
g_G.handleUI=INGAME_handleUI;
g_G.action=INGAME_action;
g_G.postaction=INGAME_postaction;
g_G.draw=INGAME_draw;
MATCH_init(w,1);
EVENT_set(w,event_GETREADY);
}
Codice HOME_set
void HOME_set(WORLD*w)
{
g_intoMUSIC=snd_play(g_wav_intro,1,0);
g_G.handleUI=HOME_handleUI;
g_G.action=HOME_action;
g_G.postaction=NULL;
g_G.draw=HOME_draw;
MATCH_init(w,1);
}
Considerazioni
cosa ancora manca / cosa si potrebbe aggiungere
Esigenze mobile
(10)
aggiunte specifiche per mobile
backkey handling
suspend mode / resume mode
(restore texture)
Codice Back
int HOME_backaction(WORLD*w)
{
#if defined(OS_ANDROID)
jni_exit_app(0);
#endif
return 1;
}
if((g_G.backaction)&&(os_BACK_pressed))
{
g_G.backaction(w);
os_BACK_pressed = 0;
}
Codice Suspend/Resume (1)
void GAME_pause(WORLD*w)
{
os_PAUSE=1;
}
void GAME_resume(WORLD*w)
{
os_PAUSE=0;
}
Codice Suspend/Resume (2)
void GAME_loop(WORLD*w)
{
if(os_PAUSE)
;
else
{
if(g_G.handleUI)
g_G.handleUI(w);
if(g_G.action)
g_G.action(w);
if(g_G.postaction)
g_G.postaction(w);
if(g_G.draw)
g_G.draw(w);
}
}
Codice restore texture
int GAME_restore_textures(WORLD*w)
{
GFXRES_reset();
return GFXRES_init();
}