从零实现俄罗斯方块(c语言+思路分析)

俄罗斯方块

文章说明:

本文大部分参考至 俄罗斯方块(C语言实现)_c语言俄罗斯方块_2021dragon的博客-CSDN博客,本人经过修改编辑,改变了文章的一些思路顺序,使得新手便于理解(个人想法)。更新后的文章最大的特色就是从零开始的思路顺序,以及如何使用搜索引擎的思路。本人水平有限,文章中间可能还有许多思路的逻辑漏洞。但是看完这篇文章,再经过你们自己按顺序调试运行一遍程序,即可了解一些做项目的过程和思路。学长的总结一定是能让你们少走许多弯路,不用再像当年的我一样抓耳挠腮不知从何下手。看完文章之后自己重新写一遍,鉴于编写文章过程中还会有许多疏漏的细节,请各位同学遇到不会的问题前来询问即可。

俄罗斯方块相信大家都知道,这里就不再介绍什么游戏背景了,我这里对本代码实现的俄罗斯方块作一些说明:

按方向键的左右键可实现方块的左右移动。
按方向键的下键可实现方块的加速下落。
按空格键可实现方块的顺时针旋转。
按Esc键可退出游戏。
按S键可暂停游戏,暂停游戏后按任意键继续游戏。
按R键可重新开始游戏。
除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。

以下是完整代码:

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
#include <time.h>
#include <conio.h>

#define ROW 29 //游戏区行数
#define COL 20 //游戏区列数

#define DOWN 80 //方向键:下
#define LEFT 75 //方向键:左
#define RIGHT 77 //方向键:右

#define SPACE 32 //空格键
#define ESC 27 //Esc键

struct Face
{
	int data[ROW][COL + 10]; //用于标记指定位置是否有方块(1为有,0为无)
	int color[ROW][COL + 10]; //用于记录指定位置的方块颜色编码
}face;

struct Block
{
	int space[4][4];
}block[7][4]; //用于存储7种基本形状方块的各自的4种形态的信息,共28种

//隐藏光标
void HideCursor();
//光标跳转
void CursorJump(int x, int y);
//初始化界面
void InitInterface();
//初始化方块信息
void InitBlockInfo();
//颜色设置
void color(int num);
//画出方块
void DrawBlock(int shape, int form, int x, int y);
//空格覆盖
void DrawSpace(int shape, int form, int x, int y);
//合法性判断
int IsLegal(int shape, int form, int x, int y);
//判断得分与结束
int JudeFunc();
//游戏主体逻辑函数
void StartGame();
//从文件读取最高分
void ReadGrade();
//更新最高分到文件
void WriteGrade();

int max, grade; //全局变量
int main()
{
#pragma warning (disable:4996) //消除警告
	max = 0, grade = 0; //初始化变量
	system("title 俄罗斯方块"); //设置cmd窗口的名字
	system("mode con lines=29 cols=60"); //设置cmd窗口的大小
	HideCursor(); //隐藏光标
	ReadGrade(); //从文件读取最高分到max变量	
	InitInterface(); //初始化界面
	InitBlockInfo(); //初始化方块信息
	srand((unsigned int)time(NULL)); //设置随机数生成的起点
	StartGame(); //开始游戏
	return 0;
}

