Qt小徐

热爱分享的开发者,欢迎大家指出项目的不足,我会尽力修改,供大家一起学习!

有趣小游戏之俄罗斯方块 铜牌收录

游戏介绍

这是我以前使用 C++ 和 EasyX 写的一个俄罗斯方块小游戏,里面用到的 C++ 特性并不多。游戏主要分成了两个类来实现:Game 和 Block 类,分别用来实现游戏逻辑和单独的俄罗斯方块,里面顶多就用到了静态成员函数和变量的特性,但主要的游戏逻辑还是放到了 main 函数中实现。这样,每个类分工明确,就实现了俄罗斯方块游戏。代码非常简单,里面的每个游戏逻辑都值得初学者分析和学习。最后,大家如果有什么建议或者代码中存在不足,还恳请在评论区留言,我会尽力修改,提高游戏的代码质量!

游戏截图

俄罗斯方块小游戏

俄罗斯方块小游戏

代码分析

1. Block 类

这个类主要封装了俄罗斯方块中的独立小方块,提供了各种函数来操作方块,比如 move 移动方块、clear 清除方块、draw 绘制方块等等。但其中的重点是方块碰撞的判断和旋转方块的算法:方块碰撞的判断可以先移动方块,接着遍历方块在地图上每一个块的位置,如果这里有块,就说明发生碰撞了;而旋转方块的算法,因为除了长条占四格以外,其它方块只占三格,所以我们采用最简单的方法,只旋转三格以内的块,具体做法是中心块不变,分别旋转十字中心和四个角,长条方块另外处理。这样简单的实现方法 “小学生” 也能看懂,于是就实现了 Block 类。

2. Game 类

这个类的主要功能是完成游戏界面的绘制和方块满行的判断。游戏界面的绘制很简单,而对于方块满行的判断,可以先遍历一行,如果满行,就将上面的每一行都下移一格位置。这样,Game 类的实现也就大功告成了!

源码下载

下载链接:点此下载游戏源代码压缩包

在线查看源码

/*
*	程序:有趣小游戏之俄罗斯方块
*	作者:Qt小徐
*	编译环境:Visual Studio 2022 + EasyX_20220610
*	最后修改:2022-7-30
*/


#include <easyx.h>
#include <ctime>


#define MAP_WIDTH 10	// 地图宽度
#define MAP_HEIGHT 20	// 地图高度
#define BLOCK_WIDTH 20	// 方块宽度


// 游戏类
class Game
{
public:
	void drawMap() const;						// 绘制地图
	void drawPrompt() const;					// 绘制提示界面
	bool checkLine(const unsigned& line) const;	// 判断满行
	void clearLine();							// 清除满行

// 内联函数
public:
	// 设置地图
	void setMap(const int& x, const int& y, const bool& value)
	{
		map[x][y] = value;
	}

	// 获取地图
	const bool& getMap(const int& x, const int& y) const
	{
		return map[x][y];
	}

	// 添加游戏时间
	void addGameTime()
	{
		gameTime += 500;
	}

private:
	bool map[MAP_WIDTH][MAP_HEIGHT] = { 0 };	// 地图数组
	unsigned score = 0;							// 游戏分数
	unsigned gameTime = 0;						// 游戏时间
};


void Game::drawMap() const
{
	// 绘制边框
	roundrect(10, 10, 340, 430, 10, 10);
	rectangle(20, 20, 220, 420);

	// 绘制地图
	setfillcolor(LIGHTCYAN);
	for (int i = 0; i < MAP_WIDTH; i++)
	{
		for (int j = 0; j < MAP_HEIGHT; j++)
		{
			if (map[i][j])
			{
				int left = 20 + 20 * i, top = 20 + 20 * j;
				fillrectangle(left, top, left + 20, top + 20);
			}
		}
	}
}

void Game::drawPrompt() const
{
	// 设置字体
	LOGFONT f;
	gettextstyle(&f);
	f.lfHeight = 30;
	wcscpy_s(f.lfFaceName, L"微软雅黑");
	f.lfQuality = DEFAULT_QUALITY;
	settextstyle(&f);
	settextcolor(BLACK);

	// 绘制 “下一方块” 提示
	outtextxy(235, 20, L"下一方块");
	rectangle(240, 60, 320, 140);

	// 绘制 “分数” 和 “时间”
	f.lfHeight = 25;
	settextstyle(&f);

	wchar_t str[10];
	wsprintf(str, L"分数:%u", score);
	outtextxy(230, 150, str);

	wsprintf(str, L"时间:%u秒", gameTime / 1000);
	outtextxy(230, 175, str);

	// 绘制 “俄罗斯方块”
	f.lfHeight = 40;
	settextstyle(&f);
	settextcolor(RED);
	outtextxy(234, 250, L"俄罗斯");
	outtextxy(250, 290, L"方块");
	outtextxy(240, 330, L"Tetris");

	// 绘制 “作者”
	f.lfHeight = 25;
	settextstyle(&f);
	outtextxy(234, 395, L"作者:小徐");
}

