俄罗斯方块-C语言(从0开始到有色界面)

大一c语言课设写的俄罗斯方块,看了好几遍原著的源码,从0开始写并对原著进行了注释、改进与优化。原著能力很强,感谢原著自写的初版参考,原著链接:https://blog.csdn.net/lanse_l/article/details/74959248

彻底从0开始写了一遍,算是对细节有了比较深的领悟。有同样想法的朋友欢迎交流,有什么看不懂的请在下方留言,看到会及时回复。

GitHub链接:https://github.com/JellyfishMIX/Tetris.c-from-0-to-colored-interface

 

阅读指引(重要):1.文字说明比较长,建议先略过前半部分文字说明,直接按2.中所说的顺序看源码,看到某个自定义函数需要参考再回来看文字说明。

                                   2.想要从0开始写的朋友,给一个思路:别的不看,先写最底部的windowsAPI。然后从main()开始入手,遇到新的自定义函数或变量,去翻源码,一点点扣源码。

                                   3.所有用到的自定义函数、全局变量、结构体均已在main()前声明,按第一次被调用的先后顺序排列,并标明了功能注释。

“消除”发生后运行效果截图

 

一、摘要

目的:做一个俄罗斯方块,具有旋转功能,满一行清除功能,记录得分、最高成绩。

方法:

1. 采用了模块化的开发方式,把程序分为好几个功能模块来实现,主函数mian()只用来调用这些功能模块。

此外,各个功能模块间也有互相调用的情况。

2. 预设了宏定义DOWN LEFT RRIGHT BOX WALL KONG等,预设给其ASCII码,方便后续调用与辨识。

3. 通过blocks[base][space_z],(一维base表示7个基础方块,二维space_z表示旋转状态),叠加space[4][4],得到一个四维数组,用来预设7+7*3=28种图形。

4. 预设了一个结构体face,其中存储着face.data[][]记录BOX WALL KONG,face.color[][]记录堆叠的图形每一块的颜色。

5. 使用了c语言文件操作知识来保存/读取最高得分。

6. 为了使界面更美观,使用自定义函数hidden_cursor()隐藏了光标,使用自定义函数color控制光标输出字符的颜色。

7. 为了渲染界面,使用了自定函数gotoxy()控制光标在cmd窗口中的坐标位置。

读取键入信息,实现操作功能,利用了控制键的ASCII码,使用switch()结构选择键入控制键对应的功能模块。

二、详细说明

1. 首先对cmd运行窗口做了处理,Hide_Cursor();中调用光标相关API隐藏光标。color();中对光标键入颜色做了预设,共计6种预设颜色。编写了gotoxy(int x,int y);函数,通过COORD这一windows API控制光标在cmd窗口中的坐标位置。

2. 在开发之初先使用Inter_Face();构设了界面(地图边界WALL,操作提示信息),之后通过blocks[base][space_z],(一维base表示7个基础方块,二维space_z表示旋转状态),叠加space[4][4],得到一个四维数组,用来预设7+7*3=28种图形。

课设报告中留下的截图,不是很清晰

3. 在main();主函数中,完成了初始化界面Inter_Face();和预设28种图形Inter_Blocks();的工作,并使用srand(time(NULL));预设了随机种子,供后续调用。使用Read_File();读取最高得分记录。而游戏部分,单独交给了套在while(1)永循环中的Start_Game();来执行。

4. Start_Game()分为两部分,开头是渲染右上角下一个提示图形,第二部分是while(1),也就是左侧游戏区图形下落和堆叠

5. Start_Game();中,对于每帧刷新的实现通过如下方法实现:

        int t=15000;while(--t){ if (kbhit() != 0);break};停顿的时间即—t跑一万五千次+调用一万五千次kbhit()函数的时间(kbhit()是一个C和C++函数,用于非阻塞地响应键盘输入事件)。

        Draw_Kong是把原图形画成空白,Draw_Blocks是在x,y位置画出图形,gotoxy(x,y)会把光标移动到cmd窗口(x,y)坐标位置, 函数Bottom()判断是否到达底部这样一直gotoxy(x,y),Draw_Blocks,Bottom判断,Draw_Kong,y++

gotoxy(x,y),Draw_Blocks,Bottom判断,Draw_Kong,y++

……

