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!

Nessun commento:

Posta un commento