//隐藏光标
void HideCursor()
{
	CONSOLE_CURSOR_INFO curInfo; //定义光标信息的结构体变量
	curInfo.dwSize = 1;  //如果没赋值的话,隐藏光标无效
	curInfo.bVisible = FALSE; //将光标设置为不可见
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorInfo(handle, &curInfo); //设置光标信息
}
//光标跳转
void CursorJump(int x, int y)
{
	COORD pos; //定义光标位置的结构体变量
	pos.X = x; //横坐标设置
	pos.Y = y; //纵坐标设置
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorPosition(handle, pos); //设置光标位置
}
//初始化界面
void InitInterface()
{
	color(7); //颜色设置为白色
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL + 10; j++)
		{
			if (j == 0 || j == COL - 1 || j == COL + 9)
			{
				face.data[i][j] = 1; //标记该位置有方块
				CursorJump(2 * j, i);
				printf("■");
			}
			else if (i == ROW - 1)
			{
				face.data[i][j] = 1; //标记该位置有方块
				printf("■");
			}
			else
				face.data[i][j] = 0; //标记该位置无方块
		}
	}
	for (int i = COL; i < COL + 10; i++)
	{
		face.data[8][i] = 1; //标记该位置有方块
		CursorJump(2 * i, 8);
		printf("■");
	}

	CursorJump(2 * COL, 1);
	printf("下一个方块:");
	
	CursorJump(2 * COL + 4, ROW - 19);
	printf("左移:←");
	
	CursorJump(2 * COL + 4, ROW - 17);
	printf("右移:→");
	
	CursorJump(2 * COL + 4, ROW - 15);
	printf("加速:↓");
	
	CursorJump(2 * COL + 4, ROW - 13);
	printf("旋转:空格");
	
	CursorJump(2 * COL + 4, ROW - 11);
	printf("暂停: S");
	
	CursorJump(2 * COL + 4, ROW - 9);
	printf("退出: Esc");
	
	CursorJump(2 * COL + 4, ROW - 7);
	printf("重新开始:R");
	
	CursorJump(2 * COL + 4, ROW - 5);
	printf("最高纪录:%d", max);
	
	CursorJump(2 * COL + 4, ROW - 3);
	printf("当前分数:%d", grade);

}
//初始化方块信息
void InitBlockInfo()
{
	//“T”形
	for (int i = 0; i <= 2; i++)
		block[0][0].space[1][i] = 1;
	block[0][0].space[2][1] = 1;

	//“L”形
	for (int i = 1; i <= 3; i++)
		block[1][0].space[i][1] = 1;
	block[1][0].space[3][2] = 1;
	
	//“J”形
	for (int i = 1; i <= 3; i++)
		block[2][0].space[i][2] = 1;
	block[2][0].space[3][1] = 1;
	
	for (int i = 0; i <= 1; i++)
	{
		//“Z”形
		block[3][0].space[1][i] = 1;
		block[3][0].space[2][i + 1] = 1;
		//“S”形
		block[4][0].space[1][i + 1] = 1;
		block[4][0].space[2][i] = 1;
		//“O”形
		block[5][0].space[1][i + 1] = 1;
		block[5][0].space[2][i + 1] = 1;
	}
	
	//“I”形
	for (int i = 0; i <= 3; i++)
		block[6][0].space[i][1] = 1;
	
	int temp[4][4];
	for (int shape = 0; shape < 7; shape++) //7种形状
	{
		for (int form = 0; form < 3; form++) //4种形态(已经有了一种,这里每个还需增加3种)
		{
			//获取第form种形态
			for (int i = 0; i < 4; i++)
			{
				for (int j = 0; j < 4; j++)
				{
					temp[i][j] = block[shape][form].space[i][j];
				}
			}
			//将第form种形态顺时针旋转,得到第form+1种形态
			for (int i = 0; i < 4; i++)
			{
				for (int j = 0; j < 4; j++)
				{
					block[shape][form + 1].space[i][j] = temp[3 - j][i];
				}
			}
		}
	}

}
//颜色设置
void color(int c)
{
	switch (c)
	{
	case 0:
		c = 13; //“T”形方块设置为紫色
		break;
	case 1:
	case 2:
		c = 12; //“L”形和“J”形方块设置为红色
		break;
	case 3:
	case 4:
		c = 10; //“Z”形和“S”形方块设置为绿色
		break;
	case 5:
		c = 14; //“O”形方块设置为黄色
		break;
	case 6:
		c = 11; //“I”形方块设置为浅蓝色
		break;
	default:
		c = 7; //其他默认设置为白色
		break;
	}
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c); //颜色设置
	//注:SetConsoleTextAttribute是一个API(应用程序编程接口)
}
//画出方块
void DrawBlock(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[shape][form].space[i][j] == 1) //如果该位置有方块
			{
				CursorJump(2 * (x + j), y + i); //光标跳转到指定位置
				printf("■"); //输出方块
			}
		}
	}
}
//空格覆盖
void DrawSpace(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[shape][form].space[i][j] == 1) //如果该位置有方块
			{
				CursorJump(2 * (x + j), y + i); //光标跳转到指定位置
				printf("  "); //打印空格覆盖(两个空格)
			}
		}
	}
}
//合法性判断
int IsLegal(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			//如果方块落下的位置本来就已经有方块了,则不合法
			if ((block[shape][form].space[i][j] == 1) && (face.data[y + i][x + j] == 1))
				return 0; //不合法
		}
	}
	return 1; //合法
}
//判断得分与结束
int JudeFunc()
{
	//判断是否得分
	for (int i = ROW - 2; i > 4; i--)
	{
		int sum = 0; //记录第i行的方块个数
		for (int j = 1; j < COL - 1; j++)
		{
			sum += face.data[i][j]; //统计第i行的方块个数
		}
		if (sum == 0) //该行没有方块,无需再判断其上的层次(无需再继续判断是否得分)
			break; //跳出循环
		if (sum == COL - 2) //该行全是方块,可得分
		{
			grade += 10; //满一行加10分
			color(7); //颜色设置为白色
			CursorJump(2 * COL + 4, ROW - 3); //光标跳转到显示当前分数的位置
			printf("当前分数:%d", grade); //更新当前分数
			for (int j = 1; j < COL - 1; j++) //清除得分行的方块信息
			{
				face.data[i][j] = 0; //该位置得分后被清除,标记为无方块
				CursorJump(2 * j, i); //光标跳转到该位置
				printf("  "); //打印空格覆盖(两个空格)
			}
			//把被清除行上面的行整体向下挪一格
			for (int m = i; m >1; m--)
			{
				sum = 0; //记录上一行的方块个数
				for (int n = 1; n < COL - 1; n++)
				{
					sum += face.data[m - 1][n]; //统计上一行的方块个数
					face.data[m][n] = face.data[m - 1][n]; //将上一行方块的标识移到下一行
					face.color[m][n] = face.color[m - 1][n]; //将上一行方块的颜色编号移到下一行
					if (face.data[m][n] == 1) //上一行移下来的是方块,打印方块
					{
						CursorJump(2 * n, m); //光标跳转到该位置
						color(face.color[m][n]); //颜色设置为还方块的颜色
						printf("■"); //打印方块
					}
					else //上一行移下来的是空格,打印空格
					{
						CursorJump(2 * n, m); //光标跳转到该位置
						printf("  "); //打印空格(两个空格)
					}
				}
				if (sum == 0) //上一行移下来的全是空格,无需再将上层的方块向下移动(移动结束)
					return 1; //返回1,表示还需调用该函数进行判断(移动下来的可能还有满行)
			}
		}
	}
	//判断游戏是否结束
	for (int j = 1; j < COL - 1; j++)
	{
		if (face.data[1][j] == 1) //顶层有方块存在(以第1行为顶层,不是第0行)
		{
			Sleep(1000); //留给玩家反应时间
			system("cls"); //清空屏幕
			color(7); //颜色设置为白色
			CursorJump(2 * (COL / 3), ROW / 2 - 3);
			if (grade>max)
			{
				printf("恭喜你打破最高记录,最高记录更新为%d", grade);
				WriteGrade();
			}
			else if (grade == max)
			{
				printf("与最高记录持平,加油再创佳绩", grade);
			}
			else
			{
				printf("请继续加油,当前与最高记录相差%d", max - grade);
			}
			CursorJump(2 * (COL / 3), ROW / 2);
			printf("GAME OVER");
			while (1)
			{
				char ch;
				CursorJump(2 * (COL / 3), ROW / 2 + 3);
				printf("再来一局?(y/n):");
				scanf("%c", &ch);
				if (ch == 'y' || ch == 'Y')
				{
					system("cls");
					main();
				}
				else if (ch == 'n' || ch == 'N')
				{
					CursorJump(2 * (COL / 3), ROW / 2 + 5);
					exit(0);
				}
				else
				{
					CursorJump(2 * (COL / 3), ROW / 2 + 4);
					printf("选择错误,请再次选择");
				}
			}
		}
	}
	return 0; //判断结束,无需再调用该函数进行判断
}
//游戏主体逻辑函数
void StartGame()
{
	int shape = rand() % 7, form = rand() % 4; //随机获取方块的形状和形态
	while (1)
	{
		int t = 0;
		int nextShape = rand() % 7, nextForm = rand() % 4; //随机获取下一个方块的形状和形态
		int x = COL / 2 - 2, y = 0; //方块初始下落位置的横纵坐标
		color(nextShape); //颜色设置为下一个方块的颜色
		DrawBlock(nextShape, nextForm, COL + 3, 3); //将下一个方块显示在右上角
		while (1)
		{
			color(shape); //颜色设置为当前正在下落的方块
			DrawBlock(shape, form, x, y); //将该方块显示在初始下落位置
			if (t == 0)
			{
				t = 15000; //这里t越小,方块下落越快(可以根据此设置游戏难度)
			}
			while (--t)
			{
				if (kbhit() != 0) //若键盘被敲击,则退出循环
					break;
			}
			if (t == 0) //键盘未被敲击
			{
				if (IsLegal(shape, form, x, y + 1) == 0) //方块再下落就不合法了(已经到达底部)
				{
					//将当前方块的信息录入face当中
					//face:记录界面的每个位置是否有方块,若有方块还需记录该位置方块的颜色。
					for (int i = 0; i < 4; i++)
					{
						for (int j = 0; j < 4; j++)
						{
							if (block[shape][form].space[i][j] == 1)
							{
								face.data[y + i][x + j] = 1; //将该位置标记为有方块
								face.color[y + i][x + j] = shape; //记录该方块的颜色数值
							}
						}
					}
					while (JudeFunc()); //判断此次方块下落是否得分以及游戏是否结束
					break; //跳出当前死循环,准备进行下一个方块的下落
				}
				else //未到底部
				{
					DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
					y++; //纵坐标自增(下一次显示方块时就相当于下落了一格了)
				}
			}
			else //键盘被敲击
			{
				char ch = getch(); //读取keycode
				switch (ch)
				{
				case DOWN: //方向键:下
					if (IsLegal(shape, form, x, y + 1) == 1) //判断方块向下移动一位后是否合法
					{
						//方块下落后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						y++; //纵坐标自增(下一次显示方块时就相当于下落了一格了)
					}
					break;
				case LEFT: //方向键:左
					if (IsLegal(shape, form, x - 1, y) == 1) //判断方块向左移动一位后是否合法
					{
						//方块左移后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						x--; //横坐标自减(下一次显示方块时就相当于左移了一格了)
					}
					break;
				case RIGHT: //方向键:右
					if (IsLegal(shape, form, x + 1, y) == 1) //判断方块向右移动一位后是否合法
					{
						//方块右移后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						x++; //横坐标自增(下一次显示方块时就相当于右移了一格了)
					}
					break;
				case SPACE: //空格键
					if (IsLegal(shape, (form + 1) % 4, x, y + 1) == 1) //判断方块旋转后是否合法
					{
						//方块旋转后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						y++; //纵坐标自增(总不能原地旋转吧)
						form = (form + 1) % 4; //方块的形态自增(下一次显示方块时就相当于旋转了)
					}
					break;
				case ESC: //Esc键
					system("cls"); //清空屏幕
					color(7);
					CursorJump(COL, ROW / 2);
					printf("  游戏结束  ");
					CursorJump(COL, ROW / 2 + 2);
					exit(0); //结束程序
				case 's':
				case 'S':  //暂停
					system("pause>nul"); //暂停(按任意键继续)
					break;
				case 'r':
				case 'R': //重新开始
					system("cls"); //清空屏幕
					main(); //重新执行主函数
				}
			}
		}
		shape = nextShape, form = nextForm; //获取下一个方块的信息
		DrawSpace(nextShape, nextForm, COL + 3, 3); //将右上角的方块信息用空格覆盖
	}
}
//从文件读取最高分
void ReadGrade()
{
	FILE* pf = fopen("俄罗斯方块最高得分记录.txt", "r"); //以只读方式打开文件
	if (pf == NULL) //打开文件失败
	{
		pf = fopen("俄罗斯方块最高得分记录.txt", "w"); //以只写方式打开文件(文件不存在可以自动创建该文件)
		fwrite(&grade, sizeof(int), 1, pf); //将max写入文件(此时max为0),即将最高历史得分初始化为0
	}
	fseek(pf, 0, SEEK_SET); //使文件指针pf指向文件开头
	fread(&max, sizeof(int), 1, pf); //读取文件中的最高历史得分到max当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}
//更新最高分到文件
void WriteGrade()
{
	FILE* pf = fopen("俄罗斯方块最高得分记录.txt", "w"); //以只写方式打开文件
	if (pf == NULL) //打开文件失败
	{
		printf("保存最高得分记录失败\n");
		exit(0);
	}
	fwrite(&grade, sizeof(int), 1, pf); //将本局游戏得分写入文件当中(更新最高历史得分)
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}

游戏代码思路分析详解

游戏图型界面

首先,定义一下界面的大小,我们这里定义游戏区的行数和列数。

#define ROW 29 //游戏区行数
#define COL 20 //游戏区列数

我这里将方块堆积的区域称为游戏区,将按键提示以及方块提示的区域称为提示区。

初始化界面
初始化界面完成基本信息的打印,包括由白色方块构成的边界和按键提示语句。

对照最终效果图片,看着代码很好理解,但是需要注意两点:

一个小方块在cmd命令窗口当中占两个单位的横坐标、一个单位的纵坐标。
光标跳转函数CursorJump接收的是光标将要跳至的横纵坐标。
例如,想要将光标跳转到 i 行 j 列(这里所说的行和列都是以一个方块为单位),就等价于让光标跳转到坐标(2*j,i)处。

方块的信息有了,接下来就是将方块在屏幕上显示出来。那么想到这里,我们要怎么在控制台的某个地方打印一个东西出来呢?在屏幕上进行输出时,我们需要光标先移动到目标位置再进行输出,因此,光标跳转函数也是必不可少的。

光标跳转

在屏幕上进行输出时,我们需要光标先移动到目标位置再进行输出,因此,光标跳转函数也是必不可少的。

//光标跳转

void CursorJump(int x, int y)
{
	COORD pos; //定义光标位置的结构体变量
	pos.X = x; //横坐标设置
	pos.Y = y; //纵坐标设置
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorPosition(handle, pos); //设置光标位置
}

有了这个方法,我们开始打印我们的界面吧

//初始化界面

void InitInterface()
{
	color(7); //颜色设置为白色
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL + 10; j++)
		{
			if (j == 0 || j == COL - 1 || j == COL + 9)
			{
				face.data[i][j] = 1; //标记该位置有方块
				CursorJump(2 * j, i);
				printf("■");
			}
			else if (i == ROW - 1)
			{
				face.data[i][j] = 1; //标记该位置有方块
				printf("■");
			}
			else
				face.data[i][j] = 0; //标记该位置无方块
		}
	}
	for (int i = COL; i < COL + 10; i++)
	{
		face.data[8][i] = 1; //标记该位置有方块
		CursorJump(2 * i, 8);
		printf("■");
	}

	CursorJump(2 * COL, 1);
	printf("下一个方块:");
	
	CursorJump(2 * COL + 4, ROW - 19);
	printf("左移:←");
	
	CursorJump(2 * COL + 4, ROW - 17);
	printf("右移:→");
	
	CursorJump(2 * COL + 4, ROW - 15);
	printf("加速:↓");
	
	CursorJump(2 * COL + 4, ROW - 13);
	printf("旋转:空格");
	
	CursorJump(2 * COL + 4, ROW - 11);
	printf("暂停: S");
	
	CursorJump(2 * COL + 4, ROW - 9);
	printf("退出: Esc");
	
	CursorJump(2 * COL + 4, ROW - 7);
	printf("重新开始:R");
	
	CursorJump(2 * COL + 4, ROW - 5);
	printf("最高纪录:%d", max);
	
	CursorJump(2 * COL + 4, ROW - 3);
	printf("当前分数:%d", grade);

}

