Guida per lo sviluppatore


Tabella dei contenuti:

1 · introduzione


RetroGear é un semplice motore di gioco 2D, generico, pensato per la realizzazione semplice e veloce di giochi retro in genere, come quelli degli anni 80.

Un motore di gioco semplice e chiaro, sviluppato sulla base delle piú comuni pratiche, tecniche e convenzioni adottate dai programmatori di videogiochi, mantenendo cosí anche una struttura interna di facile integrazione con progetti esterni.
Sviluppato con l'ottica di offrire la più alta semplicità e completezza possibile, RetroGear offre una vasta gamma di funzionalità, oltre che un sistema compatto e minimale per la gestione dei piú svariati aspetti di gioco, permettendo così anche lo sviluppo rapido di applicativi videoludici in tempi brevissimi, in maniera standard, chiara e facile.

Il progetto è in continuo aggiornamento e miglioramento, per tanto puó essere soggetto a svariate modifiche, ma siete comunque invitati a mettere mano al codice e plasmare il motore sulle vostre esigenze e volendo anche condividerle con tutti, segnalandole all'autore se possibile.

2 · Struttura del progetto

2.1 · Per cominciare

Il file principale del progetto è main.c, al suo interno vengono inizializzati i meccanismi interni del motore di gioco, nonché le librerie SDL.


/**
 * Prima di liberare le risorse dal sistema bisogna terminare il ciclo
 * principale del programma
 **/
void quitGame()
{
	quit = 1;
}

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

    // Inizializzazione sistemi interni SDL
    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) != 0)
    {
        fprintf(stderr, "Can't initialize SDL: %s\n", SDL_GetError());
        exit(-1);
    }
    atexit(SDL_Quit);

    // Inizializzazione schermo
    #ifdef DOUBLE_SCREEN

        double_screen = SDL_SetVideoMode(SCREEN_WIDTH*2, SCREEN_HEIGHT*2, 
                        0, SDL_HWSURFACE);
        if(double_screen == NULL)
        {
            fprintf(stderr, "Can't initialize SDL: %s\n", SDL_GetError());
            exit(-1);
        }
        
        screen = SDL_CreateRGBSurface(0,SCREEN_WIDTH,SCREEN_HEIGHT,32,0,0,0,0);
            
    #else

        screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 0, SDL_HWSURFACE);
        if(screen == NULL)
        {
            fprintf(stderr, "Can't initialize SDL: %s\n", SDL_GetError());
            exit(-1);
        }

    #endif
	
    //Inizializzazione sottosistemi RetroGear
    init();

    //Parametri definiti dal programmatore
    mainInit();

    //Main game loop
    mainLoop();

    //Pulizia delle risorse allocate
    cleanUp();

    return 0;
}


Nella funzione init() vengono inizializzati i sottosistemi di gioco interni di RetroGear, oltre che inizializzate alcune variabili di sistema, necessarie al corretto funzionamento del motore di gioco.

void init()
{
    //FPS per il gioco
    fps.frequency= 1000 / 100;

    quit = 0;
    Game.status = GAME;
    curr_menu = &main_menu;
	
    //Inizializzazione dei sottosistemi di gioco
    initFont();
    initAudio();
    initScore();
    initController();

    initTypewriter(&typewriter, FONT_W, (SCREEN_HEIGHT/2)+56, SCREEN_WIDTH-(FONT_W*3));
    initTransition(TILESIZE, transition_lines);

    setCurrentPlayer(&Player);
    initPlayer(curr_player);

    //Inizializzazione del livello
    initLevel();
    loadLevel(&level, "main");

    initCamera(&camera);
}


La funzione extInit() (extension init) è pensata come estensione personalizzabile di init(), per permettere al programmatore di personalizzare gli aspetti del motore di gioco in fase di avvio, in uno spazio dedicato.

void extInit()
{
	SDL_WM_SetCaption("RetroGame", "RetroGame");
	
	initMenu(menuptr, 1, 30, 10, "main", NULL, NULL);
	addMenuItem(menuptr, createItem(1,"New Game", white, doPreGame));
	addMenuItem(menuptr, createItem(5,"Quit", white, quitGame));
	alignMenuCenter(menuptr);
	alignMenuBottom(menuptr);
}


La funzione quitGame() non fa altro che informare il sistema che l'applicativo deve essere chiuso, impostando il flag globale quit del motore di gioco ad 1.
Eventuali rimozioni di risorse allocate dinamicamente, avverranno in automatico nella funzione cleanUp(), poco prima della chiusura definitiva dell'applicativo.

La struttura main_menu è propria del sistema interno del motore di gioco e fornisce un pratico strumento per la realizzazione di menù per i giochi, si consulti il capitolo 14 · Menú di gioco per maggiori informazioni al riguardo


2.2 · Stati di gioco

La funzione che si occupa di mantenere in vita l'applicativo e gestire in maniera generica i più comuni stati di gioco, è mainLoop(), il ciclo principale di giooc.

Al suo interno vengono gestiti gli input da periferiche (tastiera/gamepad) e richiamati ad intervalli regolari gli stati di gioco tramite un ciclo interno.
Il ciclo interno, mantiene l'esecuzione dell'applicativo ad una velocità costante su hardware di diversa potenza. (Si veda il capitolo 8.7 · Gestione degli FPS per maggiori informazioni al riguardo.)
Il ciclo principale termina quando il flag globale quit, viene posto ad 1.

La funzione draw(), in maniera analoga a questa, si occupa di richiamare le funzioni di disegno idonee per lo stato di gioco del momento.

void mainLoop()
{
	while(!quit)
	{
		keyboardInput();
		
		unsigned int maxl = 256;
		fps.now = SDL_GetTicks();
		fps.dtime += fps.now - fps.then;
		fps.then = fps.now;

		while (--maxl && fps.dtime >= LOGICMS)
		{
			switch(Game.status)
			{
				case MENU:
				 doTitleScreen();
				break;
				
				case PREGAME:
				 doPreGame();
				break;

				case GAME:
				 doGame();
				break;

				case LOST:
				 doLogic();
				break;

				case WIN:
				 doWin();
				break;

				case GAMEOVER:
				 doGameOver();
				break;

				case EDITOR:
				 doEditor();
				break;
			}

			fps.now = SDL_GetTicks();
			fps.dtime += fps.now - fps.then - LOGICMS;
			fps.then = fps.now;
		}
		
		// Gestisce gli stati di disegno sullo schermo
		draw();
		
		SDL_Delay(1);
	 }
}
Ad ogni status è associata una funzione particolare, per convenzione nominate nella forma doStatus per la logica, drawStatus per il rendering.
Ogni funzione di status, al suo interno racchiude la logica voluta dal programmatore per quel determinato momento di gioco.
Da subito il tutto viene presentato con una logica minimale e generica, che per la maggior parte dei casi può trovare una sua utilità in qualsiasi tipologia di gioco si voglia creare.
Il programmatore è liberissimo di usare, cambiare o semplicemente espandere ciò che già vi trova presente al suo interno.

Si consiglia in caso di necessità di espansione, di mantenere ed adottare le convenzioni di naming già in uso nel motore.


Gli stati di gioco attualmente gestiti sono i seguenti:

Per agevolare il programmatore, alcuni status di gioco sono muniti di un piccolo controllo al loro interno.
    if(Game.status!=STATUS)
    {
        setGameState(STATUS);
    }

Questo controllo, imposta in automatico lo status di gioco, in base alla funzione in cui si trova, ad esempio nella funzione doTitleScreen() verrà impostato lo stato MENU, per la funzione doGame() lo stato GAME e via dicendo.

Nonostante tutto, potrebbero presentarsi dei casi in cui il programmatore possa decidere di non volere l'impostazione forzata dello status di gioco, in questo caso si può omettere tranquillamente il controllo dalla funzione, ma bisognerà comunque avere l'accortezza di impostare correttamente il nuovo status di gioco a mano, possibilmente usando la funzione setGameState(STATUS) per indirizzare alla giusta logica il flusso di gioco.

3 · Gestione della grafica

Parallelamente alla gestione degli stati di gioco, in maniera analoga vengono gestiti gli eventi di rendering relativi allo status attuale del gioco.

Tramite la funzione draw(), presente nel file draw.c e richiamata in automatico nel ciclo principale di gioco, il programmatore potrà gestire cosa disegnare durante i vari stati di gioco.
void draw()
{
	if(transition.flag_active)
	{
		doTransition();
	}
	else
	{
		clearScreen();
	
		switch(Game.status)
		{
			case MENU:
				drawTitle();
				break;
			case PREGAME:
				drawPreGame();
				break;
			case GAME:
				drawGame();
				break;
			case LOST:
				drawGame();
				break;
			case WIN:
				drawWin();
				break;
			case GAMEOVER:
				drawGameOver();
				break;
		}
	}
	
	callback_DrawSystemMessages();

	//Aggiorna lo schermo
	#ifdef DOUBLE_SCREEN
		SDL_SoftStretch(screen, NULL, double_screen, NULL);
		SDL_Flip(double_screen);
	#else
		SDL_Flip(screen);
	#endif
}
Esattamente come nella gestione degli eventi di status del ciclo principale di gioco, lanceremo la funzione idonea allo stato di gioco attualmente in corso, per convenzione nominate nella forma drawStatus.

Il programmatore potrà decidere arbitrariamente quali logiche eseguire nelle svariate funzioni messe a disposizione, usando sia funzioni di libreria che implementandone di proprie.

Come già accennato nel capitolo 2.1 · Per cominciare, il sistema prevede la possibilità di avere stretching software sullo schermo, pertanto tale caso viene gestito anche in questa funzione, richiamando la funzione SDL_SoftStretch() nel caso sia stato richiesto lo streching dello schermo, in caso contrario una semplice chiamata alla funzione SDL_Flip() sulla superficie video principale. Il file draw.c al suo interno, fornisce anche una funzione supplementare privata di callback, callback_DrawSystemMessages().
void callback_DrawSystemMessages()
{
	if(sys_message != NULL)
	{
		drawString(screen, 8, SCREEN_HEIGHT-16, sys_message, red, 0);

		if(getSeconds(sys_timer.start_time) > 3)
		{
			strcpy(sys_message, "");
			sys_timer.start_time = 0;
		}
	}
}
Questa funzione permette di mostrare eventuali notifice di sistema, mostrando messaggi di massimo una riga alla volta per 4 secondi, nella parte bassa della finestra di gioco.

