Premesse
Per la piena comprensione di questa guida sono necessarie delle discrete
conoscenze di C++, nel caso non fosse in possesso di tali conoscenze consiglio di
studiare un buon libro sull'argomento, personalmente consiglio "C++ La guida completa"
della McGraw Hill
[2] e la documentazione della STL realizzata dalla SGI
[3].
Altre conoscenze necessarie possono essere dei fondamenti di logica booleana ed in
particolare si presume che il lettore abbia già studiato le mie precedenti guide su SDL,
pertanto alcuni concetti relativi a funzioni e strutture dati già utilizzate, non verranno
ripresi, se non brevemente.
Si fa presente che questa guida ed il programma in essa spiegato sono stati
realizzati su piattaforma linux (slackware 9.1 con gcc 3.3.3, SDL 1.2.6 ed SDL_Image 1.2),
ciò non dovrebbe comportare problemi nell'esecuzione del programma sotto altri sistemi,
ma di questo non posso dare certezze.
La libreria SDL
[4] e la sotto-libreria SDL_Image
[5] possono essere scaricate
agli indirizzi indicati nei riferimenti.
Chiedo scusa per la grafica utilizzata, ma non sono un grafico e dopotutto questa
guida è sulla programmazione di SDL e non sull'utilizzo di GIMP.
Si segnala che l'articolo è stato realizzato durante lo sviluppo di
Mars, Land of No Mercy, un gioco
strategico a turni opensource.
Il nostro obiettivo
In questa guida gestiremo l'animazione e lo spostamento di più figure su di
uno sfondo con SDL, ovvero implementeremo una gestione degli sprite in SDL.
Vista la non estrema semplicità dell'argomento sono state adottate diverse
semplificazioni nel modello e soprattutto nell'implementazione, riducendo le azioni
gestibili dalle nostre figure al minimo indispensabile.
Eventuali miglioramenti e progetti per il futuro verranno discussi in fondo
all'articolo.
Allo stato attuale della guida le classi implementate non possono essere
considerate utilizzabili in un programma (gioco) completo, ma possono essere ritenute
un buon punto di partenza per uno sviluppo concreto.
Quindi un obiettivo secondario è quello di rendere le classi utilizzate facilmente
aggiornabili, permettendo quindi, una certa flessibilità nell'aggiunta di caratteristiche.
Gli sprite
Volendo dare una definizione iniziale si potrebbe dire che uno sprite è un oggetto
grafico al quale sono associate una o più animazioni.
Da questa prima definizione si possono iniziare a dividere gli sprite in due classi
principali:
-
Gli sprite ATTIVI, ovvero quegli elementi grafici che possono essere controllati e/o con
i quali si può interagire in qualche maniera.
-
Gli sprite PASSIVI, ovvero quelli che sono inseriti per pura coreografia.
Gli sprite appartenenti alla seconda categoria (i passivi), sono i più facili da gestire,
in quanto l'unica preoccupazione del programmatore deve essere l'implementazione dell'animazione.
Gli sprite attivi invece, richiedono una gestione molto più accurata in quanto, oltre
l'animazione, possono essere gestite altre funzionalità come ad esempio il movimento e
l'interazione con degli altri sprite (la cosiddetta gestione delle collisioni).
Volendo fare degli esempi pratici, prendendo come ambientazione una schermata di un gioco
di strategia, uno sprite passivo può essere un fiume che scorre, in questo caso si deve gestire
l'animazione dell'acqua, ma non lo spostamento (solitamente i fiumi sono statici), mentre uno
sprite attivo può essere un soldato, questa volta possiamo effettuare uno spostamento dello sprite
assistendo all'animazione del movimento e possiamo farlo attaccare un bersaglio, assistendo ad un
eventuale spostamento ed all'attacco.
Il modello ad oggetti utilizzato
Prima di passare all'implementazione degli sprite è necessario comprendere la loro
struttura ad un livello di astrazione logico/teorico.
Gli sprite possono essere considerati come degli oggetti formati da animazioni che,
come abbiamo visto, possono avere la capacità di spostarsi e di interagire con degli altri
sprite, pertanto bisogna definire innanzitutto il modo di realizzare un'animazione e poi
pensare a come gestire le caratteristiche "avanzate".
Un'animazione è data da un insieme di immagini che scorrendo rapidamente in sequenza,
danno l'illusione del movimento, queste immagini sono definite FRAME.
Pertanto uno sprite, avendo almeno un'animazione (nel nostro caso tratteremo una sola
animazione, per semplicita`), deve possedere un insieme di frame da visualizzare.
Stabilito ciò useremo un modello ad oggetti basato su due classi, la classe sprite
e la classe frame. Come avrete intuito la classe sprite conterrà più oggetti frame.
Utilizzando tale modello sarà possibile distinguere le azioni sui singoli frame (modifiche,
trasparenze, rotazioni, ecc..) dalle azioni sullo sprite (movimento, interazioni, ecc...).
Si tenga presente che non è necessario realizzare una classe apposita per gli
sprite passivi, visti che questi possono essere considerati come sprite attivi che non si
muovono, quindi un loro sott'insieme.
Deciso il modello da usare possiamo passare all'implementazione del codice.
Sorgente - main.cpp
Comprendere i vari sorgenti senza leggere le singole spiegazioni non è facile,
ma per mantenere fede al modello delle guide precedenti, e soprattutto per mantenere un
riferimento non frammentato, ho inserito i vari sorgenti, inoltre un lettore con una
sufficiente conoscenza di SDL (almeno quella trattata nelle guide precedenti), dovrebbe
riuscire a districarsi tra i sorgenti senza troppe difficoltà visti gli innumerevoli commenti
in linea inseriti.
#include <iostream>
#include <vector>
#include <string>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include "frame.h"
#include "sprite.h"
#define DIM_H 800
#define DIM_V 600
using namespace std;
//dichiariamo un puntatore ad una superficie globale
SDL_Surface *screen;
//anche bg e` globale
SDL_Surface *bg;
int main()
{
// si inizializza il sistema video
if( SDL_Init(SDL_INIT_VIDEO) == -1 )
{
// SDL_GetError() ritorna una descrizione dell'errore
cout << "Errore init SDL: " << SDL_GetError() << endl;
return 1;
}
// all'uscita del programma esegui SDL_Quit per risistemare le cose
atexit(SDL_Quit);
//dichiariamo un int a 32 bit per i flag da assegnare a screen
Uint32 flags;
flags = SDL_HWSURFACE|SDL_DOUBLEBUF;
// per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode
if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) ))
{
cout << "Problemi con il settaggio dello schermo: " << SDL_GetError() << endl;
return 1;
}
//carichiamo l'immagine di sfondo
bg=IMG_Load("data/bg.png");
//disegnamo lo sfondo su screen
SDL_BlitSurface(bg, NULL, screen, NULL);
//il vector che conterra` le img dello sprite
vector<string> files;
//carichiamo le stringhe nel vector
files.push_back("data/tank_01.png");
files.push_back("data/tank_02.png");
files.push_back("data/tank_03.png");
files.push_back("data/tank_04.png");
files.push_back("data/tank_05.png");
files.push_back("data/tank_06.png");
files.push_back("data/tank_07.png");
files.push_back("data/tank_08.png");
//istanziamo gli sprite
sprite tank(files);
sprite tank2(files,0,200,1);
sprite tank3(files,0,400,3);
//flag di fine loop principale
int end=0;
//limite orizzontale da non superare
int x_limit2=DIM_H-tank2.get_w();
int x_limit3=(DIM_H-tank3.get_w())/3;
//stato della tastiera
Uint8* keys;
while(!end)
{
//dichiariamo una struttura SDL_Event
SDL_Event event;
// SDL_PollEvent attiva il gestore di eventi
while ( SDL_PollEvent(&event) )
{
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
end = 1;
if ( event.type == SDL_KEYDOWN )
{
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
end = 1;
//premendo spazio si passa dalla modalita` schermo intero alla modalita`
//finestra e viceversa
if ( event.key.keysym.sym == SDLK_SPACE )
{
flags^=SDL_FULLSCREEN;
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
//disegnamo lo sfondo su screen
SDL_BlitSurface(bg, NULL, screen, NULL);
}
}
}
//per verificare se un tasto e` mantenuto premuto usiamo SDL_GetKeyState che
//ritorna un'immagine dei tasti premuti
keys = SDL_GetKeyState(NULL);
//FRECCIA SU: se il tank non e` posizionato a (x,0) andiamo su
if ( keys[SDLK_UP] )
{
if(tank.get_y())
tank.mov_y(-1);
}
//FRECCIA GIU: approssima il bordo inferiore in base al numero di tank e a DIM_V
if ( keys[SDLK_DOWN] )
{
if(tank.get_y()<(DIM_V-tank.get_h()))
tank.mov_y();
}
//FRECCIA SX: se il tank non e` posizionato a (0,y) andiamo a sx
if ( keys[SDLK_LEFT] )
{
if(tank.get_x())
tank.mov_x(-1);
}
//FRECCIA DX: se il tank non e` posizionato a (DIM_H-tank->w,y) andiamo a sx
if ( keys[SDLK_RIGHT] )
{
if(tank.get_x()<(DIM_H-tank.get_w()))
tank.mov_x();
}
//muoviamo il secondo tank, se ci troviamo entro i limiti
if((x_limit2--)>0)
tank2.mov_x(1);
//muoviamo il terzo tank, se ci troviamo entro i limiti
if((x_limit3--)>0)
tank3.mov_x(1);
//disegnamo le parti di sfondo interessate
tank.draw_bg();
tank2.draw_bg();
tank3.draw_bg();
//disegniamo i tank
tank.draw_sprite();
tank2.draw_sprite();
tank3.draw_sprite();
//aggiorniamo lo schermo
SDL_Flip(screen);
}
return 0;
}
Sorgente - sprite.h
#ifndef __CLASS_SPRITE__
#define __CLASS_SPRITE__
#include <vector>
#include <string>
#include <SDL/SDL.h>
#include "frame.h"
using namespace std;
//accediamo alla screen globale
extern SDL_Surface *screen;
//accediamo alla bg globale
extern SDL_Surface *bg;
class sprite
{
//cordinate dell'img (x,y)
int x, y;
//velocita` di spostamento dell'animazione
int s_speed;
//indice del frame corrente
int cur_frame;
//flag che indica se ci stiamo muovendo
bool moving;
//ultimo tempo di aggiornamento del frame
long update_time;
//idealmente uno sprite e` un insieme di frame
vector<frame *> frames;
public:
//costruttore
sprite(vector <string> files, int init_x=0, int init_y=0, int speed=2);
//distruttore
~sprite();
//function di spostamento frame
void mov_x(int mov=1);
void mov_y(int mov=1);
//function che ritornano le cordinate posizionali del frame
int get_x() { return x; };
int get_y() { return y; };
//function che ritornano le dimensioni di un frame
int get_w(int ind=0) { return frames[ind]->get_w();}
int get_h(int ind=0) { return frames[ind]->get_h();}
//function principale, che gestisce il disegno e l'animazione dello sprite
void draw_sprite();
//function che ridisegna il background modificato dallo sprite
void draw_bg();
};
#endif
Sorgente - sprite.cpp
#include "sprite.h"
//costruttore che si occupa di inizializzare tutti i dati necessari
sprite::sprite(vector <string> files, int init_x, int init_y, int speed)
{
//contatori
int i,num_files=files.size();
//puntatore temporaneo
frame * temp_frame;
//alloco tutti i frame necessari
for(i=0;i<num_files;i++)
{
temp_frame= new frame (files[i],110);
frames.push_back(temp_frame);
}
//inizializziamo le cordinate dello sprite
x=init_x;
y=init_y;
//settiamo la velocita` dello sprite
s_speed=speed;
//il primo frame ha indice 0
cur_frame=0;
//il tempo iniziale e` 0
update_time=0;
//inizialmente non ci muoviamo
moving=false;
}
//function che si occupa di gestire il disegno e l'animazione dello sprite
void sprite::draw_sprite()
{
//se e` passato il tempo necessario passiamo al prossimo frame
if(update_time+(frames[cur_frame]->get_pause())<SDL_GetTicks())
{
cur_frame++;
//se siamo all'ultimo frame torniamo a 0
if(cur_frame==(frames.size()))
cur_frame=0;
//aggiorniamo il tempo dell'ultima modifica
update_time = SDL_GetTicks();
}
//indichiamo le cordinate del tank
SDL_Rect dest_t;
dest_t.x = x;
dest_t.y = y;
dest_t.w = frames[cur_frame]->get_w();
dest_t.h = frames[cur_frame]->get_h();
//disegnamo il tank su screen
if(moving)
SDL_BlitSurface(frames[cur_frame]->get_surface(), NULL, screen,&dest_t);
else
{
SDL_BlitSurface(frames[0]->get_surface(), NULL, screen,&dest_t);
cur_frame=0;
}
//consideriamo l'animazione finita
moving=false;
}
//function che ridisegna il background modificato dallo sprite
void sprite::draw_bg()
{
//aggiorniamo il rettangolo che circonda il tank
SDL_Rect rect_bg;
rect_bg.x = x-s_speed;
rect_bg.y = y-s_speed;
rect_bg.w = frames[cur_frame]->get_w()+(s_speed*2);
rect_bg.h = frames[cur_frame]->get_h()+(s_speed*2);
//copiamo la parte di bg necessaria su screen
SDL_BlitSurface(bg, &rect_bg, screen, &rect_bg);
}
//function spostamento orizzontale
void sprite::mov_x(int mov)
{
//ci spostiamo in base ad s_speed
x+=mov*s_speed;
//segnaliamo che ci stiamo muovendo
moving=true;
}
//function spostamento verticale
void sprite::mov_y(int mov)
{
//ci spostiamo in base ad s_speed
y+=mov*s_speed;
//segnaliamo che ci stiamo muovendo
moving=true;
}
//distruttore
sprite::~sprite()
{
//contatori
int i,num_files=frames.size();
//chiamiamo il distruttore dei vari frame allocati precendentemente
for(i=0;i<num_files;i++)
{
frames[i]->~frame();
}
}
Sorgente - frame.h
#ifndef __CLASS_FRAME__
#define __CLASS_FRAME__
#include <string>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
using namespace std;
class frame
{
//la superficie del frame
SDL_Surface *The_Frame;
//pausa dal prossimo frame
int The_Pause;
public:
//costruttore
frame(string img, int pause);
//distruttore
~frame();
//function che ritorna una superficie dell'animazione
SDL_Surface * get_surface() { return The_Frame; };
//function che ritorna la larghezza del frame
int get_w() { return The_Frame->w; };
//function che ritorna l'altezza del frame
int get_h() { return The_Frame->h; };
//function che ritorna la pausa impostata
int get_pause() { return The_Pause; };
};
#endif
Sorgente - frame.cpp
#include "frame.h"
//costruttore, inizializzamo i dati necessari
frame::frame(string img,int pause)
{
//carichiamo l'immagine necessaria
The_Frame=IMG_Load(img.c_str());
//colore da rendere trasparente
Uint32 colorkey;
//prendiamo la mappatura del colore #0000FF in base al nostro specifico formato di pixel
colorkey = SDL_MapRGB(The_Frame->format, 0, 0, 0xFF);
//impostiamo il colore come trasparente
SDL_SetColorKey(The_Frame, SDL_SRCCOLORKEY|SDL_RLEACCEL, colorkey);
//pausa dal prossimo frame
The_Pause=pause;
}
//distruttore, liberiamo la superficie necessaria
frame::~frame()
{
SDL_FreeSurface(The_Frame);
}
Spiegazione del sorgente - main.cpp
#include <iostream>
#include <vector>
#include <string>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include "frame.h"
#include "sprite.h"
#define DIM_H 800
#define DIM_V 600
using namespace std;
Nelle prime righe del main non facciamo altro che includere gli header necessari,
definire la nostra risoluzione e dichiarare l'utilizzo del namespace std.
//dichiariamo un puntatore ad una superficie globale
SDL_Surface *screen;
//anche bg e` globale
SDL_Surface *bg;
Di seguito dichiariamo le superifici screen e bg come globali, questo per poterle
utilizzare con maggiore comodità nelle altre classi (in particolare in sprite).
int main()
{
// si inizializza il sistema video
if( SDL_Init(SDL_INIT_VIDEO) == -1 )
{
// SDL_GetError() ritorna una descrizione dell'errore
cout << "Errore init SDL: " << SDL_GetError() << endl;
return 1;
}
// all'uscita del programma esegui SDL_Quit per risistemare le cose
atexit(SDL_Quit);
Inizializziamo il sotto-sistema video, gestendo l'errore, poi associamo la
function SDL_Quit ad ogni uscita corretta del nostro programma.
//dichiariamo un int a 32 bit per i flag da assegnare a screen
Uint32 flags;
flags = SDL_HWSURFACE|SDL_DOUBLEBUF;
// per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode
if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) ))
{
cout << "Problemi con il settaggio dello schermo: " << SDL_GetError() << endl;
return 1;
}
Associamo il video alla superficie screen impostado i flag necessari per una superficie
hardware con doppio buffer.
//carichiamo l'immagine di sfondo
bg=IMG_Load("data/bg.png");
//disegnamo lo sfondo su screen
SDL_BlitSurface(bg, NULL, screen, NULL);
Carichiamo l'immagine di sfondo e la copiamo sulla superficie screen, visto che la
dimensione dell'immagine è la stessa della superficie, possiamo evitare di specificare i
rettangoli sorgente e destinazione nella function SDL_BlitSurface.
//il vector che conterra` le img dello sprite
vector<string> files;
//carichiamo le stringhe nel vector
files.push_back("data/tank_01.png");
files.push_back("data/tank_02.png");
files.push_back("data/tank_03.png");
files.push_back("data/tank_04.png");
files.push_back("data/tank_05.png");
files.push_back("data/tank_06.png");
files.push_back("data/tank_07.png");
files.push_back("data/tank_08.png");
In queste righe dichiariamo un vector di string che utilizziamo per contenere
tutti i file che costituiscono i nostri frame.
Tale vettore sarà passato al costruttore della classe sprite per l'inizializzazione dei vari
frame.
//istanziamo gli sprite
sprite tank(files);
sprite tank2(files,0,200,1);
sprite tank3(files,0,400,3);
Una volta definite le immagini che compongono i frame possiamo creare i tre tank che
utilizzeremo nel nostro programma.
Come potete notare la prima dichiarazione presenta un solo parametro, ovvero il vector di string
contenente i percorsi dei file delle immagini da utilizzare.
Gli altri tre parametri (posizione orizzontale, posizione verticale e velocita`) sono lasciati
impostati sui valori di default. Il secondo ed il terzo tank invece, specificano questi valori.
//flag di fine loop principale
int end=0;
//limite orizzontale da non superare
int x_limit2=DIM_H-tank2.get_w();
int x_limit3=(DIM_H-tank3.get_w())/3;
//stato della tastiera
Uint8* keys;
In queste linee vengono dichiarate ed inizializzate la variabile che controlla
l'uscita dal loop principale, poi le variabili che indicano il limite orizzontale
dell'avanzamento del secondo e del terzo tank ed infine viene dichiarato l'intero senza
segno ad otto bit che conterrà lo stato della tastiera.
while(!end)
{
//dichiariamo una struttura SDL_Event
SDL_Event event;
// SDL_PollEvent attiva il gestore di eventi
while ( SDL_PollEvent(&event) )
{
Iniziato il ciclo principale andiamo ad attivare il gestore di eventi passandogli
la struttura necessaria.
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
end = 1;
Il primo evento gestito è la pressione della x sulla finestra aperta, l'evento comporta
la fine del nostro ciclo principale, quindi del programma.
if ( event.type == SDL_KEYDOWN )
{
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
end = 1;
Di seguito verifichiamo se è stato premuto e rilasciato il tasto ESC, anche in tal caso,
terminiamo il ciclo principale ed usciamo.
//premendo spazio si passa dalla modalita` schermo intero alla modalita`
//finestra e viceversa
if ( event.key.keysym.sym == SDLK_SPACE )
{
flags^=SDL_FULLSCREEN;
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
//disegnamo lo sfondo su screen
SDL_BlitSurface(bg, NULL, screen, NULL);
}
La pressione del tasto spazio comporta il passaggio da modalità finestra a modalità
schermo intero e viceversa.
Tale passaggio necessità il ridisegnamento dello sfondo sulla superficie screen (riga 104).
//per verificare se un tasto e` mantenuto premuto usiamo SDL_GetKeyState che
//ritorna un'immagine dei tasti premuti
keys = SDL_GetKeyState(NULL);
Alla riga 112 recuperiamo l'immagine della tastiera, quindi i tasti che sono premuti
al momento.
//FRECCIA SU: se il tank non e` posizionato a (x,0) andiamo su
if ( keys[SDLK_UP] )
{
if(tank.get_y())
tank.mov_y(-1);
}
//FRECCIA GIU: approssima il bordo inferiore in base al numero di tank e a DIM_V
if ( keys[SDLK_DOWN] )
{
if(tank.get_y()<(DIM_V-tank.get_h()))
tank.mov_y();
}
//FRECCIA SX: se il tank non e` posizionato a (0,y) andiamo a sx
if ( keys[SDLK_LEFT] )
{
if(tank.get_x())
tank.mov_x(-1);
}
//FRECCIA DX: se il tank non e` posizionato a (DIM_H-tank->w,y) andiamo a sx
if ( keys[SDLK_RIGHT] )
{
if(tank.get_x()<(DIM_H-tank.get_w()))
tank.mov_x();
}
In queste righe gestiamo lo spostamento del primo tank verificando se è premuta una freccia
direzionale.
I vari if interni si preoccupano di mantenere il tank all'interno della superficie screen.
I due metodi per la gestione del movimento verranno trattati all'interno della classe sprite.
//muoviamo il secondo tank, se ci troviamo entro i limiti
if((x_limit2--)>0)
tank2.mov_x(1);
//muoviamo il terzo tank, se ci troviamo entro i limiti
if((x_limit3--)>0)
tank3.mov_x(1);
Gli spostamenti del secondo e del terzo tank avvengono lungo l'asse orizzontale finché
la superficie lo permette, ovvero li facciamo avanzare finché non si raggiunge il bordo destro.
//disegnamo le parti di sfondo interessate
tank.draw_bg();
tank2.draw_bg();
tank3.draw_bg();
//disegniamo i tank
tank.draw_sprite();
tank2.draw_sprite();
tank3.draw_sprite();
Per disegnare i tank sullo schermo correttamente abbiamo bisogno di ridisegnare le parti
di sfondo che vengono alterate dallo spostamento dei tank (148-150). Una volta aggiornate le parti
di sfondo possiamo disegnare i tre sprite (153-155).
Per la spiegazione sul come funzionano questi metodi vi rimando sempre alla classe sprite.
//aggiorniamo lo schermo
SDL_Flip(screen);
}
Una volta terminati i disegni sulla superficie screen la riversiamo sullo schermo con
la function SDL_Flip.
Terminato il ciclo principale non ci resta che uscire.
Spiegazione del sorgente - sprite.h
sprite.h è l'header file della classe sprite, in esso è dichiarata la
struttura della classe. L'implementazione dei metodi qui presentati, verrà illustrata
nel prossimo paragrafo.
#ifndef __CLASS_SPRITE__
#define __CLASS_SPRITE__
#include <vector>
#include <string>
#include <SDL/SDL.h>
#include "frame.h"
using namespace std;
//accediamo alla screen globale
extern SDL_Surface *screen;
//accediamo alla bg globale
extern SDL_Surface *bg;
Nelle prime righe dell'header della classe sprite includiamo gli header necessari
e dichiriamo l'utilizzo del namespace std, poi accediamo alle variabili globali screen e bg
dichiarate nel main (quindi esterne).
class sprite
{
//cordinate dell'img (x,y)
int x, y;
//velocita` di spostamento dell'animazione
int s_speed;
//indice del frame corrente
int cur_frame;
//flag che indica se ci stiamo muovendo
bool moving;
//ultimo tempo di aggiornamento del frame
long update_time;
//idealmente uno sprite e` un insieme di frame
vector<frame *> frames;
Nella parte iniziale della classe sono dichiarati gli attributi privati.
Andando per ordine discendente possiamo incontrare x e y che indicano la posizione dello sprite
sullo schermo, poi la velocità s_speed con cui facciamo muovere il nostro sprire, ovvero il
numero di pixel di cui lo spostiamo ad ogni movimento, di seguito troviamo il contatore
cur_frame che indica il numero del frame visualizzato al momento, poi la flag booleana moving,
la quale indica lo stato dello sprite, ovvero se si è in movimento o se si è fermi, poi
la variabile update_time che tiene traccia dell'ultimo aggiornamento dell'animazione dello
sprite ed infine frames, il vector di puntatori agli oggetti frame che costituiscono la nostra
animazione.
public:
//costruttore
sprite(vector <string> files, int init_x=0, int init_y=0, int speed=2);
//distruttore
~sprite();
Alla riga 30 iniziano i metodi pubblici, nelle prime righe troviamo il costruttore ed
il distruttore (32-34).
Notate i valori di default del costruttore che ci hanno permesso di dichiarare il primo tank
con un solo parametro (riga 66 del main.c).
//function di spostamento frame
void mov_x(int mov=1);
void mov_y(int mov=1);
Subito dopo troviamo i metodi che si occupano del movimento dello sprite, rispettivamente
di quello orizzontale (mov_x) e di quello verticale (mov_y). Anche qui possiamo notare i valori
di default associati al parametro.
//function che ritornano le cordinate posizionali del frame
int get_x() { return x; };
int get_y() { return y; };
//function che ritornano le dimensioni di un frame
int get_w(int ind=0) { return frames[ind]->get_w();}
int get_h(int ind=0) { return frames[ind]->get_h();}
Alle righe 42 e 43 sono presenti due metodi implementati in linea che ritornano
le cordinate x ed y dello sprite.
Alle righe 46 e 47 invece, troviamo altri due metodi in linea che ritornano le dimensioni
orizzontali e verticali del frame di indice ind. Nel caso non venga indicato alcun indice, viene
utilizzato il parametro di default, quindi viene ritornata la dimensione del primo frame.
Ciò avviene perché solitamente i frame hanno tutti la stessa dimensione, quindi è comodo
evitare di passare esplicitamente un parametro.
//function principale, che gestisce il disegno e l'animazione dello sprite
void draw_sprite();
//function che ridisegna il background modificato dallo sprite
void draw_bg();
};
#endif
Gli ultimi due metodi implementati sono quelli per il disegno dello sprite (riga 50)
e della parte di sfondo modificata dallo spostamento di quest'ultimo (riga 52).
Spiegazione del sorgente - sprite.cpp
Ovviamente includiamo l'header sprite.h per operare correttamente sulla classe.
//costruttore che si occupa di inizializzare tutti i dati necessari
sprite::sprite(vector <string> files, int init_x, int init_y, int speed)
{
//contatori
int i,num_files=files.size();
//puntatore temporaneo
frame * temp_frame;
//alloco tutti i frame necessari
for(i=0;i<num_files;i++)
{
temp_frame= new frame (files[i],110);
frames.push_back(temp_frame);
}
Il primo metodo che troviamo è il costruttore, il cui compito è quello di inizializzare
gli attributi in maniera appropriata.
La prima azione che effettuiamo è l'istanziazione dei frame necessari alla nostra
animazione, per fare ciò utilizziamo l'allocazione dinamica allocando tanti frame quanti sono
i file immagine a disposizione (13-17).
I puntatori ai frame allocati vengono inseriti nel vector frames, attributo della classe sprite.
//inizializziamo le cordinate dello sprite
x=init_x;
y=init_y;
//settiamo la velocita` dello sprite
s_speed=speed;
//il primo frame ha indice 0
cur_frame=0;
//il tempo iniziale e` 0
update_time=0;
//inizialmente non ci muoviamo
moving=false;
Le altre inizializzazioni sono decisamente più semplici in quanto andiamo ad assegnare
a x,y ed s_spees i valori passatti al costruttore ed andiamo ad inizializzare cur_frame e update_time
al valore 0, con ovvio significato. Infine consideriamo l'oggetto, inizialmente, fermo, quindi
impostiamo il valore di moving su false.
//function che si occupa di gestire il disegno e l'animazione dello sprite
void sprite::draw_sprite()
{
//se e` passato il tempo necessario passiamo al prossimo frame
if(update_time+(frames[cur_frame]->get_pause())<SDL_GetTicks())
{
cur_frame++;
//se siamo all'ultimo frame torniamo a 0
if(cur_frame==(frames.size()))
cur_frame=0;
//aggiorniamo il tempo dell'ultima modifica
update_time = SDL_GetTicks();
}
Il metodo draw_sprite si occupa di determinare quale frame disegnare di volta in volta
che viene invocato, per fare ciò si utilizza un semplice if che verifica se il tempo dell'ultimo
aggiornamento (inizialmente 0) sommato alla pausa del frame corrente è inferiore al valore ritornato
dalla function SDL_GetTicks. La function SDL_GetTicks ritorna il numero di millisecondi trascorsi
dall'inizializzazione di SDL (da notare che se il programma viene utilizzato per più di 49 giorni ci
possono essere dei comportamenti anomali).
Nel caso la verifica è positiva andiamo ad incrementare il valore di cur_frame prestando attenzione
al caso in cui abbiamo raggiunto l'ultima immagine, quando si verifica ciò riazzeriamo il contatore
(in pratica utilizziamo una sorte di coda circolare per la gestione dei frame).
Infine aggiorniamo update_time, per conservare il tempo dell'ultimo aggiornamento in modo da
ripetere il procedimento alla prossima invocazione.
//indichiamo le cordinate del tank
SDL_Rect dest_t;
dest_t.x = x;
dest_t.y = y;
dest_t.w = frames[cur_frame]->get_w();
dest_t.h = frames[cur_frame]->get_h();
Per disegnare il frame corrente abbiamo bisogno di indicare un rettangolo su screen
dove inserirlo, per fare cià utilizziamo una struttura SDL_Rect inizializzata con le cordinate
attuali dello sprite e con le dimensioni del frame corrente.
//disegnamo il tank su screen
if(moving)
SDL_BlitSurface(frames[cur_frame]->get_surface(), NULL, screen,&dest_t);
else
{
SDL_BlitSurface(frames[0]->get_surface(), NULL, screen,&dest_t);
cur_frame=0;
}
Prima di disegnare il frame verifichiamo se ci troviamo in movimento (moving è true) oppure
se siamo fermi. Nel primo caso non dobbiamo fare altro che copiare la superficie del frame
corrente nell'apposito rettangolo su screen (riga 61), mentre, se stiamo fermi, copiamo la superficie
del primo frame e impostiamo cur_frame a 0, ovvero all'indice del primo frame, in modo da far
ripartire le future animazioni dall'inizio.
//consideriamo l'animazione finita
moving = false;
Prima di terminare impostiamo moving su false, presumendo che il movimento sia finito,
di seguito vedremo che il flag verrà aggiornato ad ogni movimento.
//function che ridisegna il background modificato dallo sprite
void sprite::draw_bg()
{
//aggiorniamo il rettangolo che circonda il tank
SDL_Rect rect_bg;
rect_bg.x = x-s_speed;
rect_bg.y = y-s_speed;
rect_bg.w = frames[cur_frame]->get_w()+(s_speed*2);
rect_bg.h = frames[cur_frame]->get_h()+(s_speed*2);
Il metodo draw_bg si occupa di risistemare la parte di sfondo alterata dal moviemnto del
nostro sprite. Per fare ciò andiamo a ridisegnare un rettangolo che ha come cordinate le cordinate
dello sprite meno il valore della velocità, e che ha come dimensioni le dimensioni del frame
aumentate del doppio della velocità.
Ciò ci permette di avere un rettangolo più grande dello srpite di s_speed pixel per lato, in
pratica si usa la stessa tecnica usata nella guida mia precedente guida sulla gestione delle
immagini in SDL
[6].
//copiamo la parte di bg necessaria su screen
SDL_BlitSurface(bg, &rect_bg, screen, &rect_bg);
La parte di screen alterata viene sovrascritta con la corrispettiva parte di bg,
la quale non subendo modifiche si è mantenuta inalterata.
//function spostamento orizzontale
void sprite::mov_x(int mov)
{
//ci spostiamo in base ad s_speed
x+=mov*s_speed;
//segnaliamo che ci stiamo muovendo
moving=true;
}
Il metodo mov_x è semplice, in pratica modifica la posizione orizzontale dello
sprite in base al valore dell'attributo s_speed, che determina il numero di pixel dello
spostamento e del parametro mov che costituisce la direzione di quest'ultimo (quindi
viene passato 1 o -1 a seconda che ci si voglia spostare a destra o a sinistra).
Ogni volta che si esegue questo metodo viene impostato il flag moving su true, in modo
da effettuare le azioni appropriate in draw_sprite.
//function spostamento verticale
void sprite::mov_y(int mov)
{
//ci spostiamo in base ad s_speed
y+=mov*s_speed;
//segnaliamo che ci stiamo muovendo
moving=true;
}
mov_y è il corrispettivo di mov_x per gli spostamenti verticali, pertanto è inutile
ripetere quanto già detto.
//distruttore
sprite::~sprite()
{
//contatori
int i,num_files=frames.size();
//chiamiamo il distruttore dei vari frame allocati precendentemente
for(i=0;i<num_files;i++)
{
frames[i]->~frame();
}
}
L'ultimo metodo della classe è il distruttore, nel nostro caso si occupa di
invocare i distruttori di tutti i frame allocati.
Spiegazione del sorgente - frame.h
frame.h è l'header file della classe frame, in esso è dichiarata la
struttura della classe. L'implementazione dei metodi qui presentati, verrà illustrata
nel prossimo paragrafo.
#ifndef __CLASS_FRAME__
#define __CLASS_FRAME__
#include <string>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
using namespace std;
Come sempre la prima parte è dedicata all'inclusione degli header necessari e
alla dichiarazione di utilizzo del namespace.
class frame
{
//la superficie del frame
SDL_Surface *The_Frame;
//pausa dal prossimo frame
int The_Pause;
Gli attributi privati della classe sono soltanto due, un puntatore a SDL_Surface
(The_Frame), che servità per l'associazione del frame ad un'immagine e un intero (The_Pause),
che servirà per indicare la pausa (in millisecondi) che si desidera dal prossimo frame.
public:
//costruttore
frame(string img, int pause);
//distruttore
~frame();
Anche qui i primi metodi dichiarati sono il costruttore ed il distruttore.
//function che ritorna una superficie dell'animazione
SDL_Surface * get_surface() { return The_Frame; };
Alla riga 23 è presente un metodo in linea che ritorna il puntatore alla superficie
del frame, ovvero alla superficie contenente l'immagine, come abbiamo visto nella classe sprite,
tale informazione è necessaria per poter disegnare i vari frame.
//function che ritorna la larghezza del frame
int get_w() { return The_Frame->w; };
//function che ritorna l'altezza del frame
int get_h() { return The_Frame->h; };
//function che ritorna la pausa impostata
int get_pause() { return The_Pause; };
};
#endif
Gli ultimi metodi presenti sono implementati tutti in linea e forniscono funzioni
"informative" per la classe sprite, infatti abbiamo due metodi che ritornano le dimensioni
del frame (righe 26 e 28) e un metodo che ritorna la pausa impostata (riga 31).
Spiegazione del sorgente - frame.cpp
Ovviamente includiamo l'header frame.h per operare correttamente sulla classe.
//costruttore, inizializzamo i dati necessari
frame::frame(string img,int pause)
{
//carichiamo l'immagine necessaria
The_Frame=IMG_Load(img.c_str());
//colore da rendere trasparente
Uint32 colorkey;
//prendiamo la mappatura del colore #0000FF in base al nostro specifico formato di pixel
colorkey = SDL_MapRGB(The_Frame->format, 0, 0, 0xFF);
//impostiamo il colore come trasparente
SDL_SetColorKey(The_Frame, SDL_SRCCOLORKEY|SDL_RLEACCEL, colorkey);
Il primo metodo è il costruttore, il quale si occupa di caricare l'immagine passata e di
rendere trasparente lo sfondo di quest'ultima, per fare ciò utilizziamo il presupposto che
l'immagine abbia uno sfondo di colore #0000FF.
//pausa dal prossimo frame
The_Pause=pause;
}
La seconda azione del costruttore è l'impostazione della pausa secondo il parametro
passato.
//distruttore, liberiamo la superficie necessaria
frame::~frame()
{
SDL_FreeSurface(The_Frame);
}
Il distruttore è banale, in quanto si occupa solo di disallocare lo spazio riservato
per l'immagine.
Compilazione
Per compilare il nostro sorgente è necessario utilizzare come segue il gcc:
g++ -s -o sprite main.cpp sprite.cpp frame.cpp `sdl-config --cflags --libs` -lSDL_image
L'opzione -s effettuerà lo strip sull'eseguibile, rimuovendo le parti non
utilizzate, rendendo quindi, le dimensioni decisamente ridotte.
L'opzione -o imposterà il nome dell'eseguibile in tank.
L'uso di `sdl-config --cflags --libs` ci darà tutte le opzioni necessarie per
includere ed utilizzare la libreria SDL, mentre -lSDL_image caricherà la
libreria SDL_image.
Oppure è possibile sfruttare il Makefile digitando
Miglioramenti futuri
Il livello di questo programma è stato mantenuto davvero molto basso per
permettere la scrittura di questa guida, ciò significa che c'è ancora molto lavoro da
fare pere rendere le classi implementate utilizzabili all'interno di un'applicazione.
Di seguito riporto una lista di caratteristiche e migliorie che ho pensato di realizzare
nelle implementazioni future:
-
Gestione di animazioni multiple a seconda dei tempi e degli eventi.
-
Gestione dello spostamento da un punto ad un altro con relativa animazione.
-
Gestione di operazioni grafiche sulle singole immagini dei frame.
-
Gestire le collisioni con relative animazioni.
-
Gestire ulteriori azioni grafiche sui frame.
-
Migliorare la gestione della trasparenza passando il colore da rendere trasparente.
-
Migliorare il passaggio dei nomi dei file delle immagini, magari passando un solo nome
e lasciando alla classe sprite il compito di aggiungere la numerazione.
La lista è un punto di partenza per la realizzazione di classi utilizzabili,
la pubblico insieme alla guida per dare uno spunto a chi voglia iniziare a lavorarci
per i propri scopi.