Developer's Guide
Table of contents:
- 1 · Introduction
- 2 · Project Structure
- 3 · Graphics handling
- 4 · Game's entities
- 4.1 · Game's entities management
- 4.2 · Definition of an entity
- 4.3 · Types and states of an entity
- 4.4 · Animate entities
- 4.5 · Removal of an entity
- 4.6 · Player entity
- 5 · Font Management
- 6 · Sounds and Musics management
- 6.1 · Game states and sounds
- 6.2 · Load and play sound effects and music
- 6.3 · Cleaning allocated sound resources
- 7 · Input handling
- 8 · Score's management
- 9 · Level management
- 10 · Collision management
- 11 · Camera's management
- 12 · Timer management
- 13 · Game events management
- 13.2 · Events type
- 13.3 · Defining a generic event
- 13.4 · Defining a warp event
- 13.5 · Defining a chat event
- 14 · Game menus
- 14.1 · Types of menus
- 14.2 · Structure of a menu
- 14.3 · Structure of menu contents (Item)
- 14.4 · Definition of a menu item
- 14.5 · Menu management
- 14.6 · Adding dynamic elements to the Menu
- 14.7 · Adding static elements to the menu
- 14.8 · Deleting a Menu
- 15 · FPS management
1 · Introduction
RetroGear is a simple and generic 2D game engine, made for a fast and simple retro games realization, like those of the 80's.
A simple game engine, developed on the most common tecniques and conventions used by videogame programmers, as keeping an easy internal framework with external projects.
Developed in order to offer the highest semplicity and completeness possible, RetroGear offers a wide range of features, as well as a comptact and minimal system for the management of various aspects of games, thus granting also the quick developing of videgaming applications in really short time, in a standard, clear and simple way.
The project is constantly updated and improved, therefore may be subject to several variations, anyway you're invited to mess with the code and shaping the engine according to your needs, and if you want, share with everyone else, author included if possible.
2 · Project Structure
2.1 · To begin
The main file of the project is main.c, at its core are initialized the standard mechanisms of the sub-system of the game engine and the SDL libraries./** * Before freeing system resources, you have to close the main * program cycle **/ void quitGame() { quit = 1; } int main(int argc, char *argv[]) { // SDL internal systems initialization 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); // Screen Initialization #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 // Retrogear subsystems initialization init(); // Parameters defined by the programmer mainInit(); //Main game loop mainLoop(); // Cleaning of allocated resources cleanUp(); return 0; }
In the init() function the RetroGear internal game sub-systems, and some other additional system values, necessary for the game engine proper behaviour, are initialized.
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); }
The extInit() function (extension init) is intended as a customizable extension of init(), to allow the programmers to customize any aspect of the game engine in its bootstrap, in a dedicated space.
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); }
The quitGame() function does nothing more than inform the system that the application must be closed, by setting the game engine global flag quit to 1.
Any removals of dynamically allocated resources, will take place automatically in the function cleanUp(), just before the application closing.
The main_menu structure is an internal game engine structure, and provides a practical tool for the realization of menus for games, see the chapter 14 · Game menus for more information about
2.2 · Game states
mainLoop(), the game's main cycle, is the function that deals with the application and to handle the most common game states, generically.Inside it, inputs are managed from peripherals (keyboard / gamepad) and game states are called up at regular intervals via an internal cycle.
The internal cycle keeps the application running at a constant speed on different power hardware. (See the chapter 8.7 · FPS management for further information.)
The main loop ends when the global flag quit is placed as 1.
The draw() function, similar to this, it deals with recalling the design functions suitable for the state of play of the moment.
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); } }Each status is associated with a particular function, conventionally named in the doStatus form for the logic, drawStatus for the rendering.
Each status function includes the logic desired by the programmer for that specific moment of play.
Everything is immediately presented with a minimal and generic logic, which for most of the cases finds its utility in any type of game you want to create.
The programmer is very free to use, change or simply expand what is already there in it.
In case of need for expansion, maintain and adopt the naming conventions already in use in the engine.
The game states currently managed are the following:
- MENU
It handles the display of titles, and any game menu.
- PREGAME
Pre-Game screen, time for showing to the player, summary information such as level number, available lives and more, like in Super Mario Bros. games.
This function is provided with an internal timer to automatically switch to the GAME status. - GAME
The game itself, here will be managed all the game-related logic defined by the programmer.
It handle and updates all the individual active game entities, manage the player's input and updates the status of the player object, and everything related to the gameplay as well. (See the chapter on Entities for more information about) - LOST
It manages the moment in which the player loses a life, for example when he comes into contact with an enemy of the level.
The status is designed to facilitate the developer in the realization of particular logics for these moments, without having to clog the GAME status with cascade controls.
Obviously everything is at the expense of the programmer. - WIN
The moment in which the player wins, designed to execute the logic in a clean and detached way to be performed at the player's victory.
A sort of end-of-game screen, and as in the case of LOST, the programmer is facilitated by allowing him to write clean and layered code. - GAMEOVER
Management of the game over status, where it is possible to manage all the logic related to the game over phase in a clean and detached way, such as cleaning up all allocated resources, resetting the internal values of the engine, resetting scores and anything else you want .
To make it easier for the programmer, some game status have a little control in them.
This control, automatically sets the game status, based upon the function in which it is.
For example, inside doTitleScreen() function, the MENU status will be set, or inside doGame() function the GAME sttus and so on...
Despite all, there may be cases where the programmer may not want to force the game status value, in those cases, we can safely omitting this control, but we must care to properly set the new game status by hand, possibly using the setGameState (STATUS) function, to set the right logic.
if(Game.status!=STATUS) { setGameState(STATUS); }
This control, automatically sets the game status, based upon the function in which it is.
For example, inside doTitleScreen() function, the MENU status will be set, or inside doGame() function the GAME sttus and so on...
Despite all, there may be cases where the programmer may not want to force the game status value, in those cases, we can safely omitting this control, but we must care to properly set the new game status by hand, possibly using the setGameState (STATUS) function, to set the right logic.
3 · Graphics handling
In parallel with the management of the game states, rendering events related to the current state of the game are managed in a similar manner.Using the draw() function, present in the draw.c file and called automatically in the main game loop, the programmer can manage what to draw during various game states.
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 }Exactly as in the main game loop, the appropriate function to the current state of play will be launched, conventionally declared as drawStatus.
The programmer will be able to decide arbitrarily which logic to perform in the various functions made available, using both library functions and implementing their own.
As already mentioned in the chapter 2.1 · To begin, the system provides a software based screen stretching function, therefore this case is also managed in this function, calling up the SDL_SoftStretch() function if the screen has been requested, otherwise a simple call to the SDL_Flip() function on the main video surface. The draw.c file also includes an additional private callback function, 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; } } }This function shows any system notifications messages, of maximum one line at a time for 4 seconds, at the bottom of the game window.
The engine level editor uses this function, to notify the player of unleashed events, such as loading of a level file, saving a level or other.
3.1 · Draw simple graphics
The engine provides library functions for drawing basic geometric shapes, such as lines, circles and squares, as well as low-level management of individual pixels on drawing surfaces.In the gfx library there are functions for drawing and general graphic manipulation, and a minimal color palette, both in rgb and hexadecimal format.
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);
The present functions have the following utilities:
- get_pixel
It returns the value of a pixel present to the coordinates specified in the specified surface, safely in relation to the bytes offered in the surface.
- put_pixel
It draws a pixel at the coordinates specified on the specified surface, safely in relation to the bytes used in the surface.
- replaceColor
It replaces a specific color (target) with a replacement (replecement) within a given surface pixel by pixel.
- drawFillRect
It draws a rectangle of the dimensions specified on the main system surface
- drawRect
It draws a rectangle with an empty interior and dimensions specified on the main system surface
- drawGui
It draws a simple frame using system fonts, useful for dialog boxes or game menus. - loadImage
It uploads a specific BMP image to a surface and convert the format to the current screen size for faster drawing
- getSprite
It takes a specific size frame from a sprite sheet to the indicated index, and show it to the desired coordinates on the screen surface
The declared colors as well as the functions are used by numerous other design functions inside the RetroGear system, we do not recommend elimination or alteration to avoid system errors.
3.2 · Sprites usage
RetroGear provides a generic structure for representing sprites, and the related animation system.typedef struct _Sprite { SDL_Surface *surface; int x, y; int w, h; int index; float animation_timer; float animation_speed; } Sprite;The Sprite structure can be used for the free representation of graphic contents within the playing field and for the representation of game entities.
The structure is explained below.
- SDL_Surface *surface
Pointer to the graphical surface in which the sprite image will be contained. -
int x, y, w, h
Coordinates where the sprites will be drawn.
They can be those of an entity, or free, for a representation of images released from the game objects.
int w, h
Image size to represent.
In the case of entity, they will be the size of the single frame of the entity.
int index
Numerical index of the currently displayed frame.
float animation_timer
Timer used for the animation advancement.
float animation_speed
Animation speed, this value increases the variable 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);
Each SDL_Surface type structure must be correctly evaluated with the content of an image file BMP, which can be executed using the loadImage() function.
skel_spr=loadImage("data/skel.bmp", 0x00FF00);As the first argument it accepts the relative path of the image file, with respect to the game executable, and as second it accepts the parameter the transparency color for the image expressed in hexadecimal.
As far as the dynamic design of animated entities is concerned, the internal function drawEntity(), called automatically by the internal mechanisms of the game engine for each single entity present in the list with a simple cycle while within the drawGame() function.
This function accepts a pointer to a Entity structure, from which it retrieves the associated sprite and draws the current frame.
4 · Game's entities
Every interactive object in the game is called entity, and its represented by the Entity structure which provides everything needed to manage and represent the entity itself within the program.
We can divide the entity into 4 groups of variables.
The first represents the real object, with some main properties such as the coordinates in the playing field, relative height and width, directions and previous coordinates of the last position, as well as additional variables such as score, number of lives, type, and internal timers.
The second group, of which the member sprite is part, provides a structure for representation of the entity on the screen, as a static or multiple sequence of images (See the chapter concerning the sprite).
The third group, "physical", is represented by additional variables, used for the implementation of physics on game entities, such as gravity and inertia for example.
The last group has utility variables for the game engine, such as a function pointer, "update", for calling the entity's update function, and a pointer to an "Entity" structure to implement a linked list.
4.1 · Game's entities management
The game entities, by default, are managed in a completely dynamic manner, they are automatically allocated at level file loading, or simply on explicit request of the programmer through the use of function createEntity().This function accepts the main basic parameters required for a game entity to be usable correctly, including:
- int type
Entity type, such as enemy, bonus or power-up.
The numerical association value/type is linked to the enumerative variable OBJECTS, which gives greater readability to the code, as well as a tool common to all game entities.
It finds its usefulness when it is necessary to check for collisions with certain types of objects, to trigger particular events.
Example:
Player collision < - > COLLECTABLE entity - > Increase the player's score by 100 points.
Player collision < - > ENEMY entity - > Decreases the player's number of lives.
For more information about the OBJECTS variable, see chapter 3.3 · Types and states of an entity -
int solid
Flag that specifies whether the entity is to be considered a solid object or not.
Solid objects are to be considered obstacles for the player, objects that are impossible to cross.
Useful to manage those types of objects placed as temporary obstacles on the playing field, and destructible through the execution of particular actions. -
int x, int y, int w, int h
Coordinates and relative width and height of the entity. -
int lives
Counts the number of available lives of the entity. -
RG_Sprite sprite
Pointer to an RG_Sprite structure type, which represents the image of the entity in the game window.
It needs to be setup in the initSprite() function. -
float speed
Speed of the entity in the playing field, that is the number of pixels that will occur per movement at each cycle. -
float gravity
Gravity factor applied to the entity, this value will act on the game entity as a downward driving force. - void (*update)()
Pointer to entity update callback function.
See the specific chapter for more information about.
This list is handled by the pointers *headList and *tailList, which respectively are the first (head) and the last (tail) of the list.
These pointers are automatically initialized by the createEntity() function, that will check the content of the list and set them.
Even if the programmer is not very familiar with the Linked List, the system automatically manages these dynamic lists, as well as cleaning them.
Each entity on the list will be updated through the doEntities() function, which will the call the entity's update() callback function if its internal status is "ACTIVE", or it just will destroy and free the memory if its status is "DESTROY".
In case of loading a level file, the entities allocation will be performed automatically using the createEntityFromMap() function, called by the loadLevel() function.
For more information about, see the specific chapter.
For more information about, see the specific chapter.
4.2 · Definition of an entity
Each entity can present their own set of behaviors and logics, and RetroGear adopts a convention for the manage the most varied types of entities, based on the unique declaration of each of them.A convetional code base, is adopted for the management of the entities and their possible status, along with their personal logics and data structures like sprites, sizes, speeds and so on.
To define an entity, we need to create their own source and header files, like this Skeleton example, Skel for short:
#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(); #endifHeader will contain the global information and data structures that our entity needs, along with their handling functions, which by adopted convention are declared in the forms of [entityName]_create/clean and update[EntityName].
The core source code of our entity will contain the entity own logics, check this example:
#include "skel.h" //Possible obtainable score #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); //Set up the 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 entity 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"); } //Initial speed and directions of the entity EntityList_Tail->hspeed = 0; EntityList_Tail->vspeed = 1; EntityList_Tail->direction_x = -1; EntityList_Tail->direction_y = 0; } /** * Custom entity 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) { Entity *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 entity viene schiacciata pobj->timer[0]=fps.t; return; } moveEntity_X(pobj); animateEntity(pobj); //onAnimate(pobj); //Custom animations onCollision(pobj); }By convention, the creation of each game entity must take place through the appropriate "constructor", named by the convention of [EntityName]_create, that should will have to deal to setup the entity resources correctly, such as it's sprites, sound and default values.
Each new entity will be automatically included in the RetroGear global list of entities, thanks to the createEntity() function, and it's update, managed internally by the engine, through a callback to the defined update[EntityName] function.
The update callback function can be filled with standard RetroGear functions, or custom defined as well.
Entity statuses have their own static function, defined by convention as on[EventName].
By convention, three default callback functions are defined for states: onAnimate,onCollision,onDestroy.
You can use onAnimate to write your own animations logics, onCollision to perform custom actions on entity collision as well, and onDestroy to perform some actions before the entity destructions, like add some points.
For more information on managing entity statuses, see chapter States of an entity
Regarding the destruction of entities, please refer to the chapter Removal of an entity
Regarding the destruction of entities, please refer to the chapter Removal of an entity
This basic example is the way to adopt to make new entities for your game, and every new entity must be also declared in the Makefile as well.
EXT_OBJ = skel.o
4.3 · Types of entities and states
Every entity can be associated within a type, for example Collectable, Enemy and so on, through the enumerative OBJECTS variable applied to type variable entity.typedef enum { PLAYER, COLLECTABLE, ENEMY, BULLET, WALL, OBSTACLE } ENTITY_TYPE;
The use of enumeration allows a greater clear code, where an entity creation is intended to specify its nature within the game and to manage particular cases such as in the previous example.
createEntity(ENEMY, x, y, SKEL_W, SKEL_H, lives, speed, gravity, &updateSkel); [...] if(current!=pobj && current->active && current->status!=KILL && current->type!=COLLECTABLE)
The ENTITY_TYPE enumerative variable can be used can be used by creating the entity and checking for collisions with other entities. Here, for example, all collisions are ignored with any COLLECTABLE type objects, or objects like Power-Up, Bonus etc, do not affect the movement of entities.
Entities use also the status variable, as we have seen before.
This variable is intended as a status index for the entity's current activities, it considers what the entity is currently doing.
At the expense of the programmer, it would be a good practice to assign, according to the logic defined, at least one entity status among those provided by the entity.h file.
typedef enum { MOVE, ACTION, JUMP, FALL, CLIMB, STAND, BLINK, KILL, DESTROY } ENTITY_STATUS;
For a moving entity, we could use for example the MOVE value, JUMP for jumping, KILL for a defeat as we have seen before, and DESTROY for one to be destroyed (strictly).
4.4 · Animate entities
Per convenzione, gli sprite delle entity, 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 entity.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 interno di RetroGear, ci si può affidare alla funzione interna animateEntity(), la quale provvederà a gestire la struttura RG_Sprite interna all'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
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 sicurezza, ne si imposta lo status su DESTROY, ed il sistema provvederà in automatico a rimuoverla.4.6 · L'entità Player
Nel motore di gioco è a disposizione sin da subito una struttura di tipo Entity per la rappresentazione del giocatore, la struttura Player.Questa struttura è 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.
L'oggetto Player è definito in maniera analoga a quello delle entity mostrato nei capitoli precedenti, salvo l'assenza di un costruttore, e la presenza di funzionalità aggiuntive per la gestione dei comportamenti del giocatore.
Nel caso in cui sia necessario implementare logiche supplementari o modificare quelle pre-esistenti, il programmatore è invitato a farlo direttamente in questo file, potendo anche utilizzare le funzioni generiche della liberia entity.c all'occorrenza.
Per convenzione, l'accesso all'oggetto Player all'interno del motore di gioco, avviene tramite il puntatore curr_player, di cui si consiglia l'uso preferibilmente all'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); [...] }Al posto del costruttore, qui troviamo 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 entity, 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 entity giocatore attualmente in uso.
RetroGear tende a fornire un'insieme minimo di logica al programmatore, il quale potrà a seconda dei casi e delle necessità, plasmare a proprio piacere, tenendo però in conto che eventuali logiche pre impostate sono atte a mantenere continuativo e funzionale il lifecycle del gioco.
Può esservi la necessità di controllare alcuni status particolari del giocatore, come ad esempio in un platform game, l'avvenuta collisione con il suolo.
In questi casi, invece di agire sulla struttura Entity, appesantendola con ulteriori variabili di nessuna utilità per tutti gli altri oggetti del gioco, tranne Player, possiamo ricorrere a variabili private all'interno del sorgente player.c ed usarle tranquillamente nella logica delle funzioni.
In questi casi, invece di agire sulla struttura Entity, appesantendola con ulteriori variabili di nessuna utilità per tutti gli altri oggetti del gioco, tranne Player, possiamo ricorrere a variabili private all'interno del sorgente player.c ed usarle tranquillamente nella logica delle funzioni.
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, non è ancora effettivo
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 · Using Fonts and writing
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 · Using the Typewriter system
Il sistema Typewriter é un sottosistema del motore di gioco, che permette la stampa di testo a schermo, con un effetto macchina da scrivere.6 · Sounds and Musics management
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 · Game states and sounds
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 · Load and play sound effects and music
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 · Cleaning allocated sound resources
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 · Input handling
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.Questa struttura è intermediaria per la gestione di eventi pressione/rilascio dei tasti sulle periferiche di input fisiche, con le librerie SDL e meccanismi interni per la gestione di eventi di input.
La mappatura per la tastiera con i relativi valori di SDLK, é definita all'interno del file controls.h:
#define DEAD_ZONE 3200 #define BUTTON_A SDLK_x #define BUTTON_B SDLK_z #define BUTTON_START SDLK_RETURN #define BUTTON_SELECT SDLK_LSHIFT typedef enum { NOPRESS=0, PRESSED=1, LOCKED=-1 } INPUT_KEY_STATUS; typedef struct _Gamepad { int button_A, button_B; int button_Start, button_Select; int button_Left, button_Right, button_Up, button_Down; } Gamepad; //Gamepad virtuali Gamepad gamepad; //[2]; //Gamepad virtuale corrente (Per la gestione di sistemi multiplayer) Gamepad *curr_gamepad; SDL_Joystick *joystick_ptr; // Joystick device pointer SDL_Event input_event; Uint8 *keystate; // keyboard state void initController(); void inputHandler(); int konamiCode(); void cleanInput();Di default i tasti associati alla tastiera sono Z e X per i tasti A e B, mentre Right Shift ed 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.
Per la gestione degli input da tastiera vi sarà la funzione privata handle_keyboard(), per gli eventi gamepad handle_joystick(), per gli eventi del gamepad fisico, e handle_mouse() per gli eventi relativi al mouse.
Quest'ultimo avrà una struttura propria, Mouse, che ricalcherà esattamente la struttura di un classico mouse a due tasti, senza appoggiarsi alla struttura gamepad virtuale.
Ogni interazione con la struttura Gamepad deve avvenire tramite l'apposito puntatore curr_gamepad, pensato per aiutare il programmatore nella gestione di più periferiche di input, nel caso di giochi multiplayer ad esempio.
7.1 · Usage of the virtual gamepad
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 uno stato di permanenza dell'input, che altrimenti per natura via del sistema interno alle librerie SDL, si perderebbe ad ogni ciclo del programma.
Volendo impedire la ripetizione dell'input all'interno del nostro programma, si potrà tranquillamente valorizzare a 0 il membro specifico della struttura gamepad virtuale, non appena questa risulti premuto, esattamente come fatto nel secondo if dell'esempio.
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.
Al momento non sono previsti meccanismi di mapping dinamico dell'input, ne meccanismi per il gioco in multiplayer in simultanea.
Il multiplayer attualmente prevede l'alternanza dei due giocatori, interfacciati con le stesse perifiche di input, configurate allo stesso modo.
Il multiplayer attualmente prevede l'alternanza dei due giocatori, interfacciati con le stesse perifiche di input, configurate allo stesso modo.
7.2 · Definition of a 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 · Usage of the virtual mouse
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 · Score's management
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 può ricorrere alla funzione void createScore, che come per le entità, provvederà ad allocare in memoria il nuovo oggetto e ad aggiungerlo in una lista apposita.
Il ciclo di vita degli 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 specificat.
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.
8.1 · Incremental points management
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 · Level management
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 grafica del livello, è la struttura Level, formata da 300 colonne e 300 righe su 3 strati (layers), ognuno pensato per uno specifico utilizzo.
#define COLS 300 #define ROWS 300 #define LAYERS 3 #define SOLID_LAYER 0 #define BACKG_LAYER 1 #define ALPHA_LAYER 2 unsigned int level_index; //Indice del livello corrente typedef struct _Level { char name[20]; char description[20]; char theme[5]; char song_title[10]; //~ char bkgd_image[100]; int bkgd_red, bkgd_green, bkgd_blue; int map[LAYERS][ROWS][COLS]; unsigned int cols, rows; unsigned int curr_layer; unsigned int num_layers; int flag_complete; //Flag completamento livello } Level;La struttura di livello può essere popolata manualmente da codice, accedendo ai membri della struttura, o tramite la definizione di appositi file di livello.
Semplici file di testo con estensione .map, di default residenti e caricati nella cartella maps, che ogni progetto dovrà contenere.
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) |
Le sequenze numeriche separate da virgola (csv), rappresenteranno ognuna i layers del livello.
Per il primo gruppo di valori, ogni numero corrisponderà ad una entità, per il secondo e il terzo, ogni numero corrisponderà ad un fotogramma specifico nel tile sheet relativo al proprio strato.
9.2 · Caricamento di un livello da file
Per il caricamento di un livello da file, occorre salvarlo nella cartella maps, ed usare la funzione loadLevel(Level* plevel, char *filename), la quale accetta in argomento la 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 entity, 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) { switch(id) { case PLAYER_ID: //ID riservato all'oggetto Player setPlayerPosition(x, y); break; case 3: badguy_create(x, y); break; case 4: coin_create(x, y); break; } }La funzione createEntityFromMap() di default viene proposta con la gestione della sola entity Player, ogni altra entity dovrà essere definita manualmente dal programmatore, richiamando l'apposito costruttore.
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 entity 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
10 · Collision management
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 entity accanto al tile } else { pobj->x += pobj->hspeed; //Muoviamo la entity } } //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 entity, 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 · Camera's management
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 · Free scrolling
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 · Lazy scrolling
É 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 · Monitoring of scrolling area
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 · Timer management
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 · Game events management
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 · Events type
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 · Defining a generic event
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 · Defining a warp event
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 · Eventi di dialogo
Questo capitolo attualmente è una bozza in attesa di revisione
Gli eventi di tipo dialogo, sono utilizzati per la rappresentazione di testi o messaggi nel gioco.
A differenza delle altre tipologie di evento, il valore parameters qui rappresenterà interamente il testo parte del dialogo.
Si potrà ad esempio associare un messaggio al verificarsi di una determinata azione, al raggiungimento di una specifica posizione nella mappa o nell'interazione con personaggi non utilizzabili del gioco, come accade nei giochi di tipo RPG.
14 · Game menus
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
Per avere un menù a tabella, una volta inizializzata correttamente la struttura Menu tramite la funzione initClassicMenu, ed aggiunti gli items, si può utilizzare la funzione setMenuTable come segue://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
Il menù a pagine è un menù classico, i cui elementi sono visualizzati un numero di righe alla volta, e percorribili in quattro direzioni.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 |