
Guida per lo sviluppatore
Tabella dei contenuti:
- 1 · introduzione
- 2 · Struttura del progetto
- 3 · Gestione della grafica
- 4 · Entità di gioco
- 4.1 · Gestione delle entità di gioco
- 4.2 · Definizione di una entità
- 4.3 · Tipologie e stati di una entità
- 4.4 · Animare le entità
- 4.5 · Rimozione di una entità
- 4.6 · L'entità Player
- 5 · Gestione dei font
- 6 · Gestione del sonoro
- 6.1 · Stati di gioco e sonoro
- 6.2 · Caricare ed eseguire effetti sonori e musiche
- 6.3 · Pulizia delle risorse sonore allocate
- 7 · Gestione dell'input
- 8 · Gestione dei punteggi
- 9 · Gestione dei livelli
- 10 · Gestione delle collisioni
- 11 · Gestione delle visuali
- 12 · Gestione dei timer
- 13 · Gestione degli eventi di gioco
- 13.1 · Tipologie di evento
- 13.2 · Definizione di un evento generico
- 13.3 · Definizione di un evento di warp
- 13.4 · Definizione di un evento di dialogo
- 14 · Menú di gioco
- 14.1 · Tipologie di menú
- 14.2 · Struttura di un menú
- 14.3 · Struttura dei contenuti del menú (Item)
- 14.4 · Definizione di un oggetto Menú
- 14.5 · Gestione dei menú
- 14.6 · Aggiunta dinamica di elementi al menú
- 14.7 · Aggiunta statica di elementi al menú
- 14.8 · Eliminazione di un Menú
- 15 · Gestione degli FPS
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:
MENU
Si occupa di gestire lo schermo dei titoli, ed eventuali menù di gioco.
PREGAME
Schermata di pre-gioco, momento in cui si potranno mostrare al giocatore informazioni sommarie come numero del livello, vite disponibili ed altro, un po' come nei giochi di Super Mario Bros.
La funzione viene presentata con un timer interno per gestire in modo automatico il passaggio allo status GAME.GAME
Il gioco vero e proprio, qui dentro verrano gestite tutte le logiche inerenti al gioco, definite dal programmatore.
In generale gestisce tutte le singole entità di gioco attive presenti in lista, gestisce gli input del giocatore e aggiorna lo stato del giocatore, oltre che all'occorrenza gestire anche entità di punteggio o quant'altro si voglia. (Si veda il capitolo riguardante le Entità per maggiori informazioni)LOST
Gestisce il momento in cui il giocatore perde una vita, ad esempio quando entra in contatto con un nemico del livello.
Lo status è pensato per agevolare lo sviluppatore nella realizzazione di logiche particolari per questi momenti, senza dover intasare lo status di GAME con controlli a cascata.
Ovviamente il tutto è a discapito del programmatore.WIN
Il momento in cui il giocatore vince, pensato per eseguire in modo pulito e distaccato della logica da eseguire a vittoria avvenuta del giocatore.
Una sorta di schermo di fine gioco per intenderci, e come per il caso di LOST, agevolare il programmatore permettendogli di scrivere codice pulito e stratificato.GAMEOVER
Gestione dello status di game over, in cui sarà possibile gestire in modo pulito e distaccato, tutta la logica relativa alla fase di game over, come ad esempio ripulire tutte le risorse allocate, reimpostare i valori interni del motore, azzerare punteggi e quant'altro si voglia.
EDITOR
Gestisce l'editor di livello e relativi eventi.
In alternativa, si consiglia l'uso dell'editor ufficiale del progetto, Leveler.
Per agevolare il programmatore, alcuni status di gioco sono muniti di un piccolo controllo al loro interno.
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.
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 disegnoNella 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à:
- get_pixel
Ritorna il valore di un pixel presente alla coordinate specificate nella superficie specificata, in modo sicuro e badando al numero di byte utilizzati nella superficie.
- put_pixel
Disegna un pixel alle coordinate specificate sulla superifice specificata, in modo sicuro e badando al numero di byte utilizzati nella superficie.
- replaceColor
Rimpiazza un colore specifico (target) con uno un rimpiazzo (replecement) all'interno di una determinata superficie pixel per pixel.
- drawFillRect
Disegna un rettangolo delle dimensioni specificate sulla superficie principale di sistema
- drawRect
Disegna un rettangolo dall'interno vuoto e delle dimensioni specificate sulla superficie principale di sistema
- drawGui
Disegna una semplice cornice utilizzando i font di sistema, utile per finestre di dialogo o menù di gioco. - loadImage
Carica un'immagine BMP specifica in una superficie e ne converte il formato in quello attuale dello schermo per un disegno più rapido
- getSprite
Preleva un fotogramma da uno sprite sheet all'indice indicato, di dimensioni specifiche e lo mostra alle coordinate volute sulla superficie dello schermo
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_Surface *surface
Puntatore alla superficie grafica in cui verrà contenuta l'immagine dello sprite -
int x, y, w, h
Coordinate a cui verrà disegnato lo sprites.
Possono essere quelle di una entità, o libere, per una rappresentazione di immagini svincolate dagli oggetti di gioco.
int w, h
Dimensioni dell'immagine da rappresentare.
Nel caso di entità, saranno la dimensione del singolo fotogramma dell'entità.
int index
Indice numerico del fotogramma attualmente visualizzato.
float animation_timer
Timer utilizzato per l'avanzamento delle animazioni.
float animation_speed
Velocità dell'animazione, questo valore incrementa la variabile animation_timer.
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:
- int type
Tipologia della entità, come ad esempio nemico, bonus o power-up.
L'associazione numerica Valore/Tipologia è rilegata alla variabile enumerativa OBJECTS, che dona maggiore leggibilità al codice, oltre che uno strumento comune a tutte le entità di gioco.
Trova una sua utilità nei momenti in cui sia necessario controllare eventuali collisioni con determinate tipologie di oggetti, per scatenare particolari eventi.
Esempio:
Collisione Player <-> Entità COLLECTABLE -> Aumenta il punteggio del giocatore di 100 punti.
Collisione Player <-> Entità ENEMY -> Decrementa il numero di vite del giocatore.
Per maggiori informazioni riguardo la variabile OBJECTS, si consulti il capitolo 3.3 · Tipologie e stati di una entità -
int solid
Flag che specifica se la entità è da considerarsi oggetto solido o meno.
Gli oggetti solidi sono da considerarsi come ostacoli per il giocatore, oggetti impossibili da attraversare.
Utile per gestire quei tipi di oggetti piazzati come ostacoli provvisori sul campo di gioco, e distruttibili tramite l'esecuzione di particolari azioni. -
int x, int y, int w, int h
Rispettivamente, le coordinate x, y, e relativa larghezza ed altezza della entità. -
int lives
Tiene conto del numero di vite disponibili della entità, da poter utilizzare come energia della entità o numero di possibilità disponibili per il giocatore, a partita. -
RG_Sprite sprite
Puntatore ad una struttura di tipo RG_Sprite, che rappresenta l'immagine dell'entità nella finestra di gioco. Si tenga conto che si tratta di un semplice puntatore a struttura, e che la sua valorizzazione avviene tramite apposita funzione initSprite(). -
float speed
La velocità dell'entità nel campo di gioco, ovvero il numero di pixel che avremo per spostamento ad ogni ciclo. -
float gravity
Gravità esercitata sull'entità, questo valore agirà sull'entità di gioco come forza motrice verso il basso. - void (*update)()
Puntatore alla funzione di aggiornamento dell'entità.
Il puntatore contiene solamente l'indirizzo di memoria di una funzione precedentemente dichiarata dal programmatore, con la logica d'azione dell'entità.
Si veda il capitolo apposito per maggiori informazioni.
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.
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(); #endifIl 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à
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.
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.0fLa 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);
- int dest_x, int dest_y
Destinazione in cui si andrà a copiare il contenuto di origine -
int asciicode
Il valore decimale del singolo carattere -
SDL_Color color
Struttura contenente i valori RGB di colore da usare per la superficie.
Essa si appoggia alla funzione drawChar(), richiamandola per ogni singolo carattere della stringa da disegnare e passandogli i parametri necessari.
Accetta i seguenti argomenti:
-
int dest_x, int dest_y
-
char *text
La stringa da disegnare nella superficie video specificata. -
SDL_Color color
Il colore da utilizzare per il testo.
Il parametro viene gestito dalla funzione drawChar(). -
int alpha
Flag per l'attivazione della trasparenza nello sfondo dei carattteri.
Il parametro viene gestito dalla funzione drawChar().
Rispettivamente, le coordinate x, y a cui andare a disegnare il testo
//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:
- collectable_snd - Il suono di default per gli oggetti di tipo COLLECTABLE.
- stomp_snd - Il suono di default per l'azione/status STOMP di una entità.
- action_snd - Il suono di default da eseguire per lo status ACTION di una entità.
- extralife_snd - Il suono di default da eseguire per il conseguimento di una vita extra.
- title_music - Musica da eseguire nello status di gioco MENU.
- pregame_music - Musica da eseguire nello status di gioco PREGAME.
- game_music - Musica da eseguire nello status di gioco GAME.
- gameover_music - Musica da eseguire nello status di gioco LOST.
- goal_music - Musica da eseguire nello status di gioco WIN.
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 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.
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.
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:
Riga | Membro struttura | Descrizione |
---|---|---|
1 | char name[20] | Titolo del livello | 2 | char description[20] | Descrizione del livello |
3 | char theme[5] | Tema grafico da utilizzare |
4 | char song_title[10] | Nome del file audio di sottofondo |
5 | int bkgd_red, bkgd_green, bkgd_blue | Valore RGB del colore di sfondo |
6 | unsigned int num_layers | Numero di layer utilizzati dal livello |
7 | unsigned int cols, rows | Numero 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.
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.
Valore | Valenza |
---|---|
1 | Posizione occupata da un ostacolo solido |
2 | La posizione di partenza del giocatore. | 3 | Posizione 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.-
Il layer 0, SOLID_LAYER, è utilizzato per la creazione delle entità e la definizione di ostacoli (blocchi solidi) nel livello.
Questo layer, non viene disegnato su schermo.
-
Il layer 1, BACKG_LAYER, è utilizzato per il disegno del livello, ogni singolo valore rappresenterà un tile da disegnare, prelevato dallo sprite sheet tile_sheet.
-
Il layer 2, ALPHA_LAYER, è utilizzato per il disegno del livello, come il layer 1, ma su di uno strato superiore.
Questo layer disegnerà ogni cosa al di sopra di ogni altro contenuto grafico presente nel gioco, comprese entità e giocatori.
L'implementazione di questo layer nel file di livello è facoltativo.
-
Questo è un esempio del risultato ottenibile dal disegno di ogni singolo layer nel campo di gioco.
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.
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:
- Rect collision
- Tile collision
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_SCROLLQuesto 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.
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 warpLa struttura tipo di un evento di gioco, presenta le seguenti informazioni:
-
coordinate: Coordinate dell'evento, come valori chiave univoci, per recuperare l'evento in una posizione ben specifica.
-
parametri: Stringa contenente informazioni di varia natura, nel formato char *,int,int. Lunghezza massima consentita: 100 caratteri.
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:Tipologia | Identificativo | Descrizione |
---|---|---|
Generic | GENERIC_EVT | Eventi generici, informazioni di qualsiasi natura non specifica. | Warp | WARP_EVT | Eventi di transizione, informazioni specifiche per il passaggio da un livello ad un altro |
Dialog | DIALOG_EVT | Eventi 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:-
Event **pEvent: Puntatore al nodo principale della lista, da passare per deferenziazione (&eventList).
-
char *mapname: Nome della file di livello di appartenenza dell'evento.
-
unsigned int evt_x, unsigned int evt_y: Coordinate dell'evento, punto specifico in cui l'evento risiede ed in cui le entità di gioco devono trovarsi per scatenarlo.
-
char *parameters: Stringa contenente informazioni di varia natura, a discrezione del programmatore. Lunghezza massima consentita: 100 caratteri.
-
int flag_saves: Flag per la permanenza dell'evento durante la transizione da un livello di gioco ad un altro.
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.


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:
- Eseguire un effetto grafico di transizione ed un effetto sonoro, entrambi impostati di default
- Ripulire la lista delle entità allocate
- Caricare il nuovo file di livello
- Inizializzare l'oggetto Camera
- Impostare le coordinate del giocatore nel nuovo livello
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.

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:
- Una struttura per la definizione dei menú
- Una struttura per la definizione dei contenuti
- Funzioni standard per l'interazione
- Funzioni standard per il disegno delle varie tipologie di menù
- Funzioni standard per l'assegnazione veloce è pratica di parametri specifici dei menù
- Un menù di default per lo schermo dei titoli
- Un puntatore globale per la gestione del menù attualmente in uso da parte del giocatore
- Un ciclo proprio per l'esecuzione automatica delle funzioni di gestione e disegno all'interno dei vari status di gioco
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:
Menú Classico
Un classico menú di gioco, i cui elementi sono disposti verticalmente uno sotto l'altro.
Menú a Tabella
Come il menú classico, ma con elementi disposti su piú colonne.Menú a Pagine
Visivamente identico al menú classico, questo menú mostra i propri contenuti a "pagine", mostrando un numero di elementi in colonna alla volta, permettendo di scorrere gli elementi lateralmente e mostrando una pratica freccia lampeggiande sul bordo inferiore ad indicare la presenza di altre "pagine".
15.2 · Struttura di un menú
La struttura per i menú di gioco è definita nel file menu.h e presenta le seguenti variabili:-
int flag_active
Flag per l'attivazione/disattivazione di un menú, puó assumere i seguenti stati:-
-1: Menú persistente - E' lo status in cui un Menú risulta visibile, ma non attivo per l'interazione con l'utente.
Il menú continuerà ad essere visibile nel gioco ad oltranza, ma l'interazione sarà disponibile solo al momento della sua riattivazione. - 0: Menú invisibile/inattivo - E' lo status in cui un Menú risulta chiuso, ma ancora presente in memoria e quindi riutilizzabile.
- 1: Menú visibile/attivo - E' lo status in cui un Menú risulta visibile e vi è permessa l'interazione.
-
-1: Menú persistente - E' lo status in cui un Menú risulta visibile, ma non attivo per l'interazione con l'utente.
-
int flag_title
Flag per mostrare/nascondere il nome del menù.
Se il menú usa la funzione di default per il disegno, con questo flag si potrà abilitiare o disabilitare il disegno del nome del menú nella posizione di default (bordo alto del menú)
Valore di default: 0; -
int flag_border
Flag per mostrare/nascondere il bordo del menù.
Se il menú usa la funzione di default per il disegno, con questo flag si potrà abilitiare o disabilitare il disegno del bordo del menú
Valore di default: 1; -
int x, y
Le coordinate del menú.
-
int w, h
Larghezza e altezza del menú.
-
char name
Stringa contenente il nome del menú.
Larghezza massima: 20 caratteri compreso il terminatore di stringa. -
char cursor
Carattere rappresentate il cursore degli elementi nel menú.
Questo carattere è rappresentato tramite il font standard di RetroGear, è possibile valorizzare la variabile anche con un valore ASCII. -
int rows, cols
Rispettivamente, numero di righe e colonne del menú.
Queste variabili tengono conto del numero di righe e colonne occupate nel menú da parte degli Items inseriti. -
unsigned int page_start
Variabile di supporto per la paginazione dei menú.
Questa variabile tiene conto della posizione del primo Item per la pagina corrente, partendo da esso, disegna gli elementi successivi per il numero di righe assegnate al menú.
-
int num_items
Variabile contenente il numero di elementi presenti nel menú.
Questa variabile è incrementata/decrementata in automatico all'aggiunta o rimozione di una voce dal menú. -
int max_items
Variabile contenente il numero massimo di elementi inseribili nel menú.
Un valore pari a -1 indica un menú senza limiti di inserimenti, 0 che non accetta alcun elemento. -
int curr_item
Variabile contenente l'elemento attualmente selezionato nel menú.
-
SDL_Color color
Colore da utilizzare per testi e bordi del menú.
-
struct _Item *items
Puntatore a struttura di tipo Item, contenente le voci presenti nel menú.
-
struct _Menu *previous, *next
Puntatori a strutture di tipo Menu, per menù padre e figlo nel caso si necessiti di associarne.
-
void (*update)()
Puntatore alla funzione di gestione del menù.
-
void void (*draw)()
Puntatore alla funzione di disegno del menù
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:-
int flag_used
Flag per segnalare l'inizializzazione o meno di un Item, usato nel caso di array statici di tipo Item per l'assegnazione al primo oggetto libero. -
int x, y
Coordinate dell'elemento.
Di default, il sistema imposta queste coordinate in base alla posizione assunta dall'oggetto Menu che le andrà a contenere, tramite apposite funzioni, tutta via potranno assumere qualunque valore a discrezione del programmatore. -
char name[15]
Stringa contenente il testo della voce di menù.
-
char * description;
Stringa contenente una descrizione della voce di menù.
Questo valore è opzionale, in quanto attualmente nessuna funzione di gestione standard di RetroGear se ne prende carico di gestione, ma nulla vieta al programmatore di implementarne una eventuale gestione all'interno della logica del proprio gioco. -
int value;
Valore numerico intero assegnato alla voce di menù.
Questo valore è considerato come proprietà della voce, alla quale il programmatore potrà fare riferimento tramite le apposite variabili ausiliari della struttura Menu (curr_item) ed intendere la voce come oggetto di inventario del giocatore. -
SDL_Color color;
Colore del testo da applicare alla voce durante la fase di disegno.
-
void (*func)();
Puntatore a funzione di callback per la voce di menù.
Utilizzando le funzioni standard di interazione con i menù di gioco, su pressione del tasto BUTTON_A, verrà eseguita una logica associata alla voce di menù, definibile dal programmatore.
15.4 · Definizione di un oggetto Menú
Definire un oggetto di tipo Menu richiede semplicemente la dichiarazione di una variabile di tipo Menu e successivamente l'inizializzazione tramite apposita funzione.Menu my_menu; //Inizializzazione standard initMenu(&my_menu, NULL, 30, 10, "Menu", -1, white, NULL, NULL, &menuInput, &drawMenuClassic);In questo esempio è stato dichiarato un nuovo menú, chiamato my_menu, ed inizializzato tramite la funzione initMenu, dichiarata in menu.h.
La funzione initMenu è una funzione generica a cui poter passare in completa libertà e a discrezione del programmatore, tutti i valori fondamentali per l'inizializzazione corretta di un oggetto Menu, permettendo all'occorrenza di assegnare funzioni di logica definite in un secondo momento dal programmatore, ed ampliare così sulla base delle proprie esigenze il motore di gioco con menù personalizzati.
Nel caso si voglia un menú di tipo classico, si faccia riferimento alla funzione initClassiMenu, o alle apposite funzioni dedicate per la tipologia di menù voluta.
15.3a · Menú classico
La maggior parte delle tipologie di menù disponibili in RetroGear, di base sono tutti menu di tipo Classico, le differenze applicate su questa base, riguardano la disposizione degli elementi, e talvolta le modalità di interazione e disegno.L'inizializzazione di un menu classico, avviene tramite la funzione initClassicMenu:
initClassicMenu(&my_menu, NULL, 30, 10, "Menu", -1, white, NULL, NULL);Questa funzione accetta in argomento i seguenti valori:
-
Menu* pmenu
Puntatore (o referenza) alla struttura Menu da inizializzare. -
Item* pitem
Puntatore (o referenza) alla struttura Item contenente gli elementi del menú.
Se impostato a NULL, il menú viene considerato dinamico, in caso contrario statico.
Nel caso di menú statici, vengono applicati controlli sul numero di elementi da gestire, verranno acccettati solo array di elementi con almeno un Item. -
int x, y
I valori di coordinate del menú.
Questi valori saranno punto di partenza per la disposizione e il disegno dei vari elementi del menú, nonché del bordo dello stesso. -
char *name
Il nome del menú, che all'occorrenza verrà monstrato nella posizione di default (bordo alto). -
int max_items
Numero totale di elementi che andranno gestiti nel menú.
Un valore pari a -1 indica un menú senza limiti di item, 0 che non accetta alcun elemento. -
SDL_Color color
Colore di default per il menú. -
Menu *parent
Puntatore (o referenza) alla struttura Menu padre.
Utilizzato nel caso di menú in sequenza, per permettere il ritono al menú precedente alla chiusura. -
Menu *child
Puntatore (o referenza) alla struttura Menu figlio.
Utilizzato nel caso di menú in sequenza, per permettere l'accesso a menú successivi.
//Disposizione a tabella degli elementi setMenuTable(&my_menu, 4, 2, 4*FONT_W, 1); //Disposizione a pagine degli elementi (5 per pagina) setMenuPaginator(&my_menu, 5); //Impostare un nuovo cursore per gli elementi setMenuCursor(&my_menu, '*');
15.3b · Menú a tabella

