用C/C++制作一个简单的俄罗斯方块小游戏

用C/C++制作一个简单的俄罗斯方块小游戏


在这里插入图片描述

0 准备

EasyX 的安装非常简单,百度搜索以下即可,但是在安装前一定要先安装 Visual Studio。下面的章节给出如何使用它。

1 游戏界面设计

1.1 界面布局

首先,我们要选择一章图片作为游戏的背景,我们可以在图片网站上下载合适的背景图片。

其次,在背景图片上划分出,游戏区和显示区,一般游戏区在正中间,两边为显示区。游戏区用于控制方块的移动、消除和旋转等;显示区用于速度和分数的展示。下面是我设计的游戏界面:

在这里插入图片描述

用画图工具打开图片,可以看到背景大小为800*600像素,同时在图片中间设置游戏区(虚线框,大小自定义),添加速度和分数的文本框。

1.2 用 EasyX 显示界面

此时,我相信你应该会建立 VS 工程了,并且安装了 EasyX。

建立一个文件夹 imp ,把1.1小节中的图片放在里面。imp 文件夹和 main.cpp 文件同一目录。
显示图形需要调用头文件 graphics.h。我们需要知道显示的图形的大小,这边是800*600,显示的位置为(0,0)

#include <graphics.h>

// 绘图窗口初始化
	initgraph(imp_width, imp_heght);
	loadimage(&background, _T("img/background.png"));
	putimage(0, 0, &background);

显示如下:

背景显示

这是一个简单的显示案例,后续方块的显示也是用这种方式。

1.3 音乐播放

先用 酷狗 下载一首好听的音乐,然后将音乐放在和 main.cpp 同一目录下。

我这边用了两首音乐,每次打开都是随机播放

程序:

#include "Windows.h"
#include <time.h>

#pragma comment (lib, "winmm.lib")

void Music::palyMusic()
{
	int chFlag;
	srand((unsigned)time(NULL));
	chFlag = rand() % 2;
	
	if (chFlag == 0)
	{
		//mciSendString("close 1.mp3", NULL, 0, NULL);
		mciSendString("open 2.mp3", NULL, 0, NULL);
		mciSendString("play 2.mp3 repeat", NULL, 0, NULL);
		mciSendString("setaudio 2.mp3 volume to 100", 0, 0, 0);
	}
	else
	{
		//mciSendString("close 2.mp3", NULL, 0, NULL);
		mciSendString("open 1.mp3", NULL, 0, NULL);
		mciSendString("play 1.mp3 repeat", NULL, 0, NULL);
		mciSendString("setaudio 1.mp3 volume to 100", 0, 0, 0);
	}
}

2 方块设计

2.1 方块显示

设计7种方块类型:

const int blocks[7][4] = {
		1,3,5,7, // I
		2,4,5,7, // Z 1型
		3,5,4,6, // Z 2型
		3,5,4,7, // T
		2,3,5,7, // L
		3,5,7,6, // J
		2,3,4,5, // 田
	};

方块的表示如下图所示:

方块显示

上面两张图表示在游戏中方块是如何表示的,方块可以由界面的横纵坐标表示,我们可以将下落的初始位置作为横坐标,方块的左边界作为纵坐标。其实就是游戏区的左上角作为坐标。

那么显示的原理知道了,就是操作数组,方块的图形呢?

小方块显示

小方块的显示可以根据这张图,从上图不难看出,小方块的大小为:20*20像素。如果我们要显示第一个小方块,我们可以加载这张图片然后从坐标(0,0)开始,显示长宽为20像素的图片。同意,如果要显示第三个绿色方块,就是从坐标(40,0)开始。

下面是实现的部分程序:

//计算小方块位置
blockType = rand() % 7;
for (int i = 0; i < 4; i++)
{
	smallBlock[i][0] = blocks[blockType][i] / 2;
	smallBlock[i][1] = blocks[blockType][i] % 2 + 1;//离左边界一格显示,方便旋转
}

//小方块显示函数
void Graph::block()
{
	IMAGE imgTmp;
	loadimage(&imgTmp, _T("img/small.png"));
	SetWorkingImage(&imgTmp);
	//putimage(0, 0, &imgTmp);
	for (int i = 0; i < 7; i++) {
		this->imgs[i] = new IMAGE;
		getimage(this->imgs[i], i * blocks_size, 0, blocks_size, blocks_size);
	}
	SetWorkingImage();
}