6. 堆叠部分,使用face,data[][]和face,color[][]来保存堆叠的BOX数据。

7. 判断是否触碰底部函数Bottom(),也是在测算当前光标坐标(x,y)渲染出的blocks[base][space_z].space[4][4],如果再次y++下移,是否会与face.data[][]中的BOX或WALL重叠,如果重叠,那么把当前光标坐标(x,y)位置渲染出的blocks[base][space_z].space[4][4]中1(即图形部分),存在对应face.data[][]位置,置为BOX。

三、开发过程中遇到的问题及解决办法

1. 旋转算法,让7种基础形状变换出剩余的21种

解决办法:blocks[base][space_z + 1].space[i][j] = tem[4 - j - 1][i];    //控制一边坐标不变,另一边为4 - i - 1,然后再行列互换,这样可以保证四次旋转不同,如果仅仅行列互换,将会导致只有两种旋转状态

2. Windows cmd控制台光标信息COORD中,x是横坐标,y是纵坐标,在开发过程中,这两者总是搞混,多数BUG出自于此。

解决办法:这种横纵坐标搞混的错误现象还是很明显的,根据运行情况调试,设置断点,定位BUG位置

 

3. 清除函数Eliminate()约占100行,比较复杂,开发时耗时较多,这里面出现了x,y横纵坐标搞混,只能清除单行、清除后上部堆叠方块无法下移等问题。

解决方法:x,y横纵坐标搞混问题根据调试情况,设置断点,定位BUG位置,肉眼Debug。清除单行改进办法,追加了一次判断上一行是否堆满,如果堆满那么执行下一次Eliminate()。清除后上部堆叠方块无法下移问题,利用face.data[m][n] = face.data[m - 1][n];和face.color[m][n] = face.color[m - 1][n];  ,使清除后的该行,继承上一行的face,data和face,color数据,上一行再继承上上行,以此类推。

 

源码(.c):

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

#define FACE_Y 29	//游戏区行数
#define FACE_X 20	//游戏区列数
#define WALL  2
#define KONG 0
#define BOX 1	//已经堆积完毕的方块
#define LEFT  75
#define RIGHT 77
#define DOWN 80
#define SPACE 32
#define ESC 27

struct Face
{
	int data[FACE_Y][FACE_X + 10];	//结构体定义时数组均已被置零
	int color[FACE_Y][FACE_X + 10];
}face;
struct Blocks
{
	int space[4][4];
}blocks[7][4];	//blocks[base][space_z],一维base表示7个基础方块,二维space_z表示旋转状态

void Read_File();	//读取最高得分
void Write_File();	//写入最高得分
void Inter_Face();	//初始化界面
void Inter_Blocks();	//初始化方块信息
void Start_Game();	//开始游戏
void Draw_Kong(int base, int space_z, int x, int y);	//覆盖前一个blocks的方块,取而代之画出空格
void Draw_Blocks(int base, int space_z, int x, int y);	//画出当前blocks的方块
int Bottom(int base, int space_z, int x, int y);	//WALL与BOX称为底部,判断是否触碰到底部,触碰到底部返回1,未触碰到底部返回0
int Eliminate();	//一行堆积满后清除该行,并记录分值,并询问玩家是否继续
void Hide_Cursor();	//隐藏光标
void color(int c);	//改变输出字符的颜色
void gotoxy(int x,int y);	//坐标跳转

int nn, max=0, grade=0;	//nn,全局变量,用来取blocks[base][space_z]中的base,表示7种基础图形之一
int main()
{
	system("title Tetris(DIY)");    //设置cmd窗口标题
	system("mode con lines=29 cols=60");    //设置cmd窗口高度、宽度
	color(7);    //改变输出字符的颜色
	Hide_Cursor();    //隐藏光标
	Read_File();    //读取最高得分
	Inter_Face();    //初始化界面
	Inter_Blocks();    //初始化方块信息
	srand(time(NULL));	//main函数中设置srand((unsigned)time(NULL));时,影响的将是所有的rand
	nn = rand() % 7;    //nn,全局变量,用来取blocks[base][space_z]中的base,表示7种基础图形之一。这里使nn随机取得0~6中的一个值,即随机取得7种基础图形之一
	while (1)
	{
		Start_Game();
	}
}