如何存储方块数据?

我们需要一个结构体,该结构体记录界面的每个位置是否有方块,若有方块还需记录该位置方块的颜色。

struct Face
{
	int data[ROW][COL + 10]; //用于标记指定位置是否有方块(1为有,0为无)
	int color[ROW][COL + 10]; //用于记录指定位置的方块颜色编码
}face;

其次,我们还需要一个结构体,该结构体当中存储着一个4行4列的二维数组,这个二维数组就用于存储单个方块的基本信息。那我们如何把,4行4列的二维数组可以容纳下游戏当中的每一种方块)

而俄罗斯方块当中有7种基本形状的方块,而每种方块通过旋转后又可以得到3种方块,共28种。

因此,我们可以用该结构体定义一个7行4列的二维数组存储这28个方块的信息。

struct Block{
	int space[4][4];
}block[7][4]; 		//用于存储7种基本形状方块的各自的4种形态的信息,共28种

俄罗斯方块有7种基本形状,便是以下7种:

我们先将这7种基本形状的方块信息存储在各自的第0种形态处,如下:

然后取第0种形态顺时针旋转后得到第1种形态,取第1种形态顺时针旋转后得到第2种形态,取第2种形态顺时针旋转后得到第3种形态。这7种形状都按此方法操作,最终得到全部28种方块信息,如下:

