原文: http://zetcode.com/gui/wxwidgets/thetetrisgame/

俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。

俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为 tetrominoes。 S 形,Z 形,T 形,L 形,线形,镜像 L 形和正方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以便它们尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。

wxWidgets 中的俄罗斯方块游戏 - 图1

图:Tetrominoes

wxWidgets 是一个用于创建应用的工具包。 还有其他一些旨在创建计算机游戏的库。 不过,可以使用 wxWidgets 和其他应用工具包来创建简单的游戏。

开发

我们的俄罗斯方块游戏没有图像,我们使用 wxWidgets 编程工具包中提供的绘图 API 绘制四面体。 每个计算机游戏的背后都有一个数学模型。 俄罗斯方块也是如此。

游戏背后的一些想法。

  • 我们使用wxTimer创建游戏周期。
  • 绘制四方块。
  • 形状以正方形为单位移动(而不是逐个像素移动)。
  • 从数学上讲,棋盘是一个简单的数字列表。

Shape.h

  1. #ifndef SHAPE_H
  2. #define SHAPE_H
  3. enum Tetrominoes { NoShape, ZShape, SShape, LineShape,
  4. TShape, SquareShape, LShape, MirroredLShape };
  5. class Shape
  6. {
  7. public:
  8. Shape() { SetShape(NoShape); }
  9. void SetShape(Tetrominoes shape);
  10. void SetRandomShape();
  11. Tetrominoes GetShape() const { return pieceShape; }
  12. int x(int index) const { return coords[index][0]; }
  13. int y(int index) const { return coords[index][1]; }
  14. int MinX() const;
  15. int MaxX() const;
  16. int MinY() const;
  17. int MaxY() const;
  18. Shape RotateLeft() const;
  19. Shape RotateRight() const;
  20. private:
  21. void SetX(int index, int x) { coords[index][0] = x; }
  22. void SetY(int index, int y) { coords[index][1] = y; }
  23. Tetrominoes pieceShape;
  24. int coords[4][2];
  25. };
  26. #endif

Shape.cpp

  1. #include <stdlib.h>
  2. #include <algorithm>
  3. #include "Shape.h"
  4. using namespace std;
  5. void Shape::SetShape(Tetrominoes shape)
  6. {
  7. static const int coordsTable[8][4][2] = {
  8. { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
  9. { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } },
  10. { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } },
  11. { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } },
  12. { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } },
  13. { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } },
  14. { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } },
  15. { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }
  16. };
  17. for (int i = 0; i < 4 ; i++) {
  18. for (int j = 0; j < 2; ++j)
  19. coords[i][j] = coordsTable[shape][i][j];
  20. }
  21. pieceShape = shape;
  22. }
  23. void Shape::SetRandomShape()
  24. {
  25. int x = rand() % 7 + 1;
  26. SetShape(Tetrominoes(x));
  27. }
  28. int Shape::MinX() const
  29. {
  30. int m = coords[0][0];
  31. for (int i=0; i<4; i++) {
  32. m = min(m, coords[i][0]);
  33. }
  34. return m;
  35. }
  36. int Shape::MaxX() const
  37. {
  38. int m = coords[0][0];
  39. for (int i=0; i<4; i++) {
  40. m = max(m, coords[i][0]);
  41. }
  42. return m;
  43. }
  44. int Shape::MinY() const
  45. {
  46. int m = coords[0][1];
  47. for (int i=0; i<4; i++) {
  48. m = min(m, coords[i][1]);
  49. }
  50. return m;
  51. }
  52. int Shape::MaxY() const
  53. {
  54. int m = coords[0][1];
  55. for (int i=0; i<4; i++) {
  56. m = max(m, coords[i][1]);
  57. }
  58. return m;
  59. }
  60. Shape Shape::RotateLeft() const
  61. {
  62. if (pieceShape == SquareShape)
  63. return *this;
  64. Shape result;
  65. result.pieceShape = pieceShape;
  66. for (int i = 0; i < 4; ++i) {
  67. result.SetX(i, y(i));
  68. result.SetY(i, -x(i));
  69. }
  70. return result;
  71. }
  72. Shape Shape::RotateRight() const
  73. {
  74. if (pieceShape == SquareShape)
  75. return *this;
  76. Shape result;
  77. result.pieceShape = pieceShape;
  78. for (int i = 0; i < 4; ++i) {
  79. result.SetX(i, -y(i));
  80. result.SetY(i, x(i));
  81. }
  82. return result;
  83. }