初始位置

2.2 随机生成一个方块

随机生成方块的原理就是将记录正在下落方块的数组清空初始化

void Graph::random()
{
	blockType = rand() % 7;

	for (int i = 0; i < 4; i++)
	{
		smallBlock[i][0] = blocks[blockType][i] / 2;
		smallBlock[i][1] = blocks[blockType][i] % 2 + 1;//离左边界一格显示,方便变形
	}
	colBasis = 0;
	rowBasis = 0;
}

2.3 方块记录

我们不仅需要对当前正在操作的方块进行记录,还需要对已经下落还未被消除的方块进行记录。可以将游戏区看作一个二维数组,开辟一个 29*14 的二维数组进行记录。

	//背景图像大小
	const unsigned int imp_width = 800;
	const unsigned int imp_heght = 600;
	//小方块大小
	const unsigned int blocks_size = 20;

	//游戏区边界
	const unsigned int left_margin = 240;
	const unsigned int right_margin = 485;
	const unsigned int down_margin = 570;
	const unsigned int up_margin = 10;
	const int rows = 29;
	const int cols = 14;
	
	//记录方块的数组
	vector<vector<int>> allBlock;

这里有个小技巧,因为方块要显示不同的颜色,因此我们可以用二维数组allBlock的值当作颜色的值

当allBlock[i][j]的值为0时,表示该位置没有方块;
当allBlock[i][j]的值大于0时,表示该位置有方块,显示的颜色用allBlock[i][j]的之表示

//已静止方块显示
	for (int i = rows-1; i > 3; --i)
	{
		for (int j = 0; j < cols; ++j)
		{
			if(allBlock[i][j]!=0)
				putimage(left_margin + j * blocks_size, up_margin + i * blocks_size, imgs[allBlock[i][j]-1]);
		}
	}

3 方块移动和旋转

3.1 方块的移动

方块的移动就是一个核心:方块的移动 = 对数组的操作

方块的下落 = 行坐标+1
方块的左移 = 列坐标-1
方块的右移 = 列坐标+1

前提是需要判断是否出界或者移动的下一个位置是否有方块

程序如下:

void Graph::moveLeft()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][1] <= 0 || allBlock[smallBlock[i][0]][smallBlock[i][1] - 1] >= 1)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		--smallBlock[i][1];
	}
	--colBasis;
}

void Graph::moveDown()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][0] >= rows)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		++smallBlock[i][0];
	}
	++rowBasis;
}

void Graph::moveRight()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][1] >= cols - 1 || allBlock[smallBlock[i][0]][smallBlock[i][1] + 1] >= 1)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		++smallBlock[i][1];
	}
	++colBasis;
}

当然移动的前提说需要用户按键输入的,所以需要有判断按键输入的函数和读取按键值的函数,我这边使用函数 _kbhit() 来判断是否有按键输入,用函数 _getch() 读取按键值

//控制方块移动
	if (_kbhit() && graph.startFlag)//如果键盘有输入
	{
		graph.keyPlay();
	}
void Graph::keyPlay()
{
	int ch = 0;
	ch = _getch();
	switch (ch)
	{
		//WASD键(小写)
		case 119: changeBlock();//上键
			break;
		case 97: moveLeft();//左键
			break;
		case 115: moveDown();//下键
			break;
		case 100: moveRight();//右键
			break; 
		//上下左右键
		case 72: changeBlock();//上键
			break;
		case 75: moveLeft();//左键
			break;
		case 80: moveDown();//下键
			break;
		case 77: moveRight();//右键
			break;
	}
}

3.2 方块的旋转

方块旋转

如上图所示,这样可以用几行代码实现了方块的旋转,但是仍然需要注意下面的几个问题:

  • 以什么为中心旋转?
  • 方块是不断下落的,行和列是一直在变化的
  • 在边界处有部分方块是不能旋转的

针对第一个问题,如果想让方块的旋转看起来不那么别捏,以4*4方格的中心旋转是最合适的,即图中的2,3,4,5作为旋转的核心。

针对第二个问题,可以将方块的行列切换至初始位置,再进行上图的公式,然后再切回来,这边可以设置两个变量确定方块离初始位置的距离。

