基于Linux系统和ncurses库的贪吃蛇小游戏

目录

前言

 一、地图,蛇身,食物设计

二、蛇和食物的初始化

食物

三、添加和删除蛇身节点

四、main函数和蛇运行方向线程

五、地图刷新线程

最终源码


前言

        ncurses库是什么我并没有深入了解,本文的重点也不是ncurses的使用,至于为什么要使用ncurses库,我们先来看一个问题:我们要通过方向键来控制蛇的走向,用c的库函数scanf和getchar都需要我们输入字符后再按回车,这显然不合理,而ncurses库就能解决这些问题,我们不需要知道他是怎么实现的,我们只需要会用就行了,因为这节主要是巩固链表的知识。最后有源码展示

最终效果:


 一、地图,蛇身,食物设计

蛇活动的范围为 20x20 ,地图的左右边界用 "|" 符号表示,上下边界用 "--" ,蛇身用 "[]"表示,食物用 "##" ,第0行和第0列是边界,除边界外每行每列都由两个空格表示,这样看起来比较方正,就是说每个单元都是两个空格的大小,所以蛇身和食物就设计成两个符号。 上下边界有42个 "-" ,因为 "|" 只占一个空格的位置,上下边界除去第一个和最后一个,其他两个为一组,刚好有20组,这个设计简直完美。

void gamemap()
{
	int hang,lie;

    /*要不断刷新地图来更新蛇和食物的位置,这里把光标归零,覆盖地图*/
	move(0,0);
    
    /*一共有22行,0和21行是边界*/
	for(hang=0;hang<=21;hang++)
	{
        /*打印上边界*/
		if(hang==0)
		{
            /*因为一个'-'和一个'|'对齐了,所以只需要打印21个"--"*/
			for(lie=0;lie<=20;lie++)
			{
				printw("--");
			}
            /*换行*/
			printw("\n");
		}
        /*打印蛇的活动范围,1到20行*/
		if(hang>=1 && hang<=20)
		{
            /*打印列,这里和上面不一样,要打印22列,因为打印上下边界的时候其中一组"--"被拆开来
            当成边界*/
			for(lie=0;lie<=21;lie++)
			{
                /*第0列和第21列打印左右边界*/
				if(lie==0 || lie==21)
				{
					printw("|");
				}
                /*判断蛇身是否存在,传入循环到这里的地图坐标,存在打印蛇身*/
				else if(hassnakenode(hang, lie))
				{
					printw("[]");
				}
                /*判断食物是否存在,传入循环到这里的地图坐标,存在打印食物*/
				else if(hasfood(hang, lie))
				{
					printw("##");
				}
                /*打印空格,两个空格为一个单元*/
				else
				{
					printw("  ");
				}
			}
            /*换行*/
			printw("\n");
		}
        /*最后一行*/
		if(hang == 21)
		{
			for(lie=0;lie<=20;lie++)
			{
				printw("--");
			}
			printw("\n");
            /*可以自行添加打印信息,我这里打印食物的位置*/
			printw("By sakabu,food.hang=%d,food.lie=%d\n",food.hang,food.lie);
		}
	}
}

/*判断蛇身是否存在*/
int hassnakenode(int i, int j)
{
	struct snake *p;
	p = head;

    /*遍历蛇的链表,如果蛇身的坐标等于传进来的地图坐标,返回1*/
	while(p != NULL)
	{
		if(p->hang==i && p->lie==j)
	        {
                	return 1;
       		}
		p = p->next;//遍历
	}
	return 0;
}

/*判断食物是否存在*/
int hasfood(int i, int j)
{
    /*如果传进来的地图坐标等于食物的坐标,返回1*/
	if(food.hang==i && food.lie==j)
	{
		return 1;
	}
	return 0;
}

在地图上显示蛇身和食物其实就是不断循环读取蛇身和食物的位置,不断更新覆盖,最终实现动画的效果,理解这一点剩下的就好办了。


二、蛇和食物的初始化

初始化一个结构体,里面只有三个元素