Board.h

  1. #ifndef BOARD_H
  2. #define BOARD_H
  3. #include "Shape.h"
  4. #include <wx/wx.h>
  5. class Board : public wxPanel
  6. {
  7. public:
  8. Board(wxFrame *parent);
  9. void Start();
  10. void Pause();
  11. void linesRemovedChanged(int numLines);
  12. protected:
  13. void OnPaint(wxPaintEvent& event);
  14. void OnKeyDown(wxKeyEvent& event);
  15. void OnTimer(wxCommandEvent& event);
  16. private:
  17. enum { BoardWidth = 10, BoardHeight = 22 };
  18. Tetrominoes & ShapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }
  19. int SquareWidth() { return GetClientSize().GetWidth() / BoardWidth; }
  20. int SquareHeight() { return GetClientSize().GetHeight() / BoardHeight; }
  21. void ClearBoard();
  22. void DropDown();
  23. void OneLineDown();
  24. void PieceDropped();
  25. void RemoveFullLines();
  26. void NewPiece();
  27. bool TryMove(const Shape& newPiece, int newX, int newY);
  28. void DrawSquare(wxPaintDC &dc, int x, int y, Tetrominoes shape);
  29. wxTimer *timer;
  30. bool isStarted;
  31. bool isPaused;
  32. bool isFallingFinished;
  33. Shape curPiece;
  34. int curX;
  35. int curY;
  36. int numLinesRemoved;
  37. Tetrominoes board[BoardWidth * BoardHeight];
  38. wxStatusBar *m_stsbar;
  39. };
  40. #endif

