一、Ncurse图形库

ncurses(new curses)是一个程序库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面。它是一个虚拟终端中的“类GUI”应用软件工具箱。它还优化了屏幕刷新方法,以减少使用远程shell时遇到的延迟。

在ubuntu系统上安装库文件:

apt-get install libncurses5-dev

如何使用ncurse?

#include <curses.h>

int main()
{
       initscr();//ncurse 界面的初始化函数
       printw("This is curses window\n");//在ncurse模式下的printf
       getch();//等待用户输入,如果没有这句话,程序就退出了,看不到运行的结果,也就是看不到上面那句话
       endwin();//程序退出,调用改函数来恢复shell终端的显示,如果没有这句话,shell终端字乱码,坏掉
       return 0;
}

贪吃蛇项目中还会用到的一些ncurses库中的函数(代码如下):

#include<curses.h>  // Linux下图形界面库头文件 
noecho();           // 不回显用户输入的内容
keypad();           // 允许用户终端的键盘,允许getch()函数获取功能键
move(x,y);			//move(int x, int y);移动光标到x,y
refresh();			//更新终端屏幕

为什么要用ncurse?

ncurse是终端下实现简单图形界面的不二选择,按键响应快速。

编译curses文件

gcc xxx.c -lcurses

ncurse的上下左右键

vi /usr/include/curses.h 可查看ncurse的宏定义
在这里插入图片描述

二、重要步骤

1、准备工作

准备一些初始化函数以及用到的全局变量。

创建蛇身节点结构体和结构体变量食物food,并且定义全局变量key,dir保存值:

//定义贪吃蛇节点结构体
struct Snake
{
	int hang;//行
	int lie;//列
	struct Snake *next;//下一个节点
};

struct Snake *head = NULL;//定义蛇尾(链表头) 
struct Snake *tail = NULL;//定义蛇头(链表尾) 
int key;//记录键入的值 changeDirection()函数中使用到
int dir;//记录方向的值 addNode()函数中使用到

struct Snake food;//定义结构体变量食物 

封装一些ncurse界面的初始函数:

//函数封装初始化Ncurse界面
void initNcurse()
{
	initscr();//ncurse界面的初始化函数
	keypad(stdscr,TRUE);//从标准stdscr中接受功能键,TRUE代表是否接收
	noecho();//大多数的交互式应用程序在初始化时会调用noecho()函数,用于在进行控制操作时不显示输入的控制字符。
}

2、打印输出游戏界面(地图、蛇身、食物)

设计游戏界面样式,运用for循环嵌套打印输出游戏地图、蛇身以及食物。

先准备两个函数:
int judgeFood(int i,int j);//判断蛇头是否吃到食物
int judgeSnakeNode(int i,int j);//判断地图上的点是否是蛇身节点
如果传过来的i和j与行和列相等,则通过gamePic()打印输出。

//判断地图上的点是否有食物
int judgeFood(int i,int j)
{
	if(food.hang == i && food.lie == j){
		return 1;
	}
	return 0;
}

//判断地图上的点是否是蛇身节点
int judgeSnakeNode(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;
}

游戏界面设计(20X20):
食物“##”;
蛇身“[]”;
地图第1行就是hang=0;

在这里插入图片描述

gamePic()打印输出游戏界面:

//打印输出游戏界面
void gamePic()
{
	int hang;
	int lie;
	move(0,0);//将光标定位到0行0列
	for(hang=0;hang<=19;hang++){
		if(hang == 0){
			printw("           Gluttonous snake\n");
			for(lie=0;lie<=20;lie++){
				printw("--");
			}
			printw("\n");		
		} 
		if(hang >= 0 || hang <= 19){
			for(lie=0;lie<=21;lie++){
				if(lie == 0 || lie == 21){
					printw("|");
				}else if(judgeSnakeNode(hang,lie)){//判断蛇身节点,打印蛇身
					printw("[]");
				}else if(judgeFood(hang,lie)){//判断食物节点,打印食物
					printw("##");
				}else{
					printw("  ");
				}					
			}
			printw("\n");
		}
		if(hang == 19){
			for(lie=0;lie<=20;lie++){
                                printw("--");
                        }
                        printw("\n");
			printw("By Apibro,food.hang=%d,food.lie=%d\n",food.hang+1,food.lie);//打印作者及当前食物坐标(hang+1)
		}
	}
}

3、蛇的移动及食物生成

首先要初始化蛇身initSnake(),蛇需要方向,下面再阐述,蛇(蛇头就是链表尾tail)的移动通过moveSnake()来实现,移动过程中通过addNode()在蛇头增加一节点(尾插法),与此同时在蛇尾删除一节点deleteNode(),移动过程中如果吃到食物(judgeFood()判断是否吃到食物)则增加一节,增加节点通过addNode()来实现,食物的随机生成通过initFoot()来实现,移动过程中如果撞墙或则蛇身(judgeSnakeDeath()判断蛇是否死亡)则死亡,游戏重新开始。

