wxWidgets 中的俄罗斯方块游戏
最后修改于 2023 年 10 月 18 日
俄罗斯方块游戏是有史以来最受欢迎的电脑游戏之一。这款游戏最初由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程。从那时起,俄罗斯方块游戏就在几乎所有电脑平台上以各种不同的版本提供。
俄罗斯方块被称为下落方块益智游戏。在这个游戏中,我们有七种不同的形状,称为 tetrominoes(四格骨牌)。S 形、Z 形、T 形、L 形、直线形、镜像 L 形和正方形。这些形状中的每一个都由四个正方形组成。这些形状从屏幕上方落下。俄罗斯方块游戏的目标是移动和旋转这些形状,使它们尽可能地互相吻合。如果我们设法形成一行,该行就会被消除,我们就可以得分。我们玩俄罗斯方块游戏直到堆满。

wxWidgets 是一个用于创建应用程序的工具包。还有其他专门用于创建电脑游戏的库。尽管如此,wxWidgets 和其他应用程序工具包也可以用于创建简单的游戏。
开发
我们的俄罗斯方块游戏没有图像,我们使用 wxWidgets 编程工具包中提供的绘图 API 来绘制四格骨牌。在每个电脑游戏的背后,都有一个数学模型。俄罗斯方块游戏也是如此。
一些关于游戏的想法。
- 我们使用
wxTimer
来创建一个游戏循环。 - 俄罗斯方块被绘制出来。
- 这些形状以逐个方块为基础移动(而不是逐个像素)。
- 在数学上,一个棋盘是一个简单的数字列表。
#ifndef SHAPE_H #define SHAPE_H enum Tetrominoes { NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape }; class Shape { public: Shape() { SetShape(NoShape); } void SetShape(Tetrominoes shape); void SetRandomShape(); Tetrominoes GetShape() const { return pieceShape; } int x(int index) const { return coords[index][0]; } int y(int index) const { return coords[index][1]; } int MinX() const; int MaxX() const; int MinY() const; int MaxY() const; Shape RotateLeft() const; Shape RotateRight() const; private: void SetX(int index, int x) { coords[index][0] = x; } void SetY(int index, int y) { coords[index][1] = y; } Tetrominoes pieceShape; int coords[4][2]; }; #endif
#include <stdlib.h> #include <algorithm> #include "Shape.h" using namespace std; void Shape::SetShape(Tetrominoes shape) { static const int coordsTable[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] = coordsTable[shape][i][j]; } pieceShape = shape; } void Shape::SetRandomShape() { int x = rand() % 7 + 1; SetShape(Tetrominoes(x)); } int Shape::MinX() const { int m = coords[0][0]; for (int i=0; i<4; i++) { m = min(m, coords[i][0]); } return m; } int Shape::MaxX() const { int m = coords[0][0]; for (int i=0; i<4; i++) { m = max(m, coords[i][0]); } return m; } int Shape::MinY() const { int m = coords[0][1]; for (int i=0; i<4; i++) { m = min(m, coords[i][1]); } return m; } int Shape::MaxY() const { int m = coords[0][1]; for (int i=0; i<4; i++) { m = max(m, coords[i][1]); } return m; } Shape Shape::RotateLeft() const { if (pieceShape == SquareShape) return *this; Shape result; result.pieceShape = pieceShape; for (int i = 0; i < 4; ++i) { result.SetX(i, y(i)); result.SetY(i, -x(i)); } return result; } Shape Shape::RotateRight() const { if (pieceShape == SquareShape) return *this; Shape result; result.pieceShape = pieceShape; for (int i = 0; i < 4; ++i) { result.SetX(i, -y(i)); result.SetY(i, x(i)); } return result; }
#ifndef BOARD_H #define BOARD_H #include "Shape.h" #include <wx/wx.h> class Board : public wxPanel { public: Board(wxFrame *parent); void Start(); void Pause(); void linesRemovedChanged(int numLines); protected: void OnPaint(wxPaintEvent& event); void OnKeyDown(wxKeyEvent& event); void OnTimer(wxCommandEvent& event); private: enum { BoardWidth = 10, BoardHeight = 22 }; Tetrominoes & ShapeAt(int x, int y) { return board[(y * BoardWidth) + x]; } int SquareWidth() { return GetClientSize().GetWidth() / BoardWidth; } int SquareHeight() { return GetClientSize().GetHeight() / BoardHeight; } void ClearBoard(); void DropDown(); void OneLineDown(); void PieceDropped(); void RemoveFullLines(); void NewPiece(); bool TryMove(const Shape& newPiece, int newX, int newY); void DrawSquare(wxPaintDC &dc, int x, int y, Tetrominoes shape); wxTimer *timer; bool isStarted; bool isPaused; bool isFallingFinished; Shape curPiece; int curX; int curY; int numLinesRemoved; Tetrominoes board[BoardWidth * BoardHeight]; wxStatusBar *m_stsbar; }; #endif
#include "Board.h" Board::Board(wxFrame *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) { timer = new wxTimer(this, 1); m_stsbar = parent->GetStatusBar(); isFallingFinished = false; isStarted = false; isPaused = false; numLinesRemoved = 0; curX = 0; curY = 0; ClearBoard(); Connect(wxEVT_PAINT, wxPaintEventHandler(Board::OnPaint)); Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Board::OnKeyDown)); Connect(wxEVT_TIMER, wxCommandEventHandler(Board::OnTimer)); } void Board::Start() { if (isPaused) return; isStarted = true; isFallingFinished = false; numLinesRemoved = 0; ClearBoard(); NewPiece(); timer->Start(300); } void Board::Pause() { if (!isStarted) return; isPaused = !isPaused; if (isPaused) { timer->Stop(); m_stsbar->SetStatusText(wxT("paused")); } else { timer->Start(300); wxString str; str.Printf(wxT("%d"), numLinesRemoved); m_stsbar->SetStatusText(str); } Refresh(); } void Board::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); wxSize size = GetClientSize(); int boardTop = size.GetHeight() - BoardHeight * SquareHeight(); for (int i = 0; i < BoardHeight; ++i) { for (int j = 0; j < BoardWidth; ++j) { Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1); if (shape != NoShape) DrawSquare(dc, 0 + j * SquareWidth(), boardTop + i * SquareHeight(), shape); } } if (curPiece.GetShape() != NoShape) { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); DrawSquare(dc, 0 + x * SquareWidth(), boardTop + (BoardHeight - y - 1) * SquareHeight(), curPiece.GetShape()); } } } void Board::OnKeyDown(wxKeyEvent& event) { if (!isStarted || curPiece.GetShape() == NoShape) { event.Skip(); return; } int keycode = event.GetKeyCode(); if (keycode == 'p' || keycode == 'P') { Pause(); return; } if (isPaused) return; switch (keycode) { case WXK_LEFT: TryMove(curPiece, curX - 1, curY); break; case WXK_RIGHT: TryMove(curPiece, curX + 1, curY); break; case WXK_DOWN: TryMove(curPiece.RotateRight(), curX, curY); break; case WXK_UP: TryMove(curPiece.RotateLeft(), curX, curY); break; case WXK_SPACE: DropDown(); break; case 'd': OneLineDown(); break; case 'D': OneLineDown(); break; default: event.Skip(); } } void Board::OnTimer(wxCommandEvent& event) { if (isFallingFinished) { isFallingFinished = false; NewPiece(); } else { OneLineDown(); } } void Board::ClearBoard() { for (int i = 0; i < BoardHeight * BoardWidth; ++i) board[i] = NoShape; } void Board::DropDown() { int newY = curY; while (newY > 0) { if (!TryMove(curPiece, curX, newY - 1)) break; --newY; } PieceDropped(); } void Board::OneLineDown() { if (!TryMove(curPiece, curX, curY - 1)) PieceDropped(); } void Board::PieceDropped() { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); ShapeAt(x, y) = curPiece.GetShape(); } RemoveFullLines(); if (!isFallingFinished) NewPiece(); } void Board::RemoveFullLines() { int numFullLines = 0; for (int i = BoardHeight - 1; i >= 0; --i) { bool lineIsFull = true; for (int j = 0; j < BoardWidth; ++j) { if (ShapeAt(j, i) == NoShape) { lineIsFull = false; break; } } if (lineIsFull) { ++numFullLines; for (int k = i; k < BoardHeight - 1; ++k) { for (int j = 0; j < BoardWidth; ++j) ShapeAt(j, k) = ShapeAt(j, k + 1); } } } if (numFullLines > 0) { numLinesRemoved += numFullLines; wxString str; str.Printf(wxT("%d"), numLinesRemoved); m_stsbar->SetStatusText(str); isFallingFinished = true; curPiece.SetShape(NoShape); Refresh(); } } void Board::NewPiece() { curPiece.SetRandomShape(); curX = BoardWidth / 2 + 1; curY = BoardHeight - 1 + curPiece.MinY(); if (!TryMove(curPiece, curX, curY)) { curPiece.SetShape(NoShape); timer->Stop(); isStarted = false; m_stsbar->SetStatusText(wxT("game over")); } } bool Board::TryMove(const Shape& 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 >= BoardWidth || y < 0 || y >= BoardHeight) return false; if (ShapeAt(x, y) != NoShape) return false; } curPiece = newPiece; curX = newX; curY = newY; Refresh(); return true; } void Board::DrawSquare(wxPaintDC& dc, int x, int y, Tetrominoes shape) { static wxColour colors[] = { 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 light[] = { 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 dark[] = { 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 pen(light[int(shape)]); pen.SetCap(wxCAP_PROJECTING); dc.SetPen(pen); dc.DrawLine(x, y + SquareHeight() - 1, x, y); dc.DrawLine(x, y, x + SquareWidth() - 1, y); wxPen darkpen(dark[int(shape)]); darkpen.SetCap(wxCAP_PROJECTING); dc.SetPen(darkpen); dc.DrawLine(x + 1, y + SquareHeight() - 1, x + SquareWidth() - 1, y + SquareHeight() - 1); dc.DrawLine(x + SquareWidth() - 1, y + SquareHeight() - 1, x + SquareWidth() - 1, y + 1); dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(wxBrush(colors[int(shape)])); dc.DrawRectangle(x + 1, y + 1, SquareWidth() - 2, SquareHeight() - 2); }
#include <wx/wx.h> class Tetris : public wxFrame { public: Tetris(const wxString& title); };
#include "Tetris.h" #include "Board.h" Tetris::Tetris(const wxString& title) : wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(180, 380)) { wxStatusBar *sb = CreateStatusBar(); sb->SetStatusText(wxT("0")); Board *board = new Board(this); board->SetFocus(); board->Start(); }
#include <wx/wx.h> class MyApp : public wxApp { public: virtual bool OnInit(); };
#include "main.h" #include "Tetris.h" IMPLEMENT_APP(MyApp) bool MyApp::OnInit() { srand(time(NULL)); Tetris *tetris = new Tetris(wxT("Tetris")); tetris->Centre(); tetris->Show(true); return true; }
我已经稍微简化了游戏,以便更容易理解。游戏在启动后立即开始。我们可以通过按 p 键来暂停游戏。 空格键 将立即将俄罗斯方块块掉落到底部。 d 键将方块下落一行。(它可以用来稍微加快下落速度。)游戏以恒定速度运行,没有实现加速。分数是我们已消除的行数。
... isFallingFinished = false; isStarted = false; isPaused = false; numLinesRemoved = 0; curX = 0; curY = 0; ...
在开始游戏之前,我们初始化一些重要的变量。 isFallingFinished
变量确定了俄罗斯方块形状是否已经完成下落,然后我们需要创建一个新的形状。numLinesRemoved
统计了我们到目前为止已消除的行数。curX
和 curY
变量确定了下落俄罗斯方块形状的实际位置。
for (int i = 0; i < BoardHeight; ++i) { for (int j = 0; j < BoardWidth; ++j) { Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1); if (shape != NoShape) DrawSquare(dc, 0 + j * SquareWidth(), boardTop + i * SquareHeight(), shape); } }
游戏的绘制分为两个步骤。在第一步中,我们绘制所有形状,或者已经掉落到棋盘底部的形状的残余物。所有的正方形都保存在 board
数组中。我们使用 ShapeAt
方法来访问它。
if (curPiece.GetShape() != NoShape) { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); DrawSquare(dc, 0 + x * SquareWidth(), boardTop + (BoardHeight - y - 1) * SquareHeight(), curPiece.GetShape()); } }
下一步是绘制正在下落的实际方块。
... switch (keycode) { case WXK_LEFT: TryMove(curPiece, curX - 1, curY); break; ...
在 Board::OnKeyDown
方法中,我们检查按下的键。如果我们按下左箭头键,我们尝试将方块向左移动。我们说尝试,因为方块可能无法移动。
void Board::OnTimer(wxCommandEvent& event) { if (isFallingFinished) { isFallingFinished = false; NewPiece(); } else { OneLineDown(); } }
在 Board::OnTimer
方法中,我们在前一个方块落到底部之后创建一个新的方块,或者我们向下移动一个下落的方块一行。
void Board::DropDown() { int newY = curY; while (newY > 0) { if (!TryMove(curPiece, curX, newY - 1)) break; --newY; } PieceDropped(); }
Board::DropDown
方法将下落的形状立即掉落到棋盘的底部。当按下空格键时,就会发生这种情况。
void Board::PieceDropped() { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); ShapeAt(x, y) = curPiece.GetShape(); } RemoveFullLines(); if (!isFallingFinished) NewPiece(); }
在 Board::PieceDropped
方法中,我们将当前形状设置在其最终位置。我们调用 RemoveFullLines
方法来检查我们是否至少有一条完整的线。如果尚未在 Board::PieceDropped
方法中创建新的俄罗斯方块形状,则创建一个新的俄罗斯方块形状。
if (lineIsFull) { ++numFullLines; for (int k = i; k < BoardHeight - 1; ++k) { for (int j = 0; j < BoardWidth; ++j) ShapeAt(j, k) = ShapeAt(j, k + 1); } }
此代码删除完整的行。在找到完整行后,我们增加计数器。我们将完整行上方所有行向下移动一行。这样我们就可以消除完整的行。请注意,在我们的俄罗斯方块游戏中,我们使用所谓的 *朴素重力*。这意味着正方形可能会悬浮在空隙上方。
void Board::NewPiece() { curPiece.SetRandomShape(); curX = BoardWidth / 2 + 1; curY = BoardHeight - 1 + curPiece.MinY(); if (!TryMove(curPiece, curX, curY)) { curPiece.SetShape(NoShape); timer->Stop(); isStarted = false; m_stsbar->SetStatusText(wxT("game over")); } }
Board::NewPiece
方法随机创建一个新的俄罗斯方块。如果该方块无法进入其初始位置,则游戏结束。
bool Board::TryMove(const Shape& 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 >= BoardWidth || y < 0 || y >= BoardHeight) return false; if (ShapeAt(x, y) != NoShape) return false; } curPiece = newPiece; curX = newX; curY = newY; Refresh(); return true; }
在 Board::TryMove
方法中,我们尝试移动我们的形状。如果形状在棋盘边缘或与另一个形状相邻,则返回 false。否则,我们将当前的下落形状放置到一个新的位置并返回 true。
Shape
类保存关于俄罗斯方块块的信息。
for (int i = 0; i < 4 ; i++) { for (int j = 0; j < 2; ++j) coords[i][j] = coordsTable[shape][i][j]; }
coords 数组保存俄罗斯方块块的坐标。例如,数字 { 0, -1 }、{ 0, 0 }、{ 1, 0 }、{ 1, 1 } 表示一个旋转的 S 形。下图说明了该形状。

当我们绘制当前下落的方块时,我们将其绘制在 curX
、curY
位置。然后我们查看坐标表并绘制所有四个正方形。

这是一个用 wxWidgets 编写的俄罗斯方块游戏。