俄罗斯方块小游戏

工程声明

本文包括方块加速,方块消行,方块自动下落,方块碰撞,分数和等级等功能,以此记录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+2*(4-shape[line][column][16])-1

 说明:

24为初始图形x坐标点。

4 - shape[line][column][16]测试图形的宽度。

2*(4 - shape[line][column][16])代表数据一个点本质是[],占用两个坐标点。

-1 24这个坐标点本质是计算过了,向后数的值应该-1

计算最下侧图标的y坐标

6+(4-shape[line][column][17])-1

 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系统的英文手册阅读,对以后的开发会更加得心应手。

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值