struct snake
{
    int hang;//行
    int lie;//列
    struct snake *next;//下个节点
};

食物

我们需要让食物被吃掉后随机生成,用到 rand() 函数,它会随机生成0~32767之间的数,但我们要控制食物生成范围为1~20,所以食物的初始化代码如下

/*定义食物为全局变量*/
struct snake food;

void initfood()
{
    /*rand余20后结果为0~19,再加1就是我们想要的范围*/
	int x = (rand()%20)+1;
	int y = (rand()%20)+1;

	food.hang = x;
	food.lie = y;
}

每当食物被蛇“吃掉”,后,调用initfood函数就可以随机生成下一个食物。

这里我们设计蛇的时候有点反常识,head节点是最开始生成的节点,后面生成的节点的最后一个叫做tail节点,所以我们的蛇移动的时候,都是在tail节点后添加节点,然后删除head节点,如果吃了食物就不删除。所以tail节点是蛇的头,head节点是蛇的尾巴!!!这一点一定要事先搞清楚,不然后续对链表的操作就会让人云里雾里。(你当然可以自己更改,只不过我这个程序链表头是蛇的尾巴,链表尾是蛇的头)

/*定义两个全局变量*/
struct snake *head = NULL;
struct snake *tail = NULL;

/*设定蛇的初始运动方向为向右走,这个程序设计成蛇从左上角出生*/
int dir = RIGHT;

void initsnake()
{
	struct snake *p;

    /*蛇死掉之后会调用这个函数,以防最后一次不是向右走的,这里再初始化一次*/
	dir = RIGHT;

    /*死掉之后遍历链表,把内存全部释放*/
	while(head != NULL)
	{
		p = head;
		head = head->next;
		free(p);
	}

    /*运行到这里就是一条新蛇了,蛇尾默认坐标为(2,2)*/
	head = (struct snake *)malloc(sizeof(struct snake));
	head->hang = 2;
	head->lie = 2;
	head->next = NULL;

    /*蛇头初始时指向蛇尾*/
	tail = head;

    /*蛇的初始长度为3*/
	addnode();
	addnode();
}

初始蛇: 

三、添加和删除蛇身节点

 大概思路是这样的,用户输入小键盘的上下左右键,ncurses库有关于这四个按键的宏定义

#define KEY_DOWN         0402

#define KEY_UP               0403

#define KEY_LEFT           0404

#define KEY_RIGHT         0405

 添加节点的时候,是在tail节点添加下一个新节点,也就是蛇的头部,那自然要判断按键的输入,根据输入的按键来设置新节点的行列坐标,可以看到上一个代码定义了dir这个全局变量,初始值为RIGHT,我们先来看添加节点的代码。

void addnode()
{
    /*为新节点分配内存空间*/
	struct snake *new = (struct snake *)malloc(sizeof(struct snake));

    /*初始化new节点的next*/
	new->next = NULL;

    /*条件选择*/
	switch(dir)
	{
        /*蛇运动方向为向上,就让新节点的行坐标相比蛇头-1,列不动*/
		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;
	}

    /*蛇头的next为新节点,“建立链接”*/
	tail->next = new;
    /*蛇头为新节点*/
	tail = new;
}

可以看到,添加节点是根据蛇的运动方向来改变新节点的坐标的,接下来看看删除节点。

/*如果没吃食物,就需要不断的添加节点,同时删除节点*/
void delnode()
{
	struct snake *p;
	
    /*保存蛇尾的地址*/
    p = head;

    /*蛇尾指向他的next*/
	head = head->next;

    /*释放蛇尾的内存*/
	free(p);
}

没吃食物,就添加new节点(新蛇头,根据运动方向);删除head节点(蛇尾)。


四、main函数和蛇运行方向线程