Questa funzione è usata principalmente dall'editor di livello interno al motore, per notificare al giocatore gli eventi scatenati, come caricamento avvenuto di un file di livello, salvataggio di un livello o altro.

3.1 · Disegnare grafica semplice

Il motore di gioco, fornisce una serie di funzioni di libreria per il disegno di forme geometriche di base, come linee, circonferenze e quadrati, oltre che la gestione a basso livello dei singoli pixel sulle superfici di disegno

Nella libreria gfx sono presenti le funzioni per il disegno e la manipolazione grafica generica, oltre che una palette di colori minimale, sia in formato rgb che esadecimale.
SDL_Surface *screen, *tile_sheet, *alpha_sheet;

/** Colori RGB **/
static const SDL_Color white = {255, 255, 255};
static const SDL_Color black = {0, 0, 0};
static const SDL_Color cyan  = {0, 255, 255};
static const SDL_Color blue  = {0, 0, 255};
static const SDL_Color yellow  = {255, 255, 0};
static const SDL_Color purple = {255, 0, 255};
static const SDL_Color red  = {255, 0, 0};
static const SDL_Color green  = {0, 255, 0};
static const SDL_Color gray  = {192, 192, 192};

/** Colori Hex **/
#define RED 0xFF0000
#define BLUE 0x0000FF
#define GREY 0xC0C0C0
#define WHITE 0xFFFFFF
#define BLACK 0x000000
#define GREEN 0x008000
#define ORANGE 0xFF9D2E
#define PURPLE 0xFF00FF
#define YELLOW 0xFFFF00
#define COLORKEY 0x00FF00

#define SKYBLUE 0x8080FF

Uint32 get_pixel(SDL_Surface *surface, int x, int y);
void put_pixel(SDL_Surface *_ima, int x, int y, Uint32 pixel);
void replaceColor (SDL_Surface * src, Uint32 target, Uint32 replacement);
void drawFillRect(int x, int y, int w, int h, int color);
void drawRect(int x, int y, unsigned int w, unsigned int h, int color);
void drawGui(int x, int y, unsigned int w, unsigned int h, int bg, SDL_Color color);
SDL_Surface * loadImage(char *file, Uint32 key);


Le funzioni presenti hanno la seguente utilità:
I colori dichiarati come anche le funzioni, sono utilizzate da numerose altre funzioni di disegno interne al sistema di RetroGear, per tanto se ne scoraggia vivamente l'eliminazione o l'alterazione per evitare disfunzioni nel sistema.


3.2 · Utilizzo degli sprite

RetroGear fornisce una struttura generica per la rappresentazione degli sprite, ed anche relativo sistema per l'animazione di essi.

    typedef struct _Sprite {
        SDL_Surface *surface;
        int x, y;
        int w, h;
        int index;
        float animation_timer;
        float animation_speed;
    } Sprite;
La struttura Sprite, può essere usata sia per la rappresentazione libera di contenuti grafici all'interno del campo di gioco, sia per la rappresentazione di entità di gioco.
Di seguito ne è spiegata la struttura.

	SDL_Rect dest;
	dest.x = 20;
	dest.y = 50;
	dest.w = 16;
	dest.h = 16;

	getSprite(skel_spr, 0, dest.w, dest.h, &dest);
Ogni struttura di tipo SDL_Surface deve essere valorizzata correttamente con il contenuto di un file immagine BMP, ciò può essere fatto tramite la funzione loadImage().
skel_spr=loadImage("data/skel.bmp", 0x00FF00);
Come primo argomento accetta il path relativo del file immagine, rispetto all'eseguibile di gioco, e come secondo parametro il colore di trasparenza per l'immagine espresso in esadecimale.

Per quanto riguarda il disegno dinamico di entità animate, si ricorrere alla funzione interna drawEntity(), richiamata in automatico dai meccanismi interni del motore di gioco per ogni singola entità presente in lista con un semplice ciclo while all'interno della funzione drawGame().

Questa funzione accetta in argomento un puntatore ad una struttura di tipo entità, dalla quale recupera lo sprite associato e ne disegna il fotogramma attuale.

4 · Entità di gioco


Ogni oggetto interattivo nel gioco è detto entità, ed rappresentato dalla struttura entity che provvede a fornire tutto il necessario per gestire e rappresentare l'entità stessa all'interno del gioco.

Possiamo dividere l'entità in 4 gruppi di variabili.
Il primo rappresenta l'oggetto vero e proprio, con alcune proprietà principali come le coordinate nel campo di gioco, relativa altezza e larghezza, direzione coordinate di partenza e precedenti dell'ultima posizione occupata, oltre che variabili supplementari come punteggio, numero di vite, tipologia, e timer interni.

Il secondo gruppo, di cui fa parte il membro sprite, fornisce una struttura necessaria a rappresentare graficamente sullo schermo l'entità, permettendo di associarle un'immagine statica o composta da poter animare (Si veda il capitolo riguardante gli sprite).

Il terzo gruppo, "fisica", è rappresentato da variabili supplementari, utilizzate per l'implementazione della fisica sulle entità di gioco, come gravità ed inerzia ad esempio.

L'ultimo gruppo presenta variabili di utilità per il motore di gioco, un puntatore a funzione, update, per il richiamo alla funzione di aggiornamento della entità, ed un puntatore ad una struttura di titpo entità per l'implementazione e gestione di tutte le entità in una lista linkata.

4.1 · Gestione delle entità di gioco

Le entità di gioco, di default, sono gestite in maniera del tutto dinamica, allocate in memoria a runtime in base alla loro presenza nel file di livello, o semplicemente su richiesta esplicita del programmatore tramite l'utilizzo della funzione createEntity().

Questa funzione accetta in argomento i parametri base principali, necessari ad una entità di gioco per poter essere utilizzabile correttamente, tra cui:

Tutte le entità create tramite questa funzione, verranno allocate in memoria dinamicamente, e gestite tramite lista linkata (Linked list).
Il loro valore di id sarà gestito ed incrementato in automatico, in base al valore dell'ultima entità creata, trovando utilità in momenti di debug, aiutando così il programmatore a tenere traccia dei vari oggetti.

La lista delle entità di gioco presenti in memoria, viene gestita tramite appositi puntatori a struttura di tipo entità, tra cui *headList e *tailList, che rispettivamente terranno conto del primo oggetto della lista (testa, ovvero head) e dell'ultimo (coda, ovvero tail).
Questi puntatori sono inizializzati in automatico dalla funzione createEntity(), che provvederà ad aggiornarli in base al popolamento della lista.
In caso di lista vuota, il primo oggetto allocato verrà assegnato sia ad headList che tailList, mentre l'ultimo ad aggiungersi, dal secondo in poi, verrà associato a tailList in automatico, oltre ciò, ogni oggetto avrà un puntamento a quello successivo.

Ogni entità presente in lista, viene aggiornata con la propria logica in base alla funzione update(), richiamata tramite la funzione doEntities().
Dalla cima della lista fino all'ultima entità disponibile, viene controllato lo stato delle entità o eseguita la funzione di aggiornamento, alla quale verrà passato come argomento un puntatore all'entità stessa per essere gestita con la logica specificata.
Prima dell'esecuzione della logica, viene controllato lo stato dell'entità, in particolare se quest'ultima risulta distrutta e quindi da liberare in memoria. In caso affermativo vengono liberate le risorse associate all'oggetto e reimpostati i collegamenti all'interno della lista per garantirne la continuità.

Nel caso dell'utilizzo di un file di livello, la loro allocazione verrà eseguita automaticamente tramite la funzione createEntityFromMap(), richiamata dalla funzione loadLevel().
Per maggiori informazioni sulla questione, si rimanda al capitolo apposito.

4.2 · Definizione di una entità

Ogni entità di gioco può presentare comportamenti e logiche del tutto proprie e discostanti (talvolta di molto) da tutte le altre presenti nel gioco, nonostante siano tutte "figlie" della struttura entità, può sorgere la necessità di dichiarare più logiche diverse per diverse entità.

RetroGear adotta una convenzione particolare per la gestione delle più svariate tipologie di entità, basata sulla dichiarazione unica di ognuna di esse.

Di base si adotta una struttura standard per la gestione delle entità e dei possibili status di essa, parallelamente alla sua logica, oltre che dichiarare localmente nel proprio header file eventuali strutture di tipo SDL_Surface o quant'altro possa interessare solo quel tipo di entità.

Per definire un'entità, dobbiamo realizzare un sorgente proprio e relativo header, che in questo caso chiameremo Skel, abbreviazione di Skeleton (Scheletro).

    #ifndef _SKEL_H
    #define _SKEL_H

    #define SKEL_SPRITE_W 16
    #define SKEL_SPRITE_H 16

    #define SKEL_W 12
    #define SKEL_H 12

    #define MIN_H_SPEED -1.0f
    #define MAX_H_SPEED 1.0f

    #define MIN_V_SPEED -1.0f
    #define MAX_V_SPEED 1.0f

    SDL_Surface *skel_spr;

    #include "entity.h"

    void skel_create(int id, int x, int y);
    void updateSkel(Entity *pobj);
    void skel_clean();

    #endif
    
Il file di header le informazioni globali e le strutture dati necessarie alla nostra entità, oltre che le sue funzionalità di gestione, che per convenzione saranno nominate nella forma: [entityName]_create/clean e update[entityName].

