用C/C++加Easyx实现俄罗斯方块游戏(爆肝4万字,完全免费)

前言

        相信大家一定玩过俄罗斯方块这款小游戏,简单容易上手是老少皆宜的小游戏,今天大家就跟着我来实现这个小游戏吧!让自己学的C语言有用武之地。

        为了让俄罗斯方块的开发更为简单些,图像更为丰富,在这里就利用了Easyx库来实现,大家如果没了解过Easyx库,可以先到官网了解下,我们用到的函数不多,大家也可以在代码中用到那个就查看文档中这个函数的用法。官网如下EasyX 文档 - flushmessage。

        由于Easyx库必须在C++文件中才能用,我们的源文件必须命名为.cpp,但不用担心,C++是兼容C的,我们依然可以使用C的语法进行操作。所以这篇博客不需要太大的C++知识,总体而言是用C语言写的,学过C语言便可以了。当然如果想简单了解下C++,也可以看我写的这几篇博客。

【从C到C++过渡知识上 - CSDN App】http://t.csdnimg.cn/eRs9m

【从C到C++过渡知识 中(为什么C++支持函数重载,而C不支持函数重载) - CSDN App】http://t.csdnimg.cn/bPaCC

【从C到C++过渡知识 下(深入理解引用与指针的关系) - CSDN App】http://t.csdnimg.cn/NAkzO

        我们最终的实现结果如下。

俄罗斯方块

目录

前言

源码

分析

游戏初始化

功能区初始化

设置背景

打印文字        

打印分数

   读取历史最高分

struct project

方块结构体

加载六色方块

初始化背景

加载七个方块

方块旋转

        方法一

方法二

    方法三

下落方块

游戏进行

下降判断

        遇到LAND或者到达底部

消除检测

正常下落

检测按键

左移

右移

下落

退出

暂停

旋转

游戏结束

保存分数


源码

        源码如下,大家在下载好Easyx后,配置好图片就可以运行了。

可以看百度网盘或者我的资源下载源码图片。

链接: https://pan.baidu.com/s/1ZRwoI9d-53BIIfCwxNIcmg 提取码: 0000 

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<graphics.h>
#include<time.h>

#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
enum  block_status
{
	EXIST,//存在
	NO,//不存在
	LAND,
	ROATE//旋转点
};
struct block
{
	IMAGE img;
	enum  block_status flag;
	int x;
	int y;
};

enum STATUS
{
	OK,
	ESC,
	GAME_OVER
};


typedef struct project
{
	struct block BackGropund[20][10];//背景
	IMAGE original[7];//原始六色方块图片,最后一个为背景
	struct block  Bag[7][4][4];//原始方块包
	struct block Down[4][4];//下落方块

	enum STATUS status;//状态
	int SleepTime;//速度
	int Score;//分数
	int max;//最高分
	ExMessage m;
}SP;

//加载方块包
void LoadImg(SP* p)
{
	IMAGE img0;
	loadimage(&img0, L"../橙色方块.jpg", 40, 40);
	p->original[0] = img0;

	IMAGE img1;
	loadimage(&img1, L"../紫色方块.jpg", 40, 40);
	p->original[1] = img1;


	IMAGE img2;
	loadimage(&img2, L"../红色方块.jpg", 40, 40);
	p->original[2] = img2;

	IMAGE img3;
	loadimage(&img3, L"../黄色方块.jpg", 40, 40);
	p->original[3] = img3;

	IMAGE img4;
	loadimage(&img4, L"../绿色方块.jpg", 40, 40);
	p->original[4] = img4;

	IMAGE img5;
	loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
	p->original[5] = img5;

	IMAGE img6;
	loadimage(&img6, L"../背景.jpg", 40, 40);
	p->original[6] = img6;

}

//加载方块的模板
void LoadBag(SP* p)
{
	int i1 = 0, i2 = 0;
	//长形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i2 == 1)
				p->Bag[0][i1][i2].flag = EXIST;
			else
				p->Bag[0][i1][i2].flag = NO;

			if(i1==2 && i2==1)
				p->Bag[0][i1][i2].flag = ROATE;

			p->Bag[0][i1][i2].x = i2 + 1;
			p->Bag[0][i1][i2].y = i1 - 3;
		}
	}
	//正方形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((i2 == 1 || i2 == 2) && (i1==1||i1==2))
				p->Bag[1][i1][i2].flag = EXIST;
			else
				p->Bag[1][i1][i2].flag = NO;

			if (i1 == 1 && i2 == 1)//正方形特殊处理
				p->Bag[1][i1][i2].flag = ROATE;

			p->Bag[1][i1][i2].x = i2 + 1;
			p->Bag[1][i1][i2].y = i1 - 3;
		}
	}
	//山形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 1)
				p->Bag[2][i1][i2].flag = EXIST;
			else
				p->Bag[2][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[2][i1][i2].flag = ROATE;

			p->Bag[2][i1][i2].x = i2 + 1;
			p->Bag[2][i1][i2].y = i1 - 3;
		}
	}
	//右七
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 2)
				p->Bag[3][i1][i2].flag = EXIST;
			else
				p->Bag[3][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 2)
				p->Bag[3][i1][i2].flag = ROATE;

			p->Bag[3][i1][i2].x = i2 + 1;
			p->Bag[3][i1][i2].y = i1 - 3;
		}
	}
	//左七
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 > 0 || i1 == 2 && i2 == 1)
				p->Bag[4][i1][i2].flag = EXIST;
			else
				p->Bag[4][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[4][i1][i2].flag = ROATE;

			p->Bag[4][i1][i2].x = i2 + 1;
			p->Bag[4][i1][i2].y = i1 - 3;
		}
	}
	//右Z
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 2 && i2 < 2 || i1 == 1 && (i2 == 1 || i2 == 2))
				p->Bag[5][i1][i2].flag = EXIST;
			else
				p->Bag[5][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[5][i1][i2].flag = ROATE;

			p->Bag[5][i1][i2].x = i2 + 1;
			p->Bag[5][i1][i2].y = i1 - 3;
		}
	}
	//左Z
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 2 && i2 > 1 || i1 == 1 && (i2 == 1 || i2 == 2))
				p->Bag[6][i1][i2].flag = EXIST;
			else
				p->Bag[6][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 2)
				p->Bag[6][i1][i2].flag = ROATE;


			p->Bag[6][i1][i2].x = i2 + 1;
			p->Bag[6][i1][i2].y = i1 - 3;
		}
	}

}
//字符转换
void Change(WCHAR* des, char* src)
{
	while (*des++ = *src++);
}

//打印背景
void PrintBackGround(SP* p)
{
	int i1 = 0, i2 = 0;
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
		}
	}
	FlushBatchDraw();
}
//打印分数信息
void  PrintMessage(SP* p)
{
	char arr1[50];
	WCHAR arr2[50];
	//打印当前分数
	RECT r = { 400, 200, 600, 300 };
	drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 300, 600, 350 };
	sprintf(arr1, "%d", p->Score);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);


	//打印最高分数
	r = { 400, 0, 600, 200 };
	drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 100, 600, 200 };
	sprintf(arr1, "%d", p->max);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);



	r = { 400, 400, 600, 450 };
	drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 450, 600, 500 };
	drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 500, 600, 550 };
	drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	//打印版权
	r = { 400, 700, 600, 800 };
	drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
//打印下落方块
void PrintDown(SP* p)
{
	int i1, i2;

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
			{
				putimage(p->Down[i1][i2].x * 40, p->Down[i1][i2].y * 40, &p->Down[i1][i2].img);
			}
		}
	}
}
//打印全部内容
void Print(SP* p)
{
	PrintBackGround(p);
	PrintDown(p);
	solidrectangle(400, 0, 600, 800);
	PrintMessage(p);
	FlushBatchDraw();
}



 //创造下落方块
void CreatBag(SP* p)
{
	int t = rand() % 7;
	int colour = (t + rand()) % 6;

	int i1, i2;

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2] = p->Bag[t][i1][i2];
			//不同方块不同
			if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
			{
				// p->Down[i1][i2].flag != NO
				p->Down[i1][i2].img = p->original[colour];
			}
		}
	}
	
}