我们现在来看看main函数,理所当然的,我们需要不断刷新地图,来更新蛇的位置,实现动画效果;与此同时,我们还需要不断检测用户的按键输入,这显然需要两个while循环,但正常的裸机程序,它跑不了两个while,你要说把他们放进同一个while里,可能会出现响应太慢的结果或者其他问题,这时候就需要引入线程编程,在我之前的博客里有讲Linux线程编程,不需要实现同步等复杂的关系,我们只需要使用最简单的创建线程,一个线程运行刷新地图,另一个线程运行监测按键。

#include <pthread.h>

int main()
{
    /*创建两个线程标识符*/
	pthread_t t1;
	pthread_t t2;

    /*初始化ncurses界面的函数,后续会给出,先不看*/
	initcurses();

    /*初始化蛇和食物*/
	initsnake();
	initfood(); 
    
    /*初始化地图*/
	gamemap();

    /*创建两个线程,第二个参数默认为NULL,第四个参数表示传入运行程序的参数,我们不需要传入参数*/
	pthread_create(&t1, NULL, refreshmap, NULL);
	pthread_create(&t2, NULL, changedirection, NULL);

    /*死循环防止主线程退出*/
	while(1);

	return 0;
}

void initcurses()
{
    /*都是ncurses库的函数,具体用法我也不太清楚*/
	initscr();
	keypad(stdscr,1);
	noecho();
}

后面几个函数只不过互相调用比较多,但还是很好理解的。我们先看 changedirection 函数,这个函数的功能就是捕获用户输入按键,来改变之前定义的全局变量 dir 。

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

/*定义全局变量存放捕获的按键值*/
int key;

void* changedirection() 
{
	while(1)
	{
        /*捕获按键值*/
		key = getch();
		switch(key)
		{
			case KEY_DOWN:
                /*重点看turn函数*/
				turn(DOWN);
				break;
			case KEY_UP:
				turn(UP);
				break;
			case KEY_LEFT:
				turn(LEFT);
				break;
			case KEY_RIGHT:
				turn(RIGHT);
				break;
		}
	}
	return NULL;	
}

void turn(int direction)
{
    /*dir默认为RIGHT(-2),如果传进来的绝对值不和之前保存的dir的值一样,才允许改变方向*/
	if(abs(dir) != abs(direction))
	{
        /*将改变的方向赋值给dir,这个dir就是我们添加新节点判断的dir*/
		dir = direction;
	}
}

为什么要用turn函数,为什么相反的方向要定义成一正一负?在之前调试的过程中,发现蛇往右运行,我按下左键后直接掉头了,这显然不合理,所以要限制蛇的运行方向,只能向左转或向右转,结合宏定义和turn函数的判断就能理解这个函数的用途。abs是取绝对值函数。


五、地图刷新线程

void* refreshmap()
{
	while(1)
	{
        /*蛇移动的函数,后续有*/
		movesnake();
        /*不断刷新地图*/
		gamemap();
        /*ncurses库的函数,刷新界面*/
		refresh();
        /*控制刷新速度,单位为1us*/
		usleep(100000);
	}
	return NULL;
}

void movesnake()
{
    /*根据当前运行方向更新蛇头位置*/
	addnode();
    /*把蛇头的坐标传入判断食物的函数*/
	if(hasfood(tail->hang, tail->lie))
	{
        /*如果蛇头的坐标和食物重合,刷新食物*/
		initfood();
	}
	else
	{
        /*没吃到食物才进入这里,删除蛇尾*/
		delnode();
	}

    /*如果蛇撞到边界或撞到自己*/
	if(ifsnakedie())
	{
        /*刷新蛇*/
		initsnake();
	}
}

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

    /*判断蛇头和边界的坐标是否重合*/
	if(tail->hang==0 || tail->lie==0 || tail->hang==21 || tail->lie==22)
    {
        /*撞墙死亡*/
        initsnake();
    }
    /*遍历蛇身*/
	while(p->next != NULL)
	{
        /*如果蛇身和蛇头重合*/
		if(p->hang==tail->hang && p->lie==tail->lie)
		{
            /*撞自己而死*/
			return 1;
		}
		p = p->next;
	}
	return 0;
}