Il sorgente principale conterrà le logiche proprie dell'entità, come in questo esempio:
    #include "skel.h"

    //Eventuale punteggio ottenibile
    #define SCORE 100
    #define TYPE 3

    static void onAnimate(Entity *pobj);
    static void onCollision(Entity *pobj);
    static void onDestroy(Entity *pobj);

    void skel_create(int id, int x, int y)
    {
        float speed = 0.8f;
        float gravity = 0.05f;
        float animation_speed = 0.05f;
        int lives = 0;

        createEntity(ENEMY, x, y, SKEL_W, SKEL_H, lives,
                     speed, gravity, &updateSkel);

        //Impostiamo lo sprite
        int sprite_diff_w = (SKEL_SPRITE_W - SKEL_W) / 2;
        int sprite_diff_h = (SKEL_SPRITE_H - SKEL_H) -1;

        //entityList_Tail è l'ultima entità creata
        if( !entityList_Tail->sprite.surface )
        {
            initSprite(&entityList_Tail->sprite,
                       entityList_Tail->x + sprite_diff_w,
                       entityList_Tail->y + sprite_diff_h,
                       SKEL_W, SKEL_H,
                       animation_speed,
                       "data/skel.bmp");
        }

        //Velocità e direzioni iniziali della entità
        entityList_Tail->hspeed = 0;
        entityList_Tail->vspeed = 1;
        
        entityList_Tail->direction_x = -1;
        entityList_Tail->direction_y = 0;
    }


    /**
     * Custom entità animator
     **/
    static void onAnimate(Entity *pobj)
    {
        pobj->sprite.animation_timer += pobj->sprite.animation_speed;

        if (pobj->sprite.animation_timer > 2)
        {
            pobj->sprite.animation_timer = 0;
        }

        pobj->sprite.index = (pobj->direction_x < 0 ? 0 : SPRITE_FRAMES)+
                              abs(pobj->sprite.animation_timer);
    }

    static void onCollision(Entity *pobj)
    {
        entità *current = entityList_Head;
        while(current!=NULL)
        {
            if(current!=pobj && current->active && 
               current->status!=KILL && current->type!=COLLECTABLE)
            {
                if(rectCollision(current->x, current->y,
                                 current->w, current->h, 
                                 (int)(pobj->x)+pobj->direction_x,
                                 pobj->y, pobj->w, pobj->h))
                {
                    pobj->direction *=-1;
                }
            }
            current=current->next;
        }
    }

    static void onDestroy(Entity *pobj)
    {
        if(getSeconds(pobj->timer[0]) >= 1)
        {
            pobj->status=DESTROY;
            pobj->visible=0;
        }
    }

    void updateSkel(Entity *pobj)
    {
        if(pobj->status==KILL)
        {
            //Indice dell'immagine dell'entità sconfitta nello sprite sheet
            pobj->frame_index = DIE;		
            onDestroy(pobj);
            return;
        }

        if(rectCollision(Player.x, Player.y, Player.w, Player.h,
                    pobj->x, pobj->y, pobj->w, pobj->h))
        {
            pobj->ystart=pobj->y;
            //Impostiamo lo status a KILL
            pobj->status=KILL;			
            pobj->direction=0;

            createScore(pobj->x+camera.offsetX, pobj->y, 0.4f);
            addScore(getScore(points_index));
            points_index++;
            
            //Salviamo il momento in cui la entità viene schiacciata
            pobj->timer[0]=fps.t;	
            return;
        }

        moveEntity_X(pobj);
        animateEntity(pobj);
        //onAnimate(pobj);  //Alternativa per le animazioni
        onCollision(pobj);
    }
    
Per convenzione, la creazione di ogni entità deve avvenire attraverso un apposito "costruttore", chiamato per convenzione [NomeEntità]_create, che dovrà occuparsi di impostare l'entità e suoi dati correttamente, tipo i suoi sprite, suoni e valori di defualt.

Ogni nuova entità sarà inclusa automaticamente nella lista globale delle entità di RetroGear, grazie alla funzione createEntity(), ed il suo aggiornamento, gestito internamente dal motore tramite la funzione di callback definita: update[entityName].
La funzione di callback potrà essere popolata con le funzionalità standard di RetroGear o da logiche custom ovviamente.

Gli stati dell'entità, avranno funzioni statiche dedicate, definite per convenzione come: on[NomeEvento].

Per convenzione, sono definite tre funzioni di stato: onAnimate,onCollision,onDestroy.

Si può usare onAnimate per definire logiche d'animazione personalizzate, onCollision per gestire eventi di collisione tra le entità, e onDestroy per definire logiche da eseguire al momento della distruzione dell'entità, tipo aumentare il punteggio.

Per maggiori informazioni sulla gestione degli status delle entità, si consulti il capitolo Stati di una entità

Per quanto riguarda la distruzione delle entità, si rimanda al capitolo Rimozione di una entità

La funzione di aggiornamento updateSkel, sarà eseguita in automatico dai meccanismi interni del motore di gioco, predisposti a lanciare la funzione update di qualsiasi entità di gioco presente in lista ed impostata come attiva.
L'indirizzo passato come argomento alla funzione permette di gestire più entità di tipo skel, da un solo punto e con una sola logica comune.

Questo semplice esempio è la maniera standard adottata, per realizzare nuove entità nel gioco, ed ogni nuova entità dovrà anche essere definita nel Makefile di progetto.

4.3 · Tipologie e stati di una entità

Ogni entità può essere associata ad una tipologia, ad esempio Collezionabile, Nemico e quant'altro, tramite l'uso della variabile enumerativa OBJECTS applicata alla variabile di entità type.
typedef enum
{
    PLAYER, COLLECTABLE, ENEMY, BULLET, WALL, OBSTACLE
} ENTITY_TYPE;
    
L'utilizzo della enumerazione, permette una maggiore chiarezza nel codice, la dove alla creazione di una entità, ne si voglia specificare la natura all'interno del gioco e gestirne casi particolari come nell'esempio precedente.

createEntity(ENEMY, x, y, SKEL_W, SKEL_H, lives,
             speed, gravity, &updateSkel);
[...]

if(current!=pobj && current->active
   && current->status!=KILL && current->type!=COLLECTABLE)
La variabile enumerativa ENTITY_TYPE puó essere usata sia in fase di creazione dell'entità, che in fase di controllo su eventuali collisioni con altre entità. Qui ad esempio si impone di ignorare tutte le collisioni con eventuali oggetti di tipo COLLECTABLE, ovvero tutti quei tipi di oggetti come Power-Up, Bonus e via dicendo, che non hanno motivo di influenzare il movimento delle entità in circolo.

Le entità possono anche contare sulla variabile status, come si è visto.
Questa variabile é intesa come indice di stato per le attività correnti dell'entità, ovvero tiene conto di cosa l'entità in quel momento sta facendo.

A discapito del programmatore, sarebbe buona norma assegnare a seconda della logica definita, almeno uno stato d'entità tra quelli forniti dal file entità.h .

typedef enum
{
    MOVE, ACTION, JUMP, FALL, CLIMB, STAND, BLINK, KILL, DESTROY
} ENTITY_STATUS;

Per un'entità in movimento, si potrebbe usare il valore MOVE ad esempio, per una che salta JUMP, per una sconfitta KILL come già visto, e per una da distruggere (tassativamente) DESTROY.

4.4 · Animare le entità

Per convenzione, gli sprite delle entità, adottano uno standard di rappresentazione basato su di una mappa fotogrammi, come la seguente:



Ogni posizione numerata dello sprite sheet, corrisponde ad una determinata azione, o fotogramma dell'azione dell'entità.
Queste azioni sono indicizzate tramite enumerazione nel file entità.h, in maniera standard ed "universale", cercando di soddisfare le più svariate necessità.
enum
{
	//Left
	STANDLEFT = 0,
	WALKLEFT1 = 1,
	WALKLEFT2 = 2,
	WALKLEFT3 = 3,
	RETURN1 = 4,
	JUMPLEFT = 5,

	//Right
	STANDRIGHT = 6,
	WALKRIGHT1 = 7,
	WALKRIGHT2 = 8,
	WALKRIGHT3 = 9,
	RETURN2 = 10,
	JUMPRIGHT = 11,

	//Down
	STANDDOWN = 12,
	WALKDOWN1 = 13,
	WALKDOWN2 = 14,
	WALKDOWN3 = 15,
	RETURN3 = 16,

	//Up
	STANDUP = 18,
	WALKUP1 = 19,
	WALKUP2 = 20,
	WALKUP3 = 21,
	RETURN4 = 22,

	DIE = 17
} spriteSheet;
Nel caso in cui si voglia animare una qualche entità aderendo allo standard di RetroGear, ci si può affidare alla funzione animateEntity(), la quale provvederà a gestire la struttura RG_Sprite dell'entità, calcolando il fotogramma di animazione corretto anche sulla base dello stato e movimento dell'oggetto.
Pensata per essere utile e funzionale in svariate tipologie di gioco, come ad esempio platform game o rpg, senza alcun intervento da parte del programmatore
La funzione attualmente non gestisce l'animazione per tutti i tipi di azione di gioco.
In futuro le animazioni saranno gestite anche tramite array di fotogrammi per generare le sequenze.

Nel caso la funzione animateEntity() non soddisfi le vostre esigenze, si potrà in ogni caso scrivere una funzione di animazione propria all'interno della definizione dell'entità di gioco., l'uso di animateEntity() non è tassativo.

Per convenzione, si consiglia di utilizzare la funzione privata static void onAnimate(Entity *pobj), in cui il programmatore potrà definire nella maniera che preferisce la gestione dei singoli frame di animazione dell'entità.

static void onAnimate(Entity *pobj)
{
    pobj->sprite.animation_timer += pobj->sprite.animation_speed;

    if (pobj->sprite.animation_timer > 2)
    {
        pobj->sprite.animation_timer = 0;
    }

    pobj->sprite.index = (pobj->direction_x < 0 ? 0 : SPRITE_FRAMES)+
                          abs(pobj->sprite.animation_timer);
}

4.5 · Rimozione di una entità

Per rimuovere un'entità dal gioco oltre che dalla memoria, per convenzione adottata dal motore e per sicurezza, ne si imposta lo status su DESTROY, ed il sistema provvederà in automatico a rimuoverla.
Eventuali dati allocati dinamicamente e che necessitino di essere liberati a mano, come gli sprite, dovranno essere liberati manualmente.

4.6 · L'entità Player

È a disposizione del programmatore una struttura di tipo entità per la rappresentazione del giocatore, la struttura Player, definita staticamente nel file player.h, dislocata dalla lista globale delle entità e gestita tramite apposita funzione di aggiornamento updatePlayer().
Il suo identificativo numerico all'interno delle strutture di livello è il numero 2, e viene fornito con un insieme di logiche standard e di base, personalizzabili a seconda delle necessità senza alcun vincolo.