void Read_File()    //读取最高得分
{
#pragma warning(disable:4996)    //对 Visual Studio 2019 进行的警告消除,IDE不是Visual Studio可以删去
	FILE *fp;
	fp = fopen("俄罗斯方块最高得分记录.txt", "r+");	//r+ 以可读写方式打开文件,该文件必须存在
	if (fp == NULL)
	{
		fp = fopen("俄罗斯方块最高得分记录.txt", "w+");
		fwrite(&max, sizeof(int), 1, fp);
	}
	fseek(fp, 0, 0);	//函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere(偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))为基准,偏移offset(指针偏移量)个字节的位置。如果执行失败(比如offset超过文件自身大小),则不改变stream指向的位置。
	fread(&max, sizeof(int), 1, fp);
	fclose(fp);
}

void Write_File()    //写入最高得分
{
#pragma warning(disable:4996)    //对 Visual Studio 2019 进行的警告消除,IDE不是Visual Studio可以删去
	FILE* fp;
	fp = fopen("俄罗斯方块最高得分记录.txt", "w+");
	fwrite(&grade, sizeof(int), 1, fp);
	fclose(fp);
}

void Inter_Face()    //初始化界面
{
	int i, j;
	for (i = 0; i < FACE_Y; i++)
	{
		for (j = 0; j < FACE_X + 10; j++)
		{
			if (j == 0 || j == FACE_X - 1 || j == FACE_X + 9)
			{
				face.data[i][j] = WALL;
				gotoxy(2 * j, i);
				printf("■");
			}
			else if (i == FACE_Y - 1)
			{
				face.data[i][j] = WALL;
				printf("■");
			}
			else
				face.data[i][j] = KONG;
		}
	}
	gotoxy(2 * FACE_X + 4, FACE_Y - 18);
	printf("左移:←");

	gotoxy(2 * FACE_X + 4, FACE_Y - 16);
	printf("右移:→");

	gotoxy(2 * FACE_X + 4, FACE_Y - 14);
	printf("旋转:space");

	gotoxy(2 * FACE_X + 4, FACE_Y - 12);
	printf("暂停: S");

	gotoxy(2 * FACE_X + 4, FACE_Y - 10);
	printf("退出: ESC");

	gotoxy(2 * FACE_X + 4, FACE_Y - 8);
	printf("重新开始:R");

	gotoxy(2 * FACE_X + 4, FACE_Y - 6);
	printf("最高纪录:%d", max);

	gotoxy(2 * FACE_X + 4, FACE_Y - 4);
	printf("分数:%d", grade);
}

void Inter_Blocks()    //初始化方块信息
{
	int i;

	///基础七个形状
	//倒置土字形
	for (i = 0; i < 3; i++)
		blocks[0][0].space[1][i] = 1;
	blocks[0][0].space[2][1] = 1;

	//L形-1
	for(i=1;i<4;i++)
		blocks[1][0].space[i][1] = 1;
	blocks[1][0].space[1][2] = 1;

	//L形-2
	for(i=1;i<4;i++)
		blocks[2][0].space[i][2] = 1;
	blocks[2][0].space[1][1] = 1;

	for (i = 0; i < 2; i++)
	{
		//Z形-1
		blocks[3][0].space[1][i] = 1;
		blocks[3][0].space[2][i + 1] = 1;

		//Z形状-2
		blocks[4][0].space[1][i + 1] = 1;
		blocks[4][0].space[2][i] = 1;

		//田字形
		blocks[5][0].space[1][i + 1] = 1;
		blocks[5][0].space[2][i + 1] = 1;
	}

	//1字形
	for (i = 0; i < 4; i++)
	{
		blocks[6][0].space[i][2] = 1;
	}

	///7个基础形状的旋转状态space_z,旋转状态共计7*3+7=21+7种
	int base, space_z, j, tem[4][4];
	for (base = 0; base < 7; base++)
	{
		for (space_z = 0; space_z < 3; space_z++)
		{
			for (i = 0; i < 4; i++)
			{
				for (j = 0; j < 4; j++)
				{
					tem[i][j] = blocks[base][space_z].space[i][j];
				}
			}
			for (i = 0; i < 4; i++)
			{
				for (j = 0; j < 4; j++)
				{
					blocks[base][space_z + 1].space[i][j] = tem[4 - j - 1][i];	//控制一边坐标不变,另一边为4 - i - 1,然后再行列互换,这样可以保证四次旋转不同,如果仅仅行列互换,将会导致只有两种旋转状态
				}
			}
		}
	}
}