地图刷新还是比较复杂,函数调用较多,但每个函数实现的功能都不难理解,自己捋一遍思路会更清晰。


最终源码

#include <curses.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

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

void initcurses()
{
	initscr();
	keypad(stdscr,1);
	noecho();
}

struct snake
{
	int hang;
	int lie;
	struct snake *next;
};

struct snake *head = NULL;
struct snake *tail = NULL;
int key;
int dir = RIGHT;

struct snake food;

void initfood()
{
	int x = (rand()%20)+1;
	int y = (rand()%20)+1;

	food.hang = x;
	food.lie = y;
}

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;
	}
	return 0;
}

void gamemap()
{
	int hang,lie;

	move(0,0);

	for(hang=0;hang<=21;hang++)
	{
		if(hang==0)
		{
			for(lie=0;lie<=20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
		if(hang>=1 && hang<=20)
		{
			for(lie=0;lie<=21;lie++)
			{
				if(lie==0 || lie==21)
				{
					printw("|");
				}
				else if(hassnakenode(hang, lie))
				{
					printw("[]");
				}
				else if(hasfood(hang, lie))
				{
					printw("##");
				}
				else
				{
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang == 21)
		{
			for(lie=0;lie<=20;lie++)
			{
				printw("--");
			}
			printw("\n");
			printw("By sakabu,food.hang=%d,food.lie=%d\n",food.hang,food.lie);
		}
	}
}

void addnode()
{
	struct snake *new = (struct snake *)malloc(sizeof(struct snake));

	new->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;
}

void initsnake()
{
	struct snake *p;

	dir = RIGHT;

	while(head != NULL)
	{
		p = head;
		head = head->next;
		free(p);
	}

	initfood();
	head = (struct snake *)malloc(sizeof(struct snake));
	head->hang = 2;
	head->lie = 2;
	head->next = NULL;

	tail = head;

	addnode();
	addnode();
//	addnode();
}

void delnode()
{
	struct snake *p;
	p = head;

	head = head->next;

	free(p);
}

int ifsnakedie()
{
	struct snake *p;
	p = head;

	if(tail->hang==0 || tail->lie==0 || tail->hang==21 || tail->lie==22)
        {
                initsnake();
        }
	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
	{
		delnode();
	}

	if(ifsnakedie())
	{
		initsnake();
	}
}

void* refreshmap()
{
	while(1)
	{
		movesnake();
		gamemap();
		refresh();
		usleep(100000);
	}
	return NULL;
}

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;
		}
	}
	return NULL;	
}

int main()
{
	pthread_t t1;
	pthread_t t2;

	initcurses();

	initsnake();

	gamemap();

	pthread_create(&t1, NULL, refreshmap, NULL);
	pthread_create(&t2, NULL, changedirection, NULL);

	while(1);

//	getch();
//	endwin();

	return 0;
}

代码有300多行,来看看最终运行效果:

        可以看到运行的时候一开始有乱码,我看别人加了noecho(curses.h)这个函数后就不会有了,我这里要重新按一下全屏键才能正常显示,我也不知道什么原因,不过不影响最终效果。
        实际上还有个小bug,就是食物生成的位置可能会被蛇挡住,(我的解决方法是生成食物的时候遍历一遍链表,如果有重合就重新生成)这里就留给你们自己去改了;可以看到,撞自己、撞墙都能正常死亡,有时候蛇身过长死亡后会卡住(具体原因没有深入研究),总的来说程序能跑起来,但能优化的地方还有很多,大伙们可以自己按照自己的想法修改一下代码,锻炼动手能力。

本文就到这里,这个贪吃蛇小游戏将很多c语言的知识点都串联起来了,我是根据最终代码来讲思路的,肯定不如一步一步写一步一步调试效果来的好,但如果你看完这篇文章,能完全理解所有代码,相信你的编程水平也会上一层楼,自己去运行试试,自己设计一些关卡,把上述的bug解决一下,能很好的锻炼我们的编程水平。 

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sakabu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值