Per convenzione, l'accesso all'oggetto Player all'interno del motore di gioco, avviene tramite il puntatore curr_player, di cui se ne consiglia, rispetto l'accesso diretto alla struttura Player.

//Funzioni private
void playerAction();
void movePlayerStatic();
void movePlayerDynamic();

/**
 * Reimposta il giocatore ai valori di default
 * 
 * @param entity *player
 * 		Puntatore alla struttura del giocatore
 **/
void initPlayer(entity *player)
{
    player->type = PLAYER;
    player->visible = 1;
    player->flag_active = 1;
    player->w= PLAYER_W;
    player->h= PLAYER_H;
    player->lives = 3;
    Player.speed = 0.05f;

    //Differenza tra dimensioni del giocatore e sprite
    int sprite_diff_w = (PLAYER_SPRITE_W - PLAYER_W) / 2;
    int sprite_diff_h = (PLAYER_SPRITE_H - PLAYER_H) -1;

	if( !player->sprite.surface )
	{
        initSprite(&player->sprite, player->x-sprite_diff_w,
                   player->y-sprite_diff_h, 16, 16, 0.09f, "data/player.bmp");
	}

	player->hspeed = 0;
	player->vspeed = 0;
    
        Player.gravity=0.1f;
    
	player->direction_x = 1;
	player->direction_y = 0;
}

void setPlayerPosition(int x, int y)
{
	curr_player->x = x;
	curr_player->y = y;
	curr_player->xstart = x;
	curr_player->ystart = y;
}

void playerExtraLife()
{
	curr_player->lives++;
	playSound(extralife_snd);
}

void playerAction()
{
	curr_player->sprite.index = 0;	//Action sprite index
	//Action time
	//Action function	
}

/**
 * Principale funzione di aggiornamento per il giocatore
 **/
void updatePlayer()
{
    scrollCameraX(&camera, curr_player->x);
    scrollCameraY(&camera, curr_player->y);

    animateEntity(curr_player, 0);
    movePlayerStatic();
}

/**
 * Move the player with dynamic speed
 **/
void movePlayerDynamic()
{
	/**
     * Movimento orizontale
     **/

	if (curr_gamepad->button_Left)
	{
		Player.direction_x = -1;
        Player.status = MOVE;

        Player.hspeed += Player.speed * Player.direction_x;
	}
	
	[...]

	/**
     * Movimento verticale
     **/
	if (curr_gamepad->button_A==PRESSED)// && !lockjump)
	{
        curr_gamepad->button_A = LOCKED;
        if(!isEntityOnFloor(&Player))
        {
            return;
        }

		//if the player isn't jumping already
		Player.vspeed = -2.6f;		//jump!
	}

	[...]

    doEntityGravity(&Player);
}

/**
 * Move the player with dynamic speed
 **/
void movePlayerStatic()
{
    RG_Point *point = NULL;

    //Move only if there's no obstacles
    //TODO: To be tested
    point = tileCollision(&Player, Player.x+Player.hspeed, Player.y);
    if( point != NULL )
    {
        Player.x += Player.hspeed;
        Player.y += Player.vspeed;
    }
    
    //Se il giocatore non è allineato con la griglia
	if(!isInTile(Player.x,Player.y))
	 return;
    
	//horizontal
	if (curr_gamepad->button_Left)
	{
		Player.direction_x = -1;
        Player.direction_y = 0;
        
        Player.hspeed = -1.0f;
	}
	
	[...]
}

void drawPlayer()
{
    if(Game.status < GAME) return;

    int dest_x = (int)curr_player->x-2 - camera.offsetX;
    int dest_y = (int)curr_player->y+1 - camera.offsetY;

    drawSprite(&curr_player->sprite, dest_x, dest_y);
	
    [...]
}
Il costruttore è la funzione initPlayer(entity *player), richiamata da init() in fase di avvio del motore di gioco, in cui viene inizializzata la stuttura del giocatore con valori di deault, adatti a svariate tipologie di movimento e gioco.

Le funzioni di movimento sono gestite da due funzioni private interne al sorgente, movePlayerStatic() e movePlayerDynamic(), richiamate dalla funzione di aggiornamento della entità, updatePlayer().

La prima funzione fornisce un movimento "statico", in cui il giocatore si muoverà a velocità costante ed un tile alla volta nelle quattro direzioni, un movimento tipico dei giochi RPG, che definisco "a griglia"
Nel caso si voglia disabilitare il movimento "a griglia", si commentino le righe sottostanti:
	if(!isInTile(Player.x,Player.y))
        return;
Qui verrà aggiunta in futuro una macro per abilitare/disabilitare il comportamento in fase di compilazione
La seconda funzione, fornisce un movimento "dinamico", in cui il giocatore si muoverà a velocità incrementale orizontalmente, e verticalmente con gestione della gravità, sino ad un massimo definito con le costanti:
    #define MIN_H_SPEED -1.0f
    #define MAX_H_SPEED 1.0f

    #define MIN_V_SPEED -1.0f
    #define MAX_V_SPEED 1.0f
La funzione drawPlayer(), si occupa di disegnare il giocatore nella giusta posizione all'interno del campo di gioco, specie in presenza di scrolling attivo, e all'occorrenza di avere una rappresentazione di debug per esso.
La funzione è parte integrante del motore di gioco, e viene usata nel file draw.c.

Sono fornite anche una serie di funzioni standard parallele per la gestione degli stati d'azione (attacco/altro), e aumento delle vite del giocatore.

La funzione privata playerAction() è pensata per contenere tutte le logiche relative agli stati d'azione del giocatore, come ad esempio momenti di attacco, lancio di proiettili, uso della spada o altro, e relative logiche di animazione.

La funzione playerExtraLife() permette di avere un'interfaccia comune in tutto il motore di gioco, per l'incremento delle vite della entità giocatore attualmente in uso.

4.7 · Gestione multigiocatore

Nel caso si voglia implementare un sistema di multiplayer, in cui più giocatori si alternano uno alla volta nel completare i livelli di gioco, si potrà ridefinire la struttura Player nel file player.h, in un array di giocatori.
entity Player[2];
entity *curr_player;
Tramite l'uso del puntatore curr_player, si potranno gestire eventi e sistemi di gioco, senza dover riscrivere alcuna logica, potendo utilizzare una sola variabile per più entità di tipo giocatore.
Attualmente il sistema multiplayer è solo una bozza in attesa di revisione

5 · Gestione dei font

Per la rappresentazione dei font, RetroGear utilizza uno sprite sheet composto da un minimo di 4 righe ed un massimo di 32 colonne, dai caratteri di dimensione 8x8 pixel.



Il font fornito di default dal motore di gioco, è ispirato a quello del Nintendo NES, inserendo il minimo set di caratteri necessario al programmatore per poter scrivere messaggi alfanumerici e rappresentare alcuni simboli.

La definizione della grandezza e larghezza di ogni singolo carattere, viene specificata nel file font.h, tramite le costanti FONT_W e FONT_H, di default valorizzate entrambe ad 8.

L'inizializzazione del font, avviene nella funzione initFont(), dichiarata all'interno del file font.c, e richiamata in fase di avvio del motore da util().

5.1 · Utilizzo dei font e scrittura

Per la scrittura di testi o singoli caratteri su schermo, sono fornite due funzioni di libreria, drawChar() e drawString()

La funzione drawChar(), è la principale funzione di disegno del testo.
Permette il disegno di un singolo carattere su schermo, impostandone anche colore e trasparenza di sfondo.

drawChar(int dest_x, int dest_y, int asciicode, SDL_Color color, int alpha);

La funzione drawString(), permette la rapprestazione di stringhe.
Essa si appoggia alla funzione drawChar(), richiamandola per ogni singolo carattere della stringa da disegnare e passandogli i parametri necessari.
Accetta i seguenti argomenti:

Alcuni esempi di utilizzo:

	//Esempio di cursore per menù di gioco
	drawChar(screen, menuptr->items[menuptr->curr_item].x-10, 
			 menuptr->items[menuptr->curr_item].y, '*', white, 1);

	//Esempio su come mostrare il numero di vite rimaste al giocatore
	sprintf(message,"%d", Player.lives);
	drawString(screen, 115, 117, message, white, 1);

	//Esempio di testo libero
	drawString(screen, 10, 20, "Hello World!", white, 1);

	

Entrambe le funzioni, fanno uso internamente delle costanti FONT_W e FONT_H per gestire la grandezza dei singoli caratteri.

5.2 · Utilizzo del sistema Typewriter

Il sistema Typewriter é un sottosistema del motore di gioco, che permette la stampa di testo a schermo, con un effetto macchina da scrivere.
Bozza in attesa di revisione

6 · Gestione del sonoro

RetroGear utilizza la libreria SDL_mixer per fornire funzionalità audio di base, che comprendono l'esecuzione di semplici effetti sonori e musiche di sottofondo, oltre che la possibilità di eseguirli, interromperli e caricarli in memoria in qualsiasi momento.

L'inizializzazione del sistema sonoro avviene nella funzione initAudio(), all'interno del file sfx.c.
void initAudio()
{
	audio_rate = 22050;			//Frequenza di playback
	audio_format = AUDIO_S16; 	//Formato dell'audio
	audio_channels = 2;			//2 canali = stereo
	audio_buffers = 4096;		//Dimensione del buffer per i file sonori
	
	//Inizializzazione SDL_Mixer
	if(Mix_OpenAudio(audio_rate, audio_format, audio_channels, audio_buffers)) {
		printf("Unable to initialize audio: %s\n", Mix_GetError());
		exit(1);
	}

	//Caricamente effetti sonori
	collectable_snd = loadSound("snd/collectable.wav");
	stomp_snd = loadSound("snd/stomp.wav");
	jump_snd = loadSound("snd/jump.wav");
	action_snd = loadSound("snd/action.wav");
	extralife_snd = loadSound("snd/extra_life.wav");

	//Caricamento musiche
	title_music = loadMusic("snd/title_theme.wav");
	pregame_music = loadMusic("snd/pregame_theme.wav");
	game_music = loadMusic("snd/level_theme.wav");
	gameover_music = loadMusic("snd/gameover_theme.wav");
	goal_music = loadMusic("snd/goal.wav");
}
	