//初始化
void Init(SP *p)
{
	initgraph(600, 800);// EX_SHOWCONSOLE  控制台

	//初始配置
	setlinecolor(BLACK);
	setlinestyle(PS_SOLID | PS_JOIN_BEVEL, 3);
	setbkcolor(WHITE);
	settextcolor(BLACK);
	settextstyle(20, 0, L"楷体");
	setfillcolor(WHITE);
	setbkmode(TRANSPARENT);//透明文字
	cleardevice();
	srand((unsigned int)time(NULL));
	BeginBatchDraw();//防止闪屏
	//结构体初始化
	p->Score = 0;
	p->SleepTime = 500;
	p->status = OK;

	FILE* pf=fopen("../date.text","a+");
	if (pf == NULL)
	{
		perror("Init:fopen");
		return;
	}
	if (fscanf(pf, "%d", &p->max) == EOF)
	{
		p->max = 0;
	}
	fclose(pf);

	//加载图片,基础方块
	LoadImg(p);

	//加载七个方块
	LoadBag(p);

	//创建方块
	CreatBag(p);

	//设置边界线
	line(400, 0, 400, 800);

	//初始化背景
	int i1 = 0, i2 = 0;
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			p->BackGropund[i1][i2].img = p->original[6];
			p->BackGropund[i1][i2].flag = NO;
		}
	}

	//打印背景
	PrintBackGround(p);

	//打印提示版权信息
	PrintMessage(p);

	FlushBatchDraw();
}


void LeftMove(SP* p)
{
	int i1 = 0, i2 = 0;
	//全部检查是否合法
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
				(p->Down[i1][i2].x-1<0 ||p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x - 1].flag==LAND))
				return;
		}
	}

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].x -= 1;
		}
	}
}

void RightMove(SP*p)
{
	int i1 = 0, i2 = 0;
	//全部检查一遍
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE) &&
				(p->Down[i1][i2].x +1 >9|| p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x +1].flag == LAND))
				return;
		}
	}

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].x += 1;
		}
	}
}

//旋转方块
void Rorate(SP* p)
{
	
	int i1, i2;
	int dx, dy;
	struct block tmp[4][4];

	//找到旋转点
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (p->Down[i1][i2].flag == ROATE)
			{
				dx = p->Down[i1][i2].x;
				dy = p->Down[i1][i2].y;

				goto end;
			}
		}
	}
	end:
	//正方形直接退出
	if (i1 == 1 && i2 == 1)
		return;

	//临时旋转数组
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			tmp[i1][i2] = p->Down[i1][i2];
			tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
			tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
		}
	}

	
	//判断是否合法
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
					(
						tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19 
						|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
					)
				)
				return;
		}
	}

	
	//合法则复制
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2]= tmp[i1][i2];
		}
	}

}


void CheckKey(SP*p)
{
	 peekmessage(&p->m);
	if (p->m.vkcode==(VK_DOWN))
	{
		p->SleepTime = 60;
	}
	else
	{
		p->SleepTime = 500;
		if (p->m.vkcode == (VK_UP))
		{
			Rorate(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_LEFT))
		{
			LeftMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_RIGHT))
		{
			RightMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_ESCAPE))
		{
			p->status = ESC;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			while (!KEY_PRESS(VK_SPACE))
			{
				Sleep(50);
			}
		}
	}

	//消除影响
 	p->m.vkcode = 0;
	flushmessage();
}



//检查删除方块
void CheckDelste(SP* p)
{
	int arr[20] = {0};
	int i1 = 0, i2 = 0;
	int flag = 0;
	int sum = 0;

	//查找消除行
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			if (p->BackGropund[i1][i2].flag == LAND)
			{
				flag = 1;
			}
			else
			{
				flag = 0;
				break;
			}
		}
		arr[i1] = flag;
		sum += arr[i1];
	}
	
	//检查有无消除行
	if (sum == 0)
		return;
	p->Score += 50 * sum;//加分数,一行50分

	//闪烁功能
	for (int j = 0; j < 3; j++)
	{
		int i1 = 0, i2 = 0;
		for (i1 = 0; i1 < 20; i1++)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				if(arr[i1]== 0)
					putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
				else
					putimage(i2 * 40, i1 * 40, &p->original[6]);
			}
		}
		FlushBatchDraw();
		Sleep(150);
		PrintBackGround(p);
		Sleep(150);
		FlushBatchDraw();
	}

	//建立临时数组
	struct block tmp[20][10];
	int t = 19;
	for (i1 = 19; i1 >=0; i1--)
	{
		if (arr[i1] == 0)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				tmp[t][i2] = p->BackGropund[i1][i2];
			}
			t--;
		}
	}
	//多余赋值为背景
	for (i1 = t; i1 >= 0; i1--)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			tmp[t][i2].flag = NO;
			tmp[t][i2].img = p->original[6];
		}
	}
	//转换
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			p->BackGropund[i1][i2] = tmp[i1][i2];
		}
	}

	Print(p);
}



void  DownJudge(SP*p)
{
	int i1, i2;
	//检查是否到底
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			//行为y列为x
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)&&
				(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
			{
				//复制方块,变为地
				for (i1 = 0; i1 < 4; i1++)
				{
					for (i2 = 0; i2 < 4; i2++)
					{//
						if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
						{
							if (p->Down[i1][i2].y < 0)
							{
								p->status = GAME_OVER;
								return;
							}
							p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
							p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
						}
					}
				}
				//加分数
				p->Score += 10;
				//检测是否可以消除
				CheckDelste(p);
				//创建新的下落方块
				CreatBag(p);
				//打印图片
				Print(p);
				return;
			}
		}
	}


	//正常则下降一格
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].y += 1;
		}
	}

	//打印图片
	Print(p);

}

//游戏结束
void GameOver(SP*p)
{
	settextcolor(RED);
	settextstyle(50, 0, L"楷体");
	//创造最高记录
	if (p->max < p->Score)
	{
		RECT r = { 0, 300, 600, 400 };
		drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
		FILE* pf = fopen("../date.text", "w+");
		fprintf(pf, "%d", p->Score);
		fclose(pf);
	}

	RECT r = { 0, 500, 600, 600 };
	drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

	r = { 0, 700,600, 800 };
	drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	FlushBatchDraw();
	//不断检测信息,直到按下enter
	ExMessage m = getmessage();
	while (m.vkcode != VK_RETURN)
	{
		m = getmessage();
	}
	closegraph();
}




int main()
{
	SP a;

	//初始化
	Init(&a);
	while (a.status == OK)
	{
		//检测按键
		CheckKey(&a);

		//下降处理
		DownJudge(&a);

		//休眠
		Sleep(a.SleepTime);
	}

	//游戏结束
	GameOver(&a);

	return 0;
}

        图片如下,记得在.cpp上一级目录保存,名字正确,否则会报错。可以用qq截图保存。也可以下载压缩包。

        Easyx下载地址EasyX Graphics Library for C++

分析

        我们可以先在网上搜索一些俄罗斯方块的视频游戏,然后再根据自己的想法设计游戏。最终我们可以发现整个的游戏页面可以划分为功能区和游戏区。

        游戏区主要进行方块的下落,消除,功能区主要进行分数的显示,操作的介绍,版权等信息。

        我们想要的游戏结果便是方块不断地下落,消除记录分数,游戏介绍保存最大的分数。于是我们可以将游戏的整体逻辑分为三大块,游戏的初始化,游戏进行中,游戏的结尾,显然第二个是整个游戏的核心。一次直接写出很难做到,我们可以利用C语言面向过程的特点,逐步解决每个小问题,最终解决整个问题。

游戏初始化

        在这里大家遇到不知道的函数,可以在Easyx文档中查看用法。EasyX 文档 - flushmessage为了尽量精简文章不会过多的讲解函数的用法,主要注重在程序的设计思想。

功能区初始化

       首先我们就要创建一个窗口,这个窗口的大小,大家可以自己调试设计,我在这里采用的是600*800的窗口。然后我们可以现在画图软件上设计我们的功能区文字,内容。标注出大致的位置,方块的设计。最终设计如下。

        功能区划分在(400,0) (600,800)组成的矩形中。方块的大小设置为40*40,于是便是最终的游戏区长20个方块,宽10个方块。当然这是我们在纸上的设计。下一步便是要实现。

