【C++大作业】实现俄罗斯方块(附代码+实现思路带详细注释)

由于最近C++大作业需要,所以来记录下自己写大作业的记录(自留)

效果图

有目标才有动力
先来看下最终的效果图吧
最终实现效果
代码加上注释总计不足三百行,简化版本200行左右

环境配置SFML

首先由于我们想要最终的结果不仅仅是一个简单的终端用文字输出的界面,希望有一个图形化的界面,所以我们决定采用一个图像-----SFML

将这个库搭载到VS上的过程和我们之前说过的讲OPENCV搭载到VS上的操作过程是类似的,我们在这里就简单的提一下

首先去官网下载这个库
百度里直接搜索SFML就可以了
在这里插入图片描述
下载到电脑中,并且解压
然后就可以打开VS并且新建一个c++项目文件了
并且在解决方案资源管理器进行右键->属性->VC++目录->包含文件这里加上刚刚下载压缩包的include文件的目录
在库目录里面加上lib文件

在这里插入图片描述
在这里插入图片描述
这是我电脑中的位置,每个人下载解压的位置不一样这个路径也是不一样的

然后我们在链接器->输入->附加依赖项中添加

sfml-graphics-d.lib
sfml-audio-d.lib
sfml-window-d.lib
sfml-system-d.lib

到这里有些同学的电脑上可能已经可以实现调用库了
但是在我的电脑上还是不行
于是经过查找资料之后我发现解决的方法是讲解压文件里面的lib文件拷贝到对应的debug文件内即可。在这里插入图片描述
在完成环境的配置之后,我们就可以愉快的开始写代码了。

代码书写

首先进行最常见最常见的操作------导入库和床照命名空间

#include<SFML/Graphics.hpp>//处理图像的头文件
#include<SFML/Audio.hpp>//处理声音文件
#include<time.h>//处理时间文件
using namespace sf;

要做一个小游戏,首先得有游戏界面对不,所以我们先把游戏界面显示出来

int main(void) {
	srand(time(0));//用当前时间生成随机种子
	RenderWindow window(VideoMode(320, 416),"HEY");//创建窗口

	Texture bg;//加载背景图片
	bg.loadFromFile("C:/Users/Hey/Desktop/俄罗斯方块/ConsoleApplication1/image/bg.jpg");
	Sprite spriteBg(bg);//根据图片创造对象
	window.draw(spriteBg);
	system("pause");
	return 0;
}

这样就可以展现出一个简单的背景图像了
有背景之后我们要一步步丰富我们的代码,就是要开始编写游戏循环了

//进入游戏循环
	//进入游戏
	while (window.isOpen()) {//如果窗口没有被关闭
		window.draw(spriteBg);
		window.draw(spriteFrame);
		//绘制方块,绘制游戏
		drawBlocks(&window,&spriteBlock);
		//渲染方块
		window.draw(textScore);//显示分数
		window.display();//
	}
	system("pause");
	return 0;
}
//启动到现在的时间
		float time = clock.getElapsedTime().asSeconds();
		clock.restart();
		timer += time;
		
		//等待用户按键
		keyEvent(&window);//左右移动和旋转

		if (timer > delay)
		{
			//降落
			drop();//下降一个位置
			timer = 0;
		}
		for (int i = 0; i < 10; i++) {
			if (table[0][i]||table[1][0]|| table[1][1] || table[2][0]||table[2][1]||table[3][0]||table[3][1] ) {
				printf("游戏结束,最终得分为%d\n", score);
				system("pause");
				return -1;
			}
		}
		//消分处理
		clearLine();
		delay = SPEED_NORMAL;//速度还原

在游戏界面中展示方块,这时候就出现了问题,俄罗斯方块中一共有7中形状的方块,我们应该如何让合理的用计算机语言把他表示出来呢?
一波分析之后我们决定用矩阵来将其表述

实际用图形来表示如下图所示
在这里插入图片描述

在计算机代码语言里面

int blocks[7][4] = {//定义七种方块的数组
	{1,3,5,7},//I
	{2,4,5,7},//Z
	{3,5,4,6},//Z
	{3,5,4,7},//T
	{2,3,5,7},//L
	{3,5,7,6},//J
	{2,3,4,5}//田
};

与此同时我们还定义了一个全局变量

int blockIndex;//当前方块的种类

来保存当前代码块的种类

