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!

1 commento:

  1. Ciao, ho trovato molto interessante il tuo tutorial e quindi ho subito provato a digitarlo in Code::Blocks.
    Purtroppo non hai messo la funzione LoadOptimizedImage() nel tuo tutorial. Saresti così gentile ad inserirlo ? ggiorgio63@yahoo.it

    RispondiElimina