void Start_Game()    //开始游戏。分为两部分,开头是渲染右上角下一个提示图形,第二部分是while(1),也就是左侧游戏区图形下落和堆叠
{
	int space_z = 0, n, x = FACE_X / 2 - 2, y = 0, t = 0, i, j, ch;	//x,y初始值即屏幕顶端掉落blocks处
	Draw_Kong(nn, space_z, FACE_X + 3, 4);
	n = nn;	//因为右上角要显示下一块blocks,因此这里先记录下当前blocks的base
	nn = rand() % 7;	//随机生成下一块blocks的base
	color(nn);
	Draw_Blocks(nn, space_z, FACE_X + 3, 4);
	while (1)
	{
		color(n);	//把光标颜色调回当前blocks的base
		Draw_Blocks(n, space_z, x, y);
		if (t == 0)
			t = 15000;
		while (--t)
		{
			if (kbhit() != 0)	//kbhit()是一个C和C++函数,用于非阻塞地响应键盘输入事件。其中文可译为“键盘敲击”(keyboard hit)
				break;
		}
		if (t == 0)
		{
			if (Bottom(n, space_z, x, y+1) != 1)
			{
				Draw_Kong(n, space_z, x, y);
				y++;
			}
			else
			{
				for (i = 0; i < 4; i++)
				{
					for (j = 0; j < 4; j++)
					{
						if (blocks[n][space_z].space[i][j] == 1)
						{
							face.data[y + i][x + j] = BOX;
							face.color[y + i][x + j] = n;
							while (Eliminate());
						}
					}
				}
				return; 
			}
		}
		else
		{
			ch = getch();
			switch (ch)
			{
			case LEFT:
				if (Bottom(n, space_z, x - 1, y) != 1)
				{
					Draw_Kong(n, space_z, x, y);
					x--;
				}
				break;
			case RIGHT:
				if (Bottom(n, space_z, x + 1, y) != 1)
				{
					Draw_Kong(n, space_z, x, y);
					x++;
				}
				break;
			case DOWN:
				if (Bottom(n, space_z, x , y+1) != 1)
				{
					Draw_Kong(n, space_z, x, y);
					y++;
				}
				break;
			case SPACE:
				if (Bottom(n, (space_z + 1) % 4, x, y + 1) != 1)
				{
					Draw_Kong(n, space_z, x, y);
					y++;
					space_z = (space_z + 1) % 4;
				}
				break;
			case ESC:
				system("cls");
				gotoxy(FACE_X, FACE_Y / 2);
				printf("   ---游戏结束---");
				gotoxy(FACE_X, FACE_Y / 2 + 1);
				printf("---请按任意键退出---");
				system("pause>nul");
				exit(0);
			case 'S':
			case 's':
				system("pause>nul");
				break;
			case 'R':
			case 'r':
                system("cls");    // 重新开始游戏前,清屏
				main();
			}
		}
	}
}

void Draw_Kong(int base, int space_z, int x, int y)    //覆盖前一个blocks的方块,取而代之画出空格
{
	int i, j;
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 4; j++)
		{
			gotoxy(2 * (x + j), y + i);
			if (blocks[base][space_z].space[i][j] == 1)
				printf("  ");
		}
	}
}

void Draw_Blocks(int base, int space_z, int x, int y)    //画出当前blocks的方块
{
	int i, j;
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 4; j++)
		{
			gotoxy(2 * (x + j), y + i);
			if (blocks[base][space_z].space[i][j] == 1)
				printf("■");
		}
	}
}

int Bottom(int base, int space_z, int x, int y)    //WALL与BOX称为底部,判断是否触碰到底部,触碰到底部返回1,未触碰到底部返回0
{
	int i, j;
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 4; j++)
		{
			if (blocks[base][space_z].space[i][j] == 0)
				continue;
			else if (face.data[y + i][x + j] == WALL || face.data[y + i][x + j] == BOX)
				return 1;
		}
	}
	return 0;
}

