最近在学习Linux操作系统,为了加深自己对于C语言的掌握和理解,这两天写了一个比较复杂的小游戏。贪吃蛇代码量大概有300行左右,基本上运用上了C语言的常见的知识点(指针,链表,结构体,函数封装与调用等),对于自己的C语言能力的加强和逻辑思考能力的提升有较大的帮助。
啰嗦的话不多说了,先上代码吧!
#include <curses.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define UP 0403
#define DOWN 0402
#define LEFT 0404
#define RIGHT 0405
#define TURN_UP 1
#define TURN_DOWN -1
#define TURN_RIGHT 2
#define TURN_LEFT -2
struct snake
{
int hang;
int lie;
struct snake *next;
};
struct snake *head=NULL;
struct snake *tail=NULL;
struct snake food;
int key;
int turn;
int now_turn;
void Init_Curses()
{
initscr();
keypad(stdscr,1);
noecho();
}
void Turn_Statu(int now_turn)
{
if(abs(now_turn)!=abs(turn))
turn=now_turn;
}
void *Snack_Direct_Control()
{
while(1)
{
key=getch();
switch(key)
{
case UP:
printw("up\n");
now_turn=TURN_UP;
Turn_Statu(now_turn);
break;
case DOWN:
printw("down\n");
now_turn=TURN_DOWN;
Turn_Statu(now_turn);
break;
case LEFT:
printw("left\n");
now_turn=TURN_LEFT;
Turn_Statu(now_turn);
break;
case RIGHT:
printw("right\n");
now_turn=TURN_RIGHT;
Turn_Statu(now_turn);
break;
}
}
}
void Add_Snake_Node()
{
struct snake *new=(struct snake*)malloc(sizeof(struct snake));
switch(turn)
{
case TURN_UP:
new->hang=tail->hang-1;
new->lie=tail->lie;
break;
case TURN_DOWN:
new->hang=tail->hang+1;
new->lie=tail->lie;
break;
case TURN_LEFT:
new->hang=tail->hang;
new->lie=tail->lie-1;
break;
case TURN_RIGHT:
new->hang=tail->hang;
new->lie=tail->lie+1;
break;
}
new->next=NULL;
tail->next=new;
tail=new;
}
void Init_Food()
{
int x=rand()%19;
int y=rand()%19;
food.hang=x;
food.lie=y;
food.next=NULL;
}
int Deter_Food(int x,int y)
{
if(x==food.hang&&y==food.lie)
return 1;
else
return 0;
}
void Init_Snake_Node()
{
Init_Food();
turn=TURN_LEFT;
struct snake *p;
while(head!=NULL)
{
p=head;
head=head->next;
free(p);
}
head=(struct snake*)malloc(sizeof(struct snake));
head->hang=1;
head->lie=17;
head->next=NULL;
tail=head;
Add_Snake_Node();
Add_Snake_Node();
Add_Snake_Node();
}
int Deter_Link_Node(int hang,int lie)
{
struct snake *p=head;
while(p!=NULL)
{
if(p->hang==hang&&p->lie==lie)
return 1;
p=p->next;
}
return 0;
}
void Snack_Picture()
{
int hang,lie;
move(0,0);
for(hang=0;hang<20;hang++)
{
if(hang==0)
{
for(lie=0;lie<20;lie++)
printw("--");
printw("\n");
}
if(hang>=0&&hang<20)
{
for(lie=0;lie<=20;lie++)
{
if(lie==0||lie==20)
printw("|");
else if(Deter_Link_Node(hang,lie))
printw("[]");
else if(Deter_Food(hang,lie))
printw("##");
else
printw(" ");
}
printw("\n");
}
if(hang==19)
{
for(lie=0;lie<20;lie++)
printw("--");
}
}
printw("\npower by Vincent-NJW\n");
}
void Delete_Snake_Node()
{
struct snake *p;
p=head;
head=head->next;
free(p);
}
int Loop_Snake()
{
struct snake *p=head;
while(p->next!=NULL)
{
if(tail->hang==p->hang&&tail->lie==p->lie)
return 1;
else
p=p->next;
}
}
void *Move_Snake()
{
while(1)
{
Add_Snake_Node();
if(Deter_Food(tail->hang,tail->lie))
Init_Food();
else
Delete_Snake_Node();
Snack_Picture();
refresh();
usleep(100000);
if(tail->hang==-1||tail->hang==20||tail->lie==0||tail->lie==20)
Init_Snake_Node();
if(Loop_Snake())
Init_Snake_Node();
}
}
void main()
{
Init_Curses();
pthread_t t1;
pthread_t t2;
pthread_create(&t1,NULL,Snack_Direct_Control,NULL);
Init_Snake_Node();
Snack_Picture();
pthread_create(&t2,NULL,Move_Snake,NULL);
while(1);
endwin();
}
//Vincent——NJW
效果图:
由于在ubantu的vim工具下,我不是很清楚怎么打汉字,所以代码段就没有写注释。
重点的配置:
1:对此项目里面我用了Ncurses,目的是为了使用“getch()”函数。此函数区别于“getchar”的特点是在键盘输入字符之后不需要在按回车键来结束这个输入。我们可以想一下,如果我们每次操控贪吃蛇的方向都需要按一下方向键在按一下回车键,操作起来会非常的反人类。另外还用了它的几个函数,比如说move()(设置打印光标移动)还有一些我也不是很懂的函数。大家不需要深究,只需要大概知道这个函数怎么用即可,我们还是要要重点放到对于C语言和Linux上来。
安装Ncurses:命令行下输入sudo apt-gat install libncurses5-dev
2:第二个是对于Linux多进程函数的初次使用,我之前在学习FreeRTOS的时候学习了多进程的概念。可以这样理解:同一个时间点可以有多个任务在运行(任务指的就是函数)。
对于此项目而言,我们只用了Linux环境下的创建进程的函数:
pthread_t t2;
pthread_create(&t1,NULL,Snack_Direct_Control,NULL);
目的是为了从键盘获取输入的同时还可以控制贪吃蛇的运动轨迹(两个while(1))。
3:由于我们用了Ncurses和Linux的函数,所以我们在gcc编译的时候就不能像常规的编译那样直接gcc test.c,我们需要加一些后缀,gcc test.c -lcurses -lpthread。
代码思路讲解:
具体的思路就是如下的while(1)循环:
图片的格式不是很好调,麻烦点开看吧。
第一个while(1)是用来作用蛇的风骚走位。第二个while(1)是用来获取用户从键盘输入的数据,用来决定蛇的风骚走位(上下左右)。这两个while(1)用了Linux的任务调度函数,让这两个while(1)同时一起工作。
第一个while(1)
第一步:增加蛇的一个节点
注意这个节点是尾节点,每次都增加一个节点,但是怎么增加是有讲究的。我们会在第二个while(1)里面获取用户输入的数据,根据这个数据我们来决定蛇的走向,也就是下一个尾节点是在之前一个尾节点的上下左右方向的其中一个。最后把这个最新的尾节点和之前的链表连接上。
第二步:判断这个新增的尾节点是否和食物的坐标重合
如果重合,咋们就用Init_Food()函数从新随机产生一个食物。如果没有就把蛇的头结点删除
这样就会在没有吃食物的情况下保持蛇的长度不变。
Deter_Food(int hang,int lie)函数输入行和列,判断这个行和列坐标下有没有食物,如果有返回1,没有返回0。
Init_Food()函数用来随机产生一个食物,用rand函数实现。
Delete_Snake_Node()函数删除链表头结点。
第三步:刷新界面
Snack_Picture()函数不光绘制了地图的边框,还绘制了蛇的各个节点和食物。具体思路是用for嵌套。我们绘制的地图是20*20的方形。第一个for遍历行,第二个for遍历列,第一行和最后一行是'--'组成, 中间20行的两侧是'|'中间是空白(注意的是第一行是由一行的'-'和两侧的两个'|'组成,最后一行也一样)。
中间的空白我们用多个if语句优先级判断有没有蛇的节点或者食物或者空白,判断有没有食物用函数Deter_Food(int hang,int lie)前面讲过。判断有没有蛇的节点用Deter_Link_Node()函数,内涵是链表遍历的同时判断有没有节点和这个位置的坐标相同。
refresh()函数是Ncurse的封装函数用来刷新。
usleep(100000)函数us延时用来决定蛇走的快慢。
第四步:判断蛇是怎么gg的
1:撞墙gg,就是新的尾节点的行列坐标有没有到边界(0,20)。
2:自己咬自己gg,遍历链表的同时把尾节点和各个节点的坐标比较,如果有一样的就gg。
Loop_Snake()函数用来判断蛇有没有自己咬自己,咬了自己返回1,没有返回0.
第一个while(1)结束
第二个while(1)
这个while(1)就比较简单了,用getch()来获得用户的输入,我在上面也说过这个函数不用在输入之后加回车键结束,这也是它的最大优势。之后的switch就很简单了,学过单片机的肯定都写过这种结构,按键控制灯,那个按键控制那个灯的亮灭,这里我就不具体描述了。
第二个while(1)结束
好了,大致的思路就是这些。但是里面还有一些具体的小细节,大家可以先把代码跑起来,通过现象来慢慢学习本质。
本人也是小白,有讲的不对的,欢迎指正。
NAME:Vincent-NJW QQ:1504012979
想让学习变得简单——Vincent-NJW