就是对应数组的简单表示方法,现在我们让各种模式的方块都记忆到数组里面了,但是离我们能够将他图形化的表示出来还有一定的距离。
于是我们开始了我们第一个函数编写,就是将我们的代码块表示到图像上
我们把我们整个图形界面划出一块游戏区域,游戏区域用数组表述

int table[ROW_COUNT][COL_COUNT] = { 0 };//游戏区域的表示
				  //若table[i][j]=0则下标为ij的空白,并用值表示颜色取值

若为0,则表示该块没有俄罗斯方块,若不为0,则该位置的数字表述其颜色和方块选项。
然后我们让其产生方块newBlock()

struct Point {//俄罗斯方块的表示(都是4小块的)位置不断发生变化

	int x;
	int y;
} curBlock[4];
void newBlock() {//生成方块
	//随机生成
	blockIndex = 1 + rand() % 7;
	int n = blockIndex - 1;
	for (int i = 0; i < 4; i++) {
		curBlock[i].x = blocks[n][i] % 2;
		curBlock[i].y = blocks[n][i] / 2;
	}
}

newblock之后我们要将其画到界面上,就有了drawblock(),

void drawBlocks(RenderWindow* window, Sprite* spriteBlock) {
	// 绘制已降落完毕的方块
	for (int i = 0; i < ROW_COUNT; i++)
		for (int j = 0; j < COL_COUNT; j++)
		{
			if (table[i][j] == 0) continue;
			spriteBlock->setTextureRect(IntRect(table[i][j] * 18, 0, 18, 18));
			spriteBlock->setPosition(j * 18, i * 18);
			spriteBlock->move(28, 31); //调整边框位置
			window->draw(*spriteBlock);
		}
	// 绘制当前方块
	for (int i = 0; i < 4; i++)
	{
		spriteBlock->setTextureRect(IntRect(blockIndex * 18, 0, 18, 18));
		spriteBlock->setPosition(curBlock[i].x * 18, curBlock[i].y * 18);//用其切割一个小方块
		spriteBlock->move(28, 31); //调整边框位置
		window->draw(*spriteBlock);
	}
}