那么我们很自然的想到,俄罗斯方块是可以旋转的呀,在旋转过程中,一个方块顺时针旋转一次后其位置变换规律如下(你们肯定会觉得,这我哪想的出来啊,哈哈没错,这自然是不好想的,我们遇到这种问题不要焦虑,先自己思考思考,当然你肯定是大概率思考不出来,然后我们再网上找找参考,学会他就好了):

//初始化方块信息
void InitBlockInfo()
{
	//“T”形
	for (int i = 0; i <= 2; i++)
		block[0][0].space[1][i] = 1;
	block[0][0].space[2][1] = 1;

	//“L”形
	for (int i = 1; i <= 3; i++)
		block[1][0].space[i][1] = 1;
	block[1][0].space[3][2] = 1;
	
	//“J”形
	for (int i = 1; i <= 3; i++)
		block[2][0].space[i][2] = 1;
	block[2][0].space[3][1] = 1;
	
	for (int i = 0; i <= 1; i++)
	{
		//“Z”形
		block[3][0].space[1][i] = 1;
		block[3][0].space[2][i + 1] = 1;
		//“S”形
		block[4][0].space[1][i + 1] = 1;
		block[4][0].space[2][i] = 1;
		//“O”形
		block[5][0].space[1][i + 1] = 1;
		block[5][0].space[2][i + 1] = 1;
	}
	
	//“I”形
	for (int i = 0; i <= 3;i++)
		block[6][0].space[i][1] = 1;
	
	int temp[4][4];
	for (int shape = 0; shape < 7; shape++) //7种形状
	{
		for (int form = 0; form < 3; form++) //4种形态(已经有了一种,这里每个还需增加3种)
		{
			//获取第form种形态
			for (int i = 0; i < 4; i++)
			{
				for (int j = 0; j < 4; j++)
				{
					temp[i][j] = block[shape][form].space[i][j];
				}
			}
			//将第form种形态顺时针旋转,得到第form+1种形态
			for (int i = 0; i < 4; i++)
			{
				for (int j = 0; j < 4; j++)
				{
					block[shape][form + 1].space[i][j] = temp[3 - j][i];
				}
			}
		}
	}

}

