一、工程声明

这篇文章包括搭建游戏背景、方块的建模、按键响应、方块的随机生成和下一块方块的生成等,以此记录一次C语言的练习。本次工程涉及到图形化,可以使用EasyX库来画出简单的图形,这个库的下载和使用均在百度可查询到。

本工程使用的软件为Visual Studio 2019,新建C++空项目编写代码。文件的工程结构如下图所示,一共有三个文件,一个头文件和两个C++文件。之所以采用cpp后缀,是因为EasyX库只可以在C++文件使用,但是工程代码都是C语言编写的。

用Python实现俄罗斯方块游戏 编写俄罗斯方块_c++

基本概念:

  • 方格:指的是游戏区域的小格子,大小为20*20;
  • 方块:指的是俄罗斯方块,由4*4共16个小格子构成,大小为80*80;
  • 游戏区域:背景为网格线为游戏区域,大小为300*500,存在25*15=375个方格;
  • 图形区域:图形区域包括游戏区域和文字显示部分,大小为500*500;
  • 方块的坐标点:定义方块左下角的坐标点为整个方块的坐标点;
  • 方块的行序号和列序号:数格子的数量,第一个格子的行序号为0,列序号为0;序号与坐标点对应的关系为:行序号*20=坐标点的y,列序号*20=坐标点的x;

二、游戏背景的搭建

//game.c
void context(void)
{
	initgraph(500, 500);					// 初始化图形模式
	setorigin(0, 500);						//重新定义原点
	setaspectratio(1, -1);					//将Y上半部分设定为正半轴
	setlinestyle(PS_SOLID);					//设定画线样式为虚线
	setlinecolor(WHITE);					//线条颜色为白色
	for (size_t i = 0; i <= 25; i++)			         //画出游戏区域网格线,宽300,高500
		line(0, 0 + 20 * i, 300, 0 + 20 * i);            //横线
	for (size_t i = 0; i <= 15; i++)
		line(0 + 20 * i, 0, 0 + 20 * i, 500);			 //竖线
	for (size_t i = 0; i <= 4; i++)						 //画出下一个方块网格线,宽80,高80
		line(370, 300 + 20 * i, 450, 300+20 * i);		 //横线
	for (size_t i = 0; i <= 4; i++)
		line(370 + 20 * i, 300, 370 + 20 * i, 380);		//竖线
	setaspectratio(1, 1);
	settextstyle(25, 0, _T("Consolas"));
	outtextxy(310, -430, _T("SCORE:0  points"));
	outtextxy(310, -410, _T("下一个方块:"));
	outtextxy(310, -270, _T("w 转换方向"));
	outtextxy(310, -240, _T("s 加快下降速度"));
	outtextxy(310, -210, _T("a 左移"));
	outtextxy(310, -180, _T("d 右移"));
	outtextxy(310, -150, _T("h 重启"));
	setaspectratio(1, -1);
}
  1. 首先对图形区域进行了初始的设定,设定线的样式为白色虚线;
  2. 用两个循环画出游戏区域的横线和竖线,共25行15列,大小为300*500;
  3. 再用两个循环画出下一块区域的网格线,共4行4列,大小为80*80;
  4. 画出提示文本,主要写着分数和操作提示等;
  5. 效果如下图所示。

用Python实现俄罗斯方块游戏 编写俄罗斯方块_c语言_02

三、方块的建模

 

 

 

用Python实现俄罗斯方块游戏 编写俄罗斯方块_游戏_03

研究俄罗斯方块的各种形状,会发现他们都在在4*4的方格当中的,考虑到C语言的short数据类型刚好有2个字节,16bit的长度。所以我们可以用short数据类型来表示我们的方块。比如下面的俄罗斯方块可以表示为,1000 1000 1000 1000,转化为十六进制就是8888H。

 

用Python实现俄罗斯方块游戏 编写俄罗斯方块_用Python实现俄罗斯方块游戏_04

考虑到每个俄罗斯方块有不同的方向,不同的方向对应不同的姿态,所以我新建一个二维数组来表示所有的俄罗斯方块。一共有7钟俄罗斯方块,每种有4个方向的变化,有一些变化还是原来的样子。在game.h头文件,声明展示俄罗斯方块和位置结构体,以方块左下角那个点的坐标为整个方块的坐标点。

 

//main.c
unsigned short Diamond[7][4] = {
		  {0x000f,0x8888,0x000f,0x8888},
		  {0x008E,0x0c88,0x00E2,0x044c},
		  {0x002E,0x088c,0x00E8,0x0c44},
		  {0x00CC,0x00CC,0x00CC,0x00CC},
		  {0x006C,0x08c4,0x006c,0x08c4},
		  {0x004E,0x08c8,0x00E4,0x04c4},
		  {0x00C6,0x04c8,0x00c6,0x04c8} };