La funzione initAudio(), oltre che ad inizializzare la libreria SDL_Mixer, provvede anche al caricamento di effetti sonori standard, pensati per venire incontro alla possibili principali esigenze di un gioco. Queste variabili sono definite nel file sfx.h, di tipo Mix_Chunk per gli effetti sonori, e Mix_Music per le musiche di gioco.
I nomi utilizzati per le variabili sonore, adottano la convenzione azione_snd per gli effetti, e status_music per le musiche.

Per gli effetti sonori abbiamo le seguenti variabili standard:
Per le musiche abbiamo le seguenti variabili standard:

6.1 · Stati di gioco e sonoro

Di default il motore di gioco, prevede l'esecuzione di ognuna di queste musiche nello stato di gioco idoneo, controllandone l'eventuale esecuzione tramite un semplice controllo del tipo:
	if(!isMusicPlaying())
	{
		playMusic(title_music, 0);
	}
	
Per motivi di logica e flusso del programma, l'esecuzione diretta delle musiche di gioco per alcuni status, in particolare GAME e LOST, viene relegata alla funzione doPregame(), che in questo caso funzionerà da sparti acque.
void doPreGame()
{
	if(Game.status!=PREGAME)
	{
		setGameState(PREGAME);
	}

	//Stop any music from the game
	if(isMusicPlaying())
	{
		pauseMusic();
	}

	if(getSeconds(timer.start_time) > 2)
	{
		//Reset generic timer
		timer.start_time = 0;
		//Let's play!
		setGameState(GAME);
		playMusic(game_music, 0);
		return;
	}
	
	if(Player.lives==0)
	{
		playMusic(gameover_music, 0);
		setGameState(GAMEOVER);
		return;
	}
}
	
Sezione in attesa di correzione

Qual'ora si decida di non volere alcuna musica in un determinato status di gioco, si può evitarne l'esecuzione omettendone il richiamo dalla funzione di status apposita nel file game.c.
Si consulti il capitolo 2.2 · Stati di gioco per maggiori informazioni.
Il sistema sonoro è incompleto, probabilmente in futuro verrà riscritto

6.2 · Caricare ed eseguire effetti sonori e musiche

Per il caricamento di effetti sonori e musiche, sono disponibili le funzioni di libreria loadSound() e loadMusic().
Entrambe le funzioni accettano come unico argomento una stringa, contenente il nome del file da caricare ed il suo percorso relativo.

my_snd = loadSound("snd/sound.wav");

my_music = loadMusic("snd/music.wav");

Una volta caricato l'effetto sonoro o musica desiderati, si può procedere alla loro esecuzione tramite una semplice chiamata alle funzioni di libreria playSound() e playMusic().
La funzione playSound() accetta in argomento un puntatore ad un'oggetto di tipo Mix_Chunk, mentre la funzione playMusic() accetta in argomento un puntatore ad un'oggetto di tipo Mix_Music, oltre che un valore intero di flag per gestire il numero di ripetizioni.
playSound(my_snd);

playMusic(my_music, 0);

Per quanto riguarda le musiche, vi è la possibilità anche di effettuare controlli sul loro stato di esecuzione, e all'occorrenza interromperlo, riprenderlo o terminarlo del tutto.
void pauseMusic();
void resumeMusic();
int isMusicPlaying();

6.3 · Pulizia delle risorse sonore allocate

Ogni effetto sonoro e musica caricata, viene allocato dinamicamente in memoria, urge quindi la necessità alla terminazione del programma di liberare anche queste risorse come accade per quelle grafiche.
Per ripulire il sistema dalle risorse sonore allocate e terminare correttamente il sistema sonoro della libreria SDL_Mixer, si ricorre alle funzioni destroySound(), destroMusic() e Mix_CloseAudio().

Il motore di gioco provvede in maniera automatica a deallocare tutti gli effetti sonori e musiche standard, all'interno della funzione cleanUp() nel file util.c.
	destroyMusic(title_music);
	destroyMusic(pregame_music);
	destroyMusic(game_music);
	destroyMusic(goal_music);
	
	destroySound(player_die_snd);

	//Free default sounds
	destroySound(collectable_snd);
	destroySound(stomp_snd);
	destroySound(bounce_snd);
	destroySound(jump_snd);
	destroySound(action_snd);

	Mix_CloseAudio();
	


In maniera analoga, il programmatore potrà liberare la memoria da risorse extra definite in un secondo momento nella funzione cleanUp(), che verrà richiamata in automatico al termine del programma.

7 · Gestione dell'input

RetroGear fornisce un sistema centralizzato per la gestione dell'input del giocatore, in simultanea sia da tastiera che da gamepad, tramite una struttura gamepad virtuale, accessibile da tutta l'applicazione.
Intermediaria per la gestione di eventi pressione/rilascio dei tasti sulle periferiche di input fisiche, come tastiera, gamepad e mouse.

La struttura del gamepad virtuale si rifà alla configurazione del classico gamepad del Nintendo NES.
La mappatura per la tastiera con i relativi valori di SDLK, é definita all'interno del file controls.h, tramite costanti personalizzabili dal programmatore.

Di default i tasti associati alla tastiera sono Z e X per i tasti A e B, mentre Maiuscolo destro e Invio sono associati rispettivamente ai tasti Select e Start del gamepad.
Per quanto riguarda il gamepad fisico, i tasti 1 e 2 sono associati ai tasti A e B, mentre 8 e 9 ai tasti Select e Start.

La gestione dell'input avviene nella funzione di sistema inputHandler(), richiamata nel ciclo principale di gioco, la quale provvederà a gestire l'input tramite la funzione più idonea per la periferica di provenienza.
È che ogni interazione con il gamepad virtuale avvenga tramite il puntatore fornito curr_gamepad.

7.1 · Utilizzo del gamepad virtuale

Per gestire l'input all'interno dell'applicativo, ci si affida all'apposito puntatore curr_gamepad, come segue:

#include "controls.h"

if (curr_gamepad->button_A)
{
    //Input continuo senza interruzioni
}
else
{
    //Input terminato
}

if (curr_gamepad->button_A)
{
    //Interrompiamo la ripetizione dell'input
    curr_gamepad->button_A = 0;
}
La struttura gamepad virtuale mantiene lo stato dell'input attraverso le diverse perifiche, permettendo anche di forzarne la stato di premuto, semplicemente azzerando il valore del tasto virtuale corrispondente.
La struttura gamepad virtuale è utilizzata promiscuamente sia da tastiera che gamepad/joystick fisici, anche in simultanea.
Gli input da tastiera verranno gestiti in tempo reale con quelli di periferiche di gioco fisiche, quali per l'appunto gamepad/joystick.

Sono previsti in futuro, meccanismi di mapping dinamico dell'input e per il gioco in multiplayer, in simultanea.
Attualmente per quest'ultimo, si prevede l'alternanza dei due giocatori, interfacciati con le stesse perifiche di input, configurate allo stesso modo.

7.2 · Definizione di cheat

Molti giochi prevedono la presenza di trucchi, o cheat in inglese, che permettono al giocatore di sbloccare extra, avere dei bonus e vantaggi di sorta.
Uno dei cheat più famosi nella storia dei videogiochi è sicuramente il Konami Code, presente in tantissimi giochi retro e non.
RetroGear ne fornisce una semplice implementazione da poter usare nei propri giochi, tramite la funzione konamiCode().

Grazie ad una variabile statica locale, index, la funzione terrà conto della sequenza dei tasti internamente, non richiedendo la delegazione della gestione esternamente.

Probabilmente questa funzione verrà riscritta o eliminata dal progetto

7.3 · Utilizzo del mouse virtuale

La libreria per l'implementazione ed uso del mouse virtuale è mouse.h.
In questa libreria è dichiarata un'apposita struttura, atta a rappresentare un mouse virtuale dotato di soli due pulsanti, destro e sinistro.

Nelle variabili x e y della struttura, vengono salvate le coordinate attuali del puntatore, mentre nelle variabili leftButton e rightButton, le pressioni dei tasti destro e sinistro.
L'utilizzo del mouse virtuale, è identico a quello delle periferiche di input, e l'accesso alla struttura avviene direttamente.
if(Mouse.leftButton)
{
//Tasto sinistro premuto
}
La gestione del mouse al momento è molto semplicista e preliminare, in futuro potrebbero essere implementate e aggiunte funzionalità extra e supporto per la gestione del terzo tasto e della rotellina.

8 · Gestione dei punteggi

RetroGear fornisce al programmatore un sistema apposito per la gestione e rappresentazione dei punteggi, tra cui l'oggetto scoreType, uno sprite sheet per i punteggi in una sequenza standard, ed una variabile enumerativa per il supporto al programmatore in fase di sviluppo.

typedef struct _scoreType
{
  int xstart, ystart;   //Coordinate iniziali
  int speed;            //Velocità di movimento

  Sprite sprite;        //Sprite
  short int status;

  struct _scoreType *next;
} scoreType;

scoreType *scoreHead, *scoreTail;

enum { pts100,
       pts200,
       pts400,
       pts500,
       pts800,
       pts1000,
       pts2000,
       pts4000,
       pts5000,
       pts8000,
       pts1UP,
       pts2UP,
       pts3UP,
       pts4UP,
       pts5UP
    } SCORES;

Gli oggetti scoreType rappresentano graficamente il punteggio nel campo di gioco, si possono creare tramite la funzione createScore, che provvederà ad allocarli in memoria e inserirli in un'apposita lista.

Il ciclo di vita di questi oggetti è delegato alla funzione doScore(), richiamata nello status GAME del motore di gioco. (Si consulti il capitolo Stati di gioco per maggiori informazioni)
void createScore(int x, int y, int speed, int sprite_index);

//Esempio d'uso
createScore( ((int)pobj->x-camera.offsetX),
             ((int)pobj->y-camera.offsetY),
             1,
             pts100);
