工程声明
本文包括方块加速,方块消行,方块自动下落,方块碰撞,分数和等级等功能,以此记录C语言的练习。
本工程使用的软件是VSCode,使用多文件编程,工程结构图如下所示。
游戏效果展示
基于Linux实现俄罗斯方块小游戏
游戏代码详解
VT100控制码
VT100是一个古老的终端定义,目前几乎大部分的终端都兼容这种终端。VT100控制码是用来在终端扩展显示的代码。所有的控制符全部以\033打头(即ESC的ASCII码),用来输出语句输出,可以输出不同颜色的字符。在C语言程序中,一般用printf来输出VT100的控制字符。
//指定坐标输出
printf("\033[%d;%dH",yy,xx);
//输出颜色
printf("\033[%dm[]",c);
//关闭属性
printf("\033[m");
第一行输出为:将光标移动到(xx,yy)的位置,
第二行输出为:(c为43):43m代表背景为黄,形状是[],将[]这个给覆盖成黄色方块,前面的\033是ESC的ASCII值,[是固定的
效果如图所示:
绘制方格
俄罗斯方块的初始状态有下图的7种类型,每种类型最多有4种变化,为了防止图形碰撞,这边把图形的右侧和下侧距离4*4格子的距离也计算进来了。
通过坐标系构成方块,以4*4的方格为最大,方格中的1个点代表我们的小方块。若是使用到该点坐标,其值为1,否则为0。
图形可以通过三位数组类存储,7代表方块有7种基本的变化形状,4代表有4个旋转方向,18种前16个数据代表图形的形状,第17个数据代表距离右侧边界距离,第18个数据代表距离下侧边界的距离。
int shape[7][4][18]
俄罗斯方块储存
int shape[7][4][18] =
{
{
{1,1,0,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 2,2}, //[][]
{1,1,0,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 2,2}, //[][]
{1,1,0,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 2,2}, //
{1,1,0,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 2,2}, //
},
{
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0, 3,0}, //[] [][][][]
{1,1,1,1, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,3}, //[]
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0, 3,0}, //[]
{1,1,1,1, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,3}, //[]
},
{
{0,1,0,0, 1,1,1,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] [] [][][] []
{1,0,0,0, 1,1,0,0, 1,0,0,0, 0,0,0,0, 2,1}, //[][][] [][] [] [][]
{1,1,1,0, 0,1,0,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] []
{0,1,0,0, 1,1,0,0, 0,1,0,0, 0,0,0,0, 2,1}, //
},
{
{1,1,0,0, 0,1,1,0, 0,0,0,0, 0,0,0,0, 1,2}, //[][] [] [][] []
{0,1,0,0, 1,1,0,0, 1,0,0,0, 0,0,0,0, 2,1}, // [][] [][] [][] [][]
{1,1,0,0, 0,1,1,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] []
{0,1,0,0, 1,1,0,0, 1,0,0,0, 0,0,0,0, 2,1}, //
},
{
{0,1,1,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 1,2}, // [][] [] [][] []
{1,0,0,0, 1,1,0,0, 0,1,0,0, 0,0,0,0, 2,1}, //[][] [][] [][] [][]
{0,1,1,0, 1,1,0,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] []
{1,0,0,0, 1,1,0,0, 0,1,0,0, 0,0,0,0, 2,1}, //
},
{
{0,0,1,0, 1,1,1,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] [] [][][] [][]
{1,0,0,0, 1,0,0,0, 1,1,0,0, 0,0,0,0, 2,1}, //[][][] [] [] []
{1,1,1,0, 1,0,0,0, 0,0,0,0, 0,0,0,0, 1,2}, // [][] []
{1,1,0,0, 0,1,0,0, 0,1,0,0, 0,0,0,0, 2,1}, //
},
{
{1,0,0,0, 1,1,1,0, 0,0,0,0, 0,0,0,0, 1,2}, //[] [][] [][][] []
{1,1,0,0, 1,0,0,0, 1,0,0,0, 0,0,0,0, 2,1}, //[][][] [] [] []
{1,1,1,0, 0,0,1,0, 0,0,0,0, 0,0,0,0, 1,2}, // [] [][]
{0,1,0,0, 0,1,0,0, 1,1,0,0, 0,0,0,0, 2,1}, //
},
};
//输出指定位置图形
//n------某个图形
//m------某个方向
//x,y---坐标
//c------颜色
void print_mode_shape(int n,int m,int x,int y,int c)
{
int i = 0;
int xx = x;
int yy = y;
for(i = 0;i < 16;i++){
if(i != 0 && i % 4 == 0){
yy ++;
xx = x;
}
if(shape[n][m][i] == 1){
//指定坐标输出
printf("\033[%d;%dH",yy,xx);
//输出颜色
printf("\033[%dm[]",c);
//关闭属性
printf("\033[m");
}
xx += 2;
}
return ;
}
注意:这里xx一次走两格。效果如下。
按键获取及控制
我们通过输入键盘的↑,↓,←,→发现被识别为如下字符,并且还有光标信息,这里需要剔除掉。
这里实现的思路是1,关闭光标的回显,2,去除不必要的^[[,3,再通过判断最终数据值为A,B,C,D,获取用户输入信息。
1,关闭光标的回显
int getch()
{
struct termios tm,tm_old;
//获得用户输入的属性到tm_old
tcgetattr(0,&tm_old);
//获取原始输入的属性,默认回显关闭
cfmakeraw(&tm);
//把输入的属性设置到终端上
tcsetattr(0,0,&tm);
//不回显的获取字符
int ch = getchar();
//恢复正常输入
tcsetattr(0,0,&tm_old);
return ch;
}
注意:这里getchar()是从键盘上输入一个字符
2,去除不必要的的^[[
int ch;
while(1){
ch = getch();//^[===ESC
if(ch == 'Q' ||ch == 'q'){//退出
break;
}else if(ch == '\r'){//回车键
printf("down\n");
}else if (ch == '\33'){//[
ch = getch();
if(ch == '['){
ch = getchar();//A,B,D,C
3,判断最终数据值,获取用户输入信息
switch(ch){
case 'A'://上
change_shape();
break;
case 'B'://下
move_down(n_num,n_mode);
break;
case 'D'://左
move_left(n_num,n_mode);
break;
case 'C'://右
move_right(n_num,n_mode);
break;
}
按键控制
这里图形的通过随机数种子来获取,
//图形初始化
void init_shape()
{
srandom(time(NULL));
n_num = random() % 7; //选择图形
n_mode = random() % 4; //选择方向
n_color = random() % (7+40); //选择颜色
//在指定位置输出图形
print_mode_shape(n_num,n_mode,n_x,n_y,n_color);
//刷新缓存
fflush(NULL);
return ;
}
清除指定图形和控制
//清除指定图形
void eraser_mode_shape(int n,int m,int x,int y)
{
int i = 0;
int xx = x;
int yy = y;
for(i = 0;i < 16;i++){
if(i != 0 && i % 4 == 0){
yy ++;
xx = x;
}
if(shape[n][m][i] == 1){
//指定坐标输出,将光标移动到(xx,yy)的位置
printf("\033[%d;%dH \033[m",yy,xx);//擦除为两个空格,空白替换
}
//两个横坐标成一个方块
xx += 2;
}
fflush(NULL);//刷新缓存
return ;
}
void change_shape()
{
int m = (n_mode + 1) % 4;
eraser_mode_shape(n_num,n_mode,n_x,n_y);
n_mode = m;
print_mode_shape(n_num,n_mode,n_x,n_y,n_color);
return ;
}
void move_down(int n,int m)
{
eraser_mode_shape(n,m,n_x,n_y);
n_y ++;
print_mode_shape(n,m,n_x,n_y,n_color);
}
void move_left(int n,int m)
{
eraser_mode_shape(n,m,n_x,n_y);
n_x -= 2;
print_mode_shape(n,m,n_x,n_y,n_color);
}
void move_right(int n,int m)
{
eraser_mode_shape(n,m,n_x,n_y);
n_x += 2;
print_mode_shape(n,m,n_x,n_y,n_color);
}
图形界面绘制
通过绘制图形,侧率对应的坐标点,这里坐标都是通过自己测试,得出比较好看的界面
void print_start_ui()
{
//user清屏
printf("\33[2J");
int i;
//输出黄色最顶行、最低行
for (i = 0; i < 47; i++) {
printf("\33[%d;%dH\33[43m \33[0m", 5, i + 10);
printf("\33[%d;%dH\33[43m \33[0m", 30, i + 10);
}
//输出黄色三列
for (i = 0; i < 26; i++) {
printf("\33[%d;%dH\33[43m \33[0m", i + 5, 10);
printf("\33[%d;%dH\33[43m \33[0m", i + 5, 40);
printf("\33[%d;%dH\33[43m \33[0m",
i + 5, 56);
}
//输出用户下一图形分割行
for (i = 0; i < 17; i++) {
printf("\33[%d;%dH\33[43m \33[0m", 12, 40 + i);
}
//输出分数和等级
//18 //45
printf("\33[%d;%dH分数:\33[0m", score_y, score_x);
//22 //45
printf("\33[%d;%dH等级:\33[0m", level_y, level_x);
fflush(NULL);
}
图形储存及输出
void init_game_ui()
{
//输出窗体界面
print_start_ui();
//等待用户输入,然后程序开始运行
get_ch();
//获取随机数
//设置随机数种子
srand(time(NULL));
//random()%(max-min+1)+min;
dynamic_num = random() % 7;
dynamic_mode = random() % 4;
dynamic_color = random() % 7 + 40;
dynamic_x = init_x;
dynamic_y = init_y;
//生成图形
print_mode_shape(dynamic_num, dynamic_mode, dynamic_x, dynamic_y, dynamic_color);
print_next_shape();
printf("\33[?25l");
}
//指定位置输出 图形
//n----7个图案选择某个图案
//m----4个方向中某个方向
// x,y指定的坐标
//c 颜色
void print_mode_shape(int n, int m, int x, int y, int c)
{
int i = 0;
int xx = x;
int yy = y;
for (i = 0; i < 16; i++)
{
//每经过4行,纵坐标+1
if (i != 0 && i % 4 == 0)
{
yy += 1;
xx = x;
}
if (shape[n][m][i] == 1) {
printf("\033[%d;%dH\033[%dm[]\033[0m",yy,xx,c);
}
xx += 2;
}
fflush(NULL);
}
//右侧准备生成下一个位置的图形并输出
void print_next_shape()
{
erase_last_shape(next_num, next_mode, next_x, next_y);
next_num = random() % 7;
next_mode = random() % 4;
next_color = random() % 7 + 40;
print_mode_shape(next_num, next_mode, next_x, next_y, next_color);
fflush(NULL);
}
方块自动下落
这里需要定时器来自动下落,其中tm = 800000us = 0.8s,每隔0.8s发送SIGALRM信号
//微秒定时器,当定时器启动后,每隔一段时间会发送SIGALRM信号
void alarm_us(int n)
{
struct itimerval value;
//设置定时器启动的初始化值n
value.it_value.tv_sec = 0;
value.it_value.tv_usec = n;
//设置定时器启动后的间隔数
value.it_interval.tv_sec = 0;
value.it_interval.tv_usec = n;
setitimer(ITIMER_REAL, &value, NULL);
}
void alarm_handle()
{
move_down(dynamic_num,dynamic_mode);
}
int main()
{
init_game_ui();
signal(SIGALRM,alarm_handle);
alarm_us(tm);
key_control();
return 0;
}
方块触底碰撞及显示
触底碰撞
这里碰撞有三种情况,第一种就是跟底部碰撞,另外两种就是跟左右两侧碰撞,但是因为左侧的可以根据图形左侧来判断,这里主要是右侧和底部的分析。
计算右侧图标的x坐标点
说明:
24为初始图形x坐标点。
4 - shape[line][column][16]测试图形的宽度。
2*(4 - shape[line][column][16])代表数据一个点本质是[],占用两个坐标点。
-1 24这个坐标点本质是计算过了,向后数的值应该-1
计算最下侧图标的y坐标
6为初始图形y坐标点。
(4 - shape[line][column][16])测试图形高度。
-1 代表多计算了一个像素点。
//碰撞检测
int judge_shape(int num, int mode, int x, int y)
{
int m_line = y - 6;
int m_column = x - 12;
int i = 0;
for (; i < 16; i++) {
if (i != 0 && i % 4 == 0) {
m_line++;
m_column = x - 12;
}
if (shape[num][mode][i] == 1) {
if (matrix[m_line][m_column] != 0) {
return 1;
}
}
m_column += 2;
}
return 0;
}
int move_down(int num, int mode)
{
if ((dynamic_y + (4 - shape[num][mode][17]) - 1 >= 29) || judge_shape(num, mode, dynamic_x, dynamic_y + 1))
{
//已经触底或越界,不能再向下移动
store_current_shape();
return 1;
}
//清除现有的图形
erase_last_shape(num, mode, dynamic_x, dynamic_y);
dynamic_y++;
print_mode_shape(num, mode, dynamic_x, dynamic_y, dynamic_color);
return 0;
}
int move_right(int n, int m)
{
//边界检测
if (dynamic_x + 2 * (4 - shape[n][m][16]) - 1 >= 39)
return 1;
//碰撞检测
if (judge_shape(n, m, dynamic_x + 2, dynamic_y))
return 1;
erase_last_shape(n, m, dynamic_x, dynamic_y);
dynamic_x += 2;
print_mode_shape(n, m, dynamic_x, dynamic_y, dynamic_color);
return 0;
}
储存显示思路
中间可供我们储存方块的范围为24行*28列,故定义int matrix[24][28]储存坐标信息。
而数组的行列与(x,y)坐标点的值相反,数组的行line代表坐标点的列,数组的列column代表了坐标点的行,
//显示储存坐标
void print_matrix()
{
int i, j;
for (i = 0; i < 24; i++) {
for (j = 0; j < 28; j += 2) {
if (matrix[i][j] == 0) {
printf("\33[%d;%dH \33[0m", i + 6, j + 12);
}
else {
printf("\33[%d;%dH\33[%dm[]\33[0m", i + 6, j + 12, matrix[i][j]);
}
}
}
return;
}
i+6是指图形上边留出的空间,j+12是指图形左边留出的空间。
游戏结束设置
判断行是否已经用完
//获得储存结果
int get_matrix_result(int n_line)
{
int i = 0;
if (n_line < 0)
{
return 1;
}
for (i = 0; i < 28; i++)
{
if (matrix[n_line][i] != 0)
{
return 1;
}
}
return 0;
}
//判断游戏结束
int judge_end_game()
{
int n_line = 23;
int n_count = 0;
int i = 0;
for (i = 0; i < 24; i++)
{
int no_zero = get_matrix_result(n_line);
if (no_zero != 0)
{
n_line--;
}
else
{
return 0;
}
}
return 1;
}
void game_over()
{
printf("\33[32;9H********** Game Over ********\33[0m");
printf("\33[?25h");
printf("\n\n");
}
void recover_attribute()
{
//恢复正常输入属性
tcsetattr(0, 0, &tm_old);
}
void sig_handler(int signum)
{
//图形向下移动
move_down(dynamic_num, dynamic_mode);
if (judge_end_game() == 1)
{
game_over();
recover_attribute();
exit(0);
}
}
方块消行
//matrix[24][28]
//方块消行
void destory_cond_line()
{
int i, j, k;
int flag = 0;
for (i = 0; i < 24; i++) {
flag = 1; //满行标志,如果为1 满,0表示不满
for (j = 0; j < 28; j++) {
if (matrix[i][j] == 0) {
flag = 0;
break;
}
}
//说明第i行满了
if (flag == 1) {
user_score += 10;
if (user_score % 100 == 0) {
user_level++;//等级加一
tm /= 2;//时间加快
alarm_us(tm);
}
//删除i行,整体下移
for (k = i; k > 0; k--) {
for (j = 0; j < 28; j++) {
matrix[k][j] = matrix[k - 1][j];
}
}
print_matrix();
print_score_level();
}
}
}
总结
通过这次俄罗斯方块的设计实现,不仅仅提升了我的逻辑思维能力,还让我对库函数的使用有了更深的了解,以及Linux系统的英文手册阅读,对以后的开发会更加得心应手。