做到这里框架已经基本构建好了。

游戏主体逻辑(玩法)

主体逻辑:

在打印当前下落的方块前,先随机获取下一次将要下落的方块,并打印到提示区的右上角。
将当前下落的方块首先打印到游戏区顶部,给定一定的时间间隔。

若在给定时间间隔内键盘被敲击,则根据所敲击的按键给出相应反馈

若在该时间内键盘未被敲击,则方块下落一格,方块下落前需先判断下落后的合法性

若方块落到底部,则判断得分与结束
若游戏未结束,则循环进行以上步骤。

那么说到这里,我们来思考思考如何实现游戏的玩法,我已经在上面用加粗标出了,首先肯定是画出方块和怎么让他移动起来了,那么我们接下来来实现一下。

我们先开始实现画出方块的函数,将第shape种形状的第form种形态的方块打印在屏幕的指定位置处。

所给x和y,指的是方块信息当中第一行第一列的方块的打印位置。

//画出方块

void DrawBlock(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[shape][form].space[i][j] == 1) //如果该位置有方块
			{
				CursorJump(2 * (x + j), y + i); //光标跳转到指定位置
				printf("■"); //输出方块
			}
		}
	}
}

空格覆盖

无论是游戏区方块的移动,还是提示区右上角下一个方块的显示,都需要方块位置的变换,而在变化之前肯定是要先将之前打印的方块用空格进行覆盖,然后再打印变化后的方块。