设置背景

        initgraph是Easyx中的函数,用来创建窗口,我们刚创建的窗口是黑色的,为了为了美观,我们可以将背景设置为白色。然后用背景色刷新屏幕。效果图如下

    initgraph(600, 800);
    setbkcolor(WHITE);
    cleardevice();

        

        注意我们在这里加了个getchar,否则程序运行完就会结束,不会停留在这个界面。

        接下来我们便可以画区分线。我们可以设置线为3像素,否则太细了。我们知道两点确定一条直线,我们画直线也十分的简单,只需要用给line函数提供两个坐标即可。代码如下

//设置边界线
	line(400, 0, 400, 800);

打印文字        

        接下来我们便可以设计具体的操作了,我们可以利用←向左移动→向右移动↑旋转↓快速下落,光有这些还不够,我们还可以增加空格暂停,Esc退出的功能。←可以利用输入法中的特殊字符打出。

        然后就是打印字符了,但我们不可以用printf,他是在控制台输出文字,我们要在窗口中输出文字要用到drawtext来实现。他是在一个矩形框中输出文字,所以我们要定义一个矩形的位置信息这个位置可以用RECT类型变量实现。只要我们给出矩形左上顶点与右下顶点,那么这个矩形就确定了。于是便有如下的代码。_T()是将里面的内容当成宽字符存储,与我们平时使用的单字节不同。

RECT r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

        由此通过不断地调试文字的位置,我们便可以写出如下的代码。

    r = { 400, 400, 600, 450 };
	drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 450, 600, 500 };
	drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 500, 600, 550 };
	drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	//打印版权
	r = { 400, 700, 600, 800 };
	drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

打印分数

        分数背我们以整型的形式进行存储,但要想使用drawtext必须为宽字符,我们便需要将分数先转换为单字符,然后再转换为宽字符。将数字转为单字符我们可以利用C语言的函数sprintf,格式化输出,转换到一个临时数组中存储,然后将单字节转为多字节。

        接下来我们来实现Change函数,让单字节变为多字节。实现方式也十分的简单,多字节采用的Unicode编码兼容ASCII,数字在ASCII表中,那么数字在多字节与单字节的唯一区别便是存储大小不一样。我们只需要将单字节的内容一个个拷贝过来就行。类似于strcpy的实现,只不过类型不同罢了。

//字符转换
void Change(WCHAR* des, char* src)
{
	while (*des = *src)
	{
		des++;
		src++;
	}
}

        注意while判断中是一个=,赋值操作,当遇到字符串的结尾时,就停止。'\0'在对于的值为0,上面的代码还可以简化为如下

void Change(WCHAR* des, char* src)
{
	while (*des++ = *src++);
}

        有了上面的铺垫我们就可以输出当前的分数了。具体位置信息还需要调试。

    char arr1[50];
	WCHAR arr2[50];
	//打印当前分数
	RECT r = { 400, 200, 600, 300 };
	drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 300, 600, 350 };
	sprintf(arr1, "%d", Score);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

   读取历史最高分

        我们在这里便可以用C语言的文件处理函数。首先我们要理解../表示上一级目录,默认当前目录为.cpp所在的目录。当我们第一次运行游戏的时候,可能还没有存储历史最高分数的文件,便可以采用fopen("../date.text","a+")方式,不存在就创建一个。

        然后fscanf(pf, "%d", &max)读取数据,第一次可能会读取失败,就初始化为0.最终的代码如下。


	FILE* pf=fopen("../date.text","a+");
	if (pf == NULL)
	{
		perror("Init:fopen");
		return;
	}
	if (fscanf(pf, "%d", &max) == EOF)
	{
		max = 0;
	}
	fclose(pf);

        一定记得fclose,关闭文件,否则后续的文件操作就不会成功。

        综上而言,我们整个的代码如下。

//初始化
void Init(SP *p)
{
	initgraph(600, 800);// EX_SHOWCONSOLE  控制台
    
    setbkcolor(WHITE);
    cleardevice();
    score=0;
	//设置边界线
	line(400, 0, 400, 800);

	
	char arr1[50];
	WCHAR arr2[50];
	//打印当前分数
	RECT r = { 400, 200, 600, 300 };
	drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 300, 600, 350 };
	sprintf(arr1, "%d", Score);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);


	//打印最高分数
	r = { 400, 0, 600, 200 };
	drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 100, 600, 200 };
	sprintf(arr1, "%d", max);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);



	r = { 400, 400, 600, 450 };
	drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 450, 600, 500 };
	drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 500, 600, 550 };
	drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	//打印版权
	r = { 400, 700, 600, 800 };
	drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

}

struct project

        然后我们可以定义一个结构体 struct project,用这个结构体来保存我们所需要的变量,在这里肯定会有许多人疑惑为什么要定义结构体,我一个个变量用的十分好?在这里我想说的是,定义结构体简化了我们后序设计函数的复杂度。我们设计函数参数的时候就不需要过多的考虑参数,只需要传递一个结构体指针,便可以在任何地方修改结构体内的值。这个只有当自己写完一次的时候才会恍然大悟,我刚开始也是不理解,直到自己写过几个项目后才恍然大悟,如果你目前不接受这个观点,等到写完后接受的话,就在评论区打出我悟了!

typedef struct project
{
	struct block BackGropund[20][10];//背景
	IMAGE original[7];//原始六色方块图片,最后一个为背景
	struct block  Bag[7][4][4];//原始方块包
	struct block Down[4][4];//下落方块

	enum STATUS status;//状态
	int SleepTime;//速度
	int Score;//分数
	int max;//最高分
	ExMessage m;
}SP;

        这里面有几个变量,max:历史最高分,Score :当前分数, SleepToime: 速度,第一次写游戏的读者可能不太清楚,速度和休眠时间有什么关系? 在我们运行俄罗斯方块游戏的时候,我们必定会写一个循环来不断地执行程序,直到游戏结束。那方块下落的速度如何体现呢? 我们便可以设定一定的时间间隔来实现,暂停500ms(ms为毫秒)后,方块下落一格打印,暂停500ms后,方块下落一个打印,如此循环就实现了方块的下落操作。说个题外话,为什么我们要主动的暂停500ms,程序的运行需要消耗时间,即使我们不主动暂停,下次打印也会有一定时间。但现在电脑的运行太快了,运行完我们写的几百行代码可能才几毫秒,远远超过了正常人的反应速度。

        由上面的分析我们可以明白,我们核心游戏下落的代码是个循环,具体为while还是for根据个人习惯,那么我们便可以设计一个循环判断变量,当这个数为0时就结束,为1的时候就继续,但这种代码的可读性较差,我们只知道0的时候结束,却不理解0时什么含义.于是我们便可以利用枚举常量来定义状态。如下代码。这样我们判断循环的结束便可以写为 while(status == OK),这样写本质还是与0,1进行比较,但他的代码可读性大大提升,代码的健壮性更好。

enum STATUS
{
	OK,
	ESC,
	GAME_OVER
};

        ExMessage是用来存储键盘信息的,我们上下左右的实现离不开他。可以用peekmessage函数读取键盘信息并存储在m中。

方块结构体

        相信大家一定看到了上面的多个数组,他们都是用来存储方块信息的。下面我们来一一介绍。

        首先我们可以看出他都是struct block结构体的数组,我们来看下这个结构体。

struct block
{
	IMAGE img;
	enum  block_status flag;
	int x;
	int y;
};

        每个方块都有其对应的位置,所以x,y便是其位置信息。IMAGE是Easyx中的一种变量,用来表示图片的信息,然后就是flag表示当前方块的状态,是下落,还是已经下落完,成为背景。同理,我们可以枚举来表示状态。

        这里的LAND表示已经成为背景,其他几种要在后面讲解。

enum  block_status
{
	EXIST,//存在
	NO,//不存在
	LAND,
	ROATE//旋转点
};

        首先根据我们刚才的分析,可以将游戏输出屏幕用struct block BackGropund[20][10];数组来表示。这样我们后续的消除就方便许多。