addScore( 100 );
La funzione addScore() incrementa il punteggio di gioco con il valore specificato.
La funzione createScore realizza un oggetto di tipo scoreType, assegnandone lo sprite sheet standard di default, mostrando solo il fotogramma specificato.

Per aiutare il programmatore nella localizzazione del corretto fotogramma nello sprite sheet dei punteggi, si potrà ricorrere alla variabile enumerativa SCORES, che fornirà valori parlanti per la sua localizzazione.

In futuro, questi due comportamenti potrebbero venire unificati in una sola funzione.

8.1 · Gestione incrementale dei punteggi

Molti giochi offrono la possibilità per il giocatore di incrementare il proprio punteggio di gioco al verificarsi di alcune situazioni ripetute, come ad esempio per un platform game il rimbalzare tra un nemico e l'altro senza toccare terra, oppure per uno shooter game la distruzione consecutiva di una serie di nemici.

Questi punteggi non sempre sono multipli di quelli precedenti, e quindi non sempre possono vantare una linearità nel calcolo, per questo motivo il motore di gioco mette a disposizione un pratico array con una sequenza standard di punteggio, e funzioni per la gestione automatica di questi casi speciali, basati sulla sequenza offerta dallo sprite sheet standard dei punteggi.

createScore( ((int)pobj->x-camera.offsetX),
             ((int)pobj->y-camera.offsetY),
             1,
             points_index);
addScore( getScore() );
La sequenza dei punteggi ottenibili è dichiarata nell'array privato points all'interno di score.c.

static int points[] = {100, 200, 400, 500, 800, 1000, 2000, 4000, 5000, 8000};

La funzione getScore(), provvede a ritornare il valore di punteggio attualmente puntato dall'indice globale points_index, oltre che incrementarlo sino alla soglia 8000 punti, oltre la quale vengono assegnate un numero di vite extra al giocatore, da un minimo di 1 ad un massimo di 5.
Raggiunto il numero massimo di vite extra, pts5UP, l'indice points_index non viene più incrementato, dovrà essere premura del programmatore, effettuare il reset manuale dell'indice definendo le proprie logiche.
int getScore()
{
    if(points_index>=pts1UP)
    {
        if(points_index>=pts5UP)
        {
            points_index = pts5UP;
        }

        // 1up...
        playerExtraLife(points_index);
    }
    else if(points_index<=pts8000)
    {
        //Per punteggi normali, ritorniamo il valore di punteggio
        return points[points_index++];
    }
	
	return 0;
}

9 · Gestione dei livelli

La gestione dei livelli di gioco, avviene tramite la libreria level, la quale fornisce un'apposita struttura per la gestione e rappresentazione dei livelli in genere, oltre che funzionalità per il caricamento/salvataggio di essi, in appositi file.

La struttura standard per la rappresentazione del livello, è la struttura Level, nella quale risiede la variabile map, una matrice formata da 300 colonne, 300 righe su 3 strati (layers), pensata per la rappresentazione grafica del livello.
La struttura di livello può essere popolata manualmente da codice o tramite appositi file di testo con estensione .map, di default presenti e caricati dalla cartella maps.
level title
level description
tile_sheet
backgroun_music.wav
0,255,255
2
7,7
0,0,0,0,0,0,0,
0,2,0,0,0,0,0,
0,0,0,0,0,0,0,
0,0,0,0,0,0,0,
0,0,0,0,0,0,0,
0,0,0,0,0,3,0,
0,0,0,0,0,0,0,

2,2,2,2,2,2,2,
2,2,2,2,2,1,2,
2,2,1,1,2,1,2,
2,2,1,1,1,1,2,
2,1,1,1,1,1,2,
2,1,1,1,1,7,2,
2,2,2,2,2,2,2,
Ogni riga del file si interfaccia con un membro specifico della struttura del livello:

RigaMembro strutturaDescrizione
1char name[20]Titolo del livello
2char description[20]Descrizione del livello
3char theme[5]Tema grafico da utilizzare
4char song_title[10]Nome del file audio di sottofondo
5int bkgd_red, bkgd_green, bkgd_blueValore RGB del colore di sfondo
6unsigned int num_layersNumero di layer utilizzati dal livello
7unsigned int cols, rowsNumero di righe e colonne utilizzate dal livello (massimo 300x300)

L'utilizzo dei layers è spiegato nel dettaglio al capitolo 9-3.

9.2 · Caricamento di un livello da file

I file di livello hanno estensione .map e devono risiedere sotto cartella maps, e possono essere caricati tramite la funzione: loadLevel(Level* plevel, char *filename).
Questa funzioine accetta in argomento una struttura Level da popolare e il nome del file di livello, senza estensione.
    loadLevel(&level, "main");
Il motore di gioco in fase di inizializzazione e avvio, ricerca e carica di default il file main.map attraverso la funzione init().
Si consulti il capitolo 2.1 · Per cominciare, per maggiori informazioni.

La creazione di eventuali entità, verrà gestita dalla funzione createEntityFromMap(), richiamata automaticamente all'interno di loadLevel(), per ogni valore diverso da 0 presente nel SOLID_LAYER.

Si consulti il capitolo 4.2 · Definizione di una entità per maggiori informazioni.
void createEntityFromMap(int id, int x, int y)
{
    col = tilesToPixels(col);
    row = tilesToPixels(row);
	
	switch(id)
	{
		case PLAYER_ID:
			setPlayerPosition(x, y);
		break;

		case 4:
			badguy_create(x, y);
		break;

		case 5:
			coin_create(x, y);
		break;
	}
}
La funzione createEntityFromMap() di default viene proposta con la gestione della sola entità Player, ogni altra entità dovrà essere definita manualmente dal programmatore, richiamando l'apposito costruttore.

Il sistema prevede 3 valori riservati nel layer 0, per cui al programmatore sarà richiesto partire dal valore 4 per l'assegnazione di entit´ personalizzate.

ValoreValenza
1Posizione occupata da un ostacolo solido
2La posizione di partenza del giocatore.
3Posizione occupata da un Evento di transizione, utilizzato per il passaggio ad altri livelli

Questa funzione in futuro sarà estesa con una versione custom, richiamata tramite puntatore, per separare i contenuti del sistema centrale da quelli definiti dall'utente.

9.3 · Disegno di un livello

Il disegno del livello è delegato alla funzione drawTileMap(Level *plevel), la quale si occuperà di rappresentare graficamente gli strati BACKG_LAYER e ALPHA_LAYER.

Questa funzione fa uso del sistema interno Camera, per disegnare solo la porzione attualmente visibile del livello.
In caso di scrolling attivo, la porzione è determinata dalla posizione del giocatore all'interno del livello, in alternativa sarà determinata dal numero massimo di tile visualizzabili nella risoluzione della finestra.
Si veda il capitolo Gestione delle visuali per maggiori informazioni.

Il programmatore tenga a mente, che anche giochi privi di scorrimento, necessitano la corretta inizializzazione del sottosistema interno Camera, all'interno della funzione init().
Si veda il capitolo 2.1 · Per cominciare per maggiori informazioni.

L'utilizzo del sottosistema interno Camera potrà essere reso opzionale


Per convenzione, i tiles dei livelli, adottano uno standard di rappresentazione basato su di una mappa fotogrammi, come per gli sprite delle entità:


In questa rappresentazione minimale di un tileset tipo RPG, si nota come ogni posizione numerata del tilset, corrisponda ad una determinato valore nella matrice del file di livello.

10 · Gestione delle collisioni

Per gestire l'interazione tra giocatore, entità di gioco e livelli, è a disposizione una libreria dedicata, collision.c.
La libreria presenta una funzionalità specifica per vari tipi di collisioni, tra cui:
In futuro potrebbero essere implementate altre tipologie di collisione

10.1 · Rect Collision

La rect collision, è una collisione basata sulla sovrapposizione di due aree rettangolari (rect) di dimensioni variabili, calcolando eventuali punti di intersezione tra le coordinate di essi.

Il calcolo viene effettuato dalla funzione rectCollision(), che accetta in argomento una coppia di coordinate e dimensioni:
if(rectCollision(Player.x, Player.y, Player.w, Player.h,
		 pobj->x, pobj->y, pobj->w, pobj->h))
{
	//Un qualche tipo di azione...
}
	
La funzione esegue un semplice controllo sulle coordinate dei rettangoli, nel caso in cui le coordinate rappresentino una intersezione tra i due rettangoli che rappresentano, si ha una collisione, come rappresentato nell'immagine sottostante.
In caso di collissione viene ritornato il valore 1, altrimenti 0.

10.2 · Tile Collision

La tile collision, è una collisione tra entità di gioco e tiles.
Essa si basa sul calcolo della distanza in pixel tra le coordinate di un'entità e la posizione dei tiles, considerate in pixel a loro volta.

La funzione tileCollision(), si occupa della gestione di questa tipologia di collisioni.
Essa accetta in argomento un puntatore ad entità e le coordinate presso cui controllare eventuali collisioni.



RG_Point *point = NULL;     //Coordinate della collisione

if(curr_player->hspeed<0)
{
    //Collide a sinistra?
    point = tileCollision(pobj, floorf(pobj->x+pobj->hspeed), pobj->y);
    if( point != NULL )
    {
        pobj->x= point->x+TILESIZE;      //Posizioniamo la entità accanto al tile
    }
    else
    {
        pobj->x += pobj->hspeed;         //Muoviamo la entità
    }
}
    //Collide a destra?
else if(pobj->hspeed>0)
{
    point = tileCollision(pobj, ceilf(pobj->x+pobj->hspeed), pobj->y);
    if( point != NULL )
    {
        pobj->x= point->x-pobj->w;
    }
    else
    {
        pobj->x += pobj->hspeed;
    }
}
	
Nell'esempio viene gestito il caso di movimento orizzontale di una entità, la quale potrà muoversi liberamente in caso di assenza di collisioni nella direzione di movimento, a velocità variabile, in caso contrario ritrovarsi bloccata davanti ad un ostacolo del livello.

Eventuali collisioni vengono controllate dal punto di partenza della entità, sino al suo punto di arrivo, rappresentati in immagine dalla linea rossa.
Nel caso in cui vi sia tra il punto di partenza e il punto di arrivo, sia presente un ostacolo, l'entità verrà riposizionata il più vicino possibile al tile, evitando che l'ostacolo venga superato.



