C语言俄罗斯方块小游戏练习
C语言俄罗斯方块小游戏练习
Table of Contents
完整代码已放到github,传送们:俄罗斯方块
继重构贪吃蛇之后,又有了新的骚点子,何不再做个俄罗斯方块来玩?说干就干, 那么依旧是先整理思路,需求分析走起。
1 需求分析
和贪吃蛇一样,都是控制台游戏,那么前两点就不再提,直接进入正题,游戏逻辑。
不过在写逻辑之前,还是要先把游戏规则搞明白的。什么?这谁不会啊,俄罗斯方块谁没玩过。 说的对,但稍安勿躁,下面说的规则,你未必都了解。
俄罗斯方块,英文名叫 Tetris ,游戏中玩家需要操控几种不同的方块,在下落过程中左右移动, 旋转,最终落地,当落地时有任意一行被填满,当即消去这一行,上面的方块也会下落等量的行数。 当方块落地时溢出屏幕顶部,即游戏结束。
游戏中的方块被称为 tetromino ,共 七种 ,分别对应七个英文字母: T O Z S L J I , 每个tetromino都由 四个小方块 组成。
操作方式一般为,左右移动,上旋转方块,下直接落地。
了解了规则之后,就可以定制我们的游戏逻辑啦
- 依旧是初始化
- 下落循环
- 键盘控制,包括左右移动,边界判定,旋转操作,旋转可行性判定,直接落地操作
- 触底判定,消除判定,结束判定
1.1 难点
绝对是旋转了,每个方块都有不同的旋转策略,需要为它们单独定制。
而且每一个tetromino都由不同位置的几个小方块组成,那么它们的位置又是一个问题。
1.2 解决思路
这里我先用一个结构体存储单个小方块的位置信息,也就是(x, y),又用一个结构体来存储tetromino的信息, 每一个tetromino分为一个主块位置和四个偏移向量,主块的位置作为整个方块的中心,每个偏移量代表小方块相对主块的位置。 用循环链表来存储每一个tetromino每一个方向旋转的四个偏移量,因为是循环链表,首尾相接, 所以只要不停的next就可以循环的旋转了,不用做什么检测。
比如方块T,我将主块定义为第一行第二个,主块的偏移量即为(0, 0),它左边的块就是(-1, 0), 下边的就是(0, -1),右边的就是(1, 0),像这样,就完成了T方块的初始位置定义, 然后要定义旋转之后的每一组位置,方法和上面相同
这些我单独写到了一个文件里
/** * tetromino.c * 此文件存放方块的定义以及其初始化函数的定义 */ #include<malloc.h> //单个方块 struct block{ int x,y; }; //偏移向量,用首尾相接的链表来表示 struct offset{ struct block allBlocks[4]; struct offset* next; }; //一组方块 //由一个主块和其他块的偏移量数组组成 //每次变换位置,只修改主块 //每一次旋转,只需让指针指向下一个偏移变量 //记得释放链表 struct tetromino{ struct block main; struct offset* pOffset; //指向一个offset链表 char sign; }; //新建一个偏移量变量 struct offset* newOffset(struct block* blocks){ struct offset* os = (struct offset*)malloc(sizeof(struct offset)); int i; for(i=0; i<4; i++){ os->allBlocks[i] = blocks[i]; } return os; } //新建偏移量链表 struct offset* newOffsetChain(struct block pb[][4], int num){ if(pb == NULL || num <= 0 || num >4) return NULL; struct offset *head, *next, *before; head = before = newOffset(pb[0]); int i; for(i=1; i<num; i++){ next = newOffset(pb[i]); before->next = next; before = next; } before->next = head; return head; } //释放链表 void freeOffsetChain(struct tetromino* t){ struct offset *head, *next; head = next = t->pOffset; do{ free(next); next = next->next; }while(next != head); } //初始化I形状的方块组 void initI(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[2][4] = { { {0, 0}, {-1,0}, {-2, 0}, {-3, 0} }, { {0, 0}, {0, -1}, {0, 1}, {0, 2} } }; t->pOffset = newOffsetChain(blocks, 2); t->sign = 'I'; } //初始化L形状的方块组 void initL(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[4][4] = { { {0, 0}, {-1,0}, {-2, 0}, {0, 1} }, { {0, 0}, {1, 0}, {0, 1}, {0, 2} }, { {0, 0}, {0,-1}, {1, 0}, {2, 0} }, { {0, 0}, {-1, 0}, {0, -1}, {0, -2} } }; t->pOffset = newOffsetChain(blocks, 4); t->sign = 'L'; } //初始化J形状的方块组 void initJ(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 5; //初始化偏移量链表 struct block blocks[4][4] = { { {0, 0}, {-1,0}, {-2, 0}, {0, -1} }, { {0, 0}, {-1, 0}, {0, 1}, {0, 2} }, { {0, 0}, {0,1}, {1, 0}, {2, 0} }, { {0, 0}, {1, 0}, {0, -1}, {0, -2} } }; t->pOffset = newOffsetChain(blocks, 4); t->sign = 'J'; } //初始化O形状的方块组 void initO(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[1][4] = { { {0, 0}, {-1,0}, {-1, 1}, {0, 1} } }; t->pOffset = newOffsetChain(blocks, 1); t->sign = 'O'; } //初始化T形状的方块组 void initT(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[4][4] = { { {0, 0}, {0,1}, {-1, 0}, {0, -1} }, { {0, 0}, {-1, 0}, {0, 1}, {1, 0} }, { {0, 0}, {0,1}, {1, 0}, {0, -1} }, { {0, 0}, {1, 0}, {0, -1}, {-1, 0} } }; t->pOffset = newOffsetChain(blocks, 4); t->sign = 'T'; } //初始化Z形状的方块组 void initZ(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[2][4] = { { {0, 0}, {-1,-1}, {-1, 0}, {0, 1} }, { {0, 0}, {1, 0}, {0, 1}, {-1, 1} } }; t->pOffset = newOffsetChain(blocks, 2); t->sign = 'Z'; } //初始化S形状的方块组 void initS(struct tetromino* t){ //初始化主块的位置 t->main.x = 0; t->main.y = 4; //初始化偏移量链表 struct block blocks[2][4] = { { {0, 0}, {-1,1}, {-1, 0}, {0, -1} }, { {0, 0}, {1, 0}, {0, -1}, {-1, -1} } }; t->pOffset = newOffsetChain(blocks, 2); t->sign = 'S'; }
1.3 一些常用的光标移动函数
这里的函数因为经常用到,并且比较独立,所以直接提出来放到单文件里了,用的的时候include就可以
/** * draw.c * 本文件存放光标相关的函数定义 */ void SetPos(COORD a)// 移动光标(隐) { HANDLE out=GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(out, a); } void SetPos(int i, int j)// 移动光标 { COORD pos={i, j}; SetPos(pos); } void HideCursor()//隐藏光标 { CONSOLE_CURSOR_INFO cursor_info = {1, 0}; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info); } //把第y行,[x1, x2) 之间的坐标填充为 ch void drawRow(int y, int x1, int x2, char ch) { int i; SetPos(x1,y); for(i = 0; i <= (x2-x1); i++) printf("%c",ch); } //在a, b 纵坐标相同的前提下,把坐标 [a, b] 之间填充为 ch void drawRow(COORD a, COORD b, char ch) { if(a.Y == b.Y) drawRow(a.Y, a.X, b.X, ch); else { SetPos(0, 25); printf("error code 01:无法填充行,因为两个坐标的纵坐标(x)不相等"); system("pause"); } } //把第x列,[y1, y2] 之间的坐标填充为 ch void drawCol(int x, int y1, int y2, char ch) { int y=y1; while(y!=y2+1) { SetPos(x, y); printf("%c",ch); y++; } } //在a, b 横坐标相同的前提下,把坐标 [a, b] 之间填充为 ch void drawCol(COORD a, COORD b, char ch) { if(a.X == b.X) drawCol(a.X, a.Y, b.Y, ch); else { SetPos(0, 25); printf("error code 02:无法填充列,因为两个坐标的横坐标(y)不相等"); system("pause"); } } //左上角坐标、右下角坐标、用row填充行、用col填充列 void drawFrame(COORD a, COORD b, char ch) { drawRow(a.Y, a.X, b.X, ch); drawRow(b.Y, a.X, b.X, ch); drawCol(a.X, a.Y+1, b.Y-1, ch); drawCol(b.X, a.Y+1, b.Y-1, ch); } void drawFrame(int x1, int y1, int x2, int y2, char ch) { COORD a={x1, y1}; COORD b={x2, y2}; drawFrame(a, b, ch); }
1.4 主函数
主函数非常简单:
int main(){ HideCursor(); initScene(); _getch(); while(!isOver) falling(); _getch(); return 0; }
做了这么几件事:
- 隐藏光标
- 初始化场景并等待玩家按下任意键开始游戏
- 开始下落循环,并判定是否游戏结束
- 游戏结束,任意键退出
其中第二步初始化场景,打印了场景上的包括墙壁,帮助信息,下一次出现的方块,分数等
第三步的下落循环做的事就比较多了,还是自己看代码注释吧,已经很详细了。
1.5 主文件代码
/** * tetris.c * 此文件为主程序文件 */ #include <windows.h> #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <time.h> #include "tetromino.c" #include "draw.c" //定义地图行列数 #define ROW 20 #define COL 10 //定义地图type enum mapType{ NONE, BLOCK }; //定义俄罗斯方块的类型 enum tetrominoType{ TI, TL, TJ, TO, TT, TZ, TS }; //全局变量 enum mapType map[ROW][COL]; //地图 int score = 0; //分数 int isOver = 0; //是否游戏结束 int speed = 3; //速度,1-3 bool isHolding = false; //是否处于按住键盘的状态 struct tetromino nowTetromino, nextTetromino; //当前方块和下一次要出现的方块 //函数 void initMap(); void initInfo(); void initScene(); void genTetromino(struct tetromino* t); struct block getBlock(struct tetromino* t, int num); void flushTetromino(struct tetromino* before, struct tetromino* after); void falling(); bool checkOver(); bool checkClear(int row); int checkMove(struct tetromino* after, bool isFall); void clear(); void keepOnSence(struct tetromino* t); void flushInfo(); void showScoreInfo(); void showNextInfo(struct tetromino* after); int main(){ HideCursor(); initScene(); _getch(); while(!isOver) falling(); _getch(); return 0; } //初始化地图,全置为空 void initMap(){ int i,j; for(i=0; i<ROW; i++) for(j=0; j<COL; j++) map[i][j] = NONE; } //初始化提示信息和地图边界 void initInfo(){ drawCol(30, 0, 20, '|'); drawRow(20, 0, 30, '='); SetPos(35,13); printf("Use a, d to move, space to rotate"); SetPos(35, 14); printf("s to move down, and space to control."); SetPos(35,17); printf("Any key to start!"); } //初始化变量和场景 void initScene(){ initMap(); initInfo(); //初始化即将出现的方块 genTetromino(&nowTetromino); flushTetromino(NULL, &nowTetromino); //初始化下一次出现的方块 genTetromino(&nextTetromino); flushInfo(); score = 0; isOver = 0; } //生成一块俄罗斯方块 void genTetromino(struct tetromino* t){ //为了保证随机,需要设置随机种子 int type; srand(time(0) + type); //种子是用当前时间加上一个随机内存中的数字 type = rand()%7; //结果范围是[0, 7) //根据随机值来选择讲方块初始化成什么样子 switch(type){ case TI: initI(t); break; case TL: initL(t); break; case TJ: initJ(t); break; case TO: initO(t); break; case TT: initT(t); break; case TZ: initZ(t); break; case TS: initS(t); break; } } //获得真实坐标 //参数num为此组方块的第num位 struct block getBlock(struct tetromino* t, int num){ struct block b = { t->main.x + t->pOffset->allBlocks[num].x, t->main.y + t->pOffset->allBlocks[num].y, }; return b; } //刷新俄罗斯方块 //参数before:方块移动之前的位置,为了将之抹除,为空则跳过此步 //参数after:方块移动之后的位置 void flushTetromino(struct tetromino* before, struct tetromino* after){ int i; if(before != NULL){ for(i=0; i<4; i++){ struct block beforeBlock = getBlock(before, i); if(beforeBlock.x < 0) continue; //如果此块在顶部之上,则不管他 SetPos(beforeBlock.y * 3, beforeBlock.x); //横向每个方块占3个字符,所以绘制的时候横坐标×3 puts(" "); } } for(i=0; i<4; i++){ struct block afterBlock = getBlock(after, i); if(afterBlock.x < 0) continue; SetPos(afterBlock.y * 3, afterBlock.x); printf("[%c]", after->sign); //按照方块定义的样子输出 } } //下落过程 void falling(){ int curSpeed = 40 - speed*10; struct tetromino nextStep = nowTetromino; char ch; //在每次下落的间隔时间里用一个循环来读取用户输入 while(curSpeed--){ //增加hold标志,防止持续读取输入 if(!_kbhit()) isHolding = false; else if(!isHolding){ ch = _getch(); isHolding = true; switch(ch){ case 'a': case 'A': //左移 { nextStep.main.y--; break; } case 'd': case 'D': //右移 { nextStep.main.y++; break; } case 's': case 'S': //快速下落 { do{ nextStep.main.x++; } while(checkMove(&nextStep, true) == 0); nextStep.main.x--; break; } case ' ': //旋转【此时就可以享受链表的便利了】 { nextStep.pOffset = nextStep.pOffset->next; break; } } //即时的反馈 if(checkMove(&nextStep, false) == 0){ flushTetromino(&nowTetromino, &nextStep); nowTetromino = nextStep; } else{ //在失败的时候记得复位 nextStep = nowTetromino; } } //去掉多余的输入【hold的时候会bug,于是加了这个】 while(_kbhit()) ch = _getch(); Sleep(20); } //开始下落 nextStep.main.x++; //没问题,可以下落 if(checkMove(&nextStep, true) == 0){ flushTetromino(&nowTetromino, &nextStep); nowTetromino = nextStep; } //返回2,碰到了地图底部,到底了 else{ //固定到场景上去 keepOnSence(&nowTetromino); //判定是否可以消除 clear(); //判定是否还能继续游戏 if(checkOver()){ isOver = 1; freeOffsetChain(&nowTetromino); freeOffsetChain(&nextTetromino); return; } //能的话就做收尾工作 freeOffsetChain(&nowTetromino); nowTetromino = nextTetromino; genTetromino(&nextTetromino); flushTetromino(NULL, &nowTetromino); flushInfo(); } } //检查目标位置是否可以移动 //返回0代表可以移动 //返回1代表被边框或者固定住的方块阻挡 //返回2代表下落失败 int checkMove(struct tetromino* nextStep, bool isFall){ int i; struct block b; for(i=0; i<4; i++){ b=getBlock(nextStep, i); if(b.x < 0) continue; //顶部以上略过 if(b.x >= ROW) return 2; //超过地图底部边界【仅下落】 if(b.y < 0 || b.y >= COL) return 1; //超过左右边界 if(map[b.x][b.y] != NONE){ return (isFall ? 2 : 1); //此位置上有之前落下的方块了 } } return 0; } //将方块固定到场景上面 void keepOnSence(struct tetromino* t){ int i; struct block b; for(i=0; i<4; i++){ b = getBlock(t, i); if(b.x < 0) continue; map[b.x][b.y] = BLOCK; SetPos(b.y * 3, b.x); puts("[*]"); } } //检查某一行能否消除 bool checkClear(int row){ int i; for(i=0; i<COL; i++){ if(map[row][i] == NONE) return false; } return true; } void clear(){ int i,j,k; for(i=ROW-1; i>=0; ){ if(checkClear(i)){ //加分 score++; //清空这一行 for(j=0; j<COL; j++){ map[i][j] = NONE; } //让上面的落下来 for(j=i-1; j>=0; j--){ for(k=0; k<COL; k++){ if(map[j][k] != NONE){ map[j+1][k] = BLOCK; map[j][k] = NONE; } } } } else i--; } //重绘场景 for(i=0; i<ROW; i++){ for(j=0; j<COL; j++){ SetPos(j*3, i); puts((map[i][j] == NONE)?" ":"[*]"); } } } //判断是否已经落到顶层 bool checkOver(){ int i; for(i=0; i<COL; i++){ if(map[0][i] == BLOCK){ return true; } } return false; } //刷新右边靠上的那些提示信息 //先全部用空格抹除,再输出 void flushInfo(){ int i; for(i=3; i<11; i++){ SetPos(35, i); printf(" "); } showNextInfo(&nextTetromino); showScoreInfo(); } //输出分数信息 void showScoreInfo(){ SetPos(35, 10); printf("Now score: %d", score); } //输出下一步出现的方块信息 void showNextInfo(struct tetromino* after){ SetPos(35, 3); printf("Next will be :"); struct block b1,b2 = {8, 13}; b1 = after->main; after->main = b2; flushTetromino(NULL, after); //没错,还是用的这个函数 after->main = b1; }