mercoledì 31 agosto 2011

Il gioco indie che forse non conoscete: Bomber Planet!

Ci sono alcuni indie che è sempre bello seguire, e tra questi quel tizio che risponde al nick di Adamatomic, o Mr.Canabalt per i più.

In occasione della ventunesima edizione del Ludum Dare, il buon Adam ha sfornato uno dei 509 giochi presentati. Bomber Planet non è il migliore nè il peggiore, possiamo anzi dire che si mescola tra i tanti, ma è uno di quelli che ho potuto provare (sì, ho provato anche quello di Notch e no, non ne parlerò) e devo dire che mi ha lasciato quel piacevole senso di deja vù.



Ricorda moltissimo un gioco che ho amato nell'Amiga 500: Bomberman. Il gameplay è pressochè identico e il titolo un chiaro richiamo, oltre alla grafica in pixel art. L'unica differenza è che, anzichè farsi strada livello dopo livello nella vecchia concezione arcade, in Bomber Planet occorre esplorare un complesso di caverne per trovare l'uscita (in tema con il titolo del contest: Escape), sfasciando blocchi di pietra e strane creature mediante bombe ad orologeria.

In definitiva un buon coffeebreak game, che ha avuto il merito di riuscire a rievocare davvero il retrogaming, almeno al sottoscritto.

mercoledì 24 agosto 2011

Il servizio web che forse non conoscete: Twitch TV!

Quando ero piccolo e andavo al mare con i miei passavo ore ed ore al bar nella zona cabinet. All'epoca i PC, o meglio i loro antenati, erano poco diffusi e io, neanche a dirlo, non ce l'avevo. Escludendo le sale giochi, che erano per i grandi e irraggiungibili con i miei mezzi di locomozione e la mia età, le uniche mie parentesi videoludiche erano rappresentate da quegli affari ingombranti e luccicosi che trovavo nei bar del mare. Ogni stabilimento ne aveva almeno due o tre, e non esistevano i video poker, ma solo i Wonder Boy, i Final Fight, i Bubble Bobble e gli Altered Beast.

I miei ovviamente non capivano perchè mai volessi stare davanti a quegli affari anzichè in acqua e al sole, e il più delle volte mi toccava obbedire e andare a sguazzare. Ma quando la guardia si abbassava ecco che tornavo in quell'angolino proibito, dove c'era sempre qualche ragazzo più grande che pigiava sui tre tasti colorati.

Giocare costava. 100 lire all'inizio dei miei ricordi, poi 200. Quando iniziò a costare 500 ero già più grande, avevo l'Amiga a casa ed ero più sensibile ad altri richiami da spiaggia. Ai tempi delle 200 lire raccattavo gli spicci in giro e riuscivo a garantirmi tre o quattro partite, ma di quelle partite (sempre troppo brevi) ricordo poco. Ricordo invece quanto fosse bello guardare gli altri giocare. E non ero il solo. Quando arrivava il giocatore quello bravo, si formava un capannello di ragazzini attorno al cabinet. Qualcuno strisciava per terra le sedie fino ai giochi e ci saliva sopra, per vedere meglio da sopra le teste degli altri.

"Dove è arrivato?" domandava qualcuno di passaggio, per il quale vedere lo schermo era ormai impossibile. "Al terzo mostro" rispondeva un altro, senza voltarsi. L'adrenalina saliva. Nuovi ragazzini si accalcavano. Il foglietto attaccato con lo scotch sulla testa del cabinet, quello con la scritta "record:" e il numero inarrivabile, riceveva gli sguardi di tutti man mano che lo score sul display cresceva.

"Sesto schema!" annunciava qualcuno. Chi non aveva mai visto il sesto schema sgomitava per raggiungere un varco visivo allo schermo. Qualcuno dal bar si lamentava per la cagnara e per un po' si procedeva bisbigliando. In spiaggia, dove c'era il sole, l'acqua e la sabbia e il mondo degli adulti era rimasto agli anni '60, giungevano le voci di corridoio più improbabili. Qualcuno stava finendo il gioco. Con quanti gettoni? Uno. Uno? Uno!

Non ho mai visto finire un gioco al mare. Però, in un paio di epiche occasioni, ho visto il barista farsi strada tra una dozzina di ragazzini in costume eccitati e saltellanti e cambiare il numero sul foglietto "record".

Ora, quei giorni sono finiti. Ma c'è un sito in cui questo può accadere di nuovo. Su Twitch TV ci sono infatti i video di migliaia di videogiocatori. Il sito è navigabile per gioco, quindi interessante per vedere il gameplay del tal gioco o ammirare l'abilità di quello bravo, o magari capire come superare un certo punto a noi impossibile.

Certo, non è proprio la stessa cosa. Anche perchè noi, da ragazzini, non sapevamo nemmeno cosa fosse il gameplay.

lunedì 22 agosto 2011

HowTo: Guitar recording in linux

La settimana di ferragosto ha sacrificato in parte le attività di codelicks, ma mi ha dato modo di fare qualche esperimento casalingo con la mia fida Fedora.

L'HowTo di oggi si discosta per un momento dalla programmazione e dai videogiochi e si concentra invece sul come riuscire a registrare la propria chitarra, con tanto di batteria, usando software free e open source disponibile nei repository.