//game.h
//方块的坐标
typedef struct LOCATE
{
	int x;
	int y;
} Location;


//俄罗斯方块展示函数
void DisplayDiamond(unsigned short diamond, Location Loca, int cur_color);

俄罗斯方块展示函数用到的主要是EasyX里面的fillrectangle函数,可以画有边框的填充矩形。

用Python实现俄罗斯方块游戏 编写俄罗斯方块_c语言_05

下面是俄罗斯方块展示函数具体的代码,判断short数据中每一位是否是1,是的话就填充一个小格子,不是就跳过,循环操作,进行四行四列共16次判断。擦除用的也是这个函数,只不过颜色换为了背景色(黑色),就变相达到了擦除效果。

int color[15][35] = { 0 };  //定义网格属性

//俄罗斯方块展示函数
void DisplayDiamond(unsigned short diamond, Location Loca, int draw_color)
{
	int num_x, num_y;
	int k = 0;
		setfillcolor(draw_color);	    //设定填充颜色		
		for (int i = 3; i >= 0; i--)    //四行填充
		{
			for (int j = 0; j < 4; j++) //四列填充
			{
				if (diamond & (0x8000 >> k))//判断格子是否存在1,20是格子边长
				{
					fillrectangle(Loca.x + 20 * j, Loca.y + 20 * i, Loca.x + 20 * (j + 1), Loca.y + 20 * (i + 1));
					num_x = Loca.x / 20 + j;
					num_y = Loca.y / 20 + i;
					color[num_x][num_y] = draw_color;
				 }	
				k++;
			}
		}
}
  1. 设定填充的颜色,这是EasyX的内置函数,直接引用就行;
  2. 双重循环,进行四行、四列填充;
  3. 在函数内部进行判断,对short数据类型的diamond(俄罗斯方块)的每一位进行判断,为1则进行填充,为0则跳过;
  4. color是一个二维数组,用来记录格子对应的颜色,共25行15列,刚好对应格子的数量。该变量是一个全局变量,所以game.c文件的函数都可以引用或赋值该变量。
  5. 展示一下全部俄罗斯方块,验证一下前面建模是否正确,由下图可以看到,形状是正确的,符合我们的设想。

用Python实现俄罗斯方块游戏 编写俄罗斯方块_c语言_06

 

 

 

四、按键响应

 

移动和翻转需要键盘来进行选择,所以这涉及到与键盘的交互作用,所以需要包含头文件conio.h。conio.h不是C标准库中的头文件,是vc下的一个头文件。conio是Console Input/Output(控制台输入输出)的简写,其中定义了通过控制台进行数据输入和数据输出的函数,主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等。

1.实现方块的自由下落

//main.c
while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0)
		{
			DisplayDiamond(CurDiamond, CurLocation, BLACK);      //先擦除
			CurLocation.y -= 20;
			DisplayDiamond(CurDiamond, CurLocation, cur_color);  //再更新
			Sleep(speed);

			if (_kbhit())					//如果敲击了键盘,就会返回1
			{
				userHit = _getch();			//获取敲击键盘字符
				Keyboard(userHit, &row, &column, &CurLocation, &speed);
				CurDiamond = Diamond[row][column];
			}
			
		}
  • 用MoveEnable函数进行判断是否能下降,能下降就进入循环,不能下降就跳出循环。MoveEnable函数是自己定义的一个移动判断函数,后续会进行讲解;
  • 循环内部第一步先擦除,即把原来有颜色的俄罗斯方块,填充为黑色,相当于擦除效果;
  • 把坐标点的y减去20,然后重新画出该方块,进行一定的延时,实现下降效果;
  • 接下来进行键盘的响应,_kbhit是EasyX的内部函数,主要用于检查是否有按键输入;
  • 将获得的按键字符传入Keyboard字符处理函数,获得相应的效果。
//game.h
#define EnableDown 1
#define EnableLeft 2
#define EnableRight 3

//game.c
bool MoveEnable(unsigned short diamond, Location CurLocation, int direction)
{
	bool stop=0;
	int num_x,num_y;
	switch (direction)
	{
	case EnableDown:    //判断是否可以下移
	{
		for (int i = 0; i < GetWidth(diamond); i++)		//对每一列进行判断
		{
			for (int j = 0; j < GetHigh(diamond); j++)   //对该列每一行进行判断
			{

				num_x = CurLocation.x / 20 + i ;		 //小方格的列序号
				num_y = CurLocation.y / 20 + j ;		 //小方格的行序号
				if( diamond & (0x0001<<(3-i+4*j)) )		 
                //如果该小方格是为1,即方块该列最底部
				{ 
					if ((color[num_x][num_y - 1] != BLACK) || (CurLocation.y == 0))	
                    //如果下面那个小方格也是非黑色,就要停止
					{
						stop = 1;
					}
					break;	        //找到底部小格子即可跳出循环,进行下一列的判断
				}	
			}
			if (stop == 1)  break;	//既然已经要停止了,就不用去判断下一列
		}
		break;
	}
	}
	return stop;
}

 