在覆盖方块时特别需要注意的是,要覆盖一个小方块需要用两个空格。

//空格覆盖
void DrawSpace(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			if (block[shape][form].space[i][j] == 1) //如果该位置有方块
			{
				CursorJump(2 * (x + j), y + i); //光标跳转到指定位置
				printf("  "); //打印空格覆盖(两个空格)
			}
		}
	}
}

打印方块的函数到此写完了,然后开始实现下一个目标:若在给定时间间隔内键盘被敲击,则根据所敲击的按键给出相应反馈。很显然我们需要让计算机对我们的键盘进行响应,为了让代码更美观,我们写个宏定义代表每个按键。

#define DOWN 80 //方向键:下
#define LEFT 75 //方向键:左
#define RIGHT 77 //方向键:右

#define SPACE 32 //空格键
#define ESC 27 //Esc键

讲到这里,你们可能会说,啊为什么我就要定义下键为80, 为什么左键是75,不能定义其他的东西嘛。这里我们就要考虑到计算机如何知道我们按下了上下左右键。很简单,百度一下c语言怎么知道键盘按下了什么。我们得知,想要让计算机知道按下了按键,我们要用到一个c语言中一个叫getch()的函数。好我们再搜一下getch函数怎么使用。然后就找到了以下的东西,我给你们贴在这里。

C语言中getch()函数

功 能: 从stdio流中读字符,即从控制台读取一个字符,但不显示在屏幕上

这个函数是一个不回显函数,当用户按下某个字符时,函数自动读取,无需按回车,有的C语言命令行程序会用到此函数做游戏

在用getch()(在头文件conio.h)获得上下左右键的键值时候,他们是双键值,会返回高八位和低八位的int型数值。

int key1=getch()

​ key2=getch()

在键盘中按下“上键”后,key1会返回key1=224,key2=72;

下键: key1=224,key2=80;

左键: key1=224,key2=75;

右键: key1=224,key2=77;

至此,我们知道,想要让计算机知道我们按了上下左右,我们就用getch函数捕捉键盘,其中每个值都是不一样的,所以必须设置为80,75,77。

隐藏光标

在用C语言制作动画,游戏或其他需要大量用到清屏指令的程序时,光标会闪烁不停,十分干扰视线,但是只要隐藏光标就可以让体验更佳许多。在这里我们并不讨论如何理解这段代码的使用方法,因为程序员写的大部分代码往往只是解决需求,而不是学会每一种技术的深层使用含义,善于使用Google和百度是你们的必修课,这里我们就直接搜索如何在c语言小游戏中隐藏光标,随便找到一个前人已经替我们写好的函数复制粘贴过来即可。

//隐藏光标

void HideCursor()
{
	CONSOLE_CURSOR_INFO curInfo; //定义光标信息的结构体变量
	curInfo.dwSize = 1;  //如果没赋值的话,隐藏光标无效
	curInfo.bVisible = FALSE; //将光标设置为不可见
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorInfo(handle, &curInfo); //设置光标信息
}

然后我们开始实现方块下落时判断下落后的合法性

合法性判断

其实在方块移动过程中,无时无刻都在判断方块下一次变化后的位置是否合法,只有合法才会允许该变化的进行。

所谓非法,就是指该方块进行了该变化后落在了本来就有方块的位置。

//合法性判断
int IsLegal(int shape, int form, int x, int y)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			//如果方块落下的位置本来就已经有方块了,则不合法
			if ((block[shape][form].space[i][j] == 1) && (face.data[y + i][x + j] == 1))
				return 0; //不合法
		}
	}
	return 1; //合法
}

合法性判断至此,开始实现判断得分与结束

判断得分与结束

判断得分:
从下往上判断,若某一行方块全满,则将改行方块数据清空,并将该行上方的方块全部下移,下移结束后返回1,表示还需再次调用该函数进行判断,因为被下移的行并没有进行判断,可能还存在满行。

判断结束:

直接判断游戏区最上面的一行当中是否有方块存在,若存在方块,则游戏结束。
游戏结束后,除了给出游戏结束提示语之外,如果玩家本局游戏分数大于历史最高记录,则需要更新最高分到文件当中。
游戏结束后询问玩家是否再来一局。