Il mio obiettivo era infatti questo: avevo bisogno di costruire una traccia di batteria, anche minimale, e di registrare una base di accompagnamento in modo da poterci poi suonare sopra, per esercizio, assoli, improvvisazioni e quant'altro.

Ci sono riuscito, ma c'è stato bisogno del lavoro sinergico di ben 4 software! Eccoli:

JACK

Il primo software di cui avrete bisogno è JACK, un server audio che si prefigge lo scopo di intercettare l'output audio di una applicazione e indirizzarlo come input in una seconda applicazione. Sembra complicato, ma per quel che dovremo fare noi sarà sufficiente scaricare JACK e il suo client (io ne ho usato uno basato su QT) e avviarlo. Al resto penserà lui.


Hydrogen

Il secondo software ci permetterà di editare la nostra batteria: Hydrogen. Si tratta di una drum machine veramente semplice da utilizzare e al tempo stesso molto flessibile. Editando la griglia nella parte bassa possiamo piazzare i colpi col suono del pezzo in riga: cassa, rullante, charleston (aperto e chiuso), tom alto e basso, piatti ecc. Supporta naturalmente ritmi binari e ternari e la possibilità di variare facilmente la velocità, espressa in bpm.


Hydrogen supporta due concetti: pattern e song. Il primo rappresenta il mattoncino col quale costruire il brano, ovvero la song. Il pattern è solitamente piccolo, una o due battute, e lo si edita tramite la griglia in basso. La song è l'unione consecutiva di uno o più pattern, e la si crea tramite la griglia in alto, dove le righe sono i pattern.

In questo modo è veramente possibile creare basi di batteria anche complesse con il minimo sforzo, utilizzando pochi semplici pattern.

Nel mio caso specifico, ho creato un solo pattern di una battuta con un 4/4 basilare di cassa e rullante, quindi una song di una battuta.

Al termine è possibile esportare la song in formato audio.

Rakarrack

Ecco chi fa la vera magia: trasformare il nostro pc in un rack per chitarra, con amplificazione ed effettistica! Rakarrack fa esattamente questo. E' sufficiente inserire la chitarra nella presa microfono (senza alcuna preamplificazione), assicurarsi che JACK rilevi Rakarrack e suonare. Dalle casse uscirà il suono modificato dal rack di effetti caricati.


E' possibile costruirsi il proprio rack o provarne le decine di preimpostati (dai nomi evocativi come "Satriani", "Jazzy", "Metal tone" ecc.). Gli effetti possono venire spenti/accesi, regolati nei vari parametri, riordinati come sequenza, sostituiti con altri.

Nella mia esperienza ho notato però alcune cosucce:

  1. Senza specificare nelle opzioni di preferenza l'abilitazione automatica degli effetti non ottenevo alcun suono: l'abilita/disabilita manuale sembrava non funzionare
  2. Per ottenere una migliore qualità del suono ho dovuto abbassare il livello di input (-20 circa) ed aumentare leggermente quello di output (+10 circa): così facendo ho eliminato una fastidiosa e non voluta distorsione, specie sugli accordi
  3. Probabilmente dipenderà dal mio portatile e dalla sua non proverbiale potenza, ma il segnale audio in uscita ha un ritardo di circa 1 secondo.

Audacity

Ed è finalmente giunto il momento di unire il tutto! Serve un software per fare editing multitraccia. Il più semplice, quasi banale, che conosco è Audacity.


Lo apro e importo come traccia audio la song esportata da Hydrogen, quindi in effetti->ripeti la faccio ripetere per 30 volte, ottenendo un bel 4/4 di 30 battute.

Nella toolbar ho un paio di combobox in cui specificare le sorgenti di input e output. Cerco JACK e lo imposto, quindi come input seleziono rakarrack (che mi viene mostrato grazie a JACK) e come output quello di sistema.

Faccio REC e suono: la mia chitarra distorta appare come linea bella frastagliata nella nuova traccia audio!

Al termine della registrazione mi rendo conto che il tutto è fuori tempo rispetto alla batteria... ricordate il problemino del ritardo? Già...

Ma niente paura, mi posiziono sul primo attacco della mia traccia di chitarra, seleziono tutto quel che segue, taglio e sposto il cursore con le frecce sul battere di cassa corretto. Incolla e via, il brano è di nuovo perfettamente a tempo.

Tutto il resto, ovvero registrare una buona traccia, è (purtroppo) un'altra storia...

venerdì 12 agosto 2011

Sviluppare giochi per Google+


Google+ non necessita ormai più di presentazioni. Ad ormai due mesi dal suo lancio la piattaforma social di Google ha guadagnato una notevole fetta di utenti che, per chissà quale motivo, sembrano essere in maggioranza tecnici e creativi del web: programmatori, grafici, musicisti e, naturalmente, tanti sviluppatori indie.

David Glazer, un responsabile di progetto di Google, ha annunciato sul suo profilo l'apertura di un interessante blog: the Google+ Platform blog.

Scopo del Platform Blog sarà quello di introdurre gli sviluppatori volenterosi alle API di integrazione di Google+, e aprire quindi la strada ad una nuova generazione di applicazioni e, quel che interessa a noi, giochi. E' infatti quel che viene promesso in questo post del blog ufficiale di G+.