Board.cpp

  1. #include "Board.h"
  2. Board::Board(wxFrame *parent)
  3. : wxPanel(parent, wxID_ANY, wxDefaultPosition,
  4. wxDefaultSize, wxBORDER_NONE)
  5. {
  6. timer = new wxTimer(this, 1);
  7. m_stsbar = parent->GetStatusBar();
  8. isFallingFinished = false;
  9. isStarted = false;
  10. isPaused = false;
  11. numLinesRemoved = 0;
  12. curX = 0;
  13. curY = 0;
  14. ClearBoard();
  15. Connect(wxEVT_PAINT, wxPaintEventHandler(Board::OnPaint));
  16. Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Board::OnKeyDown));
  17. Connect(wxEVT_TIMER, wxCommandEventHandler(Board::OnTimer));
  18. }
  19. void Board::Start()
  20. {
  21. if (isPaused)
  22. return;
  23. isStarted = true;
  24. isFallingFinished = false;
  25. numLinesRemoved = 0;
  26. ClearBoard();
  27. NewPiece();
  28. timer->Start(300);
  29. }
  30. void Board::Pause()
  31. {
  32. if (!isStarted)
  33. return;
  34. isPaused = !isPaused;
  35. if (isPaused) {
  36. timer->Stop();
  37. m_stsbar->SetStatusText(wxT("paused"));
  38. } else {
  39. timer->Start(300);
  40. wxString str;
  41. str.Printf(wxT("%d"), numLinesRemoved);
  42. m_stsbar->SetStatusText(str);
  43. }
  44. Refresh();
  45. }
  46. void Board::OnPaint(wxPaintEvent& event)
  47. {
  48. wxPaintDC dc(this);
  49. wxSize size = GetClientSize();
  50. int boardTop = size.GetHeight() - BoardHeight * SquareHeight();
  51. for (int i = 0; i < BoardHeight; ++i) {
  52. for (int j = 0; j < BoardWidth; ++j) {
  53. Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1);
  54. if (shape != NoShape)
  55. DrawSquare(dc, 0 + j * SquareWidth(),
  56. boardTop + i * SquareHeight(), shape);
  57. }
  58. }
  59. if (curPiece.GetShape() != NoShape) {
  60. for (int i = 0; i < 4; ++i) {
  61. int x = curX + curPiece.x(i);
  62. int y = curY - curPiece.y(i);
  63. DrawSquare(dc, 0 + x * SquareWidth(),
  64. boardTop + (BoardHeight - y - 1) * SquareHeight(),
  65. curPiece.GetShape());
  66. }
  67. }
  68. }
  69. void Board::OnKeyDown(wxKeyEvent& event)
  70. {
  71. if (!isStarted || curPiece.GetShape() == NoShape) {
  72. event.Skip();
  73. return;
  74. }
  75. int keycode = event.GetKeyCode();
  76. if (keycode == 'p' || keycode == 'P') {
  77. Pause();
  78. return;
  79. }
  80. if (isPaused)
  81. return;
  82. switch (keycode) {
  83. case WXK_LEFT:
  84. TryMove(curPiece, curX - 1, curY);
  85. break;
  86. case WXK_RIGHT:
  87. TryMove(curPiece, curX + 1, curY);
  88. break;
  89. case WXK_DOWN:
  90. TryMove(curPiece.RotateRight(), curX, curY);
  91. break;
  92. case WXK_UP:
  93. TryMove(curPiece.RotateLeft(), curX, curY);
  94. break;
  95. case WXK_SPACE:
  96. DropDown();
  97. break;
  98. case 'd':
  99. OneLineDown();
  100. break;
  101. case 'D':
  102. OneLineDown();
  103. break;
  104. default:
  105. event.Skip();
  106. }
  107. }
  108. void Board::OnTimer(wxCommandEvent& event)
  109. {
  110. if (isFallingFinished) {
  111. isFallingFinished = false;
  112. NewPiece();
  113. } else {
  114. OneLineDown();
  115. }
  116. }
  117. void Board::ClearBoard()
  118. {
  119. for (int i = 0; i < BoardHeight * BoardWidth; ++i)
  120. board[i] = NoShape;
  121. }
  122. void Board::DropDown()
  123. {
  124. int newY = curY;
  125. while (newY > 0) {
  126. if (!TryMove(curPiece, curX, newY - 1))
  127. break;
  128. --newY;
  129. }
  130. PieceDropped();
  131. }
  132. void Board::OneLineDown()
  133. {
  134. if (!TryMove(curPiece, curX, curY - 1))
  135. PieceDropped();
  136. }
  137. void Board::PieceDropped()
  138. {
  139. for (int i = 0; i < 4; ++i) {
  140. int x = curX + curPiece.x(i);
  141. int y = curY - curPiece.y(i);
  142. ShapeAt(x, y) = curPiece.GetShape();
  143. }
  144. RemoveFullLines();
  145. if (!isFallingFinished)
  146. NewPiece();
  147. }
  148. void Board::RemoveFullLines()
  149. {
  150. int numFullLines = 0;
  151. for (int i = BoardHeight - 1; i >= 0; --i) {
  152. bool lineIsFull = true;
  153. for (int j = 0; j < BoardWidth; ++j) {
  154. if (ShapeAt(j, i) == NoShape) {
  155. lineIsFull = false;
  156. break;
  157. }
  158. }
  159. if (lineIsFull) {
  160. ++numFullLines;
  161. for (int k = i; k < BoardHeight - 1; ++k) {
  162. for (int j = 0; j < BoardWidth; ++j)
  163. ShapeAt(j, k) = ShapeAt(j, k + 1);
  164. }
  165. }
  166. }
  167. if (numFullLines > 0) {
  168. numLinesRemoved += numFullLines;
  169. wxString str;
  170. str.Printf(wxT("%d"), numLinesRemoved);
  171. m_stsbar->SetStatusText(str);
  172. isFallingFinished = true;
  173. curPiece.SetShape(NoShape);
  174. Refresh();
  175. }
  176. }
  177. void Board::NewPiece()
  178. {
  179. curPiece.SetRandomShape();
  180. curX = BoardWidth / 2 + 1;
  181. curY = BoardHeight - 1 + curPiece.MinY();
  182. if (!TryMove(curPiece, curX, curY)) {
  183. curPiece.SetShape(NoShape);
  184. timer->Stop();
  185. isStarted = false;
  186. m_stsbar->SetStatusText(wxT("game over"));
  187. }
  188. }
  189. bool Board::TryMove(const Shape& newPiece, int newX, int newY)
  190. {
  191. for (int i = 0; i < 4; ++i) {
  192. int x = newX + newPiece.x(i);
  193. int y = newY - newPiece.y(i);
  194. if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
  195. return false;
  196. if (ShapeAt(x, y) != NoShape)
  197. return false;
  198. }
  199. curPiece = newPiece;
  200. curX = newX;
  201. curY = newY;
  202. Refresh();
  203. return true;
  204. }
  205. void Board::DrawSquare(wxPaintDC& dc, int x, int y, Tetrominoes shape)
  206. {
  207. static wxColour colors[] = { wxColour(0, 0, 0), wxColour(204, 102, 102),
  208. wxColour(102, 204, 102), wxColour(102, 102, 204),
  209. wxColour(204, 204, 102), wxColour(204, 102, 204),
  210. wxColour(102, 204, 204), wxColour(218, 170, 0) };
  211. static wxColour light[] = { wxColour(0, 0, 0), wxColour(248, 159, 171),
  212. wxColour(121, 252, 121), wxColour(121, 121, 252),
  213. wxColour(252, 252, 121), wxColour(252, 121, 252),
  214. wxColour(121, 252, 252), wxColour(252, 198, 0) };
  215. static wxColour dark[] = { wxColour(0, 0, 0), wxColour(128, 59, 59),
  216. wxColour(59, 128, 59), wxColour(59, 59, 128),
  217. wxColour(128, 128, 59), wxColour(128, 59, 128),
  218. wxColour(59, 128, 128), wxColour(128, 98, 0) };
  219. wxPen pen(light[int(shape)]);
  220. pen.SetCap(wxCAP_PROJECTING);
  221. dc.SetPen(pen);
  222. dc.DrawLine(x, y + SquareHeight() - 1, x, y);
  223. dc.DrawLine(x, y, x + SquareWidth() - 1, y);
  224. wxPen darkpen(dark[int(shape)]);
  225. darkpen.SetCap(wxCAP_PROJECTING);
  226. dc.SetPen(darkpen);
  227. dc.DrawLine(x + 1, y + SquareHeight() - 1,
  228. x + SquareWidth() - 1, y + SquareHeight() - 1);
  229. dc.DrawLine(x + SquareWidth() - 1,
  230. y + SquareHeight() - 1, x + SquareWidth() - 1, y + 1);
  231. dc.SetPen(*wxTRANSPARENT_PEN);
  232. dc.SetBrush(wxBrush(colors[int(shape)]));
  233. dc.DrawRectangle(x + 1, y + 1, SquareWidth() - 2,
  234. SquareHeight() - 2);
  235. }

