Programmare la GP2X - parte 2
posted in programmazione, c++, sdl, gp2x |La volta scorsa avevamo scomposto la semplice simulazione della pallina che rimbalza in due classi principali, Ambiente e Corpo, che interagivano fra loro. Adesso faremo un po’ di refactoring e quindi inizieremo a trarre un po’ di vantaggi dal design object oriented.
Iniziamo da un’osservazione. Il corpo si muove in un ambiente, il quale impone al corpo le proprie caratteristiche fisiche, ma ogni corpo reagisce alle caratteristiche dell’ambiente secondo la propria natura. Nella nostra simulazione abbiamo un ambiente chiuso ai quattro lati dello schermo e quando la palla raggiunge un lato rimbalza e torna indietro; ma potrebbe esserci un altro tipo di palla che quando si scontra con una parete rimane ferma e incollata all’ostacolo, magari perché “appiccicosa”.
Questa considerazione porta ad un refactoring, che introduce un semplice sistema con il quale l’ambiente comunica ad un corpo il raggiungimento di un lato. Per questo introduciamo in Ambiente il metodo checkBoundaries() la cui responsabilità consiste nel monitorare le collisioni fra il corpo e i confini dell’ambiente.
void Ambiente::checkBoundaries ( Corpo *corpo ) { SDL_Rect posizione = corpo->posizione(); if ( posizione.x < 0 ) { corpo->leftCollision(); // lato sinistro posizione.x = 0; } if ( posizione.x + 25 > width ) { corpo->rightCollision(); // lato destro posizione.x = width - 25; } if ( posizione.y < 0 ) { corpo->topCollision(); // lato superiore posizione.y = 0; } if ( posizione.y + 25 > height ) { corpo->bottomCollision(); // lato inferiore posizione.y = height - 25; } corpo->setPosizione ( posizione ); }
Il metodo controlla la posizione del corpo e, se questo raggiunge i bordi, invia il messaggio opportuno a Corpo (detto in altri termini, chiama il metodo di callback appropriato). Da notare come venga corretta la posizione del corpo nel caso in cui “sfori” oltre i confini: non è il miglior codice possibile, ma per il momento funziona e non è questa la parte che ci interessa al momento.
In conseguenza di ciò, il metodo loop() cambia così:
void Ambiente::loop() { //loop principale while ( !m_esci ) { /* per prima cosa disegnamo lo sfondo sullo schermo, l’operazione che copia il nostro sprite sullo schermo si chiama “blit” */ SDL_BlitSurface ( m_sfondo, NULL, m_schermo, NULL ); // movimento del corpo checkBoundaries ( m_corpo ); m_corpo->move(); //disegnamo il corpo alle coordinate calcolate SDL_BlitSurface ( m_corpo->surface(), NULL, m_schermo, & ( m_corpo->posizione() ) ); //infine facciamo apparire il frame creato sullo schermo SDL_Flip ( m_schermo ); } SDL_Quit(); }
E questa è uno dei metodi di callback di Corpo:
void Corpo::leftCollision() { m_velocita.x = abs ( m_velocita.x ); }
L’implementazione corrente, quando si raggiunge il lato sinitro, non fa altro che reimpostare opportunamente la componente orizzontale del vettore di velocità. Analogamente i metodi che gestiscono le collisioni con gli altri lati.
Il vantaggio introdotto dal refactoring del codice iniziale è la possibilità di cambiare il comportamento di Corpo semplicemente riscrivendo i metodi di risposta alle collisioni senza toccare altro codice.
A questo punto è il momento di introdurre nel nostro progetto qualche design pattern, per la precisione il pattern Strategy, il quale ci permette di isolare opportunamente il codice che regola i movimenti del corpo. Definiamo innanzitutto l’interfaccia Moto, che determina quali sono i metodi richiesti dall’algoritmo di movimento:
class Moto { public: virtual SDL_Rect velocita(const SDL_Rect velocita) = 0; };

Teniamo le cose semplici e scriviamo un algoritmo con il solo metodo velocita(), che in base alla velocità fornita come parametro calcola e restituisce la nuova velocità. E la classe MotoUniforme ne fornisce la più semplice implementazione possibile:
SDL_Rect MotoUniforme::velocita ( const SDL_Rect velocita ) { return velocita; }
Il moto uniforme fa quello che ci si aspetta: la velocità ad un certo istante è uguale a quella dell’istante precedente. Non ci resta che modificare Corpo perché faccia uso dell’algoritmo di moto, aggiungendo l’attributo moto, il metodo setMoto() e adeguando il costruttore. La nuova dichiarazione di Corpo diventa questa:
class Corpo { public: Corpo ( Moto* moto, const SDL_Rect posizione, const SDL_Rect velocita ); SDL_Rect posizione() const { return m_posizione; } SDL_Rect velocita() const { return m_velocita; } SDL_Surface* surface() const { return m_surface; } void setMoto ( Moto* moto ) { m_moto = moto; } void setPosizione ( const SDL_Rect posizione ) { m_posizione = posizione; } void leftCollision(); void rightCollision(); void topCollision(); void bottomCollision(); void move(); private: SDL_Surface* m_surface; // struttura delle sdl che conterra’ le coordinate dello sprite SDL_Rect m_posizione; // vettore per la velocita’ del corpo SDL_Rect m_velocita; // legge che regola il movimento Moto* m_moto; void setVelocita ( SDL_Rect velocita ) { m_velocita = velocita; } };
Il metodo move() adesso delega all’algoritmo di moto il calcolo della velocità e modifica di conseguenza la posizione dell’oggetto.
void Corpo::move() { setVelocita ( m_moto->velocita ( velocita() ) ); m_posizione.x += m_velocita.x; m_posizione.y += m_velocita.y; }
E siamo tornati al punto di partenza: la pallina si muove a velocità uniforme rimbalzando ai quattro lati dello schermo. Perché allora tutto questo codice e queste complicazioni per fare ciò che il solo metodo main() dell’esempio originale fa in poco più di 20 righe? La risposta sta nel fatto che, grazie a questa struttura, diventa un gioco da ragazzi modificare il movimento della pallina. Supponiamo di volere che la pallina cada verso il basso come fosse soggetta alla forza di gravità e che rimbalzi per terra: è sufficiente scrivere una nuova implementazione dell’interfaccia Moto, che chiamiamo MotoAccelerato, il cui metodo velocita() tenga conto dell’accelerazione.

SDL_Rect MotoAccelerato::velocita ( const SDL_Rect velocita ) { SDL_Rect v = velocita; // la velocita sull’asse x rimane costante // sull’asse y si applica l’accelerazione v.y += m_accelerazione; return v; }
Grazie al pattern Strategy non solo potremo aggiungere facilmente diversi tipi di moto, ma potremo avere contemporaneamente diverse palline che si muovono con algoritmi diversi, e addirittura cambiare il tipo di moto a runtime!
Per il momento ci limitiamo a decidere l’algoritmo al tempo di compilazione e lo facciamo nel metodo main():
int main ( int argc, char *argv[] ) { SDL_Rect pos; pos.x = 160; pos.y = 40; SDL_Rect vel; vel.x = 3; vel.y = 0; Corpo corpo ( &MotoAccelerato(), pos, vel ); Ambiente ambiente; ambiente.addCorpo ( &corpo ); ambiente.loop(); exit ( 0 ); }
Al momento di istanziare il corpo decidiamo anche l’implementazione di Moto iniziale. Per tornare al moto originale basta sostituire MotoAccelerato() con MotoUniforme().
Nell’archivio trovate sorgenti ed eseguibile per la console.