Una nuova corsa all'oro quindi. Resta da capire se G+ opterà per un sistema di scrematura in stile Apple (che non sembra comunque aver dato i risultati attesi, si vedano le FartApp...) o se lascerà aperte le frontiere di questo nuovo far west.

mercoledì 10 agosto 2011

Il gioco indie che forse non conoscete: Legends of Yore!

Legends of Yore è un altro roguelike grafico ben fatto, ricco e profondo, che giunge in un momento di grande vitalità per il genere (Desktop Dungeons, Dungeons of Dredmor, Cardinal Quest, solo per citarne alcuni). Si può scegliere tra tre personaggi: l'immancabile guerriero e la sua controparte, il mago, più l'arciere, che va a riempire lo spazio solitamente occupato da classi quali il ladro.


I tre personaggi offrono opzioni, abilità e stili di gioco differenti, risultando comunque ben calibrate. Il mondo di gioco appare solido e la grafica a 8 bit, molto curata e coerente, aumenta la piacevole sensazione di trovarsi davanti ad un gioco completo.

I livelli dei dungeon sono generati proceduralmente ogni volta che vi ci accede, mentre le "esterne" (città, villaggi, boschi, pianure ecc.) sono formate da mappe fisse.

Lo sviluppatore sembra aver ovviato al problema del field of view del personaggio optando per una risoluzione video veramente infima: si gioca in un rettangolino di finestra degno di uno smartphone, e forse non a caso: Legends of Yore è infatti disponibile, oltre che per PC, per iOS e Android.