//判断得分与结束
int JudeFunc()
{
	//判断是否得分
	for (int i = ROW - 2; i > 4; i--)
	{
		int sum = 0; //记录第i行的方块个数
		for (int j = 1; j < COL - 1; j++)
		{
			sum += face.data[i][j]; //统计第i行的方块个数
		}
		if (sum == 0) //该行没有方块,无需再判断其上的层次(无需再继续判断是否得分)
			break; //跳出循环
		if (sum == COL - 2) //该行全是方块,可得分
		{
			grade += 10; //满一行加10分
			color(7); //颜色设置为白色
			CursorJump(2 * COL + 4, ROW - 3); //光标跳转到显示当前分数的位置
			printf("当前分数:%d", grade); //更新当前分数
			for (int j = 1; j < COL - 1; j++) //清除得分行的方块信息
			{
				face.data[i][j] = 0; //该位置得分后被清除,标记为无方块
				CursorJump(2 * j, i); //光标跳转到该位置
				printf("  "); //打印空格覆盖(两个空格)
			}
			//把被清除行上面的行整体向下挪一格
			for (int m = i; m >1; m--)
			{
				sum = 0; //记录上一行的方块个数
				for (int n = 1; n < COL - 1; n++)
				{
					sum += face.data[m - 1][n]; //统计上一行的方块个数
					face.data[m][n] = face.data[m - 1][n]; //将上一行方块的标识移到下一行
					face.color[m][n] = face.color[m - 1][n]; //将上一行方块的颜色编号移到下一行
					if (face.data[m][n] == 1) //上一行移下来的是方块,打印方块
					{
						CursorJump(2 * n, m); //光标跳转到该位置
						color(face.color[m][n]); //颜色设置为还方块的颜色
						printf("■"); //打印方块
					}
					else //上一行移下来的是空格,打印空格
					{
						CursorJump(2 * n, m); //光标跳转到该位置
						printf("  "); //打印空格(两个空格)
					}
				}
				if (sum == 0) //上一行移下来的全是空格,无需再将上层的方块向下移动(移动结束)
					return 1; //返回1,表示还需调用该函数进行判断(移动下来的可能还有满行)
			}
		}
	}
	//判断游戏是否结束
	for (int j = 1; j < COL - 1; j++)
	{
		if (face.data[1][j] == 1) //顶层有方块存在(以第1行为顶层,不是第0行)
		{
			Sleep(1000); //留给玩家反应时间
			system("cls"); //清空屏幕
			color(7); //颜色设置为白色
			CursorJump(2 * (COL / 3), ROW / 2 - 3);
			if (grade>max)
			{
				printf("恭喜你打破最高记录,最高记录更新为%d", grade);
				WriteGrade();
			}
			else if (grade == max)
			{
				printf("与最高记录持平,加油再创佳绩", grade);
			}
			else
			{
				printf("请继续加油,当前与最高记录相差%d", max - grade);
			}
			CursorJump(2 * (COL / 3), ROW / 2);
			printf("GAME OVER");
			while (1)
			{
				char ch;
				CursorJump(2 * (COL / 3), ROW / 2 + 3);
				printf("再来一局?(y/n):");
				scanf("%c", &ch);
				if (ch == 'y' || ch == 'Y')
				{
					system("cls");
					main();
				}
				else if (ch == 'n' || ch == 'N')
				{
					CursorJump(2 * (COL / 3), ROW / 2 + 5);
					exit(0);
				}
				else
				{
					CursorJump(2 * (COL / 3), ROW / 2 + 4);
					printf("选择错误,请再次选择");
				}
			}
		}
	}
	return 0; //判断结束,无需再调用该函数进行判断
}

至此,所有的小功能已经实现完毕,开始实现游戏主体逻辑函数