Tetris.h

  1. #include <wx/wx.h>
  2. class Tetris : public wxFrame
  3. {
  4. public:
  5. Tetris(const wxString& title);
  6. };

Tetris.cpp

  1. #include "Tetris.h"
  2. #include "Board.h"
  3. Tetris::Tetris(const wxString& title)
  4. : wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(180, 380))
  5. {
  6. wxStatusBar *sb = CreateStatusBar();
  7. sb->SetStatusText(wxT("0"));
  8. Board *board = new Board(this);
  9. board->SetFocus();
  10. board->Start();
  11. }

main.h

  1. #include <wx/wx.h>
  2. class MyApp : public wxApp
  3. {
  4. public:
  5. virtual bool OnInit();
  6. };

main.cpp

  1. #include "main.h"
  2. #include "Tetris.h"
  3. IMPLEMENT_APP(MyApp)
  4. bool MyApp::OnInit()
  5. {
  6. srand(time(NULL));
  7. Tetris *tetris = new Tetris(wxT("Tetris"));
  8. tetris->Centre();
  9. tetris->Show(true);
  10. return true;
  11. }

我对游戏做了一些简化,以便于理解。 游戏启动后立即开始。 我们可以通过按 p 键暂停游戏。 空格键将把俄罗斯方块放在底部。 d 键会将棋子下降一行。 (它可以用来加快下降速度。)游戏以恒定速度运行,没有实现加速。 分数是我们已删除的行数。

  1. ...
  2. isFallingFinished = false;
  3. isStarted = false;
  4. isPaused = false;
  5. numLinesRemoved = 0;
  6. curX = 0;
  7. curY = 0;
  8. ...

在开始游戏之前,我们先初始化一些重要的变量。 isFallingFinished变量确定俄罗斯方块形状是否已完成下降,然后我们需要创建一个新形状。 numLinesRemoved计算行数,到目前为止我们已经删除了行数。 curXcurY变量确定下降的俄罗斯方块形状的实际位置。

  1. for (int i = 0; i < BoardHeight; ++i) {
  2. for (int j = 0; j < BoardWidth; ++j) {
  3. Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1);
  4. if (shape != NoShape)
  5. DrawSquare(dc, 0 + j * SquareWidth(),
  6. boardTop + i * SquareHeight(), shape);
  7. }
  8. }

游戏的绘图分为两个步骤。 在第一步中,我们绘制所有形状或已放置到板底部的形状的其余部分。 所有正方形都记在board数组中。 我们使用ShapeAt()方法访问它。

  1. if (curPiece.GetShape() != NoShape) {
  2. for (int i = 0; i < 4; ++i) {
  3. int x = curX + curPiece.x(i);
  4. int y = curY - curPiece.y(i);
  5. DrawSquare(dc, 0 + x * SquareWidth(),
  6. boardTop + (BoardHeight - y - 1) * SquareHeight(),
  7. curPiece.GetShape());
  8. }
  9. }