针对第三个问题,将方块的行列号暂存,进行变换,然后再进行检测是否有方块在边界外面,如果有,旋转这步算作废。

旋转的时候初始位置的确定也是非常关键的,因为在边界处有些旋转是做不了的

初始位置

程序实现:

void Graph::changeBlock()
{
	int temp[4][2] = { 0 };

	for (int i = 0; i < 4; i++)
	{
		//配合偏置,进行方块的旋转
		temp[i][0] = smallBlock[i][1] - colBasis;
		temp[i][1] = 3 - (smallBlock[i][0] - rowBasis);

		temp[i][0] += rowBasis;
		temp[i][1] += colBasis;

		//检查合法性
		if (temp[i][1] == 0 || temp[i][1] == cols - 1)
			return;
	}
	for (int i = 0; i < 4; i++)//若合法,实行
	{
		smallBlock[i][0] = temp[i][0];
		smallBlock[i][1] = temp[i][1];
	}
}

3.3 方块的碰撞和消除

方块的消除需要考虑下面几个问题

  • 碰撞检测
  • 一行的消除算法
3.3.1 碰撞

碰撞检测很容易实现,由于左右移动我已经设置了边界检测,这边只需要对四个方块进行判断,也就是是说判断它们下面是否有方块就行。如果有,就返回 1

int Graph::check()
{
	int row, col;

	for (int i = 0; i < 4; i++)//若合法,实行
	{
		row = smallBlock[i][0]+1;
		col = smallBlock[i][1];

		if (row >= this->rows || allBlock[row][col] >= 1)
		{
			if (rowBasis == 0)
				return 2;
			else
				return 1;
		}
	}
	return 0;
}
3.3.2 消除

对一行的消除,采用一个二维数组对所有的位置进行记录,如果在(i,j)处有方块,则 allBlock[i][j]=1;在碰撞检测完毕之后,对整个数组进行遍历,对每一行移动的行数进行记录,尽量减少时间复杂度。

int clearRowNum[30] = { 0 };
int num=0;
//unordered_map<int, int>map;
if (check()==1)
{
	for (int i = 0; i < 4; i++) 
	{
		int row = smallBlock[i][0];
		int col = smallBlock[i][1];
		allBlock[row][col] = blockType+1;
	}
	//消除一行
	for (int i = rows-1; i > 3; --i)
	{
		for (int j = 0; j < cols; ++j)
		{
			if (allBlock[i][j] == 0)
			{
				clearRowNum[i] = num;
				break;
			}
			else if (j == cols-1)//该行需要消除
			{
				++num;
				clearRowNum[i] = 0;
			}
		}
	}

	for (int i = rows - 2; i > 3; --i)
	{
		if (clearRowNum[i] != 0)
		{
			for (int j = 0; j < cols; ++j)
			{
				allBlock[i + clearRowNum[i]][j] = allBlock[i][j];
			}
		}
	}
}
3.3.3 分数和下落速度

同时,在消除函数中可以添加分数计算,速度计算。大致的逻辑是每消除一行,分数变多;分数越高,下落速度越快;

//设置速度,得分越多,速度越快
score += num * cols;

speed = 100+score/10;
3.3.4 game over

当小方块处于初始位置时,它的下方有方块时就可以判断 game over了。

if (row >= this->rows || allBlock[row][col] >= 1)
{
	if (rowBasis == 0)
		return 2;
	else
		return 1;
}

game over 之后,界面会一直显示game over,直到输入 回车键

//游戏结束
	if (!graph.startFlag)
	{
		settextcolor(WHITE);
		settextstyle(40, 0, "黑体");
		setbkmode(TRANSPARENT);
		char s[10] = "Game Over";
		outtextxy(300, 280, s);

		if (_kbhit() && _getch() == 13)//如果键盘有输入
		{
			graph.init();
		}
	}

game over

4 制作 exe 文件

如何用 Visual Studio打包项目程序可以参考:

Visual Studio 怎么将项目程序打包成软件

5 总结

最后我想说的是,对方块的移动和旋转,其根本就是在对数组进行操作。

至于后续的一些最高分数记录,下一个方块提示等功能,都是锦上添花的功能,感兴趣的小伙伴可以尝试添加一下。

程序下载:
俄罗斯方块小游戏程序下载

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木白CPP

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值