以上是字符处理函数,里面仅有判断是否可以下移部分代码,是否可以左移/右移代码类似,这里就不进行展示了。

swith语句选择究竟是判断下降、左移、右移;下降判断,其实就是找出每一列最下面那个有色小格子到底在第几行,找到之后就对其下面那个格子进行判断,如果下面那个格子也是有色小格子,就说明不能下降了;如果下面那个格子是黑色小格子,那就可以下降;接下来要找第二列的底部小格子,因为有可能第一列没被堵住,第二列被堵住了。当其中一列下面存在有色格子,就可以把stop变量置1,然后跳出循环,不用判断后面的列了。函数返回bool数据类型,stop这个变量。如果是1,代表存在阻碍,不能再移动了。如果是0,代表不存在阻碍,可以继续移动。以上便是函数的逻辑。

实现方块的改变方向和速度改变

//game.c
//响应键盘函数
void Keyboard(char userHit, int *row, int *column, Location *CurLocation, int *speed)
{
	unsigned short Diamond[7][4] = {
		  {0x000f,0x8888,0x000f,0x8888},
		  {0x008E,0x0c88,0x00E2,0x044c},
		  {0x002E,0x088c,0x00E8,0x0c44},
		  {0x00CC,0x00CC,0x00CC,0x00CC},
		  {0x006C,0x08c4,0x006c,0x08c4},
		  {0x004E,0x08c8,0x00E4,0x04c4},
		  {0x00C6,0x04c8,0x00c6,0x04c8} };
	unsigned short CurDiamond;
	switch(userHit)
	{
		case 'w': //改变方向
		{
			CurDiamond = Diamond[*row][*column];
			DisplayDiamond(CurDiamond, *CurLocation, BLACK);    //先擦除
			if ((*column) < 3)
				(*column)++;
			else
				(*column) = 0;
			CurDiamond = Diamond[*row][*column];			
			DisplayDiamond(CurDiamond, *CurLocation, cur_color);  //再更新
			break;
		}
		
		case 's': //增加下降速度
		{
			if ((*speed) > 50 )
			{
				(*speed) -= 50;
			}			
			break;
		}
		
		case 'h':  //重新开始
		{
			for (size_t i =0; i < 25; i++)			//25行
			{
				for (size_t j = 0; j < 15; j++)		//15列
				{
					color[j][i] = BLACK;		 //每一个格子填充黑色
					setfillcolor(color[j][i]);
					fillrectangle(20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));
				}
			}
			break;
		}

	}
}

以上是响应键盘函数。首先对传入的字符进行判断,看到底应该进行左移、右移、加快下降速度、改变方向、重启操作。因为我们装有俄罗斯方块的模型是7*4的二维数组,对应是7种俄罗斯方块的四种方向。所以改变方向其实就是改变Dianmond的列号,我们只要在0-4循环就可以达到这种效果。因为要把行和列都保存下来,传出去函数外部,所以这里用的是指针。

速度是通过影响中间的间隔时间来改变的,只要间隔时间短,下降速度就快。所以每次按下s,speed变量减少50,速度就会变快。同样的,speed也要传出去函数外部,用的也是指针。按下‘h’是重新开始,就是清空已经下落的俄罗斯方块,所以只要把游戏区域的所有格子填充黑色就可以达到效果,同时要改变格子的属性color。

左右移动和方块的下落是同样的原理,只不过改变的是x,每次x改变20,就是一格子的长度。先进行擦除,后重新更新方块,就可以达到预期效果。

五、方块的随机生成和下一块方块的生成

//main.c
	unsigned short CurDiamond; //下一个方块
	unsigned short NewDiamond; //下一个方块
	
	context();								//画出游戏背景
	row = 0;
	column = 0;
	CurDiamond = Diamond[row][column];
	cur_color = BLUE;
	while(1)
	{ 		
		RandomDiamond(&new_row, &new_column, &CurLocation, &speed); //随机生成方块
		NewDiamond = Diamond[new_row][new_column];
		DisplayDiamond(NewDiamond, { 370,300 }, new_color);  //画出下一块方块

		while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0)
		{
			//自由下落+按键响应代码
			
		}
		row = new_row;
		column = new_column;
		cur_color = new_color;
		CurDiamond = Diamond[row][column];
		DisplayDiamond(NewDiamond, { 370,300 }, BLACK);    //先擦除
		FullJudge();
	}