bool Game::checkLine(const unsigned& line) const
{
	for (int i = 0; i < MAP_WIDTH; i++)
	{
		if (map[i][line] == 0)
		{
			return false;
		}
	}
	return true;
}

void Game::clearLine()
{
	int line = -1;
	// 判断哪一行满行
	for (int j = 0; j < MAP_HEIGHT; j++)
	{
		if (checkLine(j))
		{
			line = j;
			break;
		}
	}

	if (line != -1)
	{
		// 将上一行移至满行
		for (int j = line; j > 0; j--)
		{
			for (int i = 0; i < MAP_WIDTH; i++)
			{
				map[i][j] = map[i][j - 1];
			}
		}
		score += 10;	// 将游戏分数加 10
	}
	drawPrompt();
}


// 方块类
class Block
{
public:
	Block(Game& game, const int& x = (MAP_WIDTH - 4) / 2, const int& y = 0);	// 初始化函数
	bool move(const unsigned& direction = 0);									// 移动函数,0 表示下移一格,1 表示左移一格,2 表示右移一格,当下移检测到碰撞时返回 true
	void draw() const;															// 绘制函数
	void clear() const;															// 清除函数
	void addMap() const;														// 添加方块到地图
	void rotate();																// 旋转方块,false 向左,true 向右
	bool checkCollision() const;												// 碰撞检查
	static void generateBlockData();											// 初始化方块数据
	void setPos(const int& x = (MAP_WIDTH - 4) / 2, const int& y = 0);			// 设置方块坐标
	void randType();															// 随机方块类型
	void operator=(const Block& block);											// 为 Block 赋值

private:
	int x;							// 方块 X 坐标
	int y;							// 方块 Y 坐标
	unsigned char type;				// 方块类型
	bool direction;					// 旋转方向,false 代表顺时针,true 代表逆时针
	bool block[4][4];				// 方块数组,true 表示有方块,false 表示没方块
	COLORREF color;					// 方块填充颜色
	static bool blockData[7][4][4];	// 方块数据,对象共享的资源
	Game& game;						// Game 引用对象
};


bool Block::blockData[7][4][4] = { 0 };


Block::Block(Game& game, const int& x, const int& y) : game(game), x(x), y(y)
{
	type = rand() % 7;	// 随机方块类型
	direction = 1;
	
	// 设置方块填充颜色
	switch (type)
	{
	case 0:
		color = LIGHTBLUE;
		break;

	case 1:
		color = GREEN;
		break;

	case 2:
		color = LIGHTGREEN;
		break;

	case 3:
		color = YELLOW;
		break;

	case 4:
		color = RED;
		break;

	case 5:
		color = CYAN;
		break;

	case 6:
		color = MAGENTA;
	}

	// 复制方块数据
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			block[i][j] = blockData[type][i][j];
		}
	}
}

bool Block::move(const unsigned& direction)
{
	switch (direction)
	{
	case 0:
		y++;
		if (checkCollision())
		{
			y--;
			return true;
		}
		break;

	case 1:
		x--;
		if (checkCollision())
		{
			x++;
		}
		break;

	case 2:
		x++;
		if (checkCollision())
		{
			x--;
		}
		break;
	}
	return false;
}

void Block::draw() const
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[i][j])
			{
				// 绘制方块,Y 坐标为负时不绘制
				if (j + y >= 0)
				{
					setfillcolor(color);
					int left = 20 + BLOCK_WIDTH * (x + i), top = 20 + BLOCK_WIDTH * (j + y);
					fillrectangle(left, top, left + BLOCK_WIDTH, top + BLOCK_WIDTH);
				}
			}
		}
	}
}

void Block::clear() const
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[i][j])
			{
				// 清除方块
				int left = 20 + BLOCK_WIDTH * (x + i), top = 20 + BLOCK_WIDTH * (j + y);
				clearrectangle(left, top, left + BLOCK_WIDTH, top + BLOCK_WIDTH);
			}
		}
	}
}

void Block::addMap() const
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[i][j])
			{
				// 添加方块
				game.setMap(x + i, y + j, block[i][j]);
			}
		}
	}
}

