大一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 27struct 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));时,影响的将是所有的randnn = 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("■");}elseface.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形-1for(i=1;i<4;i++)blocks[1][0].space[i][1] = 1;blocks[1][0].space[1][2] = 1;//L形-2for(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形-1blocks[3][0].space[1][i] = 1;blocks[3][0].space[2][i + 1] = 1;//Z形状-2blocks[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的basenn = rand() % 7;   //随机生成下一块blocks的basecolor(nn);Draw_Blocks(nn, space_z, FACE_X + 3, 4);while (1){color(n);  //把光标颜色调回当前blocks的baseDraw_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("与记录持平,请突破你的极限!");elseprintf("请继续努力,你与最高纪录之差:%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(应用程序编程接口),可以设置设置控制台窗口中光标的位置。
}

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

  1. 俄罗斯方块-C语言-详注版

    代码地址如下: http://www.demodashi.com/demo/14818.html 俄罗斯方块-C语言-详注版 概述 本文详述了C语言版俄罗斯方块游戏的原理以及实现方法,对游戏代码进行了 ...

  2. c语言代码 txt下载,俄罗斯方块C语言源代码txt.DOC

    俄罗斯方块C语言源代码txt /*名称: 俄罗斯方块 源程 For Turbo C 2.0软件编写:中国 北京 幻想空间软件创作群Illusion Space Software Design Grou ...

  3. c语言语言教程0基础_C语言基础

    c语言语言教程0基础 Hey, Folks here I am back with my second article on C language. Hope you are through with ...

  4. 国内的知名产品及其开发语言v0.0.1

    首先要声明的是:这个列表既不权威,也不全面,所有信息仅供参考,本人也不对由此导致的任何后果负任何责任. 建立这个列表的初衷来自三个方面: 一个是很多人在加入程序员这个行业前,需要对编程语言有一个大致的 ...

  5. c语言中一个整型数组结束用 0表示吗,C语言程序设计0数组.ppt

    C语言程序设计0数组 第七章 数 组 数组的定义:是有序数据的集合. 数组的特点:数组中的每个元素都属于同一数据类型. 数组的访问:我们用一个统一的数组名和下标来唯一地确定数组中的元素. 7.1 一维 ...

  6. 有c语言基础学python容易吗_学习Python需要先学C语言吗?0基础学Python难度大吗?

    今天小编要跟大家分享的文章是关于学习Python需要先学C语言吗?0基础学Python难度大吗?想要学习或者了解Python相关知识就来和小编一起看一看本篇文章吧! 1.学Python需要先学C语言吗 ...

  7. Go语言从0到1实现最简单的数据库!

    导语 | 后台开发对于数据库操作是必不可少的事情,了解数据库原理对于平常的工作的内功积累还是很有帮助的,这里实现一个最简单的数据库加深自己对数据库的理解. 一.go实现数据库目的 了解数据是如何在内存 ...

  8. C语言 学生成绩管理系统 带登录界面

    C语言 学生成绩管理系统 带登录界面 C语言课程设计 思路 部分展示 代码片段 C语言课程设计 先上要求: 思路 为了方便简单,直接利用结构体数组来存储学生学生,最后根据功能编写函数即可. 部分展示 ...

  9. android+主界面所有应用程序图标添加统一背景主题,Android 4.0替Launcher主界面所有应用程序图标添加统一背景主题...

    当前位置:我的异常网» Android » Android 4.0替Launcher主界面所有应用程序图标添加 Android 4.0替Launcher主界面所有应用程序图标添加统一背景主题 www. ...

最新文章

  1. OpenCV+python:色彩空间转换及色彩通道的分离和合并
  2. linux系统UDP的socket通信编程
  3. Spring boot配置文件两种方式
  4. java基础方法笔记
  5. 第6章 访问权限控制
  6. rtmp代理php源码_RTMP直播系统(示例代码)
  7. 使用433MHz RF模块制作一艘简易的Arduino遥控小船
  8. App创业者分享:如何攒到你的1亿用户?(前期土豪推广,后期节操全碎)
  9. ubuntu下alphapose 需要的配置小记
  10. VMware16创建虚拟机:无法创建新虚拟机,不具备执行此操作的权限
  11. 线性方程组的直接解法
  12. 若干物联网无线技术 - NB-IOT、LoRa、433、GPRS、2.4G、PKE近场通信,基础理论与开发点滴总结
  13. Linux命令之ll
  14. Linux 逻辑卷管理器(LVM)
  15. 定义函数:判断一个数是否为素数,并调用
  16. python中的ideavim有什么作用_【进击的Vimmer】为什么选择vim
  17. Java 数据转换/进制转换 工具类
  18. 不限文章大小!英文文章的编码和解码(C语言,哈夫曼编码)
  19. 邮箱 名字 地址url正则表达式
  20. vim E35: No previous regular expression

热门文章

  1. java散点世界地图_干货|可视化设计:地图四部曲之地图散点
  2. 动规(15)-最低通行费
  3. 三相发电机短路计算和画图-Matlab
  4. 中职教材计算机英语,浅析中职计算机英语校本教材的编写
  5. How to get ‘kernel config‘ when CONFIG_IKCONFIG is not set ? (Method)
  6. php 房间匹配,房间和可用性日期PHP / MySQL
  7. 投资理财启蒙之理财入门必看?
  8. 新工科的新视角:面向可持续竞争力的敏捷教学体系
  9. 谭铁牛院士:向生物学习 开启模式识别新突破
  10. 使用串口过程中遇到的问题总结