加载六色方块

        接着我们看下最重要的下落方块。我们知道最初的方块有七个如下。

       首先我们可以了解上面的图像是不同的方块组成的,我们因此想要绘制方块首先要将方块加载如文件。此时就保存在IMAGE original[7]里面。

        应为我们每个的方块大小为40*40,我们就可以在加载图片的时候缩放大小,用loadimage函数,loadimage(&img0, L"../橙色方块.jpg", 40, 40);格式。在这里../表明在.cpp上一级目录下的橙色方块.jpg图片,这个要配置好否则就会报错。接下来就是依次加载7种方块了,特殊的第七种方块是背景方块。

        在这里要记住IMAGE img0;变量只能加载一个图片,加载多个图片会出现黑屏,每加载一个图片,创建一个变量名。

        于是总的加载代码如下,在这里我们采用了一个LoadImg函数进行封装,防止主函数过长,不利于观察主题逻辑。

//加载方块包
void LoadImg(SP* p)
{
	IMAGE img0;
	loadimage(&img0, L"../橙色方块.jpg", 40, 40);
	p->original[0] = img0;

	IMAGE img1;
	loadimage(&img1, L"../紫色方块.jpg", 40, 40);
	p->original[1] = img1;


	IMAGE img2;
	loadimage(&img2, L"../红色方块.jpg", 40, 40);
	p->original[2] = img2;

	IMAGE img3;
	loadimage(&img3, L"../黄色方块.jpg", 40, 40);
	p->original[3] = img3;

	IMAGE img4;
	loadimage(&img4, L"../绿色方块.jpg", 40, 40);
	p->original[4] = img4;

	IMAGE img5;
	loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
	p->original[5] = img5;

	IMAGE img6;
	loadimage(&img6, L"../背景.jpg", 40, 40);
	p->original[6] = img6;

}

初始化背景

        我们的背景是10*20的方格,所以我们可以用数组进行保存。对背景进行初始化也十分简单。只需要将每一个方块的背景改为original[6],我们刚才加载的背景方块就可以了。然后方块的状态设置为NO。

        在这里我们要注意的是[i1][i2]表示第i1行,第i2列的方块,与坐标对应就是(i2*40,i1*40),注意数组的表示方式与我们日常坐标的表示方式不一样。i2对应X,i1对应Y。

//初始化背景
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
	for (i2 = 0; i2 < 10; i2++)
	{
		p->BackGropund[i1][i2].img = p->original[6];
		p->BackGropund[i1][i2].flag = NO;
	}
}

        我们可以简单的打印背景看看效果如何。我们要用到Easyx中的putimage。将对应下标输入如下。FlushBatchDraw();是刷新缓存区,让画面到屏幕上。

//打印背景
void PrintBackGround(SP* p)
{
	int i1 = 0, i2 = 0;
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
		}
	}
	FlushBatchDraw();
}

        效果图如下

加载七个方块

        我们可以将他们都放在4*4的方格中,这样有利于我们统一的操作。然后我们就是存一份七个方块的数据,方便我们每次使用就不用在创建了,直接复制即可。

        接着要做的就是在4*4的方块中规划每个方块的位置。大家可以自行画。经过分析七个方块的位置如下。

        然后就是加载这七个的位置坐标。我们以第一个长条为例。

        我们遍历4*4方块内的每一个方块,如果为我们要的红色区域就把方块的状态改为EXIST存在,否则就改为NO,同时我们把每个方块的坐标进行更改。因为这是用来初始化每一次下落方块的,我们的y坐标一开始不能全为正,要有部分为负才可以。具体可以根据设计调。我这里采用的是全部为负,往下降落一次出现一行。

//长形
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		if (i2 == 1)
			p->Bag[0][i1][i2].flag = EXIST;
		else
			p->Bag[0][i1][i2].flag = NO;

		p->Bag[0][i1][i2].x = i2 + 1;
		p->Bag[0][i1][i2].y = i1 - 3;
	}
}

        同理我们可以将七个方块都加载如Bag中,在这里我们先不急着加载剩下的方块,我们来讨论下方块旋转的话题。这是我们游戏核心的存在,也是最难做的一块。

方块旋转
        方法一

        我们要做的就是将对应的方块旋转90度,但这有很多做法。我们首先了解最为简单的。

如果我们将4*4的方块旋转90度,其内部的图像不就旋转了90度么。我们以下面的图像为例。

        那么如何做呢?我换个图让大家更清楚些。

        旋转后就是将行变列,列变行。我们可以创建个历史数组,然后在赋值给原来的数组。注意坐标不是原来的坐标,要进行一定的变换。

        1的坐标为(x,y),那么1旋转后的坐标为(x+3,y).

        2的坐标为(x,y),那么2旋转后的坐标为(x+2,y+1).

         3的坐标为(x,y),那么3旋转后的坐标为(x+1,y+2).

        4的坐标为(x,y),那么4旋转后的坐标为(x,y+3).

        以此类推我们不难发现新的坐标和原来的坐标存在等差数列的关系,于是我们便可以得到如下的代码。

	struct block tmp[4][4];
    int x = p->Down[0][0].x, y = p->Down[0][0].y;
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			tmp[i2][3-i1] = p->Down[i1][i2];
			tmp[i2][3 - i1].x = x + 3 - i1;
			tmp[i2][3 - i1].y = y + i2;
		}
	}

        这样写比较方便但有个问题是旋转幅度比较大。我们仔细看上面的旋转图可以发现有些不自然,第二到第三张图片,图片整体的位置提高了。相对而言有些不自然。

方法二

        这个方法十分简单,既然要旋转后的位置图片信息,我提前准备好不就可以了么。

        我们不用想他如何变换的,旋转一次,一次选第几个图片,不够这种方法要增加额外的变量保存旋转的次数, t=++t%4;r然后根据t来选取图像,再来初始化下降方块的坐标和图片信息。

        这种方法的旋转显然是可以尽最大努力达到自然的状态,但工作量有些大了!4*7我们要制作28个4*4方块的信息。感兴趣的读者也可以自行尝试,在这里就不赘述了。

    方法三

        这种方法要用到数学中旋转的公式,一个点绕另一个点顺时针旋转90度,其坐标是确定的。假设我们绕点a(x0,y0)顺时针旋转90度,b(a,b)将变为(x0+y0-b,y0-x0+a)。感兴趣的读者可以看以下数学证明。

        于是我们便可以设置旋转点,然后将方块都绕这个点旋转90度即可。对于长条方块而言,选择的旋转点是(1,2),我们便可以将这个方块状态标为ROATE。于是我们在初始化的时候假设ROATE,于是旋转的函数便可以采用如下的方法。为了方便统一将旋转点放在要下落的方块内,而不是NO中

//长形
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		if (i2 == 1)
			p->Bag[0][i1][i2].flag = EXIST;
		else
			p->Bag[0][i1][i2].flag = NO;

		if(i1==2 && i2==1)
			p->Bag[0][i1][i2].flag = ROATE;

		p->Bag[0][i1][i2].x = i2 + 1;
		p->Bag[0][i1][i2].y = i1 - 3;
	}
}
//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		if (p->Down[i1][i2].flag == ROATE)
		{
			dx = p->Down[i1][i2].x;
			dy = p->Down[i1][i2].y;

			goto end;
		}
	}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
	return;

//临时旋转数组
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		tmp[i1][i2] = p->Down[i1][i2];
		tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
		tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
	}
}

        当然这种方法的自然度不如第二种,但代码量减少了许多。

        同理可以写出剩下6个的Bag,每个方块。在这里就不一一展开了。

下落方块

        我们在此之前完成了7个方块的准备,在这了创造一个下落方块便十分简单。这个功能在其他的地方会用到,我们就将他封装成一个函数。

        在这里我们要用到rand()函数,我们可以在主函数中初始化一次种子srand((unsigned int)time(NULL));然后再获得随机数。我们可以定义两个变量。t代表是那个方块,colour代表什么颜色。但注意颜色这里只加载了6个,方块有7种,后序读者可以根据自己设计自行添加。于是便可以得到如下代码。

 //创造下落方块
void CreatBag(SP* p)
{
	int t = rand() % 7;
	int colour = (t + rand()) % 6;

	int i1, i2;

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2] = p->Bag[t][i1][i2];
			//不同方块不同
			if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
			{
				p->Down[i1][i2].img = p->original[colour];
			}
		}
	}
	
}

        循环中的if判断也可以写为p->Down[i1][i2].flag != NO,两种方式都可以。