void Block::rotate()
{
	// 田字方块不旋转
	if (type != 4)
	{
		// 只旋转左上角 3 * 3 的区域
		bool temp;
		if (direction == 0)	// 顺时针旋转
		{
			// 角旋转
			temp = block[0][0];
			block[0][0] = block[0][2];
			block[0][2] = block[2][2];
			block[2][2] = block[2][0];
			block[2][0] = temp;

			// 十字中心旋转
			temp = block[1][0];
			block[1][0] = block[0][1];
			block[0][1] = block[1][2];
			block[1][2] = block[2][1];
			block[2][1] = temp;
		}
		else	// 逆时针旋转
		{
			// 角旋转
			temp = block[0][0];
			block[0][0] = block[2][0];
			block[2][0] = block[2][2];
			block[2][2] = block[0][2];
			block[0][2] = temp;

			// 十字中心旋转
			temp = block[1][0];
			block[1][0] = block[2][1];
			block[2][1] = block[1][2];
			block[1][2] = block[0][1];
			block[0][1] = temp;
		}

		// 处理其它方块的特殊情况
		switch (type)
		{
			// 长条方块
		case 0:
			if (block[1][3])
			{
				block[1][3] = 0;
				block[3][1] = 1;
			}
			else
			{
				block[1][3] = 1;
				block[3][1] = 0;
			}
			break;

			// Z 字方块(正)和 Z 字方块(反)
		case 5:
		case 6:
			// 没有发生碰撞才执行此操作
			if (!checkCollision())
			{
				direction = !direction;
			}
		}

		// 碰撞检查,如果发生碰撞则往相反方向旋转一次
		if (checkCollision())
		{
			// 相反方向旋转
			if (direction == 1)	// 顺时针旋转
			{
				// 角旋转
				temp = block[0][0];
				block[0][0] = block[0][2];
				block[0][2] = block[2][2];
				block[2][2] = block[2][0];
				block[2][0] = temp;

				// 十字中心旋转
				temp = block[1][0];
				block[1][0] = block[0][1];
				block[0][1] = block[1][2];
				block[1][2] = block[2][1];
				block[2][1] = temp;
			}
			else	// 逆时针旋转
			{
				// 角旋转
				temp = block[0][0];
				block[0][0] = block[2][0];
				block[2][0] = block[2][2];
				block[2][2] = block[0][2];
				block[0][2] = temp;

				// 十字中心旋转
				temp = block[1][0];
				block[1][0] = block[2][1];
				block[2][1] = block[1][2];
				block[1][2] = block[0][1];
				block[0][1] = temp;
			}
			if (type == 0)
			{
				if (block[1][3])
				{
					block[1][3] = 0;
					block[3][1] = 1;
				}
				else
				{
					block[1][3] = 1;
					block[3][1] = 0;
				}
			}
		}
	}
}

bool Block::checkCollision() const
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			// 判断方块是否与地图发生碰撞,顶部不判断
			if ((game.getMap(x + i, y + j) || 20 + BLOCK_WIDTH * (x + i) < 20 || 20 + BLOCK_WIDTH * (x + i) + BLOCK_WIDTH > 220 || 20 + BLOCK_WIDTH * (j + y) + BLOCK_WIDTH > 420) && block[i][j])
			{
				return true;
			}
		}
	}
	return false;
}

void Block::generateBlockData()
{
	// 长条方块
	blockData[0][1][0] = blockData[0][1][1] = blockData[0][1][2] = blockData[0][1][3] = true;
	// 七字方块(正)
	blockData[1][0][0] = blockData[1][1][0] = blockData[1][1][1] = blockData[1][1][2] = true;
	// 七字方块(反)
	blockData[2][2][0] = blockData[2][1][0] = blockData[2][1][1] = blockData[2][1][2] = true;
	// 凸字方块
	blockData[3][1][0] = blockData[3][0][1] = blockData[3][1][1] = blockData[3][2][1] = true;
	// 田字方块
	blockData[4][0][0] = blockData[4][1][0] = blockData[4][0][1] = blockData[4][1][1] = true;
	// Z 字方块(正)
	blockData[5][0][0] = blockData[5][1][0] = blockData[5][1][1] = blockData[5][2][1] = true;
	// Z 字方块(反)
	blockData[6][2][0] = blockData[6][1][0] = blockData[6][1][1] = blockData[6][0][1] = true;
}

void Block::setPos(const int& x, const int& y)
{
	this->x = x;
	this->y = y;
}

