C语言-Linux系统下的俄罗斯方块实现

目录

[一. 前置知识](#一. 前置知识)

[1.1 VT100操作](#1.1 VT100操作)

[1.1.1 什么是VT100](#1.1.1 什么是VT100)

[1.1.2 常用的VT100操作](#1.1.2 常用的VT100操作)

[1.1.3 示例](#1.1.3 示例)

[1.2 终端屏幕相关操作](#1.2 终端屏幕相关操作)

[1.2.1 终端屏幕的坐标](#1.2.1 终端屏幕的坐标)

[1.2.2 终端屏幕控制指令](#1.2.2 终端屏幕控制指令)

[二. 游戏思路及其模块代码](#二. 游戏思路及其模块代码)

[2.1 游戏边框及俄罗斯方块](#2.1 游戏边框及俄罗斯方块)

[2.1.1 游戏边框](#2.1.1 游戏边框)

[2.1.2 俄罗斯方块](#2.1.2 俄罗斯方块)

[2.1.3 将俄罗斯方块数组与边框数组两者结合](#2.1.3 将俄罗斯方块数组与边框数组两者结合)

[2.2 碰撞检测](#2.2 碰撞检测)

[2.3 判断消行与消行](#2.3 判断消行与消行)

[2.4 主函数逻辑](#2.4 主函数逻辑)

宏定义与全局变量:

定时下落动画:

主函数总逻辑:

[三. 在游戏实现中遇到的问题](#三. 在游戏实现中遇到的问题)


一. 前置知识

1.1 VT100操作

1.1.1 什么是VT100

VT100表示终端类型的型号

终端类型: Linux 系统中,终端类型通常指的是终端模拟器或终端控制程序所支持的特定功

能和行为的集合。不同的终端类型支持不同的功能集,这些功能包括光标控制、

文本格式化、屏幕滚动等。

VT100是 DEC 公司生产的终端系列的第一个型号,广泛用于早期的 Unix 系

统。支持基本的光标移动、文本加亮和屏幕滚动。其他型号:VT102,VT200

1.1.2 常用的VT100操作

|------------------|------------------|
| 控制码 | 作用 |
| \033[0m | 关闭所有属性 |
| \033[1m | 设置为高亮 |
| \033[4m | 下划线 |
| \033[5m | 闪烁 |
| \033[7m | 反显 |
| \033[8m | 消隐 |
| \033[nA | 光标上移n行 |
| \033[nB | 光标下移n行 |
| \033[nC | 光标右移n行 |
| \033[nD | 光标左移n行 |
| \033[y; xH | 设置光标位置 y表示行,x表示列 |
| \033[2J | 清屏 |
| \033[K | 清除从光标到行尾的内容 |
| \033[s | 保存光标位置 |
| \033[u | 恢复光标位置 |
| \033[?25I | 隐藏光标 |
| \033[?25h | 显示光标 |

\033[30m - \033[37m 为设置前景色

\033[40m - \033[47m 为设置背景色

|-----------|--------|
| 30 40 | 黑死 |
| 31 41 | 红色 |
| 32 42 | 绿色 |
| 33 43 | 黄色 |
| 34 44 | 蓝色 |
| 35 45 | 紫色 |
| 36 46 | 青色 |
| 37 47 | 白色 |

1.1.3 示例

cpp 复制代码
printf("\033[31;41m"); //设置前景色,背景色都为红色
printf("%c", '#'); //输出字符
printf("\033[0m"); //格式关闭
printf("\033[1;33;5mHello World\033[0m\n"); //1代表高亮,5代表闪烁,33代表前景色为黄色

1.2 终端屏幕相关操作

1.2.1 终端屏幕的坐标

Linux终端的起始位置在左上角 ,坐标从1开始,为**(1, 1)**。

Linux终端x轴与y轴的比例为 2:1 即两个x轴大约等于一个y轴

1.2.2 终端屏幕控制指令

|----------------------------|----------------------------------------------------------------|
| system("stty -echo") | 关闭输入回显模式 **作用:**关闭键盘输入回显,字符等不会输出在屏幕上 |
| system("stty -icanno") | 关闭输入规范模式 **作用:**关闭标准行缓冲模式,一般输入字符时要按回车才可以输出到屏幕上,但关闭输入规范模式后则不需要。 |
| system("stty echo") | 恢复输入回显模式 |
| system("stty icanno") | 恢复输入规范模式 |

二. 游戏思路及其模块代码

2.1 游戏边框及俄罗斯方块

2.1.1 游戏边框

游戏的整体显示部分 采用一个二维字符数组frame 来维护,初始里面全为空格 ,初始化时将里面的边框部分赋值为字符 '*', 为后面的打印显示的逻辑做准备**。**

在打印显示时,只要将前景色与背景色都设置为红色即可显示出游戏边框。

初始化二维字符数组frame 代码**:**

cpp 复制代码
#define WIDTH 72 //整体外边框的宽
#define HEIGHT 26 //整体外边框的高

#define WIDTH_OFFSET 40//整体偏右,这个宏定义是为了将打印出的游戏边框显示偏右,尽量在终端中心
                       //为了美观

char frame[HEIGHT][WIDTH];//框图数组

//初始化地图边框
void init_frame()
{
    int size = sizeof(frame);//框图数组的大小
    memset(frame, ' ', size);
    int i = 0;
    //整体外边框
    for(i = 0; i < WIDTH; i++)//初始化上下边框
    {
        frame[0][i] = '*';
        frame[HEIGHT-1][i] = '*';
    }
        
    for(i = 0; i < HEIGHT; i++)//初始化左右边框,注意终端x轴与y轴的比例关系
    {
        frame[i][0] = '*';
        frame[i][WIDTH-1] = '*';
        frame[i][1] = '*';
        frame[i][WIDTH-2] = '*';
    }
    for(i = 0; i < HEIGHT; i++)//整体边框的2/3部分做分割,右边1/3部分用于显示其他信息
    {
        frame[i][WIDTH*2/3] = '*';
        frame[i][WIDTH*2/3+1] = '*';
    }
    for(i = WIDTH*2/3; i < WIDTH; i++)//右边1/3部分又分为上下两部分,显示其他信息
    {
        frame[HEIGHT/2][i] = '*';
    }
}

画图显示部分代码:

在打印显示时,将前景色与背景色都设置为红色即可显示出游戏边框。

cpp 复制代码
//画图
void draw()
{
        printf("\033[2J"); // 清屏

        int i = 0, j = 0;
        for(i = 0; i < HEIGHT; i++)
        {
            for(j = 0; j < WIDTH; j++)
            {
                if(frame[i][j] == '*')
                {
                    printf("\033[%d;%dH",i+1,j+1+WIDTH_OFFSET);//终端坐标是从1开始的
                    printf("\033[31;41m");//红色为边框
                    printf(" ");//这里打印什么符号已经无所谓了,因为会被前景后景覆盖
                    printf("\033[0m");
                }
                else
                {
                    printf(" ");//其他打印空格即可
                }
            }
        }
        printf("\n");//换行
}

测试结果:

2.1.2 俄罗斯方块

普通的俄罗斯方块种类有七种,经过变换后总共有十九种。

这里使用一个4行8列的二维数组来维护俄罗斯方块,这样就可以表示所有种类的俄罗斯方块。

同样,为什么要4行8列,还是因为终端中x与y轴的比例是 2:1 ,4行8列可以使显示出来的图形更美观。

相关的代码与注意事项如下:

1、俄罗斯结构体中的两个int整型变量cur与next表示的是当前俄罗斯方块的编号和变化后下一

个俄罗斯方块的编号,方便后续的用户按键变换方块这一逻辑的实现。

2、与游戏边框相同,初始化俄罗斯方块时在shape这个二维数组相应的方块占位初始化

为'#',与边框初始化的字符一定要不同,方便后续的移动,变形等逻辑的实现,但是数组

其余初始化为空格即可。

cpp 复制代码
#define SHAPE_WIDTH 8 //俄罗斯方块所占的宽
#define SHAPE_HEIGHT 4 //俄罗斯方块所占的高

//俄罗斯方块结构体
typedef struct tetris_shape{
    int cur;//俄罗斯方块的编号 cur表示当前俄罗斯方块
    int next;//next表示变换后下一个俄罗斯方块
    char shape[SHAPE_HEIGHT][SHAPE_WIDTH];//维护俄罗斯方块的二维数组
}tetris_shape;

eg:

I shape 的两种变换在数组中占位情况如下:

T shape 的四种变换在数组中占位情况如下:

其余情况同理。

相应的代码如下:

这里的tetris_shape *ps 是一个传出参数,参数no是一个随机数,作用是随机生成一个俄罗

斯方块然后传出。

cpp 复制代码
//初始化方块形状
void init_shape(tetris_shape *ps, int no)
{
    int i = 0, j = 0;
    if(no == 0)//横
    {
        ps->cur = 0;
        ps->next = 1;
        for(i = 0; i < 8; i++)
        {
            ps->shape[0][i] = '#';
        }
    }
    else if(no == 1)//竖
    {
        ps->cur = 1;
        ps->next = 0;
        for(i = 0; i < 4; i++)
        {
            ps->shape[i][0] = '#';
            ps->shape[i][1] = '#';
        }
    }
    else if(no == 2)//正Z
    {
        ps->cur = 2;
        ps->next = 3;
        for(i = 0; i < 4; i++)
        {
            ps->shape[1][i] = '#';
        }
        for(i = 2; i < 6; i++)
        {
            ps->shape[2][i] = '#';
        }
    }
    else if(no == 3)//正Z的90度
    {
        ps->cur = 3;
        ps->next = 2;
        for(i = 0; i < 2; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        for(i = 1; i < 3; i++)
        {
            ps->shape[i][0] = '#';
            ps->shape[i][1] = '#';
        }
    }
    else if(no == 4)//反Z
    {
        ps->cur = 4;
        ps->next = 5;
        for(i = 2; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
        for(i = 0; i < 4; i++)
        {
            ps->shape[2][i] = '#';
        }
    }
    else if(no == 5)//反Z的90度
    {
        ps->cur = 5;
        ps->next = 4;
        for(i = 0; i < 2; i++)
        {
            ps->shape[i][0] = '#';
            ps->shape[i][1] = '#';
        }
        for(i = 1; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
    }
    else if(no == 6)//L
    {
        ps->cur = 6;
        ps->next = 7;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        ps->shape[2][4] = '#';
        ps->shape[2][5] = '#';
    }
    else if(no == 7)//L的90度
    {
        ps->cur = 7;
        ps->next = 8;
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
        ps->shape[2][0] = '#';
        ps->shape[2][1] = '#';
    }
    else if(no == 8)//L的180度
    {
        ps->cur = 8;
        ps->next = 9;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        ps->shape[0][0] = '#';
        ps->shape[0][1] = '#';
    }
    else if(no == 9)//L的270度
    {
        ps->cur = 9;
        ps->next = 6;
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
        ps->shape[0][4] = '#';
        ps->shape[0][5] = '#';
    }
    else if(no == 10)//反L
    {
        ps->cur = 10;
        ps->next = 11;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        ps->shape[2][0] = '#';
        ps->shape[2][1] = '#';
    }
    else if(no == 11)//反L的90度
    {
        ps->cur = 11;
        ps->next = 12;
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
        ps->shape[0][0] = '#';
        ps->shape[0][1] = '#';
    }
    else if(no == 12)//反L的180度
    {
        ps->cur = 12;
        ps->next = 13;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        ps->shape[0][4] = '#';
        ps->shape[0][5] = '#';
    }
    else if(no == 13)//反L的270度
    {
        ps->cur = 13;
        ps->next = 10;
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
        ps->shape[2][4] = '#';
        ps->shape[2][5] = '#';
    }
    else if(no == 14)//凸
    {
        ps->cur = 14;
        ps->next = 15;
        for(i = 2; i < 4; i++)
        {
            ps->shape[0][i] = '#';
        }
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
    }
    else if(no == 15)//凸的90度
    {
        ps->cur = 15;
        ps->next = 16;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        for(i = 4; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
    }
    else if(no == 16)//凸的180度
    {
        ps->cur = 16;
        ps->next = 17;
        for(i = 2; i < 4; i++)
        {
            ps->shape[2][i] = '#';
        }
        for(i = 0; i < 6; i++)
        {
            ps->shape[1][i] = '#';
        }
    }
    else if(no == 17)//凸的270度
    {
        ps->cur = 17;
        ps->next = 14;
        for(i = 0; i < 3; i++)
        {
            ps->shape[i][2] = '#';
            ps->shape[i][3] = '#';
        }
        for(i = 0; i < 2; i++)
        {
            ps->shape[1][i] = '#';
        }
    }
    else if(no == 18)//田
    {
        ps->cur = 18;
        ps->next = 18;
        for(i = 0; i < 4; i++)
        {
            ps->shape[0][i] = '#';
            ps->shape[1][i] = '#';
        }
    }
}

2.1.3 将俄罗斯方块数组与边框数组两者结合

将二者初始化完成之后就要将这两个数组相结合,将俄罗斯方块数组填充进游戏边框数组中,同时在draw()函数中增加对数组内容为'#'的判断打印,即可将边框与俄罗斯方块一起显示出来,接下来详细解释。

首先定义两个宏:

这两个宏是为了保证俄罗斯方块一开始出现的位置是在游戏区域的正上方的中心。

cpp 复制代码
#define START_WIDTH ((WIDTH/3) - (SHAPE_WIDTH/2)) //俄罗斯方块开始出现的宽坐标
#define START_HEIGHT 1  //俄罗斯方块开始出现的高

两个全局变量:

保存俄罗斯方块在移动时的位置坐标

cpp 复制代码
int cur_y = START_HEIGHT; //cur_y为俄罗斯在移动时的行(高)坐标,初始化为俄罗斯方块出现的位置
int cur_x = START_WIDTH;  //cur_x为俄罗斯在移动时的列(宽)坐标,初始化为俄罗斯方块出现的位置

结合函数:

第一个参数代表要结合哪个俄罗斯方块(整个游戏同时有两个俄罗斯方块,一个当前,一个下一个将要出现的),参数y,x为俄罗斯方块当前行,列坐标。

cpp 复制代码
//将边框与俄罗斯快结合
void combine(tetris_shape *ps, int y, int x)
{
    int i = 0, j = 0;
    for(i = 0; i < SHAPE_HEIGHT; i++)
    {
        for(j = 0; j < SHAPE_WIDTH; j++)
        {
            if(ps->shape[i][j] == '#')
            {
                frame[i+y][j+x] = '#';
            }
        }
    }
}

对draw()函数做改进:

将俄罗斯方块与游戏提醒一起显示在屏幕

cpp 复制代码
//画图
void draw(char (*frame)[WIDTH])
{
        printf("\033[2J"); // 清屏

        int i = 0, j = 0;
        for(i = 0; i < HEIGHT; i++)
        {
            for(j = 0; j < WIDTH; j++)
            {
                if(frame[i][j] == '*')
                {
                    printf("\033[%d;%dH",i+1,j+1+WIDTH_OFFSET);
                    printf("\033[31;41m");
                    printf(" ");
                    printf("\033[0m");
                }
                else if(frame[i][j] == '#')
                {
                    printf("\033[%d;%dH",i+1,j+1+WIDTH_OFFSET);
                    printf("\033[32;42m");
                    printf(" ");
                    printf("\033[0m");
                }
                else
                {
                    printf(" ");
                }
            }
        }
        printf("\n");
        //显示文字
        printf("\033[%d;%dH",HEIGHT / 2 + 3, WIDTH_OFFSET + ((WIDTH*5)/6)-9);
        printf("\033[1;33;5m紧张刺激的俄罗斯方块\033[0m\n");
        printf("\033[%d;%dH",HEIGHT / 2 + 4, WIDTH_OFFSET + ((WIDTH*5)/6)-4);
        printf("\033[1;33mscore = %d\033[0m\n", score);//score是一个全局变量,记录所得分数
        printf("\033[%d;%dH",HEIGHT / 2 + 6, WIDTH_OFFSET + ((WIDTH*5)/6)-4);
        printf("\033[1;33mA/a: 左移 \033[0m\n");
        printf("\033[%d;%dH",HEIGHT / 2 + 7, WIDTH_OFFSET + ((WIDTH*5)/6)-4);
        printf("\033[1;33mD/d: 右移\033[0m\n");
        printf("\033[%d;%dH",HEIGHT / 2 + 8, WIDTH_OFFSET + ((WIDTH*5)/6)-4);
        printf("\033[1;33mS/s: 下移\033[0m\n");
        printf("\033[%d;%dH",HEIGHT / 2 + 10, WIDTH_OFFSET + ((WIDTH*5)/6)-4);
        printf("\033[1;33mP/p: 暂停\033[0m\n");
}

测试结果:

另外,由于在游戏进行过程中要进行俄罗斯方块的变换、移动与切换等等,这些逻辑的实现都是先将原有的俄罗斯方块从frame数组中移除,然后将新的俄罗斯方块与frame数组结合,最后显示,故这里需要一个从frame数组中将俄罗斯方块数组移除的函数,如下:

参数与combine()函数的参数相同:

cpp 复制代码
//移动前消除原有俄罗斯
void clear_block(tetris_shape *ps, int y, int x)
{
    int i = 0, j = 0;
    for(i = 0; i < SHAPE_HEIGHT; i++)
    {
        for(j = 0; j < SHAPE_WIDTH; j++)
        {
            if(ps->shape[i][j] == '#' && frame[i+y][j+x] == '#')
            {
                frame[i+y][j+x] = ' ';
            }
        }
    }
}

2.2 碰撞检测

在游戏进行的过程中俄罗斯方块下落时什么停下,左右移动时什么时候停下,以及能不能变形等都是十分重要的,实现这些都需要一个碰撞检测函数,来判断俄罗斯方块能否移动或者变形。

碰撞检测函数:

cpp 复制代码
int can_move(tetris_shape *ps, int y, int x)
{
    int i = 0, j = 0;
    for(i = 0; i < SHAPE_HEIGHT; i++)
    {
        for(j = 0; j < SHAPE_WIDTH; j++)
        {
            if(ps->shape[i][j] == '#' && frame[i+y][j+x] != ' ')
            {
                return 0;
            }
        }
    }
    return 1;
}

2.3 判断消行与消行

在每一个俄罗斯方块落到底部时,都要进行消行判断,而且一次可能消除多行,这些都是要进行判断的。

cpp 复制代码
//检测消行与消行
int check_and_clear_line()
{
    int i = 0, j = 0;
    int flag = 1; //消行标志位
    int clear_num = 0;//消行总数
    for(i = HEIGHT - 2; i > 0; i--)
    {
        flag = 1;//错误点1:这里一定要重置标志位,不然会存在消行失败的情况
        for(j = 2; j < WIDTH * 2 / 3; j++)
        {
            if(frame[i][j] == ' ')
            {
                flag = 0;
                break;
            }
        }
        if(flag)
        {
            clear_num++;
            int k = 0;
            for(k = i; k > 0; k--)
            {
                for(j = 2; j < WIDTH *2 / 3; j++)//将消除的上一行移动到下一行
                {
                    frame[k][j] = frame[k -1][j];
                }
            }
            for(j = 2; j < WIDTH *2 / 3; j++)//将游戏区域的第一行变换为空格
            {
                frame[1][j] = ' ';
            }
            i++;//重复判断此行还能不能被消除
        }
    }
    return clear_num;//返回消除的行数,用于计分
}

2.4 主函数逻辑

以上为游戏中所需的所有函数,现在介绍游戏的主函数逻辑。

先说明宏定义与全局变量

宏定义与全局变量:

cpp 复制代码
#define WIDTH_OFFSET 40//整体偏右
#define WIDTH 72 //整体外边框的宽
#define HEIGHT 26 //整体外边框的高
#define WIDTH_GAME (WIDTH*2/3) //游戏部分边框的宽
#define SHAPE_WIDTH 8 //俄罗斯方块所占的宽
#define SHAPE_HEIGHT 4 //俄罗斯方块所占的高
#define START_WIDTH ((WIDTH/3) - (SHAPE_WIDTH/2))
#define START_HEIGHT 1
#define GAME_START_RUNNING 1 //表示游戏运行状态
#define GAME_START_PAUSE 0  //表示游戏暂停状态

int score = 0;
char frame[HEIGHT][WIDTH];//框图数组
tetris_shape cur_block, next_block, temp_block;//当前与下一个方块
int cur_y = START_HEIGHT; //cur_y为俄罗斯在移动时的行(高)坐标,初始化为俄罗斯方块出现的位置
int cur_x = START_WIDTH;  //cur_x为俄罗斯在移动时的列(宽)坐标,初始化为俄罗斯方块出现的位置
int next_y = HEIGHT/4 - SHAPE_HEIGHT/2 + 1;  //next_y为下一个俄罗斯方块显示的高坐标位置,固定显示
int next_x = ((WIDTH*5)/6)-SHAPE_WIDTH/2; //next_x为下一个俄罗斯方块显示的宽坐标位置,固定显示

定时下落动画:

关于定时下落动画,在主函数中使用了定时器与注册信号捕捉函数(SIGAKEM)来实现,每当定时当时的时候就去信号处理函数进行相应的俄罗斯方块下落、判断消行等逻辑。

信号处理函数的内容如下:

cpp 复制代码
void handle(int num)
{
    clear_block(&cur_block,  cur_y, cur_x);//清除当前方块,准备下落
    if( ( can_move(&cur_block,  cur_y + 1, cur_x) ) )//碰撞检测
    {
        combine(&cur_block, cur_y + 1, cur_x);
        cur_y += 1;
    }
    else
    {
        combine(&cur_block, cur_y, cur_x);

        int clear_num = check_and_clear_line();//不能下落就要进行消行判断
        score += (clear_num * 100); 

        cur_block = next_block;
        clear_block(&next_block, next_y, next_x);//先将下一个俄罗斯方块从数组里面清除
        memset(&next_block,' ',sizeof(next_block));//清空下一个俄罗斯方块的数组,防止与新生成的重叠
        init_shape(&next_block, rand()%19);//随机生成下一个俄罗斯方块

        combine(&next_block, next_y, next_x);//将随机生成的俄罗斯方块与二维数组结合
        //这里的判断是判断还能不能放的下下一个俄罗斯方块
        if( can_move(&cur_block, START_HEIGHT, START_WIDTH) )
        {
            //如果可以放下,则放到初始位置
            combine(&cur_block,  START_HEIGHT, START_WIDTH);
            cur_y = START_HEIGHT;//初始化移动的行坐标 
            cur_x = START_WIDTH;//初始化移动的列坐标
        }
        else
        {
            printf("\033[%d;%dH",HEIGHT / 2,WIDTH / 2 + WIDTH_GAME / 2);
            printf("\033[1;33mGAME OVER!\033[0m\n");
            system("stty echo");
            system("stty icanon");
            exit(0);
        }

    }
    draw(frame);//画出整体
}

主函数总逻辑:

主函数总逻辑:

代码如下:

cpp 复制代码
int	main(int argc, char **argv)
{
    system("stty -echo");
    system("stty -icanon");

    signal(SIGALRM,handle);//注册信号捕捉函数

    //定时400ms
    struct itimerval new_value;
    new_value.it_value.tv_usec = 400000;
    new_value.it_value.tv_sec = 0;
    new_value.it_interval.tv_usec = 400000;
    new_value.it_interval.tv_sec = 0;
    setitimer(ITIMER_REAL, &new_value, NULL);

    srand(time(NULL));//随机数种子
    int game_status = GAME_START_RUNNING;//游戏开始
    memset(&cur_block,' ',sizeof(temp_block));
    memset(&next_block,' ',sizeof(temp_block));
    memset(&temp_block,' ',sizeof(temp_block));

    int size = sizeof(frame);//框图数组的大小
    init_frame(frame, size);//初始化边框
    init_shape(&cur_block, rand()%19);
    init_shape(&next_block, rand()%19);

    combine(&cur_block, cur_y, cur_x);
    combine(&next_block, next_y, next_x);

    draw(frame);//画出整体
 
    while(1)
    {
        char ch = getchar();
        if(ch == 'A' || ch == 'a')
        {
            clear_block(&cur_block, cur_y, cur_x);
            if( ( can_move(&cur_block, cur_y, cur_x - 2) ) )
            {
                combine(&cur_block, cur_y, cur_x - 2);
                cur_x -= 2;
            }
            else
            {
                combine(&cur_block, cur_y, cur_x);
            }
            draw(frame);//画边框
        }
        else if(ch == 'D' || ch == 'd')
        {
            clear_block(&cur_block, cur_y, cur_x);
            if( ( can_move(&cur_block, cur_y, cur_x + 2) ) )
            {
                combine(&cur_block, cur_y, cur_x + 2);
                cur_x += 2;
            }
            else
            {
                combine(&cur_block, cur_y, cur_x);
            }
            draw(frame);//画边框
        }
        else if(ch == 'S' || ch == 's')
        {
            clear_block(&cur_block, cur_y, cur_x);
            if( ( can_move(&cur_block, cur_y + 1, cur_x) ) )
            {
                combine(&cur_block, cur_y + 1, cur_x);
                cur_y += 1;
            }
            else
            {
                combine(&cur_block, cur_y, cur_x);
                int clear_num = check_and_clear_line(frame);//下落就要进行消行判断
                score += (clear_num * 100); 
            }
            draw(frame);//画边框
        }
        else if(ch == 'W' || ch == 'w')
        {
            clear_block(&cur_block, cur_y, cur_x);
            memset(&temp_block,' ',sizeof(temp_block));//这一步必须要有,不然旧的会覆盖新的图形

            init_shape(&temp_block, cur_block.next);
            if( ( can_move(&temp_block, cur_y, cur_x) ) )//这里还有进行碰撞检测
            {
                combine(&temp_block, cur_y, cur_x);
                cur_block = temp_block;//必须将临时赋值给当前的俄罗斯块
            }
            else//如果变不了形就不变
            {
                combine(&cur_block, cur_y, cur_x);
            }
            draw(frame);//画边框
        }
        else if(ch == 'p' || ch == 'P')
        {
            if(game_status ==  GAME_START_RUNNING)
            {
                printf("\033[%d;%dH",HEIGHT / 2 + 1,WIDTH / 3 + WIDTH_OFFSET);
                printf("\033[1;5;33m已暂停\033[0m\n");
                game_status = GAME_START_PAUSE;
                //alarm(0);
                struct itimerval stop = { 0 };
                setitimer(ITIMER_REAL, &stop, NULL);
                
            }
            else if(game_status == GAME_START_PAUSE)
            {
                setitimer(ITIMER_REAL, &new_value, NULL);
                game_status =  GAME_START_RUNNING;
            }
        }
        else if(ch == 'e' || ch == 'E')
            break;
    }
    printf("\033[%d;%dH",HEIGHT / 2,WIDTH / 2 + WIDTH_GAME / 2);
    printf("\033[1;33m退出游戏!\033[0m\n");
    system("stty echo");
    system("stty icanon");

    return 0;
}

三. 在游戏实现中遇到的问题

1、 在初始化边框数组与俄罗斯方块数组时没有使用memset将其全部设置为空格,在显示时除了

边框就会在游戏区域也显示红色的小块,原因是没一设置内存时内存中为随机值。

2、 在判断W键变换俄罗斯方块时一定要对temp这个俄罗斯方块清空,不然上一次的内容会影响这

一次的内容,使显示的俄罗斯方块出错,如下图显示

3、 在判断消行时一定要对同一行进行多次判断,有可能会一次进行多次消行,否则逻辑不对。

相关推荐
IT大白鼠9 小时前
Linux用户配置文件详解:.bash_history、.bash_logout、.bash_profile与.bashrc
linux·运维·bash
枕星而眠9 小时前
Linux 进程:虚拟内存、Fork原理、IPC通信与面试避坑
linux·运维·c语言·后端
yeflx9 小时前
Ubuntu常用指令
linux·运维·ubuntu
秦渝兴9 小时前
Ubuntu 电脑进不去桌面?从 TTY 到图形界面的完整排障指南
linux·运维·ubuntu
2401_827560209 小时前
【电脑和手机系统】解锁bl后刷LineageOS与Magisk各模块的安装(七)
android·linux·智能手机
Mortalbreeze9 小时前
进程间通信 ---- 基于管道来实现
linux·服务器
kebidaixu9 小时前
BCU项目CMake 构建管理
linux
Yunzenn9 小时前
深度解析字节前沿研究-Cola DLM第 04 章:Cola DLM 架构全景 —— 三层解耦的设计哲学
java·linux·python·深度学习·面试·github·transformer
皆圥忈9 小时前
Linux 进程从入门到实战(三)
linux