我们可以将上述的代码封装在一个初始化Init的函数中,传入struct project结构体指针,然后进行操作,在Init中还可以再次封装,这里就不一一叙述了,总体而言处理后的代码如下。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<graphics.h>
#include<time.h>

enum  block_status
{
	EXIST,//存在
	NO,//不存在
	LAND,
	ROATE//旋转点
};
struct block
{
	IMAGE img;
	enum  block_status flag;
	int x;
	int y;
};

enum STATUS
{
	OK,
	ESC,
	GAME_OVER
};


typedef struct project
{
	struct block BackGropund[20][10];//背景
	IMAGE original[7];//原始六色方块图片,最后一个为背景
	struct block  Bag[7][4][4];//原始方块包
	struct block Down[4][4];//下落方块

	enum STATUS status;//状态
	int SleepTime;//速度
	int Score;//分数
	int max;//最高分
	ExMessage m;
}SP;

//加载方块包
void LoadImg(SP* p)
{
	IMAGE img0;
	loadimage(&img0, L"../橙色方块.jpg", 40, 40);
	p->original[0] = img0;

	IMAGE img1;
	loadimage(&img1, L"../紫色方块.jpg", 40, 40);
	p->original[1] = img1;


	IMAGE img2;
	loadimage(&img2, L"../红色方块.jpg", 40, 40);
	p->original[2] = img2;

	IMAGE img3;
	loadimage(&img3, L"../黄色方块.jpg", 40, 40);
	p->original[3] = img3;

	IMAGE img4;
	loadimage(&img4, L"../绿色方块.jpg", 40, 40);
	p->original[4] = img4;

	IMAGE img5;
	loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
	p->original[5] = img5;

	IMAGE img6;
	loadimage(&img6, L"../背景.jpg", 40, 40);
	p->original[6] = img6;

}

//加载方块的模板
void LoadBag(SP* p)
{
	int i1 = 0, i2 = 0;
	//长形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i2 == 1)
				p->Bag[0][i1][i2].flag = EXIST;
			else
				p->Bag[0][i1][i2].flag = NO;

			if(i1==2 && i2==1)
				p->Bag[0][i1][i2].flag = ROATE;

			p->Bag[0][i1][i2].x = i2 + 1;
			p->Bag[0][i1][i2].y = i1 - 3;
		}
	}
	//正方形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((i2 == 1 || i2 == 2) && (i1==1||i1==2))
				p->Bag[1][i1][i2].flag = EXIST;
			else
				p->Bag[1][i1][i2].flag = NO;

			if (i1 == 1 && i2 == 1)//正方形特殊处理
				p->Bag[1][i1][i2].flag = ROATE;

			p->Bag[1][i1][i2].x = i2 + 1;
			p->Bag[1][i1][i2].y = i1 - 3;
		}
	}
	//山形
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 1)
				p->Bag[2][i1][i2].flag = EXIST;
			else
				p->Bag[2][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[2][i1][i2].flag = ROATE;

			p->Bag[2][i1][i2].x = i2 + 1;
			p->Bag[2][i1][i2].y = i1 - 3;
		}
	}
	//右七
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 2)
				p->Bag[3][i1][i2].flag = EXIST;
			else
				p->Bag[3][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 2)
				p->Bag[3][i1][i2].flag = ROATE;

			p->Bag[3][i1][i2].x = i2 + 1;
			p->Bag[3][i1][i2].y = i1 - 3;
		}
	}
	//左七
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 3 && i2 > 0 || i1 == 2 && i2 == 1)
				p->Bag[4][i1][i2].flag = EXIST;
			else
				p->Bag[4][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[4][i1][i2].flag = ROATE;

			p->Bag[4][i1][i2].x = i2 + 1;
			p->Bag[4][i1][i2].y = i1 - 3;
		}
	}
	//右Z
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 2 && i2 < 2 || i1 == 1 && (i2 == 1 || i2 == 2))
				p->Bag[5][i1][i2].flag = EXIST;
			else
				p->Bag[5][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 1)
				p->Bag[5][i1][i2].flag = ROATE;

			p->Bag[5][i1][i2].x = i2 + 1;
			p->Bag[5][i1][i2].y = i1 - 3;
		}
	}
	//左Z
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (i1 == 2 && i2 > 1 || i1 == 1 && (i2 == 1 || i2 == 2))
				p->Bag[6][i1][i2].flag = EXIST;
			else
				p->Bag[6][i1][i2].flag = NO;

			if (i1 == 2 && i2 == 2)
				p->Bag[6][i1][i2].flag = ROATE;


			p->Bag[6][i1][i2].x = i2 + 1;
			p->Bag[6][i1][i2].y = i1 - 3;
		}
	}

}
//字符转换
void Change(WCHAR* des, char* src)
{
	while (*des++ = *src++);
}

//打印背景
void PrintBackGround(SP* p)
{
	int i1 = 0, i2 = 0;
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
		}
	}
	FlushBatchDraw();
}
//打印分数信息
void  PrintMessage(SP* p)
{
	char arr1[50];
	WCHAR arr2[50];
	//打印当前分数
	RECT r = { 400, 200, 600, 300 };
	drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 300, 600, 350 };
	sprintf(arr1, "%d", p->Score);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);


	//打印最高分数
	r = { 400, 0, 600, 200 };
	drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 100, 600, 200 };
	sprintf(arr1, "%d", p->max);
	Change(arr2, arr1);
	drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);



	r = { 400, 400, 600, 450 };
	drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 450, 600, 500 };
	drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	r = { 400, 500, 600, 550 };
	drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	//打印版权
	r = { 400, 700, 600, 800 };
	drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
//打印下落方块
void PrintDown(SP* p)
{
	int i1, i2;

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
			{
				putimage(p->Down[i1][i2].x * 40, p->Down[i1][i2].y * 40, &p->Down[i1][i2].img);
			}
		}
	}
}
//打印全部内容
void Print(SP* p)
{
	PrintBackGround(p);
	PrintDown(p);
	solidrectangle(400, 0, 600, 800);
	PrintMessage(p);
	FlushBatchDraw();
}



 //创造下落方块
void CreatBag(SP* p)
{
	int t = rand() % 7;
	int colour = (t + rand()) % 6;

	int i1, i2;

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2] = p->Bag[t][i1][i2];
			//不同方块不同
			if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
			{
				// p->Down[i1][i2].flag != NO
				p->Down[i1][i2].img = p->original[colour];
			}
		}
	}
	
}


//初始化
void Init(SP *p)
{
	initgraph(600, 800);// EX_SHOWCONSOLE  控制台

	//初始配置
	setlinecolor(BLACK);
	setlinestyle(PS_SOLID | PS_JOIN_BEVEL, 3);
	setbkcolor(WHITE);
	settextcolor(BLACK);
	settextstyle(20, 0, L"楷体");
	setfillcolor(WHITE);
	setbkmode(TRANSPARENT);//透明文字
	cleardevice();
	srand((unsigned int)time(NULL));
	BeginBatchDraw();//防止闪屏
	//结构体初始化
	p->Score = 0;
	p->SleepTime = 500;
	p->status = OK;

	FILE* pf=fopen("../date.text","a+");
	if (pf == NULL)
	{
		perror("Init:fopen");
		return;
	}
	if (fscanf(pf, "%d", &p->max) == EOF)
	{
		p->max = 0;
	}
	fclose(pf);

	//加载图片,基础方块
	LoadImg(p);

	//加载七个方块
	LoadBag(p);

	//创建方块
	CreatBag(p);

	//设置边界线
	line(400, 0, 400, 800);

	//初始化背景
	int i1 = 0, i2 = 0;
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			p->BackGropund[i1][i2].img = p->original[6];
			p->BackGropund[i1][i2].flag = NO;
		}
	}

	//打印背景
	PrintBackGround(p);

	//打印提示版权信息
	PrintMessage(p);

	FlushBatchDraw();
}
int main()
{
	SP a;

	//初始化
	Init(&a);



	return 0;
}

        对于这种代码可以采用逐步深入的方式,看好注释,然后从上往下读,遇到函数往上找定义。相信经过上面的分析,这段代码看起来是十分简单的。

        接下来是游戏的核心,循环处理了。读者可以休息会再看。