La posizione in pixel dei tiles di livello, viene calcolata automaticamente dalla funzione.
In caso di collisione, viene ritornata una struttura RG_Point, rappresentate le coordinate presso cui è stata rilevata la collisione, oppure null in caso di mancata collisione.

11 · Gestione delle visuali

La gestione dello scrolling nei livelli di gioco, è delegata alla variabile camera, di tipo RG_Rect, presente nella libreria camera.c.
Parte integrante del motore di gioco, è istanziata in maniera statica all'interno del motore di gioco, ed è utilizzata di default dalla funzione di disegno del livello.

#define CENTER_X ((SCREEN_WIDTH - TILESIZE) / 2)
#define CENTER_Y ((SCREEN_HEIGHT - TILESIZE) / 2)

RG_Rect camera;
Questa struttura tiene traccia dell'area attualmente visibile al giocatore nella finestra di gioco, permettendo la rappresentazione parziale del livello sullo schermo, limitandola alle sole parti specificate dal programmatore.
Queste coordinate, dette offset, possono essere impostate ai valori di posizione di un'entità di gioco, tipo Player ad esempio, o a valori interi di altra natura.

La variabile Camera è utilizzata anche nel caso in cui il gioco non presenti livelli scrollabili, mostrando al giocatore solo la porzione iniziale del livello, pari alla grandezza della finestra del programma.
Si veda il capitolo 10.1 · Disegno del livello per maggiori informazioni.



Lo scrolling del livello di gioco viene gestito dalle funzioni scrollCameraX(RG_Rect *pcam, int x) e scrollCameraY(RG_Rect *pcam, int y), che rispettivamente gestiranno in maniera indipendente lo scrolling orizzontale e verticale.
Entrambe le funzioni accettano come argomenti un puntatore ad una struttura di tipo RG_Rect (come la variabile camera fornita dal sistema ad esempio), ed un valore intero su cui basare lo spostamento della visuale.

Lo spostamento della visuale avviene al raggiungimento della metà dello schermo, definito dalle costanti CENTER_X e CENTER_Y, da parte del valore intero di coordinata specificato.
// Aggiornamento posizione Camera X/Y
pcam->x += (x - pcam->x - CENTER_X);
pcam->y += (y - pcam->y - CENTER_Y);

// Aggiornamento posizione Camera max X/Y
pcam->x = (TILESIZE*curr_level->cols - SCREEN_WIDTH);
pcam->y = (TILESIZE*curr_level->rows - SCREEN_HEIGHT);
I valori di offset saranno sempre aggiornati rispetto al valore di intero passato in argomento alla funzione di scrolling.
Il valore di offset di inizio sarà pari alla posizione di scrolling, meno il valore di costante metà schermo.
Il valore di offset massimo sarà pari alla lunghezza della mappa, meno la dimensione della finestra.

11.1 · Scrolling libero

Lo scrolling di default è limitato alle dimensioni del livello, per tanto raggiungendo i limiti orizzontali e verticali di esso, non si avrà piú alcun spostamento nel suo disegno.
Tuttavia può esservi la necessità in alcuni casi di svincolarsi da questo limite, per tanto il sistema interno di Camera prevede la possibilità di eliminare questo vincolo semplicemente dichiarando la costante NO_SCROLL_BOUND nel file config.h.



11.2 · Scrolling pigro

É possibile ritardare lo scorrimento sul livello, tramite la dichiariazione della costante LAZY_SCROLL nel file config.h.
///Abilita lo scrolling "pigro"
#define LAZY_SCROLL
	
Questo effetto, ritarderà lo scorrimento del livello rispetto alle coordinate di riferimento per l'aggiornamento.

Attualmente l'implementazione dello scrolling "pigro" non è stata testata a sufficienza, non si hanno dati certi su eventuali esiti sul programma nel lungo termine.

11.3 · Monitoraggio dell'area di scrolling

Il sottosistema Camera, fornisce anche funzionalità di "monitoraggio" dello schermo, permettendo tramite la funzione isInCamera(RG_rect *pcam, RG_rect area), di controllare la presenza di un qualche tipo di oggetto all'interno degli offset di camera.

Il controllo è basato su di una semplice Rect Collion tra la variabile camera ed una RG_rect, determinante l'area occupata da un elemento.
Da questa funzione dipendono le funzioni di libreria createEntity(), countEntityOnCamera(), presenti nel file entity.c.

12 · Gestione dei timer

La gestione del tempo all'interno di RetroGear, è vincolata al tempo di esecuzione del ciclo di gioco principale.

Sono a disposizione del programmatore funzionalità per il calcolo del tempo passato, rispetto ad un determinato momento.

Le funzioni get_time_elapsed(int time) e get_time_elapsed_ms(int time), calcolano e ritornano, rispettivamente in secondi e millisecondi, la differenza di tempo passato rispetto ad un determinato momento, passato in argomento.

pobj->timer[0] = fps.t;

if(get_time_elapsed(pobj->timer[0]) >= 5)
{
    //Sono passati 5 secondi
}

if(get_time_elapsed(pobj->timer[0]) >= 5000)
{
    //Sono passati 5 secondi
}
	
Il valore passato in argomento, sarà il timestamp di un determinato momento all'interno del gioco, che potrà essere il tempo di clock calcolato dal ciclo di gioco (fps.t).

Il ciclo di gioco principale, cercando di mantenere l'esecuzione del programma costante, garantisce un livello di errore relativamente basso nel calcolo del tempo, anche su diversi hardware.

Le libreria SDL fornisce internamente dei timer specifici e più elaborati, per ogni eventuale necessità consultate la documentazione ufficiale della libreria.

13 · Gestione degli eventi di gioco

La gestione degli eventi di gioco in RetroGear, è gestita attraverso apposite strutture chiamate Event, le quali si occuperanno di fornire o registrare, informazioni utili alle logiche di gioco dinamiche.

typedef enum
{
    WARP_EVT, DIALOGUE_EVT, OBJECT_EVT, GENERIC_EVT
} EVENT_TYPE;

typedef struct _Event {
	char mapname[20];
	unsigned int evt_x, evt_y;
	char parameters[100];
	int flag_save;
	struct _Event *next;
} Event;

Event *eventsGeneric,   //Lista di eventi generica
      *eventsWarp;      //Lista di eventi warp
La struttura tipo di un evento di gioco, presenta le seguenti informazioni:
Ogni evento sarà identificato in maniera univoca dalle proprie coordinate, permettendo al programmatore di recuperare un evento specifico per la posizione attuale di un'entità o di livello.
Le informazioni in esso contenute, rappresentate dalla stringa dei parametri, potranno essere utilizzate dalle entità di gioco, per modificare le proprie logiche interne in maniera dinamica, gestire eventuali punti di transizione tra livelli, o come semplici registri di valori da mantenersi durante il gioco in maniera dinamica.

Tutte le liste degli eventi ad ogni caricamento di livello, saranno azzerate, tralasciando solo i singoli eventi impostati come permanenti. Ogni tipologia di evento, sarà incluso in una propria lista dedicata, rappresentata da apposito puntatore di tipo Event, tra cui *eventsGeneric ed *eventsWarp.

13.2 · Tipologie di evento

Tra le tipologie di evento attualmente disponibili e definibili dal programmatore vi sono le seguenti:

TipologiaIdentificativoDescrizione
GenericGENERIC_EVTEventi generici, informazioni di qualsiasi natura non specifica.
WarpWARP_EVTEventi di transizione, informazioni specifiche per il passaggio da un livello ad un altro
DialogDIALOG_EVTEventi di dialogo, informazioni riguardo i dialoghi tra giocatore e entità di gioco

Queste valori enumerati, saranno utilizzati internamente dal motore di gioco per il caricamento automatico degli eventi dai file appositi, nelle liste appropriate.
La gestione su liste separate, è pensata per limitare l'uso delle risorse e i tempi di lettura di liste molto lunghe.

Sono previste anche funzioni di gestione standard di alcune tipologie di eventi, come doWarp() e doDialog(), che implementeranno una logica standard, rispettivamente per la transizione da un livello ad un altro, e apertura di finestre di dialogo.

13.3 · Definizione di un evento generico

Per definire un evento all'interno del gioco, si può ricorrere alla funzione addEvent(), la quale accetta in argomento i seguenti parametri:
Un esempio di utilizzo è il seguente:
void playerThink()
{
    int col, row;
    //Posizione attuale in tile
    col = pixelsToTiles( Player.x );
    row = pixelsToTiles( Player.y );

    //Controlliamo la presenza di un punto di transizione
    if(curr_level->map[SOLID_LAYER][row][col] == WARP)
    {
        //Richiediamo l'evento, se presente, il puntatore sarà diverso da NULL
        Event *warp = getEvent(eventsWarp, curr_level->name, col, row);
        
        if(warp)
        {
            unsigned int dest_x, dest_y;
            char dest_map[10];

            /**
             * Il parametro dell'evento è nel formato "char*,int,int"
             * La stringa "%[^,]" significa "Leggi sino a che non incontri una virgola"
             **/
            sscanf(warp->parameters, "%[^','],%d,%d", dest_map, &dest_x, &dest_y);

            //Eseguiamo azioni di routine e facciamo uso dei parametri recuperati
            cleanEntities();
            loadLevel(curr_level, dest_map);
            initCamera(&camera);
            setPlayerPosition(dest_x, dest_y);
        }
    }

    think = 1;
}
Nell'esempio sovrastante, si gestisce la situazione in cui il giocatore abbia raggiunto un punto di transizione, si recuperano dall'evento apposito, il nome della mappa di destinazione e le coordinate a cui si vuole che il giocatore sia piazzato nel nuovo livello.

In maniera analoga, si potrebbero registrare e tenere traccia di eventuali azioni compiute dal giocatore nei vari livelli, come ad esempio la raccolta di oggetti che non dovranno ripresentarsi al ritorno nel livello stesso, l'apertura di porte o passaggi che dovranno essere mantenuti durante l'avventura e via dicendo.
Sarà a discrezione del programmatore farne l'utilizzo più congeniale alle sue esigenze.