蛇的移动:

//蛇的移动(蛇头增加新节点,删除蛇尾节点) 
void moveSnake()
{
	addNode();//增加新节点
	if(judgeFood(tail->hang,tail->lie)){
		initFoot();//随机食物生成
	}else{
		deleteNode();//删除蛇尾(头结点)
	}
	
	if(judgeSnakeDeath()){
		initSnake();//死亡,重新初始化蛇身,游戏重新开始
	}
}

其余需要用到的函数:

//随机食物生成
void initFoot()
{
	int x = rand()%20+1;
	int y = rand()%20+1;
	x = x-1;//为了第0行显示成第1行
	food.hang = x;
	food.lie = y;
}

//增加新节点(通过方向判断加在哪)
void addNode()
{
	struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));

	switch(dir){//根据键入的值来增加节点,修改tail所指结点中hang,lie并将修改后的值赋值给新节点 
		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;
	}

	new->next = NULL;
	tail->next = new;
	tail = new;
}

//初始化蛇身
void initSnake()
{
	struct Snake *p;
	
	dir = RIGHT;//蛇头初始方向为RIGHT 

	while(head != NULL){//判断蛇是否为空,清理内存 
		p = head;
		head = head->next;
		free(p);
	}

	initFoot();//随机食物生成

	head = (struct Snake*)malloc(sizeof(struct Snake));
	head->hang = 1;
	head->lie = 2;
	head->next = NULL;
	
	tail = head;//初始时尾指针指向头
	addNode();
	addNode();
	addNode();//增加3节点,设置初始蛇身长度
}

//删除蛇尾(头结点)
void deleteNode()
{
	struct Snake *p;
	p = head;
	head = head->next;
	free(p);//释放p=head原先头节点
}

//判断蛇是否死亡
int judgeSnakeDeath()
{
	struct Snake *p;
	p = head;

	if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 21){//当tail节点中数据到达最大边界return 1
		return 1;
	}

	while(p->next != NULL){
		if(p->hang == tail->hang && p->lie == tail->lie){//当tail节点中数据与蛇身一致时return 1 
			return 1;
		}
		p = p->next;
	}
	return 0;
}

4、游戏界面刷新

游戏界面固定,按回车才会变,所以需要不停打印刷新,通过moveSnack()和gamePic()函数就实现游戏界面的刷新,使贪吃蛇动起来,但此时方向固定为初始方向。

封装刷新界面函数refreshInterface();如下:

//界面刷新
void* refreshInterface()
{	
	while(1){
		moveSnake();//移动
		gamePic();//打印输出
		refresh();//更新终端屏幕
		usleep(100000);//控制蛇的运动速度,以微秒为单位,100毫秒睡眠一次,执行挂起不动
	}
}

5、扫描键入的值判断方向

有了键盘输入,就可以改变蛇的初始方向,dir可以改变,蛇也就有了方向。

宏定义及封装函数如下:

#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2

//通过绝对值判断相反方向不触发
void turn(int direction)
{
	if(abs(dir) != abs(direction)){
		dir = direction;
	}
}

//扫描键入的值判断方向
void* changeDirection()
{
	while(1){
                key = getch();
                switch(key){
                        case KEY_DOWN:
                                turn(DOWN);
                                break;
                        case KEY_UP:
                                turn(UP);
                                break;
                        case KEY_LEFT:
                                turn(LEFT);
                                break;
                        case KEY_RIGHT:
                                turn(RIGHT);
                                break;
                }
	}
}

6、多个while并存(线程解决)

在贪吃蛇运动过程中,我们需要改变蛇的移动方向,这是就需要不停扫描键盘输入的值来判断方向,同时还需要不停的刷新界面,为了多个while循环并存这里需要引入linux线程。

线程的基本用法:
参考网页:Linux多线程相关的函数
在Linux中使用线程

#include <pthread.h>  // 头文件

pthread_t:当前Linux中可理解为:typedef  unsigned long int  pthread_t;
如:pthread_t t1;  //多线程定义

pthread_create(&t1,NULL,refreshInterface,NULL); 
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
参数4:线程主函数执行期间所使用的参数,如要传多个参数, 可以用结构封装。

使用多线程的函数必须返回指针型,如void *refreshInterface()

注:gcc xxx.c -lcurses -lpthead  //编译需要连接pthead库

三、注意事项及完整代码

注意事项:

在ubuntu低版本中运行不容易乱码,在高版本中gcc时,删去原先a.out多编译几次,并等待一段时间,也不会出现乱码。

完整代码:

#include <curses.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.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 key;//记录键入的值 changeDirection()函数中使用到
int dir;//记录方向的值 addNode()函数中使用到

struct Snake food;//定义结构体变量食物 

//函数封装初始化Ncurse界面
void initNcurse()
{
	initscr();//ncurse界面的初始化函数
	keypad(stdscr,TRUE);//从标准stdscr中接受功能键,TRUE代表是否接收
	noecho();//大多数的交互式应用程序在初始化时会调用noecho()函数,用于在进行控制操作时不显示输入的控制字符。
}