int Eliminate()    //一行堆积满后清除该行,并记录分值,并询问玩家是否继续
{
#pragma warning(disable:4996)    //对 Visual Studio 2019 进行的警告消除,IDE不是Visual Studio可以删去
	int i, j, sum, m, n;
	for (i = FACE_Y - 2; i > 4; i--)
	{
		sum = 0;
		for (j = 1; j < FACE_X - 1; j++)
		{
			sum += face.data[i][j];
		}
		if (sum == 0)
			break;
		if (sum == FACE_X - 2)
		{
			grade += 100;
			color(7);
			gotoxy(2 * FACE_X + 4, FACE_Y - 4);
			printf("分数:%d", grade);
			for (j = 1; j < FACE_X - 1; j++)
			{
				face.data[i][j] = KONG;
				gotoxy(2 * j, i);
				printf("  ");
			}
			for (m = i; m > 1; m--)
			{
				sum = 0;
				for (n = 1; n < FACE_X - 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] == KONG)
					{
						gotoxy(2 * n, m);
						printf("  ");
					}
					else
					{
						gotoxy(2 * n, m);
						color(face.color[m][n]);
						printf("■");
					}
				}
				if (sum == 0)
					return 1;
			}
		}
	}
	for (j = 1; j < FACE_X - 1; j++)
	{
		if (face.data[1][j] == BOX)
		{
			Sleep(1000);	//延时,给玩家反应时间
			system("cls");
			color(7);
			gotoxy(2 * (FACE_X / 3), FACE_Y / 2 - 2);
			if (grade > max)
			{
				printf("恭喜您打破记录,目前最高记录为:%d", grade);
				Write_File();
			}
			else if (grade == max)
				printf("与记录持平,请突破你的极限!");
			else
				printf("请继续努力,你与最高纪录之差:%d", max - grade);
			gotoxy(2 * (FACE_X / 3), FACE_Y / 2);
			printf("GAME OVER!");
			char ch;
			while (1)
			{
				gotoxy(2 * (FACE_X / 3), FACE_Y / 2 + 2);
				printf("请问是否继续游戏?(y/n): ");
				scanf("%c", &ch);
				if (ch == 'Y' || ch == 'y')
                {
                    system("cls");    // 重新开始游戏前,清屏
					main();
                }
				else if (ch == 'N' || ch == 'n')
                {
                    exit(0);
                }
				else
				{
					gotoxy(2 * (FACE_X / 3), FACE_Y / 2 + 4);
					printf("输入错误,请重新输入!");
				}
			}
		}
	}
	return 0;
}

void Hide_Cursor()    //隐藏光标
{
	//做一个门把手,打开冰箱门,拿出大象,把大象修改一下,再把大象塞回去
	HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);	//HANDLE句柄指的是一个核心对象在某一个进程中的唯一索引,而不是指针。GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)。
	CONSOLE_CURSOR_INFO cci;	//CONSOLE_CURSOR_INFO包含的控制台光标的信息
	GetConsoleCursorInfo(hOut, &cci);	//检索有关指定的控制台屏幕缓冲区的光标的可见性和大小信息。(hConsoleOutput,控制台屏幕缓冲区的句柄。该句柄必须具有的 GENERIC_READ 的访问权限。; lpConsoleCursorInfo, 指向接收有关该控制台的光标的信息的CONSOLE_CURSOR_INFO结构的指针。)
	cci.bVisible = 0;	//赋值0为隐藏,赋值1为可见
	SetConsoleCursorInfo(hOut, &cci);
}

void color(int c)    //改变输出字符的颜色
{
	switch (c)
	{
	case 0: c = 9; break;
	case 1:
	case 2: c = 12; break;
	case 3:
	case 4: c = 14; break;
	case 5: c = 10; break;
	case 6: c = 13; break;
	default: c = 7; break;
	}
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c);	//SetConsoleTextAttribute是一个API(应用程序编程接口),可以设置控制台窗口字体颜色和背景色的计算机函数
}

void gotoxy(int x,int y)    //坐标跳转
{
	COORD coord;	//COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标。
	coord.X = x;
	coord.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);    //SetConsoleCursorPosition是一个API(应用程序编程接口),可以设置设置控制台窗口中光标的位置。
}

 

  • 99
    点赞
  • 420
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 71
    评论
评论 71
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JellyfishMIX

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

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

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

打赏作者

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

抵扣说明:

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

余额充值