游戏进行

        首先我们进行循环判断的条件就十分的清楚,只需要判断,结构体a的状态就可以了。然后每次循环我们主动的休眠500ms,大家也可以自行调试时间。时间越短所需要的反应越快。整体的逻辑代码如下。

int main()
{
	SP a;

	//初始化
	Init(&a);
	while (a.status == OK)
	{
		
		//休眠
		Sleep(a.SleepTime);
	}

	return 0;
}

下降判断

        这段代码显然不会是几行就能解决的,我们就可以将他封装在一个函数内。

        遇到LAND或者到达底部

        当我们的方块一直往下掉落的时候,有两者情况停止下落,一种是到达底部,另一种是遇到LAND,LAND就是其他方块到达底部后的状态。

        于是我们便可以遍历检查。

void  DownJudge(SP*p)
{
	int i1, i2;
	//检查是否到底
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			//行为y列为x
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
				(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
			{
				
				
				
			}
		}
	}


}

        这个的判断条件比较长我们逐步来看,首先判断的是状态为EXIST或者为ROATE,因为我们选取原来EXIST中一点当作旋转点,所以ROATE也是需要判断的一点,然后是后半段判断判断下落方块的y是不是19,或者当前方块的下一个为LAND,如果满足上述判断就代表要进行。复制方块,把EXIST变为LAND的操作。

        复制的操作也十分的简单,如果这个方块的状态满足要求,就复制到背景中,并将背景方块的状态改为LAND,注意如果在某些方块存在,并且y小于0,便代表了游戏结束,把游戏状态改为GAME_OVER,并且直接返回,不进行后序操作。

		//复制方块,变为地
		for (i1 = 0; i1 < 4; i1++)
		{
			for (i2 = 0; i2 < 4; i2++)
			{
				if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
				{
					if (p->Down[i1][i2].y < 0)
					{
						p->status = GAME_OVER;
						return;
					}
					p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
					p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
				}
			}
		}

        如果没有游戏结束,那么便要考虑接下来的操作了。下落一个方块,分数加10分,并且要创建新的方块,我们可以直接调用前面写的CreatBag(p);函数。但我们还有个重要的判断,假如一行全是LAND方块,我们就可以进行消除。所以在这里我们在封装一个函数,检测消除。

消除检测

        我们消除的条件是一行全为LAND,便可以定义一个数组表示每一行是否要消除。于是便有如下代码。需要消除的行赋值为1,不需要赋值为0.如果没有要消除的直接返回,否则进行下面的代码。

//检查删除方块
void CheckDelste(SP* p)
{
	int arr[20] = {0};
	int i1 = 0, i2 = 0;
	int flag = 0;
	int sum = 0;

	//查找消除行
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			if (p->BackGropund[i1][i2].flag == LAND)
			{
				flag = 1;
			}
			else
			{
				flag = 0;
				break;
			}
		}
		arr[i1] = flag;
		sum += arr[i1];
	}
	
	//检查有无消除行
	if (sum == 0)
		return;
	p->Score += 50 * sum;//加分数,一行50分
}

        接下来是处理消除行。我们如何做呢?创建一个临时数组,从后往前遍历,不是消除的复制这一行,是消除的跳过,最后临时数组还有几行全部赋值为背景,最后再打印。完整代码如下。

    //建立临时数组
	struct block tmp[20][10];
	int t = 19;
	for (i1 = 19; i1 >=0; i1--)
	{
		if (arr[i1] == 0)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				tmp[t][i2] = p->BackGropund[i1][i2];
			}
			t--;
		}
	}
	//多余赋值为背景
	for (i1 = t; i1 >= 0; i1--)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			tmp[t][i2].flag = NO;
			tmp[t][i2].img = p->original[6];
		}
	}
	//转换
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			p->BackGropund[i1][i2] = tmp[i1][i2];
		}
	}

	Print(p);

        这样可以完成我们的消除操作,但消除行直接一闪而过,为了消除效果更加好,我们增加一个闪烁的效果。整体闪烁三次。

        如果消除行就打印背景方块,然后再打印PrintBackGround(p);泽里的背景方块还未处理,所以消除行还会被打印。暂停150ms,然后刷新缓存区。

	//闪烁功能
	for (int j = 0; j < 3; j++)
	{
		int i1 = 0, i2 = 0;
		for (i1 = 0; i1 < 20; i1++)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				if(arr[i1]== 0)
					putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
				else
					putimage(i2 * 40, i1 * 40, &p->original[6]);
			}
		}
		FlushBatchDraw();
		Sleep(150);
		PrintBackGround(p);
		Sleep(150);
		FlushBatchDraw();
	}

正常下落

        如果没有检测到底,就正常下落,y坐标加一。代码如下。

	//正常则下降一格
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].y += 1;
		}
	}

	//打印图片
	Print(p);

        总体而言这块的总代码如下。

//检查删除方块
void CheckDelste(SP* p)
{
	int arr[20] = {0};
	int i1 = 0, i2 = 0;
	int flag = 0;
	int sum = 0;

	//查找消除行
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			if (p->BackGropund[i1][i2].flag == LAND)
			{
				flag = 1;
			}
			else
			{
				flag = 0;
				break;
			}
		}
		arr[i1] = flag;
		sum += arr[i1];
	}
	
	//检查有无消除行
	if (sum == 0)
		return;
	p->Score += 50 * sum;//加分数,一行50分

	//闪烁功能
	for (int j = 0; j < 3; j++)
	{
		int i1 = 0, i2 = 0;
		for (i1 = 0; i1 < 20; i1++)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				if(arr[i1]== 0)
					putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
				else
					putimage(i2 * 40, i1 * 40, &p->original[6]);
			}
		}
		FlushBatchDraw();
		Sleep(150);
		PrintBackGround(p);
		Sleep(150);
		FlushBatchDraw();
	}

	//建立临时数组
	struct block tmp[20][10];
	int t = 19;
	for (i1 = 19; i1 >=0; i1--)
	{
		if (arr[i1] == 0)
		{
			for (i2 = 0; i2 < 10; i2++)
			{
				tmp[t][i2] = p->BackGropund[i1][i2];
			}
			t--;
		}
	}
	//多余赋值为背景
	for (i1 = t; i1 >= 0; i1--)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			tmp[t][i2].flag = NO;
			tmp[t][i2].img = p->original[6];
		}
	}
	//转换
	for (i1 = 0; i1 < 20; i1++)
	{
		for (i2 = 0; i2 < 10; i2++)
		{
			p->BackGropund[i1][i2] = tmp[i1][i2];
		}
	}

	Print(p);
}
void  DownJudge(SP*p)
{
	int i1, i2;
	//检查是否到底
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			//行为y列为x
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)&&
				(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
			{
				//复制方块,变为地
				for (i1 = 0; i1 < 4; i1++)
				{
					for (i2 = 0; i2 < 4; i2++)
					{
						if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
						{
							if (p->Down[i1][i2].y < 0)
							{
								p->status = GAME_OVER;
								return;
							}
							p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
							p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
						}
					}
				}
				//加分数
				p->Score += 10;
				//检测是否可以消除
				CheckDelste(p);
				//创建新的下落方块
				CreatBag(p);
				return;
			}
		}
	}

	//正常则下降一格
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].y += 1;
		}
	}

	//打印图片
	Print(p);
}

检测按键

        写完上面的代码后,我们的游戏一句完成一半了,可以正常的下落。视频如下。

俄罗斯方块半成品

        接下来我们再次封装一个检测按钮的函数,用来实现左移右移与旋转。

int main()
{
	SP a;

	//初始化
	Init(&a);
	while (a.status == OK)
	{
		//检测按键
		CheckKey(&a);

		//下降处理
		DownJudge(&a);

		//休眠
		Sleep(a.SleepTime);
	}


	return 0;
}

        在这里我们要用到peekmessage函数,注意不能用getmessage,这个函数用于获取一个消息。如果当前消息队列中没有,就一直等待。我们要立即返回的就用peekmessage。

        于是我们便可以写出如下的结构。然后对应处理每个按钮即可,为了防止这个代码过于臃肿。可以将每个按钮的操作再分装在一个函数内。

        注意我们在和函数的最后加了两条语句,一个是刷新消息缓存区,一个是初始化当前的虚拟键码,这两步都是为了消除上一次循环的影响。

