Avviare un nuovo progetto: chiamarlo giocoTetris.
Sul frame principale inserire un wxBoxSizer;
Nel sizer inserire un wxPanel;
Sul Pannello inserire un altro wxBoxSizer;
Nel BoxSizer inserire due componenti wxStaticText sovrapposti (applicare l'orientamento verticale al box sizer).
Modificare la proprietà Label del primo in: "Istruzioni". Nel secondo componente wxStaticText scrivere le istruzioni (spazio: caduta veloce, P: pausa ecc..)
Aprire l'editor del menu ed aggiungere la voce: Nuova partita nel menu File.
File → New Class
→ [class name:] Forma. → Create.
Rispondere si alla richiesta di aggiungere la classe al progetto e accettare la creazione della versione di Debug e di Release.
Completare o sostituire il file header della classe Forma:
#ifndef FORMA_H #define FORMA_H enum Tetramini { Vuoto, FormaZeta, FormaEsse, FormaLinea, FormaT, FormaQuadra, FormaElle, FormaElleRiflessa };
Tetramini è un nuovo tipo del quale vengono forniti i possibili valori che una variabile di questo tipo può assumere.
La classe Forma Possiede le proprietà private:
coords[4][2] una matrice che contiene coppie di coordinate dei quattro quadrati che costituiscono un tetramino.
La colonna 0 contiene la coordinata x, la colonna 1 contiene la coordinata y.
Esempio:
(x) | (y) | |
0 | 0 | -1 |
1 | 0 | 0 |
2 | 1 | 0 |
3 | 1 | 1 |
nrForma uno dei possibili valori del tipo enum Tetramini
e i metodi:
SetX per scrivere il valore della coordinata x di uno dei quattro quadrati del Tetramino.
SetY per scrivere il valore della coordinata y di uno dei quattro quadrati del Tetramino.
class Forma { private: int coords[4][2]; // un tetramino è contenuto in una griglia di 4x2 quadrati Tetramini nrForma; void SetX(int riga, int valore) { coords[riga][0] = valore; } void SetY(int riga, int valore) { coords[riga][1] = valore; }
L'interfaccia pubblica della classe contiene il costruttore, il distruttore e i metodi per usare una forma:
public: Forma() { // il costruttore imposta un tetramino vuoto impostaForma(Vuoto); } void impostaForma(Tetramini IDTetramino); void generaForma(); Tetramini leggiForma() const { return nrForma; } /* il metodo x riceve una coordinata di riga e restituisce il valore della prima colonna dell'array coords */ int x(int riga) const { return coords[riga][0]; } /* il metodo y riceve una coordinata di riga e restituisce il valore della seconda colonna dell'array coords */ int y(int riga) const { return coords[riga][1]; } int MinX() const; int MaxX() const; int MinY() const; int MaxY() const; Forma RuotaAntiOrario() const; Forma RuotaOrario() const; virtual ~Forma(); // il distruttore. protected: }; #endif // FORMA_H
Per disegnare un Tetramino (una forma composta da 4 quadrati), si sceglie un quadrato di riferimento, cioè gli si assegna la posizione (0, 0).
Le posizioni degli altri quadrati vengono indicate con due coordinate che rappresentano il numero di righe e il numero di colonne che distano dalla casella di riferimento.
Le forme scelte sono indicate con le seguenti coordinate: |
a cui corrispondono i seguenti quadrati: | ||||
{0, -1}, {0, 0}, {-1, 0}, {-1, 1} | -1, 0 | -1, 1 | |||
FormaZeta | 0, -1 | 0, 0 | |||
{0, -1}, {0, 0}, {1, 0}, {1, 1} | 0, -1 | 0, 0 | |||
FormaEsse | 1, 0 | 1, 1 | |||
{0, -1}, {0, 0}, {0, 1}, {0, 2} | 0, -1 | 0, 0 | 0, 1 | 0, 2 | |
FormaLinea | |||||
{-1, 0}, {0, 0}, {1, 0}, {0, 1} | -1, 0 | ||||
FormaT | 0, 0 | 0, 1 | |||
1, 0 | |||||
{0, 0}, {1, 0}, {0, 1}, {1, 1} | 0, 0 | 0, 1 | |||
FormaQuadra | 1, 0 | 1, 1 | |||
{-1, -1}, {0, -1}, {0, 0}, {0, 1} | -1, -1 | ||||
FormaElle | 0, -1 | 0, 0 | 0, 1 | ||
{1, -1}, {0, -1}, {0, 0}, {0, 1} | 0, -1 | 0, 0 | 0, 1 | ||
FormaElleRiflessa | 1, -1 |
Completare il file di implementazione della classe:
#include <stdlib.h> #include "../include/Forma.h" using namespace std;
Il metodo impostaForma riceve, come parametro, uno dei possibili valori di tipo Tetramini.
Dichiara una matrice statica, cioè ne esiste una sola copia che viene usata da tutte le istanze della classe Forma.
Questa matrice indica, per ogni riga, la forma di uno dei 6 tetramini.
Usa il parametro ricevuto per accedere alla riga della matrice statica e copia i valori nella proprietà coords.
Infine memorizza nella proprietà nrForma il numero della riga della matrice statica da cui ha prelevato le coordinate.
void Forma::impostaForma(Tetramini IDTetramino) { static const int coordRelative[8][4][2] = { { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } }, { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } }, { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } }, { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }; for (int i = 0; i < 4 ; i++) { for (int j = 0; j < 2; ++j) coords[i][j] = coordRelative[IDTetramino][i][j]; } nrForma = IDTetramino; // memorizza la riga contenente le coordinate. }
Il metodo generaForma genera un numero casuale, compreso tra 1 e 7, che usa come indice per acquisire il corrispondente valore del tipo enumerato Tetramini. Tale valore viene passato al metodo impostaForma.
void Forma::generaForma() { int x = rand() % 7 + 1; // escludi la prima riga. impostaForma(Tetramini(x)); }
Il metodo MinX cerca, tra i quadrati del tetramino, quello che ha la coordinata X più a sinistra
int Forma::MinX() const { int m = coords[0][0]; // assume che la posizione X del primo quadrato del tetramino sia la minore tra le 4. for (int i=0; i<4; i++) m = min(m, coords[i][0]); // se ne trova una più piccola aggiorna il minimo return m; }
Il metodo MaxX cerca, tra i quadrati del tetramino, quello che ha la coordinata X più a destra
int Forma::MaxX() const { int m = coords[0][0];//assume che la posizione X del primo quadrato del tetramino sia la maggiore tra le 4. for (int i=0; i<4; i++) m = max(m, coords[i][0]); // se ne trova una più grande aggiorna il massimo return m; }
Il metodo MinY cerca, tra i quadrati del tetramino, quello che ha la coordinata Y più bassa
int Forma::MinY() const { int m = coords[0][1]; //assume che la posizione Y del primo quadrato del tetramino sia la minore tra le 4. for (int i=0; i<4; i++) m = min(m, coords[i][1]); // se ne trova una più piccola aggiorna il minimo return m; }
Il metodo MaxY cerca, tra i quadrati del tetramino, quello che ha la coordinata Y più alta
int Forma::MaxY() const { int m = coords[0][1]; //assume che la posizione Y del primo quadrato del tetramino sia la maggiore. for (int i=0; i<4; i++) m = max(m, coords[i][1]); // se ne trova una più grande aggiorna il massimo return m; }
I metodi RuotaAntiOrario e RuotaOrario restituiscono la stessa forma del tetramino se quello corrente è quadrato. Altrimenti creano un'istanza di classe Forma, a cui assegnano la stessa proprietà nrForma, e poi scambiano le coordinate. Infine restituiscono la forma con cui appare il tetramino nel nuovo orientamento.
Forma Forma::RuotaOrario() const { if (nrForma == FormaQuadra) return *this; Forma tmpForma; // crea un'istanza di Tetramino tmpForma.nrForma = nrForma; // la inizializza con la forma corrente for (int i = 0; i < 4; ++i) { /* rispetto alla casella di riferimento, le coordinate Y delle caselle vengono assegnate alle coordinate x*/ tmpForma.SetX(i, y(i)); /* rispetto alla casella di riferimento, le coordinate x delle caselle vengono ribaltate e assegnate alle coordinate y, quelle di sinistra passano in alto e quelle di destra passano in basso.*/ tmpForma.SetY(i, -x(i)); } return tmpForma; }
|
|
||||||||||||||
(x) | (y) | x | y | ||||||||||||
0 | -1 | 0 | -1, 0 | SetX(0,y(0)) | 0 | 1 | SetY(0, -x(0)) | ||||||||
1 | 0 | 0 | 0, 0 | 0, 1 | SetX(1,y(1)) | 0 | 0 | SetY(1,-x(1)) | 0, -1 | 0, 0 | 0, 1 | ||||
2 | 1 | 0 | 1, 0 | SetX(2,y(2)) | 0 | -1 | SetY(2,-x(2)) | 1, 0 | |||||||
3 | 0 | 1 | SetX(3,y(3)) | 1 | 0 | SetY(3,-x(3)) |
Forma Forma::RuotaAntiOrario() const { if (nrForma == FormaQuadra) return *this; Forma tmpForma; tmpForma.nrForma = nrForma; for (int i = 0; i < 4; ++i) { tmpForma.SetX(i, -y(i)); tmpForma.SetY(i, x(i)); } return tmpForma; }
|
|
||||||||||||||
(x) | (y) | x | y | ||||||||||||
0 | -1 | 0 | -1, 0 | SetX(0,-y(0)) | 0 | -1 | SetY(0,x(0)) | -1, 0 | |||||||
1 | 0 | 0 | 0, 0 | 0, 1 | SetX(1,-y(1)) | 0 | 0 | SetY(1,x(1)) | 0, -1 | 0, 0 | 0, 1 | ||||
2 | 1 | 0 | 1, 0 | SetX(2,-y(2)) | 0 | 1 | SetY(2,x(2)) | ||||||||
3 | 0 | 1 | SetX(3,-y(3)) | -1 | 0 | SetY(3,x(3)) |
Forma::~Forma() { //distruttore }
Per disegnare nell'area client dell'applicazione si inserisce un pannello all'interno di un frame.
Nella barra dei menu di Code::Blocks aprire il menu wxSmith e selezionare Add wxPanel. Si apre una finestra che chiede il nome da assegnare alla classe, suggerendo "NewFrame", cambiarlo in campoGioco. Accettare tutte le altre impostazioni di default, e terminare l'aggiunta del panel.
Nel menu wxSmith scegliere il comando add wxFrame. Nella casella "Class Name" scrivere Tetris.
Adesso viene mostrata un'altra griglia di punti (Tetris.wxs) che è la nuova area di disegno creata (il Frame).
Fare doppio clic su questa griglia di punti, per aprire il codice associato. Raggiungere la fine del file e completare il gestore dell'evento onClose, inserendo il richiamo del metodo Destroy().
Nel pannello del progetto fare doppio clic sul file header della classe campoGioco.
#ifndef CAMPOGIOCO_H #define CAMPOGIOCO_H #include "include/Forma.h" class campoGioco : public wxPanel { public: campoGioco(wxWindow* parent,wxWindowID id=wxID_ANY,const wxPoint& pos=wxDefaultPosition,const wxSize& dimensioni=wxDefaultSize); void Start(); void Pausa(); virtual ~campoGioco(); void lineeEliminate(int nrLinee); private: /* Numero di righe (10) e numero di colonne (22) del campo di gioco.*/ enum { nrColonne = 10, nrRighe = 22 }; /*legge una casella della griglia di gioco e restituisce la forma contenuta */ Tetramini& FormaIn(int x, int y) {return griglia[(y * nrColonne) + x];} /* calcolo delle dimensioni di un quadrato che compone un tetramino */ int Lato() { return GetClientSize().GetWidth() / nrColonne; } int Altezza() { return GetClientSize().GetHeight() / nrRighe; } void cancellaGriglia(); void Caduta(); void riduciUnaLinea(); void FormaCaduta(); void rimuoviLineeComplete(); void nuovoPezzo(); bool verificaMossa(const Forma& newPiece, int newX, int newY); void disegnaQuadrato(wxPaintDC &dc, int x, int y, Tetramini IDTetramino); bool avviato; bool inPausa; bool fineCaduta; Forma formaCorrente; int xForma; int yForma; int numLineeRimosse; Tetramini griglia[nrColonne * nrRighe]; wxStatusBar *m_stsbar; }; #endif
Nel pannello del progetto aprire il file di implementazione della classe "campoGioco".
Selezionare la scheda wxPanel contenuta nella classe campoGioco.
nella sezione Eventi/Proprietà, sulla riga EVT_PAINT selezionare new handler. Scegliere il nome onPaint, completare il gestore di evento nel file campoGioco.cpp:
void campoGioco::OnPaint(wxPaintEvent& event) { /* crea un device context */ wxPaintDC dc(this); /* ottiene le dimensioni dell'area client della finestra */ wxSize dimensioni = GetClientSize(); /* decide la quota da cui cadranno i tetramini */ int inizioCaduta = dimensioni.GetHeight() - nrRighe * Altezza(); /* per tutte le posizioni della griglia di gioco */ for (int i = 0; i < nrRighe; ++i) { for (int j = 0; j < nrColonne; ++j) { /* legge il codice del tetramino */ Tetramini IDTetramino = FormaIn(j, nrRighe - i - 1); if (IDTetramino != Vuoto) disegnaQuadrato(dc, 0 + j * Lato(), inizioCaduta + i * Altezza(), IDTetramino); } } /* dopo aver ridisegnato i tetramini caduti, disegna il tetramino in corso */ if (formaCorrente.leggiForma() != Vuoto) { for (int i = 0; i < 4; ++i) { int x = xForma + formaCorrente.x(i); int y = yForma - formaCorrente.y(i); disegnaQuadrato(dc, 0 + x * Lato(), inizioCaduta + (nrRighe - y - 1) * Altezza(), formaCorrente.leggiForma()); } } }
Aggiungere la funzione seguente, richiamata dal gestore onPaint:
void campoGioco::disegnaQuadrato(wxPaintDC& dc, int x, int y, Tetramini IDTetramino) { static wxColour colori[] = { wxColour(0, 0, 0), wxColour(204, 102, 102), wxColour(102, 204, 102), wxColour(102, 102, 204), wxColour(204, 204, 102), wxColour(204, 102, 204), wxColour(102, 204, 204), wxColour(218, 170, 0) }; static wxColour luce[] = { wxColour(0, 0, 0), wxColour(248, 159, 171), wxColour(121, 252, 121), wxColour(121, 121, 252), wxColour(252, 252, 121), wxColour(252, 121, 252), wxColour(121, 252, 252), wxColour(252, 198, 0) }; static wxColour ombra[] = { wxColour(0, 0, 0), wxColour(128, 59, 59), wxColour(59, 128, 59), wxColour(59, 59, 128), wxColour(128, 128, 59), wxColour(128, 59, 128), wxColour(59, 128, 128), wxColour(128, 98, 0) }; wxPen penna(luce[int(IDTetramino)]); penna.SetCap(wxCAP_PROJECTING); dc.SetPen(penna); dc.DrawLine(x, y + Altezza() - 1, x, y); dc.DrawLine(x, y, x + Lato() - 1, y); wxPen pennaScura(ombra[int(IDTetramino)]); pennaScura.SetCap(wxCAP_PROJECTING); dc.SetPen(pennaScura); dc.DrawLine(x + 1, y + Altezza() - 1, x + Lato() - 1, y + Altezza() - 1); dc.DrawLine(x + Lato() - 1, y + Altezza() - 1, x + Lato() - 1, y + 1); dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(wxBrush(colori[int(IDTetramino)])); dc.DrawRectangle(x + 1, y + 1, Lato() - 2, Altezza() - 2); }
Mentre la scheda campoGioco.wxs è selezionata inserire il gestore dell'evento EVT_KEY_DOWN:
void campoGioco::OnKeyDown(wxKeyEvent& event) { if (!avviato || formaCorrente.leggiForma() == Vuoto) { event.Skip(); return; } int codiceTasto = event.GetKeyCode(); if (codiceTasto == 'p' || codiceTasto == 'P') { Pausa(); return; } if (inPausa) return; switch (codiceTasto) { case WXK_LEFT: verificaMossa(formaCorrente, xForma - 1, yForma); break; case WXK_RIGHT: verificaMossa(formaCorrente, xForma + 1, yForma); break; case WXK_DOWN: verificaMossa(formaCorrente.RuotaOrario(), xForma, yForma); break; case WXK_UP: verificaMossa(formaCorrente.RuotaAntiOrario(), xForma, yForma); break; case WXK_SPACE: Caduta(); break; case 'd': riduciUnaLinea(); break; case 'D': riduciUnaLinea(); break; default: event.Skip(); } }
Aggiungere un componente wxTimer sulla scheda campoGioco.wxs e definire un gestore di evento.
void campoGioco::OnTimer(wxCommandEvent& event) { if (fineCaduta) { fineCaduta = false; nuovoPezzo(); } else riduciUnaLinea(); }
Nel pannello delle proprietà assegnare il valore 300 alla proprietà Interval.
Completare l'implementazione della classe: "campoGioco".
aggiungere le seguenti righe alla fine del costruttore:
fineCaduta = false; avviato = false; inPausa = false; numLineeRimosse = 0; xForma = 0; yForma = 0; cancellaGriglia(); } // fine costruttore
Il metodo Start() verifica che il gioco non sia in pausa ed assegna i valori iniziali alle proprietà della classe.
void campoGioco::Start() { if (inPausa) return; avviato = true; fineCaduta = false; numLineeRimosse = 0; cancellaGriglia(); nuovoPezzo(); timer1->Start(300); }
il metodo Pausa() verifica che il gioco sia avviato, commuta lo stato booleano della proprietà inPausa e, a seconda dello stato, ferma o riavvia il timer. In ogni caso mostra un messaggio nella barra di stato.
void campoGioco::Pausa() { if (!avviato) return; inPausa = !inPausa; if (inPausa) { timer1->Stop(); m_stsbar->SetStatusText(wxT("pausa")); } else { timer1->Start(300); wxString str; str.Printf(wxT("%d"), numLineeRimosse); m_stsbar->SetStatusText(str); } Refresh(); }
void campoGioco::cancellaGriglia() { for (int i = 0; i < nrRighe * nrColonne; ++i) griglia[i] = Vuoto; }
void campoGioco::Caduta() { int newY = yForma; while (newY > 0) { if (!verificaMossa(formaCorrente, xForma, newY - 1)) break; --newY; } FormaCaduta(); }
void campoGioco::riduciUnaLinea() { if (!verificaMossa(formaCorrente, xForma, yForma - 1)) FormaCaduta(); }
void campoGioco::FormaCaduta() { for (int i = 0; i < 4; ++i) { int x = xForma + formaCorrente.x(i); int y = yForma - formaCorrente.y(i); FormaIn(x, y) = formaCorrente.leggiForma(); } rimuoviLineeComplete(); if (!fineCaduta) nuovoPezzo(); }
void campoGioco::rimuoviLineeComplete() { int numFullLines = 0; for (int i = nrRighe - 1; i >= 0; --i) { bool lineaPiena = true; for (int j = 0; j < nrColonne; ++j) { if (FormaIn(j, i) == Vuoto) { lineaPiena = false; break; } } if (lineaPiena) { ++nrLineePiene; for (int k = i; k < nrRighe - 1; ++k) { for (int j = 0; j < nrColonne; ++j) FormaIn(j, k) = FormaIn(j, k + 1); } } } if (nrLineePiene > 0) { numLineeRimosse += nrLineePiene; wxString str; str.Printf(wxT("%d"), numLineeRimosse); m_stsbar->SetStatusText(str); fineCaduta = true; formaCorrente.impostaForma(Vuoto); Refresh(); } }
void campoGioco::nuovoPezzo() { formaCorrente.generaForma(); xForma = nrColonne / 2 + 1; yForma = nrRighe - 1 + formaCorrente.MinY(); if (!verificaMossa(formaCorrente, xForma, yForma)) { formaCorrente.impostaForma(Vuoto); timer1->Stop(); avviato = false; m_stsbar->SetStatusText(wxT("game over")); } }
bool campoGioco::verificaMossa(const Forma& newPiece, int newX, int newY) { for (int i = 0; i < 4; ++i) { int x = newX + newPiece.x(i); int y = newY - newPiece.y(i); if (x < 0 || x >= nrColonne || y < 0 || y >= nrRighe) return false; if (FormaIn(x, y) != Vuoto) return false; } formaCorrente = newPiece; xForma = newX; yForma = newY; Refresh(); return true; }
Associare il gestore di evento onNuovaPartita alla voce di menu nuovaPartita:
srand(time(NULL)); Tetris *tetris = new Tetris(this); campoGioco *griglia = new campoGioco(tetris); tetris->Centre(); tetris->Show(true); griglia->SetFocus(); griglia->Start();
Nella sezione di intestazione del file aperto inserire:
#include "campogioco.h" #include "Tetris.h"