方块的随机生成用的是自定义的RandomDiamond函数,生成方块在Diamond数组的行和列,将值赋给NewDiamond,然后在下一块方块区域画出来。while部分进行的curDiamond方块的自由下落和按键响应,当不能下落之后,就会跳出while循环。将上面随机生成的新行号和列号赋给现在的row和column,生成的颜色也赋给cur_color。然后就可以擦除下一块区域的方块,等待新的生成。

在最后,会进行满行判断,满行则消失,这个后面再讲。

//game.c
void RandomDiamond(int* row, int* column, Location* CurLocation, int* speed)
{
	static int num = 0;
	int color_Diamond[7] = { BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, YELLOW };
	*row = rand() % 7;			//生成0到6随机数
	*column = rand() % 4;		//生成0到3随机数	
	//*row = 0;
	*CurLocation = { 140,500 };
	*speed = 200;
	if (num == 6)	 num = 0;
	new_color = color_Diamond[num++];
}

以上是RandomDiamond俄罗斯方块随机生成函数,一开始会生成随机的row和column,这个就是相当于随机的俄罗斯方块形状。同时也会生成颜色,颜色用的是数组内部循环,于是就生成了五颜六色的俄罗斯方块。

六、满行消除和更新

//game.c
void FullJudge()
{
	int color_num=0;
	for (size_t i = 0; i < 25; i++)    //25行
	{
		for (size_t j = 0; j < 15; j++) //15列
			if (color[j][i] != BLACK) color_num++;		

		if (color_num == 15)			//遇到满行
		{
			setfillcolor(BLACK);		     //设定填充黑色
			for (size_t j = 0; j < 15; j++)  //填充该行15列,即消除该行		
			{ 
				fillrectangle(j*20, i*20 ,(j+1)*20, (i+1)*20);
				color[j][i] = BLACK;
			}

			Updata(i);//更新
			i--;    //重新判断该行
		}
		color_num = 0;
	}
}

//消除满行后,上面往后降,重新判断该行
void Updata(int row)  
{
	//从满行的第row行开始
	for (size_t i = row; i < 25; i++)
	{
		for (size_t j = 0; j < 15; j++)		//15列
		{
			color[j][i] = color[j][i + 1]; //每一个格子等于上面格子的颜色
			setfillcolor(color[j][i]);
			fillrectangle( 20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));
		}	
	}
	static int game_point = 0;
	game_point++;   //更新分数
	setaspectratio(1, 1);
	TCHAR s1[5];
	settextstyle(25, 0, _T("Consolas"));
	swprintf_s(s1, _T("%d"), game_point);
	outtextxy(380, -426, s1);
	setaspectratio(1, -1);
}

满行判断就是一行行来判断,首先判断第一行,对每个格子的color进行判断,如果统计格子颜色为非黑色(即存在方块)的数量为列数量15,这就是遇到了满行情况。判断是满行情况,那就对该行所有格子填充黑色,就相当于消除。

消除之后,要把上面的格子落下来,相当于整体往下移了一格,这就是Updata函数的任务,其中还得更新一下分数,一行一分。整体下移之后要重新对该行进行判断,也许说不定下落的那一行又是满行。之后循环判断完25行,任务就完成了。

七、判断真实宽度和真实高度

//game.c
//计算方块的真实高度
int GetHigh(unsigned short diamond)
{
	int a=0;
	for (size_t i = 0; i < 4; i++)
	{
		if ( (diamond & (0x000f << 4 * i)) > 0)
			a++;
	}
	return a;
}


//计算方块的真实宽度
int GetWidth(unsigned short diamond)
{
	int a = 0;
	for (size_t i = 0; i < 4; i++)
	{
		if ((diamond & (0x8888 >>  i)) > 0)
			a++;
	}
	return a;
}

从以下俄罗斯方块能看出,虽然每个俄罗斯方块都占据了4*4小格子的空间大小,但是实际上俄罗斯方块的真实高度和宽度并不一定是4。在进行左右移动的时候,就会需要判断俄罗斯方块的真实高度和宽度,也就是宽度为1,高度为4。判断的逻辑就是和0x000f、0x8888进行与逻辑运算,然后是否大于0。如果等于0,说明这一行/列都是0,那就是没被占用的空间。

用Python实现俄罗斯方块游戏 编写俄罗斯方块_游戏_07

以上就是俄罗斯方块代码的大部分内容,并不是全部代码,有一些只讲了逻辑,大家可以自由发挥。

八、运行效果

用Python实现俄罗斯方块游戏 编写俄罗斯方块_游戏_08