void CheckKey(SP*p)
{
	     peekmessage(&p->m);
	    if (p->m.vkcode==(VK_DOWN))
	    {
		   
	    }
	
		else if (p->m.vkcode == (VK_UP))
		{
		
		}
		else if (p->m.vkcode == (VK_LEFT))
		{
		
		}
		else if (p->m.vkcode == (VK_RIGHT))
		{
		
		}
		else if (p->m.vkcode == (VK_ESCAPE))
		{
		
		}
		else if (p->m.vkcode == (VK_SPACE))
		{
				
        }

	
	p->m.vkcode = 0;
	flushmessage();
}

左移

        这个操作十分简单只需要将方块的x值减一就可以了,但我们还要检测左移是否合法,不合法就不支持左移。VK_LEFT是虚拟键码,大家可以在官网查询虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

        如下,我们就可以完成一次左移。

void LeftMove(SP* p)
{
	int i1 = 0, i2 = 0;
	//全部检查是否合法
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
				(p->Down[i1][i2].x-1<0 ||p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x - 1].flag==LAND))
				return;
		}
	}

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].x -= 1;
		}
	}
}

        为了效果更加的好,我们在左移后就立即打印,并且停留200ms

else if (p->m.vkcode == (VK_LEFT))
{
	LeftMove(p);
	Print(p);
	Sleep(200);
}

右移

        右移与左移十分的相似,都是要检测是否合法。

void RightMove(SP*p)
{
	int i1 = 0, i2 = 0;
	//全部检查一遍
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE) &&
				(p->Down[i1][i2].x +1 >9|| p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x +1].flag == LAND))
				return;
		}
	}

	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2].x += 1;
		}
	}
}

下落

        此时我们要的是方块快速下落,我们就可以将SleepTime 改为60ms,就可以达到快速下落的操作,但我们要在不按↓键的时候将SleepTime改为500;我们可以在其他的分支语句中加上p->SleepTime = 500;也可以将上述的结构简单修改。如下代码。

void CheckKey(SP*p)
{
	 peekmessage(&p->m);
	if (p->m.vkcode==(VK_DOWN))
	{
		p->SleepTime = 60;
	}
	else
	{
		p->SleepTime = 500;
		if (p->m.vkcode == (VK_UP))
		{
			
		}
		else if (p->m.vkcode == (VK_LEFT))
		{
			LeftMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_RIGHT))
		{
			RightMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_ESCAPE))
		{
			
		}
		else if (p->m.vkcode == (VK_SPACE))
		{
			
		}

	}
	p->m.vkcode = 0;
	flushmessage();
}

退出

        这个功能也十分简单就可以实现,将状态改为ESC即可

else if (p->m.vkcode == (VK_ESCAPE))
{
	p->status = ESC;
}

暂停

        在这里我用不论我用getmessage,还是peekmessage总会出现BUG,我也找不到具体原因,不得已之下换了一种检测按钮的方式.利用其他的函数检测。当对于的虚拟键码存在时便为真。


#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

        当我们第一次按下暂停进入分支,第二次按下退出循环。

else if (KEY_PRESS(VK_SPACE))
{
	while (!KEY_PRESS(VK_SPACE))
	{
		Sleep(50);
	}
}

        在这里我只能说抱歉,Easyx对于API进行了再次封装,里面的细节看不到,我也找不出问题所在,如果读者有什么解决办法可以发在评论区,不胜感激。当然也不排除这个函数本身的问题。

旋转

        接下来是我们的最重要的操作,将方块进行旋转。首先我们还是可以创建一个临时数组,然后检测合法性,最后在进行复制。

        首先我们要找到旋转点,求出dx,dy,其中正方形要特殊处理。应为旋转点无论旋转几次,他在4*4方格中的位置不变。我们由此就可以判断是否为正方形,如果为正方形就直接退出。

int i1, i2;
int dx, dy;
struct block tmp[4][4];

//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		if (p->Down[i1][i2].flag == ROATE)
		{
			dx = p->Down[i1][i2].x;
			dy = p->Down[i1][i2].y;

			goto end;
		}
	}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
	return;

        接下来就是创建临时数组,每个点的坐标关系如上文方块旋转所说。在这里我们不需要将临时数组在4*4方格中的位置变换,因为我们最终看的是方块的坐标,而不是在4*4方格中的位置。

	//临时旋转数组
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			tmp[i1][i2] = p->Down[i1][i2];
			tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
			tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
		}
	}

        接下来就是判断是否合法,不合法直接退出,合法就复制,

//判断是否合法
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
				(
					tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19 
					|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
				)
			)
			return;
	}
}


//合法则复制
for (i1 = 0; i1 < 4; i1++)
{
	for (i2 = 0; i2 < 4; i2++)
	{
		p->Down[i1][i2]= tmp[i1][i2];
	}
}

        完整代码如下

//旋转方块
void Rorate(SP* p)
{
	
	int i1, i2;
	int dx, dy;
	struct block tmp[4][4];

	//找到旋转点
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if (p->Down[i1][i2].flag == ROATE)
			{
				dx = p->Down[i1][i2].x;
				dy = p->Down[i1][i2].y;

				goto end;
			}
		}
	}
	end:
	//正方形直接退出
	if (i1 == 1 && i2 == 1)
		return;

	//临时旋转数组
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			tmp[i1][i2] = p->Down[i1][i2];
			tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
			tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
		}
	}

	
	//判断是否合法
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
					(
						tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19 
						|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
					)
				)
				return;
		}
	}

	
	//合法则复制
	for (i1 = 0; i1 < 4; i1++)
	{
		for (i2 = 0; i2 < 4; i2++)
		{
			p->Down[i1][i2]= tmp[i1][i2];
		}
	}

}

        最终我们的按钮检测就可以如下表示。

void CheckKey(SP*p)
{
	 peekmessage(&p->m);
	if (p->m.vkcode==(VK_DOWN))
	{
		p->SleepTime = 60;
	}
	else
	{
		p->SleepTime = 500;
		if (p->m.vkcode == (VK_UP))
		{
			Rorate(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_LEFT))
		{
			LeftMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_RIGHT))
		{
			RightMove(p);
			Print(p);
			Sleep(200);
		}
		else if (p->m.vkcode == (VK_ESCAPE))
		{
			p->status = ESC;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			while (!KEY_PRESS(VK_SPACE))
			{
				Sleep(50);
			}
		}
	}

	//消除影响
 	p->m.vkcode = 0;
	flushmessage();
}

        到这里我们就完成了游戏的大部分了,现在游戏就可以正常的运行了,等不下的读者可以先玩了。下面是最后一部分结尾工作了。

游戏结束

        同理我们可以封装成一个函数解决,减少主函数的复杂度。

int main()
{
	SP a;

	//初始化
	Init(&a);
	while (a.status == OK)
	{
		//检测按键
		CheckKey(&a);

		//下降处理
		DownJudge(&a);

		//休眠
		Sleep(a.SleepTime);
	}

	//游戏结束
	GameOver(&a);

	return 0;
}

保存分数

        首先我们要做的就是看看是否创造记录,如果创造记录就保存。

        在这了我们用的是w+刚好可以将原来的文件删除,创建新的文件,保存最高记录。同时为了庆祝,还可以打印些文字,读者可以自行安排。

	settextcolor(RED);
	settextstyle(50, 0, L"楷体");
	//创造最高记录
	if (p->max < p->Score)
	{
		RECT r = { 0, 300, 600, 400 };
		drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
		FILE* pf = fopen("../date.text", "w+");
		fprintf(pf, "%d", p->Score);
		fclose(pf);
	}

        然后可以打印处游戏结束语,为了不直接闪过,我们加个循环检测Enter,让用户主动结束页面。当然在最后不要忘了结束界面    closegraph();。

RECT r = { 0, 500, 600, 600 };
drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