void Block::randType()
{
	type = rand() % 7;	// 随机方块类型
	direction = 1;

	// 设置方块填充颜色
	switch (type)
	{
	case 0:
		color = LIGHTBLUE;
		break;

	case 1:
		color = GREEN;
		break;

	case 2:
		color = LIGHTGREEN;
		break;

	case 3:
		color = YELLOW;
		break;

	case 4:
		color = RED;
		break;

	case 5:
		color = CYAN;
		break;

	case 6:
		color = MAGENTA;
	}

	// 复制方块数据
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			block[i][j] = blockData[type][i][j];
		}
	}
}

void Block::operator=(const Block& block)
{
	x = block.x;
	y = block.y;
	type = block.type;
	direction = block.direction;
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			this->block[i][j] = block.block[i][j];
		}
	}
	color = block.color;
}


int main()
{
	SetWindowText(initgraph(350, 440), L"俄罗斯方块");

	// 设置绘图颜色
	setbkcolor(WHITE);
	cleardevice();
	setlinecolor(BLACK);

	// 生成游戏界面和数据
	srand(time(NULL));
	Block::generateBlockData();

	Game game;
	game.drawMap();
	game.drawPrompt();

	Block b(game);
	Block nextBlock(game, 11, 2);	// 下一方块
	clock_t start = 0;				// 时钟开始时间
	clock_t end;					// 时钟结束时间

	ExMessage msg;

	nextBlock.draw();

	// 游戏循环
	BeginBatchDraw();
	while (true)
	{
		b.clear();
		clearrectangle(20, 20, 220, 420);
		game.drawMap();

		// 按键按下
		while (peekmessage(&msg, EX_KEY) && msg.message == WM_KEYDOWN)
		{
			switch (msg.vkcode)
			{
				// 上键旋转
			case 'W':
			case VK_UP:
				b.rotate();
				break;

				// 下键加速下落
			case 'S':
			case VK_DOWN:
				if (b.move())
				{
					// 重新设置方块
					b.addMap();
					b = nextBlock;
					b.setPos();
					nextBlock.clear();
					nextBlock.setPos(11, 2);
					nextBlock.randType();
					nextBlock.draw();

					// 如果方块刚刚生成却发生了碰撞,说明游戏结束
					if (b.checkCollision())
					{
						MessageBox(GetHWnd(), L"游戏结束!", L"提示", MB_ICONWARNING);
						goto exit;
					}
				}
				break;

				// 左键移动
			case 'A':
			case VK_LEFT:
				b.move(1);
				break;

				// 右键移动
			case 'D':
			case VK_RIGHT:
				b.move(2);
				break;

				// Esc 键退出程序
			case VK_ESCAPE:
				goto exit;
				break;
			}
		}

		// 判断每过了 500 毫秒方块下移一格
		end = clock();
		if (end - start >= 500)
		{
			if (b.move())
			{
				// 重新设置方块
				b.addMap();
				b = nextBlock;
				b.setPos();
				nextBlock.clear();
				nextBlock.setPos(11, 2);
				nextBlock.randType();
				nextBlock.draw();

				// 如果方块刚刚生成却发生了碰撞,说明游戏结束
				if (b.checkCollision())
				{
					MessageBox(GetHWnd(), L"游戏结束!", L"提示", MB_ICONWARNING);
					goto exit;
				}
			}
			start = clock();
			game.addGameTime();
		}
		b.draw();
		game.clearLine();
		FlushBatchDraw();	// 刷新缓冲区
		Sleep(50);			// 每 50 毫秒接收一次按键
	}

exit:
	EndBatchDraw();

	closegraph();
	return 0;
}

评论 (10) -

  • 大佬,为什么我这个会出边界?我就是按照你上面这个代码做的
    • 可能在边界的碰撞处理上出现了问题
  • 为什么会出现中途停止的情况
  • 我感觉自己写不出来,我每次只能跟着人家视频学写,但是只能了解点思路,自己还是写不出来

    • 学习编写程序的时候要边看边写,同时不懂的地方还要停下来思考,这样才能培养独立编写代码的习惯
  • 大佬你有bug,第566行EM_KEY打错了,应该是EX_KEY
    • 当时我用的 EasyX 版本是 EasyX_20220610,为 EM_KEY,目前最新的版本是 EasyX_20220901,改为了EX_KEY,所以项目报错了,改了就好
    • 对的,这个要改成EX_KEY,感谢大佬,让我体验了一把俄罗斯方块。
  • 膜拜大佬,我自己也在尝试写,但是问题很多,我可以借用里面的一些代码吗,感觉直接用有点不大礼貌,感谢。(落泪)

添加评论