Linux环境下 基于Ncurse图形库的 C语言贪吃蛇游戏

ncurse

ncurse可以提供强大的按键响应,如果使用scanf或者getchar来获取按键输入的话,每次输入完成还要按下回车,速度会很慢,因此必须使用ncurse。同时须知,ncurse正在被抛弃,即使是体验感比ncurse强大很多的C++图形库QT也在慢慢退出历史舞台,被越来越普及,廉价的安卓系统替代。

但是为什么还是要使用ncurse? ncurse虽然是个老东西,但还是比C语言自带的函数来说有一定优势,这次的开发重点是锻炼C语言的编程能力同时更加熟悉Linux系统的各种操作

以下是使用curse的最基本框架:(不要忘记头文件

同时,在编译带有curse的c文件时,要表达为: gcc xxx.c -lcurses

同时,为了使得curse能够识别上下左右键,必须在main中加上一句:keypad(stdscr,1); 并规定一个int或更长字节的变量类型来承载getch(),因为在curses.h源文件中,上下左右被识别为4位的整型,所以如果使用只有1个字节长度的char来承接,就会显示错误。

经过以上修改,此时输入上下左右之后出现的是固定的整型:

为了使得显示结果更为好理解,给代码添加switch case来识别:

#include <curses.h>


int main()
{
	initscr();
	int key;
	keypad(stdscr,1);
	
	while(1){
		key = getch();
		switch(key){
			case KEY_DOWN:
				printw("Down\n");
				break;
			case KEY_UP:
                printw("Up\n");
				break;
			case KEY_LEFT:
                printw("Left\n");
				break;
			case KEY_RIGHT:
                 printw("Right\n");
				break;
		}
	}

	endwin();
	return 0;
}

此时的结果比之前的就要好多了: 

 

贪吃蛇游戏的地图规划

地图大小设置:20x20

地图竖直方向上的边界:“ |

地图水平方向上的边界:“ --

贪吃蛇的身体“ [ ] "

贪吃蛇的食物” ##

贪吃蛇身体的实现

对于蛇身体的设置,定义一个链表

同时,在之前地图的绘制函数中,当绘制”  ”前,添加一个判断条件,扫描查看该点应该打印蛇的身体还是继续打印空白

同时,需要写一个函数实现链表的动态添加蛇的节点。

蛇的头从视觉上对应链表尾,蛇的尾巴从视觉上对应链表头;因此蛇的移动,就是根据上下左右键的输入值来判断新节点应该放在链表尾的上下左右,同时,将链表头删去,使新链表头成为原来链表第二个元素。

蛇吃食物了之后身体变长,只需要在蛇每一次移动前,判断蛇的头(链表尾)是否与食物重叠,如果重叠,则蛇继续前进,但是将不会删除链表头(即不会减少蛇尾的长度,相当于吃了食物多了一节身体)。

蛇在吃了很多食物之后,除了判断蛇头是否触碰边界死亡之外,每次蛇移动前还要判断蛇头(链表尾)有没有碰到身体,方法是从链表头(蛇尾)开始向后遍历查看是否有链表中非链表尾的元素的位置和链表尾相同。这两种死亡方式都会触发游戏的重新开始。

蛇可以上下左右移动,但是不可以一直左右或者上下移动,所以宏定义define蛇的上下为绝对值相同的相反数,左右同理,在接受键盘信号后,先做一步判断,是否和当前的方向绝对值相同,若相同则无视命令。

食物的实现

和蛇的身体的打印类似,在之前地图的绘制函数中,当绘制”  ”前,再添加一个判断条件,扫描查看该点应该打印 食物 还是 蛇的身体 还是 继续打印空白。

食物的出现位置需要随机,想要获得 [0,x] 范围内的随机整数,一个办法是使用 rand() 函数和取余 % 符号,在游戏中,希望得到 [0,20] 区间里的整数,食物的坐标都可以表示为 rand()%20

多线程操作

在实际的代码运行中,会出现这样一种情况:需要一个函数不断刷新界面,也需要另一个函数不断接收键盘的上下左右信号,而这两个函数都有while(1),所以正常情况下 任何一个函数调用在前面,后一个函数就无法运行。解决办法是使用Linux的线程概念,将两个函数定义为两个线程,这样就可以满足几乎同时的不断运行。对于线程需要注意以下几点:

添加线程库:#include <pthread.h>

main函数中定义线程:pthread_t th1;pthread_t th2;

main函数中创建线程:pthread_create(&th1,NULL,函数名1,NULL);pthread_create(&th2,NULL,函数名2,NULL);

对于需要成为线程的两个函数,函数类型需要是 void *

在最后编译时,除了要加 -lncurses,还要加 -lpthread

最终代码实现和完整注释:

#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>  //添加相关库

#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2  //宏定义上下左右,上下绝对值相同,左右绝对值相同


struct Snake
{
        int hang;
        int lie;
        struct Snake *next;  //蛇的结构体链表
};

struct Snake *head = NULL;
struct Snake *tail = NULL; 
int dir;

struct Snake food;   //定义全局变量的“蛇头” “蛇尾”指针,“方向”和“蛇的食物”,因为这些变量需要在多个函数中调用

void initFood() //初始化食物,使得食物随机生成在地图中
{
	int x =rand()%20;
	int y =rand()%20;
	food.hang = x;
	food.lie = y;   
}

void addNode() //添加蛇身体节点的函数
{
    struct Snake *new = (struct Snake *)malloc(sizeof(struct Snake)); //对于一个新的指针变量,如果没有一开始就给他赋予值,则应该先为他开辟空间,防止成为野指针发生段错误

	new->next = NULL; //由于是链表,结构体中还含有指针成员,所以为了结构体中的这个next指针不成为野指针,也将他先置NULL
	switch(dir){ //根据收到后得出的上下左右来判断加的蛇身体节点位于蛇头(链表尾)的哪里
		case UP:
			new -> hang = tail -> hang - 1;
 		    new -> lie = tail -> lie;
			break;
		case DOWN:
            new -> hang = tail -> hang + 1;
            new -> lie = tail -> lie;
            break;
		case LEFT:
            new -> hang = tail -> hang;
            new -> lie = tail -> lie - 1;
            break;
		case RIGHT:
            new -> hang = tail -> hang;
            new -> lie = tail -> lie + 1;
            break;
	}

        tail->next = new; //将新节点的行列值设置完之后,使得原来的tail指向new
        tail = new; //再让new成为新的tail
}

void deleNode() //删除蛇身体节点的函数
{
	struct Snake *p;
	p = head;
	head = head->next; //删除节点其实只需要这一句就够了,但是为了防止内存被不断占用,需要将原来的头所对应的地址空间给free掉
	free(p);
}

void initSnake() //初始化蛇的函数
{
	struct Snake *p;

	dir = RIGHT; //初始化方向为右

	while(head!=NULL){ //遍历是否有不使用的指针(蛇身节点),如果有则free掉,防止野指针
		p = head;
		head = head->next;
		free(p);
	}

	initFood(); //初始化食物

	head = (struct Snake *)malloc(sizeof(struct Snake)); //对于一个新的指针变量,如果没有一开始就给他赋予值,则应该先为他开辟空间,防止成为野指针发送段错误
	head -> hang = 2;
	head -> lie = 2;
	head -> next = NULL; //设置初始化时蛇的位置并将对应的next指针置NULL,防止野指针

	tail = head; //此时蛇只有一节,因此将tail等于head,此时tail必须赋值,因为之后的函数会对tail进行调用并修改它,如果tail是个空指针会报错
	addNode(); 
	addNode();
	addNode(); //连续加三个节点,由于初始化方向是RIGHT,因此,初始化的蛇是水平方向 向右 的4节身体
}


int hasSnakeNode(int i, int j) //根据输入的行列值判断是否是蛇身体节点的函数
{
	struct Snake *p;
	p = head;
	while(p != NULL){
		if(p->hang == i && p->lie == j){
            return 1;
       	}
		p = p->next; //遍历蛇的全身
	}

	return 0;
}

int hasFood(int i, int j) //根据输入的行列值判断是否是食物的函数
{
	if(food.hang == i && food.lie == j){
		return 1;
	}else{
		return 0;
	}
}

int ifSnakeDie() //判断蛇是否死亡的函数
{
	struct Snake *p;
	p = head;

	if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie ==20){ //如果蛇触碰边界,则死亡
		return 1;
	}

	while(p->next != NULL){ //如果蛇头(链表尾)碰到蛇身(链表中任一非尾节点),则死亡
		if(p->hang == tail->hang && p->lie == tail->lie){
			return 1;
		}
		p=p->next;
	}
	return 0;
}

void moveSnake() //控制蛇移动的函数
{	
	addNode(); //先根据之前封装好的逻辑,在链表尾(蛇头)加一个节点(根据键盘输入)
	if(hasFood(tail->hang,tail->lie)){ //判断蛇头(链表尾)有没有吃到食物
		initFood(); //吃到食物就重新初始化食物,并不用删除节点,即蛇增加一节身体
	}else{
		deleNode(); //没吃到就要删除节点,保持原长
	}

	if(ifSnakeDie()){ //移动了之后判断蛇有没有触发任何死亡条件
		initSnake(); //触发了就初始化蛇,没触发就继续
	}
}



void initNcurse() //初始化Ncurse的函数
{
	initscr();
	keypad(stdscr,1);

	noecho(); //ncurse库自带函数,防止乱码,但是这句话不能完全消除乱码现象,这应该是ncurse的通病
}

void gamemap() //绘制游戏图像的函数
{
	int hang;
	int lie;

	move(0,0); //ncurse库自带函数,每次调用该函数的时候,将光标移到最上方,这样不断调用该函数就可以将图像不断覆盖,实现动画的效果

	for(hang = 0; hang < 20; hang++){
		if(hang == 0){ //上边界的绘制
			for(lie = 0; lie<20; lie++){
				printw("--");
			}
			printw("\n");
	        }
		if(hang>=0 && hang <=19){ //游戏中间部分的绘制
			for(lie = 0; lie<=20; lie++){
				if(lie == 0 || lie == 20){ //左右边界的绘制
					printw("|");
				}else if (hasSnakeNode(hang,lie)){ //判断扫描到的该点是否属于蛇的身体
					printw("[]");
				}else if(hasFood(hang,lie)){ //判断扫描到的该点是否属于食物
					printw("##");
				}else{ //如果既不是蛇身也不是食物,就打印空白,作为游戏的基础背景
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang == 19){ //下边界的绘制
			for(lie = 0; lie<20; lie++){
                                printw("--");
                        }
                        printw("\n");
			printw("--by mjm\n"); 
		}

	}
}

void *refreshJiemian() //第一个作为线程的函数,功能是不断刷新界面
{
        while(1){
            moveSnake(); //不断根据输入使蛇进行下一步动作
            gamemap(); //不断覆盖绘制游戏的界面
		    refresh(); //ncurse库自带函数,刷新界面
		    usleep(50000); //控制刷新的时间,变相控制蛇运动的速度
        }
}

void turn(int direction) //决定“向代码提供蛇当前何种方向”的函数
{
	if(abs(dir) != abs(direction)){ //只有接收到的方向的绝对值和当前方向的绝对值不同,才改变当前的方向
		dir = direction;
	}
}

void *changDir()  //第二个作为线程的函数,功能是不断接受键盘的上下左右输入
{
	int key = 0;
        while(1){
                key = getch(); //使用int变量接收键盘输入
                switch(key){
                        case KEY_DOWN: //如果输入下
                                turn(DOWN); //则交给turn函数决定是否向代码提供这个方向
                                break;
                        case KEY_UP: //如果输入上
                                turn(UP); //则交给turn函数决定是否向代码提供这个方向
                                break;
                        case KEY_LEFT: //如果输入左
                                turn(LEFT); //则交给turn函数决定是否向代码提供这个方向
                                break;
                        case KEY_RIGHT: //如果输入右
                                turn(RIGHT); //则交给turn函数决定是否向代码提供这个方向
                                break;
                }
        }
}

int main()  //main函数
{
	pthread_t th1;//定义两个线程
	pthread_t th2;

	initNcurse(); //初始化Ncurse

	initSnake(); //初始化蛇
	gamemap(); //绘制界面

	pthread_create(&th1,NULL,refreshJiemian,NULL); //创建两个同时运行的线程
	pthread_create(&th2,NULL,changDir,NULL);

	while(1); //防止main函数退出
	getch();
	endwin();
	return 0;
}

最终实现效果

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值