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  }


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();
} 

Android

Eclipse / Java+K
SDK
Struttura applicazione e progetto

iOS

X-Code / Object-C
Frameworks
Struttura applicazione e progetto

Windows Phone

Visual Studio
DirectX
Struttura applicazione e progetto

A simple 2D Game

By Marco Giorgini

A simple 2D Game

How to build a simple multiplatform 2D Game

  • 5,072