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 编写的俄罗斯方块游戏。