13.4 · Definizione di un evento di warp

Questo capitolo attualmente è una bozza in attesa di revisione

Gli eventi warp, sono utilizzati per il passaggio da un livello ad un altro.

Questo tipo di eventi viene caricato in un'apposita lista di tipo Event, nominata eventsWarp, al momento del caricamento del file di livello, da appositi file .evt.

Il formato del parametro per questi eventi, per convenzione dovrà essere nella forma nome mappa,x,y, per essere compatibile con la funzione di libreria dedicata all'evento.

Ogni evento presente in questa lista, terrà traccia dei punti di transizione da un livello ad un altro, fornendo il nome del livello da raggiungere (nome del file senza estensione) e relative coordinate a cui posizionare il giocatore nella nuova destinazione.

Esempio: 3,4,underground,1,2

Nell'esempio sovrastante, il punto di transizione si trova al tile riga 4, colonna 3 il giocatore sarà posizionato al tile riga 2, colonna 1 nella mappa di destinazione, chiamata underground.

I punti di warp all'interno dei livelli, saranno caratterizzati dal valore univoco e riservato, 3, definito dalla costante WARP_TILE, e posizionati sul layer SOLID del livello stesso.

Gli eventi di warp potranno essere gestiti in maniera standard tramite la funzione di libreria doWarp(), presente in level.c, la quale accetterà in argomento i parametri char *mapname, unsigned int col e unsigned int row, implementando una logica standard di transizione da un livello ad un altro, con tanto di effetti sonori e grafici.

La funzione doWarp(), una voltra trovato un evento valido in lista, si occupa di:
  1. Eseguire un effetto grafico di transizione ed un effetto sonoro, entrambi impostati di default
  2. Ripulire la lista delle entità allocate
  3. Caricare il nuovo file di livello
  4. Inizializzare l'oggetto Camera
  5. Impostare le coordinate del giocatore nel nuovo livello
La funzione doWarp() non viene richiamata in automatico dal sistema, dovrà essere premura del programmatore richiamarla qual'ora ne vorrà fare uso, in alternativa, si può scrivere una propria versione del gestore dell'evento come nell'esempio seguente:

void playerThink()
{
    int col, row;
    //Posizione attuale in tile
    col = pixelsToTiles( Player.x );
    row = pixelsToTiles( Player.y );

    //Controlliamo la presenza di un punto di transizione
    if(curr_level->map[SOLID_LAYER][row][col] == WARP)
    {
        //Richiediamo l'evento, se presente, il puntatore sarà diverso da NULL
        Event *warp = getEvent(eventsWarp, curr_level->name, col, row);
        
        if(warp)
        {
            unsigned int dest_x, dest_y;
            char dest_map[10];

            /**
             * Il parametro dell'evento è nel formato "char*,int,int"
             * La stringa "%[^,]" significa "Leggi sino a che non incontri una virgola"
             **/
            sscanf(warp->parameters, "%[^','],%d,%d", dest_map, &dest_x, &dest_y);

            //Eseguiamo azioni di routine e facciamo uso dei parametri recuperati
            cleanEntities();
            loadLevel(curr_level, dest_map);
            initCamera(&camera);
            setPlayerPosition(dest_x, dest_y);
        }
    }

    think = 1;
}
Nell'esempio sovrastante, si gestisce la situazione in cui il giocatore abbia raggiunto un punto di transizione, si recuperano dall'evento apposito il nome della mappa di destinazione e le coordinate a cui si vuole che il giocatore sia piazzato nel nuovo livello.

13.5 · Definizione di un evento di dialogo

Questo capitolo attualmente è una bozza in attesa di revisione

Gli eventi di dialog, sono utilizzati per la rappresentazione di dialoghi nel gioco, associabili a determinate azioni, eventi di mappa o interazione con personaggi non utilizzabili del gioco, come accade nei giochi di tipo RPG.

Anche se descritta, la gestione statica degli eventi di dialogo al momento è in fase di progettazione e sviluppo, per tanto non ancora presente nel codice attuale del progetto.

Questo tipo di eventi può essere caricato staticamente in un'apposita lista di tipo Event, nominata eventsDialog, al momento del caricamento di un livello, da appositi file .dlg posti nella cartella dlg.
Importante che il nome del file sia lo stesso del nome del file di livello, per effettuare il caricamento automatico.

Il formato del parametro per questi eventi, per convenzione dovrà essere nella forma x,y,Testo del dialogo, per essere compatibile con la funzione di libreria dedicata all'evento.
Nel testo del dialogo potranno essere inseriti anche i caratteri \n e \f (letteralmente come stringhe a due caratteri), che rispettivamente verranno gestiti come nuova riga e nuova pagina.
Quest'ultimo pulirà la finestra di dialogo e riporterà il cursore all'inizio.

Esempio: 3,4,Welcome to\nRetrogear\nKingdom!


Per la rappresentazione grafica, viene fatto uso del sistema interno Typewriter, il quale all'occorrenza può essere esteso con il sottosistema Menu per generare scelte multiple selezionabili, da associabile ai dialoghi.
E' presente una libreria dedicata per la gestione di questo tipo di eventi, il file dialog.c.

I dialoghi possono essere eseguiti sia caricandoli da file in tempo reale, sia tramite apposita lista statica. Un esempio di uso del sistema di dialogo caricato da file:
static void onCollision(Entity *pobj)
{
    /**
     * Interact with the player
     * 
     * Preventing the Player walk on us
     * Handle talking request
     **/
    if(rectCollision(curr_player->x+curr_player->direction_x, curr_player->y+curr_player->direction_y,
                     curr_player->w+curr_player->direction_x, curr_player->h+curr_player->direction_y, 
                     pobj->x, pobj->y, pobj->w, pobj->h))
    {
        if(!flag_onDialog && (curr_gamepad->button_A) )
        {
            initDialog_from_file(pobj->x, pobj->y, curr_level->name);
        }
    }
}
In questo caso il giocatore ha una collisione con un NPC, se non vi sono dialoghi attivi, segnalati dalla variabile interna al sistema flag_onDialog ed il giocatore preme il stato A del gamepad virtuale, il dialogo viene caricato per la posizione attuale del NPC e per la mappa corrente.
Le azioni descritte qui di seguito sono ancora una bozza in fase di progettazione, nonostate il codice descritto sia presente, potrebbe cambiare in futuro.

In maniera analoga ma tramite funzione initDialog(char text[], Menu *pmenu), si potrà inizializzare e mostrare il dialogo al giocatore.
if(!flag_onDialog && (curr_gamepad->button_A) )
{
    char text[] = "Hello world!";
    initDialog(text, NULL);
}
La funzione accetta una stringa di testo, che potrà essere prelevata anche dalla lista eventsDialog

14 · Menú di gioco

Una parte importante dell'interazione con un gioco, è rilegata a menú di sorta, specialmente nel caso di giochi RPG, in cui questa interazione è il cuore di quasi tutto l'intero gameplay.

RetroGear mette a disposizione del programmatore strutture ideonee alla rappresentazione di menú e relativi contenuti, oltre che funzionalità specifiche per la creazione, gestione e interazione con essi, tra cui:

I menú di gioco all'interno di RetroGear sono composti da due strutture, Menu che si occupa di definire e rappresentare il menú vero e proprio, ed Item che si occupa di rappresentare i contenuti del menú, ovvero le voci selezionabili.
typedef struct _Menu {
	int flag_active, flag_title, flag_border;
	int x, y;
	unsigned int w, h;
	char name[20];
	char cursor;
	int rows, cols;
	unsigned int page_start;
	int num_items, max_items;
	int curr_item;
	SDL_Color color;
	struct _Item *items;
	struct _Menu *previous, *next;
	void (*update)();
	void (*draw)();
} Menu;

typedef struct _Item
{
	int flag_used;
	int x, y;
	char name[15];
	//~ char *description;
	int	value;
	SDL_Color color;
	void (*func)();
	//Icon stuff
	//~ SDL_Surface *icon;
	//~ float frame_index, animation_speed;
} Item;

Menu main_menu, *curr_menu;
L'astrazione del menú su due strutture diverse, è pensata per permettere di avere maggiore flessibilità nella realizzazione dei menù di gioco, oltre che ad avere la possibilità di poter anche intercambiare i contenuti tra menù, usando una sola struttura di menù e più strutture Item, o di realizzare menù personalizzati sulla base delle proprie esigenze.

Supponendo che un qualsiasi gioco tipo, abbia bisogno di almeno un menù di gioco, magari nella schermata dei titoli, RetroGear fornisce fin da subito una variabile di tipo Menu chiamata main_menu e popolata di default nella funzione mainInit del file main.c.
Questo menù presenterà le sole voci New Game e Quit, impostate di default rispettivamente per avviare lo status GAME o terminare l'esecuzione del programma.

Per agevolare la gestione dei menù, specie di quello attualmente in uso, in qualsiasi punto del programma, è presente un pratico puntatore a strutture Menu chiamato curr_menu.
L'intento di questo puntatore è quello di agevolare il programmatore nel passaggio da un menù ad un altro, utilizzando solo una variabile comune ma valorizzata con indirizzi di menù diversi.
Il motore prenderà carico del passaggio da menù a menù, nel caso di menù collegati, valorizzando automaticamente questa variabile, ma il programmatore potrà tranquillamente valorizzare questa variabile in un qualunque momento sulla base di una propria logica.

La struttura Item è pensata per poter fornire in futuro anche la possibilità di mostrare icone (anche animate) e descrizioni per ogni singola voce di menú, ma attualmente le funzioni standard di gestione e disegno non implementano queste funzionalità, che saranno aggiunte in futuro.

15.1 · Tipologie di menú

I menú disponibili in RetroGear, sono pensati per soddisfare tutte le principali necessità dello sviluppatore nella realizzazione del proprio gioco.
Tra le tipologie di menú realizzabili in RetroGear troviamo:

15.2 · Struttura di un menú

La struttura per i menú di gioco è definita nel file menu.h e presenta le seguenti variabili:

15.3 · Struttura dei contenuti del menú (Item)

La struttura per i contenuti dei menú di gioco è definita nel file menu.h e presenta le seguenti variabili: