一、游戏效果
二. 游戏背景
俄罗斯方块是久负盛名的游戏,它也和贪吃蛇,扫雷等游戏位列经典游戏的⾏列。
《俄罗斯方块》(Tetris,俄文:Тетрис)是一款由俄罗斯人阿列克谢·帕基特诺夫于1984年6月发明的休闲游戏。
该游戏曾经被多家公司代理过。经过多轮诉讼后,该游戏的代理权最终被任天堂获得。 任天堂对于俄罗斯方块来说意义重大,因为将它与GB搭配在一起后,获得了巨大的成功。
《俄罗斯方块》的基本规则是移动、旋转和摆放游戏自动输出的各种方块,使之排列成完整的一行或多行并且消除得分。
三、游戏开发日志
基本逻辑结构与贪吃蛇一致
四、游戏实现
我们有了贪吃蛇的知识储备及了解WIN32 API,理解下面内容也就不难了。
4.1 游戏逻辑主体
#include"tetris.h" int main() { //修改当前地区为本地模式,为了⽀持中⽂宽字符的打印 setlocale(LC_ALL, ""); srand((unsigned int)time(NULL)); char ch=0; do { GameInit(); GameRun(); GameOver(); system("cls"); SetPos(20, 12); printf("再来一局吗?(Y/N):\n"); SetPos(22, 15); ch = getchar(); getchar();//清理\n } while (ch == 'y' || ch == 'Y'); SetPos(0, 29); return 0; }
4.2 游戏初始化的实现
4.2.1 设置窗口大小、名称及隐藏光标
//设置控制台窗⼝的⼤⼩,30⾏,30列 system("mode con cols=60 lines=30"); //设置cmd窗⼝名称 system("title 俄罗斯方块"); //获取标准输出的句柄(⽤来标识不同设备的数值) HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
4.2.2 打印欢迎界面
void WelcomeToGame() { SetPos(20, 10); printf("欢迎来到俄罗斯方块!"); SetPos(20, 23);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); SetPos(14, 10); printf("↑为变换.↓为加速.←为左移.→为右移\n"); SetPos(20, 23);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); }
4.2.3 初始化地图
void CreateMap() { memset(IF.pos, 0, sizeof(int)*30*60);//对数组初始化(如果不初始化,FAIL后第二次循环会IF.pos会保留上次数据) int i = 0; //墙体轮廓 SetPos(40, 0); for (i = 40; i
注解:
1)一个宽字符占2个字节,我们设置 cols=60 lines=30,实际就是长宽各30个字节大小
2)如上图所示,游戏展示区长为20,高为30,右上部分为下一个方块提示区域,正下面为游戏操作提示区
3)横轴为x轴,向右坐标逐渐增大,向下为y轴,坐标逐渐增大。(x,y)确定宽字符(小方块)的位置,因为宽字符(小方块)占2个字节宽度,所以x轴坐标没有奇数值
4)为了方便检测下落方块是否靠墙、落地及碰到其它方块,我们定义一个大数组pos[60][30],一一对应地图上每个宽字符(小方块)的位置,用大数组pos[60][30]记录墙体位置及落地的方块位置
typedef struct InterFace { int pos[60][30]; //用于标记整个界面有方块的位置(1为有,0为无) }InterFace; InterFace IF;4.2.4 初始化方块信息
void InitBlockInfo() { //'T'字型 int i = 0; for (i = 0; i
注解:
1)我们定义结构体二位数组,用于存储7种基本形状方块的各自的4种形态的信息,共28种。
#define COLS 4 #define LINES 7 enum BlockState{CHANGE,LEFT,RIGHT,DOWN,NORMAL}; typedef struct Block { int space[4][4]; int speed; enum BlockState blockstate; }Block; Block block[LINES][COLS] ;//结构体二位数组,用于存储7种基本形状方块的各自的4种形态的信息,共28种2)首先初始化7种基本形状方块,然后基本形状方块通过顺时针旋转依次得到各自剩下的三种形态。
4.3 游戏运行的实现
void GameRun() { int i = rand() % 7, j = rand() % 4; //随机获取方块的形状 do { int x = 18; int y = 0; int next_i = rand() % 7, next_j = rand() % 4; PrintBlock(next_i, next_j, 46, 4);//绘制提示框图形 PrintBlock(i, j, 18, 0);//绘制方块初始位置 while(IsLegal(i, j, x, y ))//不合法,说明到底了 { if (KEY_PRESS(VK_UP)) { block[i][j].blockstate = CHANGE; } else if (KEY_PRESS(VK_DOWN)) { block[i][j].blockstate = DOWN; } else if (KEY_PRESS(VK_LEFT)) { block[i][j].blockstate = LEFT;//左移 } else if (KEY_PRESS(VK_RIGHT)) { block[i][j].blockstate = RIGHT;//右移 } else if (KEY_PRESS(VK_SPACE)) { pause();//暂停 } else if (KEY_PRESS(VK_ESCAPE)) { state = END;//退出 break; } else if (KEY_PRESS(R)) { state = RESTART;//重新开始 break; } else//什么都没按 { block[i][j].blockstate = NORMAL; } BlockMove(block, &i, &j, &x,&y); PrintBlock(i, j, x, y);//绘制方块位置 Sleep(block[i][j].speed);//每一帧休息的时间 } i = next_i; j = next_j; PrintSpace(i,j, 46, 4);//覆盖提示框图形 } while (state ==RUN); }4.3.1 合法判断(碰撞检测)
//碰撞检测 int IsLegal(int i, int j, int x, int y) { for (int m = 0; m4.3.2 覆盖方块
void PrintSpace(int i, int j, int x, int y)//覆盖指定位置方块 { for (int m = 0; m4.3.3 绘制方块
void PrintBlock(int i, int j, int x, int y) { int m = 0; int n = 0; for (m = 0; m4.3.4 判断满行消除
int JudeIsErase() { int sign = 0; int i = 0; for (i = 28; i > 3; i--) { int count = 0; for (int j = 2; j 3; m--) { for (int n = 2; n注:
1)用大数组pos[60][30],从下往上判断是否满行,如果满行,则将将该行以上全部下移一格,这样就达到消行的目的
2)若第十四和第十三行都满行了,当检测到第十四行满行,将第十三行及以上全部下移一格,再次循环,从现十三行(原十二行)开始检测,此时会漏掉现十四行(原十三行),所以引进标识变量sign,只要有消行,则变为1,当sign=0时,表示全部方块没有满行了。
4.4.5 判断游戏失败
void JudeIsOver() { for (int i = 2; i4.4.6 方块移动
void BlockMove(Block block[][COLS], int *i, int *j, int *x, int *y) { switch (block[*i][*j].blockstate) { case CHANGE: if (IsLegal(*i, ((*j) + 1) % 4, *x, (*y )+1) == 1) //判断方块变化后(变化的同时下降一格)是否碰撞 { //方块旋转后合法才进行以下操作 PrintSpace(*i, *j, *x, *y); //用空格覆盖当前方块所在位置 (*j) = ((*j) + 1) % 4; (*y)++; //PrintBlock(i, j, x, y);//绘制下一个变化的方块 block[*i][*j].blockstate = NORMAL; block[*i][*j].speed = 200; } //else //方块再下落就碰撞了(已经到达底部) //{ // //将当前方块的信息录入IF当中 // for (int m = 0; m50)//避免连续按,变成负数,会出现bug { block[*i][*j].speed -= 50; } } else //碰撞 { //将当前方块的信息录入IF当中 for (int m = 0; m注:
1)这里我们根据方块状态判断方块的下一步位置和形状,需要改变参数,所以传的是地址
2)只有在正常下落和加速下落才需要描述不合法时(碰撞时)状况,因为此时已经到底了。变换方块时不需要描述不合法(碰撞时)状况(因为'L'型,在变换时容易与边界发生碰撞,若此时将方块位置记录在pos[60][30]的大数组中,导致跳出该循环,进行下一个方块判断,导致该方块悬浮在半空的BUG)
4.4 游戏结束的实现
enum GameState{ RUN = 1,END,FAIL,RESTART }; enum GameState state; void GameOver() { switch (state) { case END: SetPos(14, 15); printf("您主动退出游戏\n"); SetPos(14, 18); system("pause"); break; case FAIL: Sleep(1000); //留给玩家反应时间 system("cls"); //清空屏幕 SetPos(20, 10); printf("小垃圾,游戏结束!\n"); SetPos(20, 14); printf("你的最终得分为: %d ", grade); SetPos(20, 18); system("pause"); break; case RESTART: system("cls"); //清空屏幕 SetPos(26, 10); printf("重新开始!\n"); SetPos(22, 14); printf("小垃圾,你准备好了吗?\n"); Sleep(2000); main(); break; } }注:
1)对游戏的三个状态分别进行描述。
五、完整代码
5.1 tetris.h
#include #include #include #include #include #include #include #define WALL L'□' #define BIOCK L'■' #define COLS 4 #define LINES 7 #define R 0x52 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0) enum GameState{ RUN = 1,END,FAIL,RESTART }; enum GameState state; enum BlockState{CHANGE,LEFT,RIGHT,DOWN,NORMAL}; int grade; typedef struct InterFace { int pos[60][30]; //用于标记整个界面有方块的位置(1为有,0为无) }InterFace; InterFace IF; typedef struct Block { int space[4][4]; int speed; enum BlockState blockstate; }Block; Block block[LINES][COLS] ;//结构体二位数组,用于存储7种基本形状方块的各自的4种形态的信息,共28种 void GameInit(); void CreateMap(); void InitBlockInfo(); void GameRun(); void PrintBlock(int i,int j,int x,int y);//ij 为方块形状,x,y为打印坐标 void BlockMove(Block block[][COLS], int *i, int *j, int *x, int *y); void PrintSpace(int i, int j, int x, int y); int JudeIsErase(); void JudeIsOver(); void GameOver();5.2 tetris.c
#include"tetris.h" //设置光标的坐标 void SetPos(short x, short y) { COORD pos = { x, y }; HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos); } void WelcomeToGame() { SetPos(20, 10); printf("欢迎来到俄罗斯方块!"); SetPos(20, 23);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); SetPos(14, 10); printf("↑为变换.↓为加速.←为左移.→为右移\n"); SetPos(20, 23);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); } void CreateMap() { memset(IF.pos, 0, sizeof(int)*30*60);//对数组初始化(如果不初始化,FAIL后第二次循环会IF.pos会保留上次数据) int i = 0; //墙体轮廓 SetPos(40, 0); for (i = 40; i 3; i--) { int count = 0; for (int j = 2; j 3; m--) { for (int n = 2; n 50)//避免连续按,变成负数,会出现bug { block[*i][*j].speed -= 50; } } else //碰撞 { //将当前方块的信息录入IF当中 for (int m = 0; m5.3 test.c
#include"tetris.h" int main() { //修改当前地区为本地模式,为了⽀持中⽂宽字符的打印 setlocale(LC_ALL, ""); srand((unsigned int)time(NULL)); char ch=0; do { GameInit(); GameRun(); GameOver(); system("cls"); SetPos(20, 12); printf("再来一局吗?(Y/N):\n"); SetPos(22, 15); ch = getchar(); getchar();//清理\n } while (ch == 'y' || ch == 'Y'); SetPos(0, 29); return 0; }