//游戏主体逻辑函数
void StartGame()
{	
	int shape = rand() % 7, form = rand() % 4; //随机获取方块的形状和形态
	while (1)
	{
		int t = 0;
		int nextShape = rand() % 7, nextForm = rand() % 4; //随机获取下一个方块的形状和形态
		int x = COL / 2 - 2, y = 0; //方块初始下落位置的横纵坐标
		color(nextShape); //颜色设置为下一个方块的颜色
		DrawBlock(nextShape, nextForm, COL + 3, 3); //将下一个方块显示在右上角
		while (1)
		{
			color(shape); //颜色设置为当前正在下落的方块
			DrawBlock(shape, form, x, y); //将该方块显示在初始下落位置
			if (t == 0)
			{
				t = 15000; //这里t越小,方块下落越快(可以根据此设置游戏难度)
			}
			while (--t)
			{
				if (kbhit() != 0) //若键盘被敲击,则退出循环
					break;
			}
			if (t == 0) //键盘未被敲击
			{
				if (IsLegal(shape, form, x, y + 1) == 0) //方块再下落就不合法了(已经到达底部)
				{
					//将当前方块的信息录入face当中
					//face:记录界面的每个位置是否有方块,若有方块还需记录该位置方块的颜色。
					for (int i = 0; i < 4; i++)
					{
						for (int j = 0; j < 4; j++)
						{
							if (block[shape][form].space[i][j] == 1)
							{
								face.data[y + i][x + j] = 1; //将该位置标记为有方块
								face.color[y + i][x + j] = shape; //记录该方块的颜色数值
							}
						}
					}
					while (JudeFunc()); //判断此次方块下落是否得分以及游戏是否结束
					break; //跳出当前死循环,准备进行下一个方块的下落
				}
				else //未到底部
				{
					DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
					y++; //纵坐标自增(下一次显示方块时就相当于下落了一格了)
				}
			}
			else //键盘被敲击
			{
				char ch = getch(); //读取keycode
				switch (ch)
				{
				case DOWN: //方向键:下
					if (IsLegal(shape, form, x, y + 1) == 1) //判断方块向下移动一位后是否合法
					{
						//方块下落后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						y++; //纵坐标自增(下一次显示方块时就相当于下落了一格了)
					}
					break;
				case LEFT: //方向键:左
					if (IsLegal(shape, form, x - 1, y) == 1) //判断方块向左移动一位后是否合法
					{
						//方块左移后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						x--; //横坐标自减(下一次显示方块时就相当于左移了一格了)
					}
					break;
				case RIGHT: //方向键:右
					if (IsLegal(shape, form, x + 1, y) == 1) //判断方块向右移动一位后是否合法
					{
						//方块右移后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						x++; //横坐标自增(下一次显示方块时就相当于右移了一格了)
					}
					break;
				case SPACE: //空格键
					if (IsLegal(shape, (form + 1) % 4, x, y + 1) == 1) //判断方块旋转后是否合法
					{
						//方块旋转后合法才进行以下操作
						DrawSpace(shape, form, x, y); //用空格覆盖当前方块所在位置
						y++; //纵坐标自增(总不能原地旋转吧)
						form = (form + 1) % 4; //方块的形态自增(下一次显示方块时就相当于旋转了)
					}
					break;
				case ESC: //Esc键
					system("cls"); //清空屏幕
					color(7);
					CursorJump(COL, ROW / 2);
					printf("  游戏结束  ");
					CursorJump(COL, ROW / 2 + 2);
					exit(0); //结束程序
				case 's':
				case 'S':  //暂停
					system("pause>nul"); //暂停(按任意键继续)
					break;
				case 'r':
				case 'R': //重新开始
					system("cls"); //清空屏幕
					main(); //重新执行主函数
				}
			}
		}
		shape = nextShape, form = nextForm; //获取下一个方块的信息
		DrawSpace(nextShape, nextForm, COL + 3, 3); //将右上角的方块信息用空格覆盖
	}
}

从文件读取最高分

首先需要使用fopen函数打开“俄罗斯方块最高记录.txt”文件,若是第一次运行该代码,则会自动创建该文件,并将历史最高记录设置为0,之后读取文件当中的历史最高记录存储在max变量当中,并关闭文件即可。

//从文件读取最高分
void ReadGrade()
{
	FILE* pf = fopen("俄罗斯方块最高得分记录.txt", "r"); //以只读方式打开文件
	if (pf == NULL) //打开文件失败
	{
		pf = fopen("俄罗斯方块最高得分记录.txt", "w"); //以只写方式打开文件(文件不存在可以自动创建该文件)
		fwrite(&grade, sizeof(int), 1, pf); //将max写入文件(此时max为0),即将最高历史得分初始化为0
	}
	fseek(pf, 0, SEEK_SET); //使文件指针pf指向文件开头
	fread(&max, sizeof(int), 1, pf); //读取文件中的最高历史得分到max当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}

更新最高分到文件
首先使用fopen函数打开“俄罗斯方块最高记录.txt”,然后将本局游戏的分数grade写入文件当中即可(覆盖式)。

//更新最高分到文件
void WriteGrade()
{
	FILE* pf = fopen("俄罗斯方块最高得分记录.txt", "w"); //以只写方式打开文件
	if (pf == NULL) //打开文件失败
	{
		printf("保存最高得分记录失败\n");
		exit(0);
	}
	fwrite(&grade, sizeof(int), 1, pf); //将本局游戏得分写入文件当中(更新最高历史得分)
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}

主函数
主函数里面就是依次调用以上函数,但有三点需要说明:

全局变量grade需要在主函数内初始化为0,不能在全局范围初始化为0,因为当玩家按下R键进行重玩时我们需要将当前分数grade重新设置为0。
随机数的生成起点建议设置在主函数当中。
主函数当中的#pragma语句是用于消除类似以下警告的:

int max, grade; //全局变量
int main()
{
#pragma warning (disable:4996) //消除IDE(visual studio)的警告,不是vs可以去掉
	max = 0, grade = 0; //初始化变量
	system("title 俄罗斯方块"); //设置cmd窗口的名字
	system("mode con lines=29 cols=60"); //设置cmd窗口的大小
	HideCursor(); //隐藏光标
	ReadGrade(); //从文件读取最高分到max变量	
	InitInterface(); //初始化界面
	InitBlockInfo(); //初始化方块信息
	srand((unsigned int)time(NULL)); //设置随机数生成的起点
	StartGame(); //开始游戏
	return 0;
}
posted @ 2023-03-02 13:29  翰林猿  阅读(3272)  评论(0编辑  收藏  举报