//随机食物生成
void initFoot()
{
	int x = rand()%20+1;
	int y = rand()%20+1;
	x = x-1;//为了第0行显示成第1行
	food.hang = x;
	food.lie = y;
}

//判断地图上的点是否有食物
int judgeFood(int i,int j)
{
	if(food.hang == i && food.lie == j){
		return 1;
	}
	return 0;
}

//判断地图上的点是否是蛇身节点
int judgeSnakeNode(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;
}

//打印输出游戏界面
void gamePic()
{
	int hang;
	int lie;
	move(0,0);//将光标定位到0行0列
	for(hang=0;hang<=19;hang++){
		if(hang == 0){
			printw("           Gluttonous snake\n");
			for(lie=0;lie<=20;lie++){
				printw("--");
			}
			printw("\n");		
		} 
		if(hang >= 0 || hang <= 19){
			for(lie=0;lie<=21;lie++){
				if(lie == 0 || lie == 21){
					printw("|");
				}else if(judgeSnakeNode(hang,lie)){//判断蛇身节点,打印蛇身
					printw("[]");
				}else if(judgeFood(hang,lie)){//判断食物节点,打印食物
					printw("##");
				}else{
					printw("  ");
				}					
			}
			printw("\n");
		}
		if(hang == 19){
			for(lie=0;lie<=20;lie++){
                                printw("--");
                        }
                        printw("\n");
			printw("By Apibro,food.hang=%d,food.lie=%d\n",food.hang+1,food.lie);//打印作者及当前食物坐标
		}
	}
}

//增加新节点(通过方向判断加在哪)
void addNode()
{
	struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));

	switch(dir){//根据键入的值来增加节点,修改tail所指结点中hang,lie并将修改后的值赋值给新节点 
		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;
	}

	new->next = NULL;
	tail->next = new;
	tail = new;
}

//初始化蛇身
void initSnake()
{
	struct Snake *p;
	
	dir = RIGHT;//蛇头初始方向为RIGHT 

	while(head != NULL){//判断蛇是否为空,清理内存 
		p = head;
		head = head->next;
		free(p);
	}

	initFoot();//随机食物生成

	head = (struct Snake*)malloc(sizeof(struct Snake));
	head->hang = 1;
	head->lie = 2;
	head->next = NULL;
	
	tail = head;//初始时尾指针指向头
	addNode();
	addNode();
	addNode();//增加3节点,设置初始蛇身长度
}

//删除蛇尾(头结点)
void deleteNode()
{
	struct Snake *p;
	p = head;
	head = head->next;
	free(p);//释放p=head原先头节点
}

//判断蛇是否死亡
int judgeSnakeDeath()
{
	struct Snake *p;
	p = head;

	if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 21){//当tail节点中数据到达最大边界return 1
		return 1;
	}

	while(p->next != NULL){
		if(p->hang == tail->hang && p->lie == tail->lie){//当tail节点中数据与蛇身一致时return 1 
			return 1;
		}
		p = p->next;
	}
	return 0;
}

//蛇的移动(蛇头增加新节点,删除蛇尾节点) 
void moveSnake()
{
	addNode();//增加新节点
	if(judgeFood(tail->hang,tail->lie)){
		initFoot();//随机食物生成
	}else{
		deleteNode();//删除蛇尾(头结点)
	}
	
	if(judgeSnakeDeath()){
		initSnake();//死亡,重新初始化蛇身,游戏重新开始
	}
}

//界面刷新
void* refreshInterface()
{	
	while(1){
		moveSnake();//移动
		gamePic();//打印输出
		refresh();//更新终端屏幕
		usleep(100000);//控制蛇的运动速度,以微秒为单位,100毫秒睡眠一次,执行挂起不动
	}
}

//通过绝对值判断相反方向不触发
void turn(int direction)
{
	if(abs(dir) != abs(direction)){
		dir = direction;
	}
}

//扫描键入的值判断方向
void* changeDirection()
{
	while(1){
                key = getch();
                switch(key){
                        case KEY_DOWN:
                                turn(DOWN);
                                break;
                        case KEY_UP:
                                turn(UP);
                                break;
                        case KEY_LEFT:
                                turn(LEFT);
                                break;
                        case KEY_RIGHT:
                                turn(RIGHT);
                                break;
                }
	}
}

int main()
{
	pthread_t t1;
	pthread_t t2;

	initNcurse();
	initSnake();
	gamePic();
	
	pthread_create(&t1, NULL, refreshInterface, NULL);
	pthread_create(&t2, NULL, changeDirection, NULL);

	while(1);
	getch();
	endwin();
	return 0;
}

四、运行效果

运行效果链接:https://v.douyin.com/6ooTQgt/

最后谢谢阅读,笔者乃小白,如有错误之处还请指正。

Logo

更多推荐