这个函数主要分两大块来绘制,一个是已经降落了的小方块,一个是还在降落过程中的小方块,这边先把代码都写出来了
然后需要说明的是,由于我原有背景图游戏区域并不明显,于是我又加了一个边框,所以这里有矫正到边框位置的代码,然后我绘制俄罗斯方块的小方框是从其他图片上裁剪下来的(图片素材放到了github上,所以这里也有相像素裁剪的部分。
其实两个循环的去边就在于,一个是在变换往下落的,而一个是已经固定下来的,而往下落的就需要判断现在的格子是否为0,若为0,则回退一步,所以这也就是为什么最终代码里面我有备份的操作。

绘制小方块的步骤写完了,既然小方块画出来了,我们就要让他动起来呀
于是就有了drop()函数

void drop() {
	//y坐标加一就可以
	for (int i = 0; i < 4; i++) {
		bakBlock[i] = curBlock[i];
		curBlock[i].y += 1;
	}
	//不能穿过地上
	if (check() == false) {
		//固定处理
		for (int i = 0; i < 4; i++) {
			table[bakBlock[i].y][bakBlock[i].x] = blockIndex;
		}
		//产生新方块
		newBlock();
	}
}

就是每次把现在的小方块进行备份然后把它的y坐标加一就可了
然后这里要加一个判断小方块有没有穿过边框,如果有就把他的备份(也就是上一步固定下来)
这里也就产生了一个check()函数

bool check() {//检查是不是编写
	for (int i = 0; i < 4; i++) {
		if (curBlock[i].x < 0 || curBlock[i].x >= COL_COUNT || curBlock[i].y >= ROW_COUNT || curBlock[i].y <= 0 || table[curBlock[i].y][curBlock[i].x])	//超出边界//会不会重复位置
			//超出边界//会不会重复位置
		{
			return false;
		}
	}
	return true;
}

做到这里,我们的小方块已经能够正常的在界面上展现然后下落,但是还不能用键盘控制其左右移动或者加速下落,旋转,接下来一个个实现。
先写一个鼠标事件的函数

void keyEvent(RenderWindow *window) {//按键反应
	bool rotate = false;//是否旋转
	int dx = 0;//偏移量
	Event e;//事件变量
	//pollEvent 从队列里面返回一个事件
	//无事件 false
	while (window->pollEvent(e)) {
		if (e.type == Event::Closed)//事件是关闭窗口
		{
			window->close();
		}
		if (e.type == Event::KeyPressed)
		{
			switch (e.key.code) {
			case Keyboard::Up:
				rotate = true;
				break;
			case Keyboard::Left:
				dx = -1;
				break;
			case Keyboard::Right:
				dx = 1;
				break;
			default:
				break;
			}
		}
	
		if (dx != 0) {
			moveLeftRight(dx);
		}

		//旋转操作
		if (rotate) {
			doRotate();
		}
	}
}

不同的按钮使用表示不一样的意思。至于旋转和移动,我们单独写两个函数来实现

旋转

void doRotate() {
	if (blockIndex == 7) {  // 田字形,不需要旋转
		return;
	}
	//备份当前方块,出问题再回退
	for (int i = 0; i < 4; i++) {
		bakBlock[i] = curBlock[i];  
	}

	Point p = curBlock[1]; //旋转中心
	//a[i].x=p.x-a[i].y+p.x
	//a[i].y=p.y+a[i].x-p.x;
	for (int i = 0; i < 4; i++)
	{
		struct Point tmp = curBlock[i];
		curBlock[i].x = p.x - tmp.y + p.y;
		curBlock[i].y = p.y + tmp.x - p.x;
	}
	if (!check()) {
		for (int i = 0; i < 4; i++) {
			curBlock[i] = bakBlock[i];
		}
	}
}

左右移动

void moveLeftRight(int dx) {
	for (int i = 0; i < 4; i++) {
		bakBlock[i] = curBlock[i];//备份
		curBlock[i].x += dx;
	}
	if (!check()) {
		for (int i = 0; i < 4; i++) {
			curBlock[i] = bakBlock[i];//备份
		}
	}
}

我注意到我们平时我玩的俄罗斯方块的游戏里面长按加速键会有加速下降的功能于是我定义了全局变量,并在事件控制的代码里加上了相应的代码。

const float SPEED_NORMAL = 0.3;
const float SPEED_QUICK = 0.05;
float delay = SPEED_NORMAL;
	//一直下降
		if (Keyboard::isKeyPressed(Keyboard::Down)) {
			delay = SPEED_QUICK;//快速
		}

这里有一个小的注意点,如果只在事件控制里面加上相应的代码而不在其他位置做相应的改变就会使方块一直加速下降而不会回到原来速度,所以要在main函数里面加一句

		delay = SPEED_NORMAL;//速度还原

然后我们要实现消除行这一个操作
于是我们又写了一个函数

void clearLine() {
	int k = ROW_COUNT - 1;//重新写方块
	for (int i = ROW_COUNT - 1; i > 0; i--) {
		int count = 0;
		for (int j = 0; j < COL_COUNT; j++) {
			if (table[i][j]) {
				count++;
			}
			table[k][j] = table[i][j];//一遍统计一边写一编
		}
		if (count < COL_COUNT) {
			k--;//拿去覆盖
		}
		else {
			score += 10;
		}
	}
}

我们采用的方法使从下往上一行行写,在写的时候顺便计数,如果有满行,就k–,从新计数。用覆盖的方法实现了消行。

到这里其实俄罗斯方块的主要部分已经写好了,接下来就是一些优化和完善部分了

玩游戏的时候音乐是灵魂
所以我给他加上了背景音乐


	//游戏背景音
	Music music;
	if (!music.openFromFile("C:/Users/Hey/Desktop/俄罗斯方块/ConsoleApplication1/bg2.wav"))
	{
		return -1;
	}
	music.setLoop(true);//循环播放
	music.play();

	SoundBuffer xiaochu;
	if(!xiaochu.loadFromFile("C:/Users/Hey/Desktop/俄罗斯方块/ConsoleApplication1/bg.wav")){
		return -1;
	}

并在对应需要消行音效的地方家里对应语句

然后玩游戏嘛,有竞技才有意思,于是我又加入了计分系统
就是加入一个函数,将成绩显示出来

void initScore() {
	if (!font.loadFromFile("C:/Users/Hey/Desktop/俄罗斯方块/ConsoleApplication1/Sansation.ttf")) {
		exit(1);
	}

	textScore.setFont(font); // font is a sf::Font
	textScore.setCharacterSize(30);// set the character size
	textScore.setFillColor(sf::Color::Black); // set the color
	textScore.setStyle(sf::Text::Bold); // set the text style
	textScore.setPosition(255, 175);
	textScore.setString("0");
}

好了
到这里这个俄罗斯方块就都完成了
完整代码的链接请点击这里
代码一共包含10个函数,共计200行+
在这里插入图片描述


版权声明:本文为m0_46480441原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。