r = { 0, 700,600, 800 };
drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FlushBatchDraw();
//不断检测信息,直到按下enter
ExMessage m = getmessage();
while (m.vkcode != VK_RETURN)
{
	m = getmessage();
}
	closegraph();

        读者可自行添加其他文字,总的代码如下

//游戏结束
void GameOver(SP*p)
{
	settextcolor(RED);
	settextstyle(50, 0, L"楷体");
	//创造最高记录
	if (p->max < p->Score)
	{
		RECT r = { 0, 300, 600, 400 };
		drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
		FILE* pf = fopen("../date.text", "w+");
		fprintf(pf, "%d", p->Score);
		fclose(pf);
	}

	RECT r = { 0, 500, 600, 600 };
	drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

	r = { 0, 700,600, 800 };
	drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	FlushBatchDraw();
	//不断检测信息,直到按下enter
	ExMessage m = getmessage();
	while (m.vkcode != VK_RETURN)
	{
		m = getmessage();
	}
	closegraph();
}

        到这里我们的代码就写完了。十分的不容易,感谢你可以读到此处。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/520535.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++之类和对象(上)

目录 1.面向过程和面向对象初步认识 2.类的引入 3.类的定义 4.类的访问限定符及封装 4.1访问限定符 4.2 类的两种定义方式 第一种&#xff1a; 第二种&#xff1a; 4.3封装 5.类的实例化 6.类对象模型 1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;…

Unity Toggle组件

Toggle Group组件 Allow Switch Off属性值为false时&#xff0c; 1&#xff0c;Toggle初始时默认会有一个被勾选&#xff08;ison为true&#xff09;&#xff0c;可以自己打勾指定 2&#xff0c;不能取消勾选 Allow Switch Off属性值为true时&#xff0c; 1&#xff0c;Toggl…

11-LINUX--信号

一.信号的基本概念 信号是系统响应某个条件而产生的事件&#xff0c;进程接收到信号会执行相应的操作。 与信号有关的系统调用在“signal.h”头文件中有声明 常见信号的值&#xff0c;及对应的功能说明&#xff1a; 信号的值在系统源码中的定义如下&#xff1a; 1. #define …

JVM 组成

文章目录 概要JVM 是 Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09;JVM 的主要组成部分运行流程&#xff1a;程序计数器堆元空间方法区常量池运行时常量池 概要 JVM 是 Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&…

BugKu: Simple_SSSTI_2

1.打开题目 由提示可知需要传入一个名为flag的参数 2.查看网页源代码 并没有得到有用的信息 3.查看config对象 http://114.67.175.224:10934/?flag{{config}} 信息太乱了&#xff0c;需要找到我们需要的信息 4.SSTI模版注入 http://114.67.175.224:10934/?flag{{ conf…

解决报错 由于目标计算机积极拒绝,无法连接

完整错误&#xff1a; WARNING: Retrying (Retry(total0, connectNone, readNone, redirectNone, statusNone)) after connection broken by ProxyError(Cannot connect to proxy., NewConnectionError(<pip._vendor.urllib3.connection.HTTPConnection object at 0x000002…

MySql并发事务问题

事务 事务概念&#xff1a; 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 事务的特性&#xff1a;ACID&#xff1a; 小…

Chapter 18 Current-Programmed Control 峰值电流模控制

Chapter 18 Current-Programmed Control 峰值电流模控制 对于PWM converter, 其输出电压由占空比d控制. 我们将直接控制占空比d称为电压模控制, 因为输出电压和占空比成正比.还有一种广泛应用的控制方法是控制开关管峰值电流的. 我们称为电流模控制. 这一章介绍峰值电流模控制…

UML2.0在系统设计中的实际使用情况

目前我在系统分析设计过程中主要使用UML2.0来表达&#xff0c;使用StarUML软件做实际设计&#xff0c;操作起来基本很顺手&#xff0c;下面整理一下自己的使用情况。 1. UML2.0之十三张图 UML2.0一共13张图&#xff0c;可以分为两大类&#xff1a;结构图-静态图&#xff0c;行…

【轻松一刻】中国茶叶探索奇妙之旅

文章目录 茶多酚 茶叶大类 龙井茶 泡茶方法 茶叶保存 参考资料 茶多酚 茶多酚是形成茶叶色香味的主要成份之一&#xff0c;也是茶叶中有保健功能的主要成份之一。茶多酚的副产品咖啡因&#xff0c;又称为咖啡碱&#xff0c;能兴奋大脑皮层&#xff0c;所以喝茶有提神作用…

代码随想录-算法训练营day04【两两交换链表中的节点、删除链表的倒数第N个节点、链表相交、环形链表II】

专栏笔记&#xff1a;https://blog.csdn.net/weixin_44949135/category_10335122.html 第二章 链表part02● day 1 任务以及具体安排&#xff1a;https://docs.qq.com/doc/DUG9UR2ZUc3BjRUdY ● day 2 任务以及具体安排&#xff1a;https://docs.qq.com/doc/DUGRwWXNOVEpyaVpG…

【原创】springboot+vue校园座位预约管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

Vivado功耗基础之功耗评估器XPE使用详述

目录 一、前言 二、XPE环境配置 2.1 XLSM下载 2.2 EXCEL启用宏 2.3 打开XLSM 三、XPE操作 3.1 单元格颜色含义 3.2 Settings表格 3.3 片上功耗表格 3.4 电源供电表格 3.5 总结表格 3.6 工具栏 四、参考资料 一、前言 XPE(Xilinx Power Estimator)是一个在工程的预…

【C++航海王:追寻罗杰的编程之路】C++的类型转换

目录 1 -> C语言中的类型转换 2 -> 为什么C需要四种类型转换 3 -> C强制类型转换 3.1 -> static_cast 3.2 -> reinterpret_cast 3.3 -> const_cast 3.4 -> dynamic_cast 4 -> RTTI 1 -> C语言中的类型转换 在C语言中&#xff0c;如果赋值运…

八股面试速成—Java语法部分

暑期实习面试在即&#xff0c;这几天八股和算法轮扁我>_ 八股部分打算先找学习视屏跟着画下思维导图&#xff0c;然后看详细的面试知识点&#xff0c;最后刷题 其中导图包含的是常考的题&#xff0c;按照思维导图形式整理&#xff0c;会在复盘后更新 细节研究侧重补全&a…

15.5 二叉排序树原理及建树实战

二叉树模拟网站&#xff1a;Binary Search Tree Visualization (usfca.edu) 图 代码&#xff1a; #include <stdio.h> #include <stdlib.h> typedef int KeyType; typedef struct BSTNode{KeyType key;struct BSTNode *lchild, *rchild; }BSTNode, *BiTree; int …

[报错解决]源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。

目录 报错信息解决办法 spring整合mvc时&#xff0c;遇到的404报错&#xff0c;梳理mvc知识供参考供 报错信息 解决办法 Controller RequestMapping("user") public class UserController {//spring整合webmvc// 请求地址 http://localhost:7070/user/quickRequest…

【游戏分析】非游戏领空追字符串来源

通过NPC名称找NPC数组 扫描 NPC名字 ASIC型 发现全部都有后缀 那么采用 字节集的方式去扫描 也是扫不到 说明:不是ASIC型字符串 扫描 NPC名字 Unicode型 没有结果 那么转换成字节集去扫描 终于发现结果了 把结果挨个修改字符串 发现 其中两个是可以用的 22和23 …

SAR教程系列7——在cadence中用Spectrum工具FFT仿真ADC的ENOB、SNR等动态性能指标

首先在仿真之前&#xff0c;你得有一个ADC。然后是思考如何仿真的问题&#xff0c;如何加激励&#xff0c;如何使用相关工具查看仿真结果。假定你有一个可以仿真的ADC&#xff0c;大致经过下列步骤可以得到ADC的相关动态性能指标。 第一步&#xff1a;在ADC后面接一个理想的DA…

实战解析:接口限流的一次简单实践

1.写这篇文章的来由 有一段时间里&#xff0c;博客总是三天两头被打&#xff0c;其中就遇到了恶意刷接口的手段&#xff0c;对方明显使用的代码IP&#xff0c;由于博客并没有做这方面的措施&#xff0c;加上被大量盗刷的接口刚好是数据量最大的一篇文章数据&#xff0c;所以不…