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:
- La Title Screen, che funge anche da menu
- 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!