(NB: La versione per desktop contiene anche l'opzione per visualizzare a 800x600, ma in questo caso la mancata implementazione del fields of view risulta in un'area visualizzata troppo ampia che può, in parte, togliere il gusto dell'esplorazione.)

Altro punto a favore è dato dallo sviluppo ancora in corso del progetto, che anzi rilascia continui aggiornamenti (scaricati e installati automaticamente all'avvio del gioco). Potete seguire le vicende delle Leggende sul forum del sito e sul canale twitter.

In definitiva un altro bel colpo messo a segno da un singolo indie!

lunedì 8 agosto 2011

Tutorial: Scriviamo un gioco in C++... Tris! (parte 5)

La volta scorsa abbiamo implementato le funzionalità di menu, permettendo di raggiungere la schermata di gioco, tornare indietro e uscire. La stato di gioco mostrava però solo una schermata nera... ben poco entusiasmante!

Oggi colmeremo questa lacuna, giungendo finalmente ad implementare il gioco. Fino ad ora, infatti, abbiamo analizzato e scritto solo codice di contorno. Quel che faremo in questo post sarà riempire invece di istruzioni la classe PlayGameState. La struttura di PlayGameState è naturalmente la stessa di TitleScreenGameState, ereditando entrambe da AbstractGameState. Vediamo, metodo per metodo, come cambia invece l’implementazione.

Il Costruttore


PlayGameState::PlayGameState(ApplicationMessageHandler* pMessageHandler) :
AbstractGameState(pMessageHandler)
{
//ctor
m_pX = 0;
m_pO = 0;
m_pWinText = 0;
}

Poca roba. I tre puntatori che vengono inizializzati a null sono puntatori a SDL_Surface e conterranno le immagini rispettivamente del simbolo X (del player), del simbolo O (del computer) e del rendering di un testo che mostreremo in caso di vittoria di qualcuno.

Il Distruttore


PlayGameState::~PlayGameState()
{
//dtor

SDL_FreeSurface(m_pX);
SDL_FreeSurface(m_pO);

TTF_CloseFont(m_pFont);

if(m_pWinText)
{
SDL_FreeSurface(m_pWinText);
}
}

Osservare il distruttore è un po’ come leggere i titoli di coda di un film prima di aver visto il film! Possiamo capire chi erano gli attori e intuire quali parti recitavano. Vediamo infatti la liberazione dalla memoria delle tre SDL_Surface e la chiusura di un inedito TTF_Font.

Load


int
PlayGameState::Load()
{
int result = 0;
result = LoadOptimizedImage("tris_x.bmp", &m_pX);
result += LoadOptimizedImage("tris_o.bmp", &m_pO);

m_pFont = TTF_OpenFont("arial.ttf", 24);

return result;
}

Beh, qui in effetti qualche nodo giunge al pettine, e si scopre cosa sia il famigerato m_pFont: SDL_ttf ci permette infatti di creare un oggetto in grado di disegnare del testo su una surface con un certo font e una certa dimensione, prendendo le informazioni da uno standard file ttf. Se non avete mai affrontato il problema del rendering del testo in altri contesti grafici, non sapete quanto questo sia utile!

HandleKeyboardInput


void
PlayGameState::HandleKeyboardInput(const SDLKey& key, const SDL_EventType& type)
{
if(type == SDL_KEYDOWN)
{

switch(key)
{
case SDLK_ESCAPE:
m_pMessageHandler->HandleMessage(EMT_TITLE);
break;
}

}
}

Ok, niente di nuovo, semplicemente alla pressione di ESC mandiamo alla classe principale il segnale EMT_TITLE, che servirà a cambiare lo stato corrente da Play a TitleScreen.

HandleMouseInput


void
PlayGameState::HandleMouseInput(const int x, const int y, Uint8 button, const SDL_EventType& type)
{
SDL_Rect gridRect;
gridRect.h = 282; gridRect.w = 282; gridRect.x = 19; gridRect.y = 99;

// se qualcuno ha già vinto non si gioca più
if(m_PlayerWins || m_ComputerWins) return;

// ho fatto click dentro la griglia?
if(IsPointInRect(x, y, gridRect))
{
// tasto sinistro?
if(button == SDL_BUTTON_LEFT && type == SDL_MOUSEBUTTONUP)
{
// quale quadrante?
int gridx = (x - 19) / 94;
int gridy = (y - 99) / 94;

if(m_Grid[gridy][gridx] != EGS_NONE) return;

m_Grid[gridy][gridx] = EGS_X;

if(CheckPlayerVictory())
{
m_PlayerWins = true;
if(m_pWinText)
{
SDL_FreeSurface(m_pWinText);
}
SDL_Color textColor = {0,255,0};
m_pWinText = TTF_RenderText_Solid(m_pFont, "Player Wins!", textColor);
}
else
{

// eseguo la mossa del computer

if(m_SymbolsPlacedCount < 8)
{

bool ok = false;
while(!ok)
{
gridx = rand()%3;
gridy = rand()%3;

if(m_Grid[gridy][gridx] == EGS_NONE)
{
ok = true;
m_Grid[gridy][gridx] = EGS_O;
}
}

m_SymbolsPlacedCount += 2;
}

if(CheckComputerVictory())
{
m_ComputerWins = true;
if(m_pWinText)
{
SDL_FreeSurface(m_pWinText);
}
SDL_Color textColor = {255,0,0};
m_pWinText = TTF_RenderText_Solid(m_pFont, "Computer Wins!", textColor);
}
}
}
}
}

Uh! Questo è un bel pezzo di codice! Corposo e denso di significati... in effetti si tratta della gestione del click del mouse, ovvero il cuore del gameplay. Qui è, in sostanza, il posto dove si fa il gioco.

Scopo di questa funzione è aggiornare lo stato dell’oggetto m_Grid, una griglia 3x3 di un enumerativo che descrive i tre stati in cui si può trovare una cella: vuota, con una X o con un O. Ci sono anche un paio di booleani che tengono conto del fatto che qualcuno tra player o computer possa aver vinto la partita.

Il primo controllo che viene fatto è per capire se il click del mouse rientra nell’area in cui abbiamo disegnato la griglia (come avviene nel metodo Draw, naturalmente). Le costanti hardcodate (19, 94 e 99) sono una piccola vergogna che ho voluto lasciare, un po’ per far nascere nel lettore il giusto senso di ribrezzo, un po’ per non appesantire la leggibilità del codice con troppi nomi. Rappresentano comunque la distanza della griglia dal bordo (19 pixel) e la dimensione del lato di ogni cella (94 pixel). 99 è invece l’offset verticale (sempre in pixel) da cui parte la griglia. Non perdeteci tempo, sono solo numeri.

Una volta stabilito che il click è ricaduto all’interno della griglia, si tratta di decidere in quale cella. A questo scopo vi è il piccolo calcolo che porta a valorizzare le due variabili gridx e gridy, con le quali possiamo accedere alla matrice m_Grid e, se la cella è libera, segnarla con il simbolo del player, la X. Subito dopo parte una procedura che verifica se la mossa sia stata vincente per il player e, nel caso, imposta il booleano a guardia di questo evento e crea la SDL_Surface con il testo “Player Wins!” di colore verde.

Se il player non ha ancora vinto si procede invece con la mossa del computer. Questo è il punto in cui dovremo tornare in un secondo momento, quando si tratterà di sviluppare la AI. Per ora il computer è veramente sciocco e si limita a piazzare il suo segno in un punto a caso, scegliendolo pure in modo non troppo furbo (ci sarebbe potenzialmente un deadlock, ma sono talmente poche le possibilità che il prima o poi uno spiazzo vuoto lo trova dovrebbe portare via pochi cicli).

Viene infine incrementato un contatore delle mosse: già, perchè le partite finiscono anche in parità, quando nessuno fa tris ma non esistono più celle disponibili.

Il caso di vittoria del computer è, naturalmente, speculare a quello già visto per il player.

Update


void
PlayGameState::Update()
{

}

Mmm, ok, lo ammetto... Tris è veramente un gioco povero! La funzione Update ce lo dimostra...

Draw

void
PlayGameState::Draw()
{
SDL_Surface* screen = SDL_GetVideoSurface();

// sbianco lo schermo

SDL_FillRect(screen, 0, 0xffffff);

// disegno la griglia di gioco 3x3

// linee verticali
SDL_Rect grid;
grid.h = 282; grid.w = 3; grid.x = 19+94; grid.y = 99;

SDL_FillRect(screen, &grid, 0x000000);

grid.h = 282; grid.w = 3; grid.x = 19+94+94; grid.y = 99;
SDL_FillRect(screen, &grid, 0x000000);

// linee orizzontali
grid.h = 3; grid.w = 282; grid.x = 19; grid.y = 99+94;
SDL_FillRect(screen, &grid, 0x000000);

grid.h = 3; grid.w = 282; grid.x = 19; grid.y = 99+94+94;
SDL_FillRect(screen, &grid, 0x000000);

// disegno i simboli in griglia
SDL_Rect dst;
for(int r=0; r<3; ++r)
{
for(int c=0; c<3; ++c)
{
dst.h = 94; dst.w = 94; dst.y = 99 + (r*94); dst.x = 19 + (c*94);

switch(m_Grid[r][c])
{
case EGS_X:
SDL_BlitSurface(m_pX, 0, screen, &dst);
break;

case EGS_O:
SDL_BlitSurface(m_pO, 0, screen, &dst);
break;

case EGS_NONE:
break;
}
}
}

if(m_PlayerWins || m_ComputerWins)
{
dst.h = m_pWinText->h; dst.w = m_pWinText->w; dst.x = 160 - (m_pWinText->w / 2); dst.y = 240 - (m_pWinText->h / 2);
SDL_Rect box;
box.h = dst.h * 2; box.w = 320; box.x = 0; box.y = dst.y - (dst.h / 2);
SDL_FillRect(screen, &box, 0x000000);
SDL_BlitSurface(m_pWinText, 0, screen, &dst);
}
}

Ecco svelati i segreti dietro ai numeretti hardcodati! Lascio a voi l’analisi di questa funzione, che altro non fa che disegnare la griglia (usando delle chiamate a SDL_FillRect e disegnando con esse le quattro linee necessarie) e interpretare i dati nella matrice m_Grid per piazzare i giusti simboli all’interno delle celle.

Nel caso di vittoria di qualcuno, poi, viene mostrato il testo all’interno di un box nero, al centro dello schermo.

Reset


void
PlayGameState::Reset()
{
m_SymbolsPlacedCount = 0;

m_PlayerWins = m_ComputerWins = false;

if(m_pWinText)
{
SDL_FreeSurface(m_pWinText);
}
m_pWinText = 0;

for(int r=0; r<3; ++r)
{
for(int c=0; c<3; ++c)
{
m_Grid[r][c] = EGS_NONE;
}
}
}

L’implementazione dello stato di gioco ha reso necessaria l’introduzione di un ulteriore metodo virtuale puro nella classe base: Reset. Con Reset si riportano alla situazione iniziale tutte le variabili membro dello stato. Viene chiamata da Tris ogni volta che uno stato diventa lo stato attuale, permettendo in questo caso partite successive.

Condizioni di vittoria


bool
PlayGameState::CheckComputerVictory()
{
return CheckVictoryForSymbol(EGS_O);
}

//-----------------------------------------------------------------------------
bool
PlayGameState::CheckPlayerVictory()
{
return CheckVictoryForSymbol(EGS_X);
}

//-----------------------------------------------------------------------------
bool
PlayGameState::CheckVictoryForSymbol(PlayGameState::GridSymbols sym)
{
if(m_Grid[0][0] == sym)
{
if(m_Grid[0][1] == sym && m_Grid[0][2] == sym) return true;
if(m_Grid[1][1] == sym && m_Grid[2][2] == sym) return true;
if(m_Grid[1][0] == sym && m_Grid[2][0] == sym) return true;
}
if(m_Grid[1][0] == sym)
{
if(m_Grid[1][1] == sym && m_Grid[1][2] == sym) return true;
}
if(m_Grid[2][0] == sym)
{
if(m_Grid[2][1] == sym && m_Grid[2][2] == sym) return true;
}

return false;
}

Ed ecco infine le tre funzioni che verificano se qualcuno ha vinto. Sono tre, ma in realtà è una, come vedete. Il controllo è banale: semplicemente si verificano tutte le possibilità, senza scienze particolari.

Nella prossima puntata vedremo come istruire il computer per essere un po’ meno tonto... sì, parleremo di AI!

Buon coding!

mercoledì 3 agosto 2011

Il gioco indie che forse non conoscete: Continuity!

Continuity è un originale puzzle game sviluppato dagli svedesi Ragtime Games nel 2009, vincitore di ben due prestigiosi premi: il Best Student Game alll’Independent Games Festival (2010) e il Gameplay Innovation Award all’IndieCade (2010).



Una tale abbondanza di titoli non giunge mai a caso, e Continuity non sembra aver rubato proprio nulla. Il gameplay è una via di mezzo tra un platform, un puzzle e il cubo di rubik! Già al secondo dei trentadue livelli saremo costretti a capire e padroneggiare i (pochi) meccanismi necessari a risolvere il problema. Che è lo stesso, in tutti i livelli: raccogliere la chiave rossa e raggiungere la porta rossa, nella buona vecchia tradizione del minimalismo indie.

In questi giorni è disponibile anche Continuity 2 per iPhone e iPad, ovviamente su AppStore.

lunedì 1 agosto 2011

Tutorial: Scriviamo un gioco in C++... Tris! (parte 4)

Nella puntata precedente siamo arrivati ad avere la title screen mostrata a video e il nostro gioco che reagisce alla pressione del tasto ESC, chiudendosi.

In questa nuova sessione di coding vogliamo aggiungere i due tasti Play e Quit, gestire la loro pressione via click del mouse e spostare la funzione di chiusura dal tasto ESC alla pressione di Quit.


Vogliamo inoltre gestire la pressione di Play mostrando una seconda schermata, quella di gioco! Per ora la schermata di gioco non sarà altro che una schermata nera, dalla quale usciremo con ESC tornando alla title screen.

Entreremo più in profondità nella scrittura di codice e nella progettazione dell’architettura del programma, in particolare capiremo come gestire i messaggi tra le varie parti dell’applicazione.

Prima di fare tutto questo, però, dobbiamo rivedere un attimo l’organizzazione del codice, attività che andrebbe considerata dopo ogni sessione in cui si completa un pezzo dell’applicazione e ci si accinge ad aggiungere altre parti. Quelli bravi le hanno anche dato un nome: Refactor.

Il Refactor è lo spostare funzionalità da una parte all’altra del sistema, creando magari sottosistemi o eliminandone alcuni per inglobare le loro funzioni dentro altri. A volte si riduce banalmente alla rinomina di una classe o di qualche metodo. Il fine ultimo è sempre quello di rimodellare il codice in modo che appaia uniforme e in armonia nelle sue varie parti.

Abbiamo già fatto in realtà un piccolo intervento di refactoring nella terza parte, quando abbiamo introdotto la classe SDLManager spostandoci dentro le istruzioni che inizialmente avevamo messo nel main.

Ora l’intervento sarà un attimo più strutturato. Si tratta infatti di come vogliamo gestire gli stati dell’applicazione.

La gestione degli stati


Durante il suo ciclo di vita ogni applicazione, e i giochi non fanno eccezione, passa attraverso più stati, cioè più configurazioni, o modalità operative. Nel nostro TrisLick abbiamo individuato principalmente due stati:

  1. La Title Screen, che funge anche da menu
  2. Lo stato di Play

e abbiamo determinato le condizioni di passaggio da uno stato all’altro:

  • Title Screen -> (pressione tasto Play) -> Stato Play
  • Stato Play -> (fine partita) -> TitleScreen
  • Title Screen -> (pressione tasto Quit) -> Uscita dal gioco

A seconda dello stato in cui si trova, il programma dovrà disegnare cose differenti, aggiornare oggetti differenti, trattare l’input in modo differente, e così via.

Si potrebbe risolvere il tutto con dei valori booleani e un bell’albero di if-else, o con un enumerativo e uno switch, e nel caso del nostro Tris andrebbe anche bene, essendo solo due gli stati e non avendo in programma di ampliare il gioco. Ma questa soluzione è sicuramente poco flessibile e per nulla scalabile.

Possiamo invece approfittare della relativa semplicità del nostro scenario per gestire in modo più elegante e ad oggetti questo problema, creando una gestione basata sulla macchina a stati finiti.

Essenzialmente, una macchina a stati finiti (FSM da qui in avanti, Finite State Machine) è un sistema in grado di gestire entità chiamate stati, operando le opportune transazioni da uno all’altro in base agli input ricevuti.

Per quanto ci riguarda, possiamo astrarre uno stato come qualcosa in grado di:

  • ricevere e processare input da tastiera e mouse
  • aggiornare i propri oggetti
  • disegnare i propri oggetti

La classe Tris fungerà da FSM gestendo le transazioni tra i due stati, ma avrà sempre e solo uno di loro impostato come stato corrente. Sfruttando l’ereditarietà e il polimorfismo potremo astrarre dal concreto stato attuale e ragionare solo in termini di stato astratto.

Ho perso qualcuno? Tranquilli, tra poco il codice renderà tutto molto più chiaro. Dobbiamo però introdurre ancora un concetto: la gestione dei messaggi dagli stati all’applicazione. Se spostiamo infatti la gestione dell’input dalla classe Tris alle classi di stato, come facciamo a reagire, ad esempio, alla pressione di ESC impostando un valore della classe Tris? La soluzione più ovvia sembrerebbe quella di fornire le classi di stato di un riferimento alla classe Tris, ma così facendo incapperemmo in un brutto riferimento circolare, in cui Tris conosce lo stato A e lo stato A conosce Tris.

Il modello ad oggetti ci permette, per fortuna, di essere un po’ più eleganti. Possiamo infatti creare una classe astratta ApplicationMessageHandler che contiene un metodo virtuale puro, HandleMessage. Gli stati devono conoscere questa classe, a loro infatti interessa solo comunicare un messaggio. Naturalmente, essendo ApplicationMessageHandler una classe astratta, andrà derivata da qualcuno, e questo qualcuno dovrà poi gestire il metodo HandleMessage. Credo siamo tutti d’accordo nel far implementare questa interfaccia alla nostra bella classe Tris!

Ma passiamo al codice. Iniziamo col definire la classe base (astratta) per gli stati: AbstractGameState.

#ifndef ABSTRACTGAMESTATE_H
#define ABSTRACTGAMESTATE_H

#include <SDL/SDL.h>
#include "ApplicationMessageHandler.h"

class AbstractGameState
{
public:
AbstractGameState(ApplicationMessageHandler* pMessageHandler);
virtual ~AbstractGameState();

virtual int Load() = 0;

virtual void HandleKeyboardInput(const SDLKey& key, const SDL_EventType& type) = 0;
virtual void HandleMouseInput(const int x, const int y, Uint8 button, const SDL_EventType& type) = 0;

virtual void Update() = 0;
virtual void Draw() = 0;

protected:

int LoadOptimizedImage(const char* filename, SDL_Surface** destination);

bool IsPointInRect(int x, int y, const SDL_Rect& rect);

protected:

ApplicationMessageHandler* m_pMessageHandler;

private:
};

#endif // ABSTRACTGAMESTATE_H

Come vedete si tratta di una classe con tutti i metodi virtuali puri tranne costruttore e distruttore. Tutto quel che possiamo fare con questa classe, o interfaccia, è derivarla su classi concrete. Non è una gran sorpresa dirvi che le due classi concrete saranno TitleScreenGameState e PlayGameState.

Il metodo LoadOptimizedImage non è altro che una utilità per svolgere il lavoro attualmente svolto nella classe Tris: caricare un’immagine da disco e provare a produrne la versione ottimizzata per il blitting. Ponendo questa funzione qui ce la ritroveremo gratis in tutti gli stati, migliorando di molto sia la gestione del codice, sia la sua leggibilità.

IsPointInRect è una seconda utilità che svolge un semplice calcolo: ci dice se il punto di coordinate x, y ricade all’interno di un rettangolo rect. Ci tornerà utile quando si tratterà di intercettare i click del mouse.

Notate anche l’inclusione di ApplicationMessageHandler, che iniettiamo nello stato attraverso il suo costruttore, ponendola nella variabile membro m_pMessageHandler.

L’interfaccia di gestione dei messaggi è molto semplice. Per ora la definiamo così, ci riserviamo però la possibilità di ampliarla in futuro:

#ifndef APPLICATIONMESSAGEHANDLER_H
#define APPLICATIONMESSAGEHANDLER_H

enum MessageType
{
EMT_QUIT
};

class ApplicationMessageHandler
{
public:
ApplicationMessageHandler();
virtual ~ApplicationMessageHandler();

virtual void HandleMessage(const MessageType& type) = 0;

protected:
private:
};

#endif // APPLICATIONMESSAGEHANDLER_H

Ora è il momento di creare le classi concrete degli stati. Entrambe avranno la stessa struttura, quindi riporto qui la dichiarazione della sola TitleScreenGameState a mo’ di esempio:

#ifndef TITLESCREENGAMESTATE_H
#define TITLESCREENGAMESTATE_H

#include "AbstractGameState.h"

class TitleScreenGameState : public AbstractGameState
{
public:
TitleScreenGameState(ApplicationMessageHandler* pMessageHandler);
virtual ~TitleScreenGameState();

virtual int Load();

virtual void HandleKeyboardInput(const SDLKey& key, const SDL_EventType& type);
virtual void HandleMouseInput(const int x, const int y, Uint8 button, const SDL_EventType& type);

virtual void Update();
virtual void Draw();

protected:
private:
SDL_Surface* m_pBackground;
SDL_Surface* m_pQuitButton;;
SDL_Surface* m_pPlayButton;
SDL_Rect m_QuitBlitRect, m_PlayBlitRect;
};

#endif // TITLESCREENGAMESTATE_H

L’implementazione di TitleScreenGameState è piuttosto semplice: non facciamo altro che caricare le immagini all’interno della Load, gestire l’input in HandleMouseInput e disegnare il tutto nella Draw.

#include "../include/TitleScreenGameState.h"
#include <iostream>

using namespace std;

//-----------------------------------------------------------------------------
TitleScreenGameState::TitleScreenGameState(ApplicationMessageHandler* pMessageHandler) :
AbstractGameState(pMessageHandler)
{
//ctor
}

//-----------------------------------------------------------------------------
TitleScreenGameState::~TitleScreenGameState()
{
//dtor
SDL_FreeSurface(m_pBackground);
SDL_FreeSurface(m_pPlayButton);
SDL_FreeSurface(m_pQuitButton);
}

//-----------------------------------------------------------------------------
int
TitleScreenGameState::Load()
{
int result = 0;
result = LoadOptimizedImage("title_screen.bmp", &m_pBackground);
result += LoadOptimizedImage("button_play.bmp", &m_pPlayButton);
result += LoadOptimizedImage("button_quit.bmp", &m_pQuitButton);

// mettiamo da parte nei due Rect le informazioni sulla posizione
// e dimensione dei due pulsanti. Le useremo quando occorrerà
// gestire il click del mouse

m_PlayBlitRect.h = m_pPlayButton->h;
m_PlayBlitRect.w = m_pPlayButton->w;
m_PlayBlitRect.x = 160 - (m_PlayBlitRect.w/2);
m_PlayBlitRect.y = 320;

m_QuitBlitRect.h = m_pQuitButton->h;
m_QuitBlitRect.w = m_pQuitButton->w;
m_QuitBlitRect.x = 160 - (m_QuitBlitRect.w/2);
m_QuitBlitRect.y = 320 + m_PlayBlitRect.h + 2;

return result;
}

//-----------------------------------------------------------------------------
void
TitleScreenGameState::HandleKeyboardInput(const SDLKey& key, const SDL_EventType& type)
{
if(type == SDL_KEYDOWN)
{

switch(key)
{
case SDLK_ESCAPE:
m_pMessageHandler->HandleMessage(EMT_QUIT);
break;
}

}
}

//-----------------------------------------------------------------------------
void
TitleScreenGameState::HandleMouseInput(const int x, const int y, Uint8 button, const SDL_EventType& type)
{
// click all'interno del button quit?
if(IsPointInRect(x, y, m_QuitBlitRect))
{
// tasto sinistro?
if(button == SDL_BUTTON_LEFT && type == SDL_MOUSEBUTTONUP)
{
m_pMessageHandler->HandleMessage(EMT_QUIT);
}
}
// button play?
else if(IsPointInRect(x, y, m_PlayBlitRect))
{
// tasto sinistro?
if(button == SDL_BUTTON_LEFT && type == SDL_MOUSEBUTTONUP)
{
m_pMessageHandler->HandleMessage(EMT_PLAY);
}
}
}

//-----------------------------------------------------------------------------
void
TitleScreenGameState::Update()
{

}

//-----------------------------------------------------------------------------
void
TitleScreenGameState::Draw()
{
SDL_Surface* screen = SDL_GetVideoSurface();

SDL_BlitSurface(m_pBackground, 0, screen, 0);

SDL_Rect dst = m_PlayBlitRect;
SDL_BlitSurface(m_pPlayButton, 0, screen, &dst);

dst = m_QuitBlitRect;
SDL_BlitSurface(m_pQuitButton, 0, screen, &dst);
}

Non ci resta altro che vedere come è cambiata la classe Tris. Non conterrà infatti più alcun riferimento alla SDL_Surface con la bitmap della TitleScreen; avrà invece un oggetto TitleScreenGameState da gestire.

Prestate attenzione al codice di gestione dell’input: noterete che è indipendente dallo stato in cui ci si trova, come altre parti dell’applicazione (Update e Draw). Viene infatti utilizzato il puntatore m_pCurrentState, di tipo generico AbstractGameState. E’ compito della FSM impersonata da Tris popolare correttamente questo puntatore con l’istanza di stato corretta (vedi gestione dei messaggi): al resto della classe non interessa sapere in quale stato ci si trova! Questo ci offre un buon grado di flessibilità e scalabilità: aggiungere/eliminare una schermata (uno stato) diventa molto più semplice, dovendo toccare solo pochi punti.

Ecco il nuovo codice di Tris:

#include "../include/Tris.h"
#include <iostream>

using namespace std;

//-----------------------------------------------------------------------------
Tris::Tris()
{
//ctor

m_pSDLManager = 0;
m_IsRunning = false;
m_pTitleScreenGS = 0;
m_pPlayGS = 0;
m_pCurrentState = 0;
}

//-----------------------------------------------------------------------------
Tris::~Tris()
{
//dtor

delete m_pSDLManager;
m_pSDLManager = 0;

delete m_pTitleScreenGS;
m_pTitleScreenGS = 0;

delete m_pPlayGS;
m_pPlayGS = 0;
}

//-----------------------------------------------------------------------------
int
Tris::StartApplication()
{
m_pSDLManager = new SDLManager();

int result = m_pSDLManager->Init();

if(result != 0)
{
return 1; // errore
}

m_IsRunning = true;

// carico lo stato TitleScreen
m_pTitleScreenGS = new TitleScreenGameState(this);
result = m_pTitleScreenGS->Load();

// carico lo stato Play
m_pPlayGS = new PlayGameState(this);
result += m_pPlayGS->Load();

// imposto come stato corrente lo stato di TitleScreen
m_pCurrentState = m_pTitleScreenGS;

return result;
}

//-----------------------------------------------------------------------------
void
Tris::StopApplication()
{
m_pSDLManager->Quit();
}

//-----------------------------------------------------------------------------
bool
Tris::IsRunning()
{
return m_IsRunning;
}

//-----------------------------------------------------------------------------
void
Tris::HandleInput()
{
SDL_Event event;

while(SDL_PollEvent(&event))
{
switch(event.type)
{

case SDL_KEYDOWN:
m_pCurrentState->HandleKeyboardInput(event.key.keysym.sym, SDL_KEYDOWN);
break;

case SDL_MOUSEBUTTONUP:
m_pCurrentState->HandleMouseInput(event.button.x, event.button.y, event.button.button, SDL_MOUSEBUTTONUP);
break;

}
}
}

//-----------------------------------------------------------------------------
void
Tris::Update()
{
m_pCurrentState->Update();
}

//-----------------------------------------------------------------------------
void
Tris::DrawAll()
{
SDL_Surface* screen = SDL_GetVideoSurface();

// pulisco lo schermo
SDL_FillRect(screen, &screen->clip_rect, 0x000000);

// disegno lo stato corrente
m_pCurrentState->Draw();

// svuoto il buffer (invio le istruzioni per il disegno reale a schermo)
SDL_Flip(screen);
}

//-----------------------------------------------------------------------------
void
Tris::HandleMessage(const MessageType& type)
{
switch(type)
{
case EMT_QUIT:
m_IsRunning = false;
break;

case EMT_PLAY:
m_pCurrentState = m_pPlayGS;
break;

case EMT_TITLE:
m_pCurrentState = m_pTitleScreenGS;
break;
}
}

Ora lanciamo l’applicazione et voilà, otteniamo quel che ci eravamo proposti!