下一步是绘制掉落的实际零件。

  1. ...
  2. switch (keycode) {
  3. case WXK_LEFT:
  4. TryMove(curPiece, curX - 1, curY);
  5. break;
  6. ...

Board::OnKeyDown()方法中,我们检查按键是否按下。 如果按向左箭头键,我们将尝试将棋子向左移动。 我们说尝试,因为这片可能无法移动。

  1. void Board::OnTimer(wxCommandEvent& event)
  2. {
  3. if (isFallingFinished) {
  4. isFallingFinished = false;
  5. NewPiece();
  6. } else {
  7. OneLineDown();
  8. }
  9. }

Board::OnTimer()方法中,我们可以创建一个新的片段,将前一个片段放到底部,或者将下降的片段向下移动一行。

  1. void Board::DropDown()
  2. {
  3. int newY = curY;
  4. while (newY > 0) {
  5. if (!TryMove(curPiece, curX, newY - 1))
  6. break;
  7. --newY;
  8. }
  9. PieceDropped();
  10. }

Board::DropDown()方法将下落的形状立即下降到板的底部。 当我们按下空格键时会发生这种情况。

  1. void Board::PieceDropped()
  2. {
  3. for (int i = 0; i < 4; ++i) {
  4. int x = curX + curPiece.x(i);
  5. int y = curY - curPiece.y(i);
  6. ShapeAt(x, y) = curPiece.GetShape();
  7. }
  8. RemoveFullLines();
  9. if (!isFallingFinished)
  10. NewPiece();
  11. }

Board::PieceDropped()方法中,我们将当前形状设置为其最终位置。 我们调用RemoveFullLines()方法来检查是否至少有一个完整的行。 如果尚未在Board::PieceDropped()方法中创建新的俄罗斯方块形状,则可以创建一个新的俄罗斯方块形状。

  1. if (lineIsFull) {
  2. ++numFullLines;
  3. for (int k = i; k < BoardHeight - 1; ++k) {
  4. for (int j = 0; j < BoardWidth; ++j)
  5. ShapeAt(j, k) = ShapeAt(j, k + 1);
  6. }
  7. }

此代码将删除所有行。 找到整条线后,我们增加计数器。 我们将整行上方的所有行向下移动一行。 这样我们就破坏了整个生产线。 注意,在俄罗斯方块游戏中,我们使用了朴素引力。 这意味着正方形可能会漂浮在空白间隙上方。

  1. void Board::NewPiece()
  2. {
  3. curPiece.SetRandomShape();
  4. curX = BoardWidth / 2 + 1;
  5. curY = BoardHeight - 1 + curPiece.MinY();
  6. if (!TryMove(curPiece, curX, curY)) {
  7. curPiece.SetShape(NoShape);
  8. timer->Stop();
  9. isStarted = false;
  10. m_stsbar->SetStatusText(wxT("game over"));
  11. }
  12. }

Board::NewPiece()方法随机创建一个新的俄罗斯方块。 如果棋子无法进入其初始位置,则游戏结束。

  1. bool Board::TryMove(const Shape& newPiece, int newX, int newY)
  2. {
  3. for (int i = 0; i < 4; ++i) {
  4. int x = newX + newPiece.x(i);
  5. int y = newY - newPiece.y(i);
  6. if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
  7. return false;
  8. if (ShapeAt(x, y) != NoShape)
  9. return false;
  10. }
  11. curPiece = newPiece;
  12. curX = newX;
  13. curY = newY;
  14. Refresh();
  15. return true;
  16. }

Board::TryMove()方法中,我们尝试移动形状。 如果形状在棋盘的边缘或与其他形状相邻,则返回false。 否则,我们将当前下降形状放置到新位置并返回true

Shape类保存有关俄罗斯方块的信息。

  1. for (int i = 0; i < 4 ; i++) {
  2. for (int j = 0; j < 2; ++j)
  3. coords[i][j] = coordsTable[shape][i][j];
  4. }

coords数组保存俄罗斯方块的坐标。 例如,数字{0, -1}, {0, 0}, {1, 0}, {1, 1}表示旋转的 S 形。 下图说明了形状。

wxWidgets 中的俄罗斯方块游戏 - 图2

图:坐标

当绘制当前下降片时,将其绘制在curXcurY位置。 然后,我们查看坐标表并绘制所有四个正方形。

wxWidgets 中的俄罗斯方块游戏 - 图3

图:俄罗斯方块

这是 wxWidgets 中的俄罗斯方块游戏。