目录
前言
一、地图,蛇身,食物设计
二、蛇和食物的初始化
食物
蛇
三、添加和删除蛇身节点
四、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这个函数后就不会有了,我这里要重新按一下全屏键才能正常显示,我也不知道什么原因,不过不影响最终效果,实际上还有个小bug,就是食物可能会生成会被蛇挡住,这里就留给你们自己去改了,撞自己、撞墙也能正常死亡,有时候会报错,有时候会直接复活,优化的地方还有很多。
本文就到这里,这个贪吃蛇小游戏将很多知识点都串联起来了,我是根据最终代码来讲思路的,肯定不如一步一步写一步一步调试效果来的好,但如果你看完这篇文章,能完全理解所有代码,相信你的编程水平也会上一层楼,自己去运行试试,自己设计一些关卡,把上述的bug解决一下,也是一种锻炼。