//Disposizione a tabella degli elementi setMenuTable(&my_menu, 4, 2, 4*FONT_W, 1);Questa funzione accetta in argomento i seguenti valori:
-
Menu* pmenu
Puntatore (o referenza) alla struttura Menu a cui applicare lo stile. -
int rows
Numero di righe. -
int cols
Numero di colonne. -
int col_width
Larghezza di una colonna espressa in pixel.
Nell'esempio si calcola 4 pixel per la larghezza di un carattere (8 pixel), totale 4 lettere (totale 32 pix3l). -
int row_spacing
Spazio tra una riga e l'altra, espresso in pixel.
Tutto il sistema di gestione e disegno del menù, sarà preso in carico da funzioni dichiarate all'interno di RetroGear, ed assegnate in automatico al menù dalla funzione setMenuTable.
L'utilizzo della funzione setMenuTable su oggetti Menu privi di items, o non iniziliazziati, crea errori di memoria, l'utilizzo deve essere anticipato dall'aggiunta di oggetti Item valorizzati, all'interno dell'oggetto Menu.
15.3c · Menú a pagine

Per realizzare un menù a pagine, basterà inizializzare un menù classico e successivamente richiamare la funzione setMenuPaginator, dichiarata nel file menu_paginator.h.
setMenuPaginator(&my_menu, 5);Questa funzione accetta in argomento i seguenti valori:
-
Menu* pmenu
Puntatore (o referenza) alla struttura Menu a cui applicare lo stile. -
int rows
Numero di righe da mostrare per pagina.
Nel caso in cui il cursore si trovi tra il sesto e il decimo elemento, verranno mostrati gli elementi da 6 a 10, e la freccia lampeggiante svanirà, in quanto non vi saranno altre pagine da mostrare.
Tutto il sistema di gestione e disegno del menù, sarà preso in carico da funzioni dichiarate all'interno di RetroGear, ed assegnate in automatico al menù dalla funzione setMenuPaginator.
L'utilizzo della funzione setMenuPaginator su oggetti Menu privi di items, o non iniziliazziati, crea errori di memoria, l'utilizzo deve essere anticipato dall'aggiunta di oggetti Item valorizzati, all'interno dell'oggetto Menu.
15.5 · Gestione dei Menú
Ogni menù di gioco dichiarato in RetroGear, può essere attivato e reso utilizzabile tramite la funzione menuInput, dichiarata nel file menu.h, che prendendo in argomento un puntatore (o referenza) ad una struttura di tipo Menu, si prenderà carico di richiamare per noi la funzione di aggiornamento (update) assegnata a tale struttura Menu.void doTitleScreen() { //Rendiamo utilizzabile un solo menù abilitandone l'interazione menuInput(&title_menu); }Nel caso avessimo una lista linkata di oggetti Menù, potremmo ricorrere alla funzione doMenu, che cliclerà ogni menù figlio impostato negli appositi puntatori all'interno della struttura:
void doGame() { //Rendiamo utilizzabili i menù di gioco //nello status GAME abilitandone l'interazione doMenu(&game_menu); }La funzione standard, intesa per l'interazione con un singolo menù di gioco è menuInput, la quale però prevede solo una interazione con tipici menù di gioco, dotati di validi elementi Item e strutturati su righe e colonne.
La funzione doMenu permette l'interazione anche di un singolo menù, delegando però il tutto alla funzione di aggiornamento assegnata alla variabile update della struttura.
L'utilizzo della funzione menuInput, non richiama la funzione assegnata alla variabile update della struttura Menu passata in argomento, ma si limita solamente a fornire solo un'interazione standard con il menù.
E' consigliabile utilizzare la funzione doMenu o in alternativa richiamare direttamente il puntatore a funzione dall'istanza stessa, per assicurarsi che l'interazione fornita si quella idonea e prevista per il menù in uso.
E' consigliabile utilizzare la funzione doMenu o in alternativa richiamare direttamente il puntatore a funzione dall'istanza stessa, per assicurarsi che l'interazione fornita si quella idonea e prevista per il menù in uso.
Il menù attualmente in uso da parte del giocatore è accessibile tramite il puntatore curr_menu, definito nel file menu.h.
Questo puntatore può essere passato in argomento sia alla funzione menuInput che alla funzione doMenu, che in questo ultimo caso eseguirà in cascata dal menù puntato in avanti, tutti i menù collegati come figli, uno dopo l'altro.
doMenu(curr_menu);
15.6 · Aggiunta dinamica di elementi al Menú
Per creare dinamicamente un oggetto di tipo Item ed aggiungerlo ad un oggetto di tipo Menu, insieme ad eventuali Item già presenti, si ricorre alla funzione addMenuItem.addMenuItem(&my_menu, "my item", NULL, white, &doSomething);La funzione addMenuItem, accetta in argomento un puntatore (o referenza) ad una struttura di tipo Menu, e nome, descrizione, colore e funzione di callback per l'oggetto Item che verrà allocato dinamicamente all'interno della struttura Menu.
Nel caso in cui vi siano già elementi di tipo Item nella struttura Menu passata in argomento, la funzione addMenuItem riallocherà dinamicamente tutti gli elementi di tipo Item sottoforma di array, appendendo il nuovo oggetto al fondo.
Dato che questi elementi sono allocati dinamicamente, bisognerà ricordarsi di deallocarli dalla memoria a fine programma. (Vedi 15.8 · Eliminazione di elementi da un Menú).
15.7 · Aggiunta statica di di elementi al Menú
Nel caso non volessimo allocare dinamicamente le voci di uno o più menù, potremmo raccoglierle in array statico di tipo Item ed assegnarle alle nostre strutture Menu. L'aggiunta di una voce ad un array statico di tipo Item, prevede l'uso della funzione addStaticMenuItem, la quale prenderà in argomento i dati essenziali alla creazione di un oggetto Item, e ciclerà l'array di tipo Item interessato, alla ricerca di una posizione libera in cui piazzare l'elemento.#define TOTAL 20 //Inizializziamo un menù classico e passiamo in argomento l'array degli elementi initClassicMenu(&my_menu, items_array, 10, 20, "Items", TOTAL, white, NULL, NULL); //Inizializziamo l'array degli elementi prima dell'uso initStaticMenuItems(quest_items, TOTAL_ITEMS); //Aggiungiamo un elemento statico al nostro array addStaticMenuItem(&my_menu, "Item 1", 0, NULL, white, &doSomething);In un array statico di tipo Item, una posizione libera è determinata dal suo valore flag_used impostato a -1.
Questo tipo di elementi non richiede alcuna operazione di deallocazione dalla memoria a fine esecuzione dell'applicazione, da parte del programmatore, ne permette la possibilità di aggiunte dinamiche.
15.8 · Eliminazione di elementi da un Menú
La deallocazione di elementi Item da un oggetto di tipo Menu avviene tramite la funzione destroyMenu, la quale prendendo in argomento un puntatore (o referenza) ad una struttura di tipo Menu, cicla ogni singolo oggetto di tipo Item allocato dinamicamente e puntato dalla variabile interna della struttura: struct _Item *items;.destroyMenu(&:my_menu);
Di default questa funzione è richiamata nella funzione cleanUp del motore di gioco, dichiarata nel file util.c, ed impostata per la pulizia del menù standard main_menu.
destroyMenu(&main_menu);
Crediti
Alcuni elementi grafici presenti in questo documento, provengono dalla collezione libera OpenGameArt, in particolare dai seguenti autori, che si ringraziano per averla resa disponibile:Autore | Descrizione | |
---|---|---|
Jason-Em | Classic hero and baddies pack Items and elements |
![]() |