目录
项目意义
学习和开发该项目时,很大程度的优化了我对C语言的学习成果,比如一级指针二级指针的用处用法,和链表的应用方法以及途经等等。
熟悉了linux环境下的文件操作指令,和代码编写,写完该项目后能熟练的应用linux环境下的大部分命令语句,对我接下来进入linux开发有着打基础的重要作用。
项目内容
项目环境
因为在普通的C语言编译的画幕下,键盘的输入进入缓冲区后要回车才能基于计算机读取,而我们项目里贪吃蛇要及时读取键盘的上下左右键进行转向操作,必然不可能一个转向按两个按键。所以我们应用了ncurses的库,他的输入数据可以及时的读取,getch下有按键值进入就读取一次,很好的解决了我们的问题。
接下来要说说ncurses库下的几个函数
打印输出函数:printw();使用方法与printf一样
输入检测函数:getch();从键盘接收一个字节,并且函数返回该字节
curses初始化必要函数
初始化ncurses:initscr();
endwin();结束窗口;
项目模块简介
项目由五个模块实现。0.主函数对curses的初始化,1.地图的扫描,2.贪吃蛇身体的定义,3.双线程对按键进行获取且转弯,4.食物随机出现与让贪吃蛇长长的机制。
0.主函数对curses的初始化
在主函数中调用curses.h的库#include<curses.h>
然后初始化ncurses:initscr();
如果单纯的调用curses的输入函数获取上下左右按键的值会是一个乱码,所以我们还要调用ncurses库中的keypad(stdscr,1);该函数的作用是对接收到的功能键进行一个数值包装,包装了几个宏定义:KEY_UP,KEY_DOWM,KEY_LIFT,KEY_RIGHT。
必须要调用endwin();不然会卡在不知名的地方,而且为了让函数不会一瞬间就跳到运行endwin退出画幕,所以在endwin前我们调用一个getch,当我们与按键输入时再退出画幕。
对了,值得一提的是当我们编译c代码的时候,linux自带的库中是没有ncurses的库的,所以我们要通过链接一个-lcurses,编译代码如下gcc a.c -lcurese
所以目前主函数是这样的:
#include <curses.h>
int main(void)
{
int key;
initcur();
keypad(stdscr,1);
key = getch();
printw("%d",key);
getch();
endwin();
}
1.地图扫描
实现我们说说什么是扫描的方式打印地图(如下面的代码)
//如果我们用符号点扫描出一个8*8的地图
//y为行标,x为列标
for(y=0;y<8;y++)
{
printw(".");
for(x=0;x<8;x++)
{
printw(".");
}
}
这就是最简单的扫描代码,通过循环打印第一行的所有列,打印完后循环下一行。
而我们为什么要用扫描的呢,为什么不直接将一列要打印的数据一次性打印出来呢,因为我们后面不仅要在画幕上显示地图,还要在画幕上将蛇身子判断位置并打印出来,所以我们在打印地图时选择扫描的方式。
现在我们说说地图该怎么打印,我们要打印一个20*20的地图。上下边界用‘--’,左右边界用‘|’,为一个单元,要20个单元,中间用打印空格来填充。
因为我们的蛇要在20*20的范围内运动(实际要求是中间的空格打印是20*20)所以我们打印的边界肯定不能是20*20。
第一点:要有20个行是打印空格的行,加两行上下边界,所以行数为22
第二点:要有20个列是打印空格的‘|’的占用的空间为一个‘-’的空间,所以要打印的列数为21列,也就是说上下边界的行输出为:--*20次。中间打印空格的行为‘|’ + ‘空格’*20 + ‘|’。
所以初始的地图扫描代码如下:
void gamePic(void)
{
int hang,lie;
for(hang = 0;hang <= 21;hang++)
{
if(hang==0) //上边界
{
for(lie=0;lie<21;lie++) //打印21*'--'
{
printw("--");
}
printw("\n");
}
if((hang>0)&&(hang<21)) //中间贪吃蛇运行范围(空格空间)20行
{
for(lie=0;lie<=21;lie++)
{
if((lie==0)||(lie==21)) //左右边界
{
printw("|");
}
else //左右边界中间的空格运行范围‘ ’*20
{
printw(" ");
}
}
printw("\n");
}
if(hang==21) //下边界
{
for(lie=0;lie<21;lie++)
{
printw("--");
}
printw("\n");
}
}
printw("by_nbb");
}
2.贪吃蛇身体定义
我们定义贪吃蛇身体的代码是用链表将他的身体连接起来,然后通过链表尾加一个和链表头减一个实现贪吃蛇的运动。
为什么要用那么反人类的尾加一前进呢,因为当我们如果要链表尾减一我们要遍历链表到尾的上一个进行,将尾的上一个的->next=NULL;然后将那个尾释放空间,这样我们的代码就需要增加一定的计算量,而用头减的话,直接将全局变量的头指向头的下一个结构体,然后释放原来头结构体的空间就好了,不需要遍历链表。
定义贪吃蛇身体节点的结构体以及贪吃蛇的头尾指针
struct Snake
{
int hang;
int lie;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
初始化贪吃蛇身体第一个节点的结构体
void InitSnake(void)
{
struct Snake *p = NULL;
head = (struct Snake *)malloc(sizeof(struct Snake));
head->hang = 2;
head->lie = 2;
head->next = NULL;
tail = head;
addNode();
addNode();
addNode();
}
先开辟了一个head的结构体指针空间
初始化这个指针里的参数,头尾指针都指向这个第一个参数
然后用addNode();增加长度;
加入链表成员的函数addNode()代码:
void addNode(void)
{
struct Snake *new;
new = (struct Snake *)malloc(sizeof(struct Snake)); //开辟一个new结构体指针空间
new->hang = tail->hang; //初始化new结构体指针
new->lie = tail->lie+1;
new->next = NULL; //将尾结构体的下一个指向new
tail->next = new; //将尾指针重新赋值为new
tail = new;
}
生成一个新的struct Snake 型的结构体指针,并将尾指针的下一个指向他,将尾指针重新赋值为新进入的链表成员new。
他的行坐标为上一个的行坐标,列坐标为上一个的列坐标加1,那么就是说是向右加一个。
然后处理完新成员结构体的地址等参数且接入链表后,我们将该链表尾重新定义为该新成员。(这是链表的基本思路,如果不理解的话,可以重新看看链表的定义,以及链表的基本操作:添加,删除,查找)
删除链表成员的函数deleNode()代码:
void deleNode(void)
{
struct Snake *p = head;
head = head->next;
free(p);
}
先用一个局部的结构体指针变量,预存运行前的head的值,然后将head指向head的下一个结构体指针,然后取出刚刚预存的地址,将地址空间释放。
3.双线程对按键进行获取且转弯
按键转弯输入处理函数
首先我们讲一下按键的控制程序,非常简单,就是一个缓存变量key存储按键输入的值,用key = getch();然后将其值分为keypad();函数中包装好的KEY_DOWM、KEY_UP等宏定义的名称;在识别到上按键时我们将我们预先定义好的方向控制变量:dir赋值为UP;
#define DOWM 1
#define UP -1
#define LIFT 2
#define RIGHT -2
void turn(int direction)
{
if(abs(direction) !=abs(dir))
{
dir = direction;
}
}
void *changeDir(void)
{
int key;
while(1)
{
key = getch();
switch(key)
{
case KEY_DOWN:
turn(DOWM);
break;
case KEY_UP:
turn(UP);
break;
case KEY_LEFT:
turn(LIFT);
break;
case KEY_RIGHT:
turn(RIGHT);
break;
}
}
}
linux双线程的使用
因为一般在main中只能执行一个while,然后一直重复执行循环,但我们函数中有两个主要的代码需要执行,一个是贪吃蛇的运动,一个是等待按键输入,如果放入一个while里的话,贪吃蛇运动了一下就检测是否有按键输入,如果没有就会一直等待,卡死不动了。
接下来讲一下在linux环境下该怎么实现双线程
首先要定义一个pthread_t型的指针t1:pthread_t t1;
需要几个线程我们就要定义几个这个类型的指针,我们需要实现双线程所以我们需要定义一个t1和一个t2
然后根据我们需要的作为线程单独运行的函数指针,在主函数中,初始化这两个线程:
pthread_create(&t1 , NULL , changDir NULL);
实现第一个参数是之前定义的那个类型的指针,然后第二个可能是是否需要返回值,第三个是我们线程函数的函数指针,第四个是要传入的参数;
要提一下的是,在编译时我们需要链接-lpthread的库才能找到线程相关的函数或者宏定义。
int main(void)
{
pthread_t t1;
pthread_t t2;
InitCurses();
InitSnake();
Initfood();
pthread_create(&t1,NULL,refreshJieMian,NULL);
pthread_create(&t2,NULL,changeDir,NULL);
while(1)
{
}
getch();
endwin();
}
贪吃蛇添加节点代码
做完前面的操作后我们需要将按键输入后的反馈操作也写出来;
实现我们要写的是一个贪吃蛇的移动代码,实际上他的原理很简单,就是用之前分装好的加入链表删除链表的函数,向不同方向运动就在不同方向加入新链表成员,然后再删除一个链表成员
根据上述思路我们实现要在加入新链表成员的函数里加入判断dir方向的代码,然后根据不同方向,向链表尾的不同方向加成员,如我们向dir为UP,那么在我们以链表尾地址的行坐标-1为新成员的地址。
还要提一下我们要给这个新的结构体开辟空间用malloc函数
void addNode(void)
{
struct Snake *new;
new = (struct Snake *)malloc(sizeof(struct Snake));
if(dir == RIGHT)
{
new->hang = tail->hang;
new->lie = tail->lie+1;
}
else if(dir == LIFT)
{
new->hang = tail->hang;
new->lie = tail->lie-1;
}
else if(dir == DOWM)
{
new->hang = tail->hang+1;
new->lie = tail->lie;
}
else if(dir == UP)
{
new->hang = tail->hang-1;
new->lie = tail->lie;
}
new->next = NULL;
tail->next = new;
tail = new;
}
贪吃蛇删除节点的代码
void deleNode(void)
{
struct Snake *p = head;
head = head->next;
free(p);
}
贪吃蛇运动的代码
做完不同方向向不同地方加链表成员后,我们要整合删除成员和添加成员函数,将我们的贪吃蛇运动代码写出来。
void moveSnake(void)
{
addNode();
deleNode();
}
双线程中实现运动效果的代码
当然这个函数是在下一个画幕下打印,或者说是在打印完第一个地图后又打印了一个新的地图,相当于画了两张画,这不是我们想要的效果,我们想要的效果是,在一张画中不停的运动,那该怎么办呢。
我们要在第二幅画开始画前将打印的光标复位到0,0。然后下一幅画画的时候就会覆盖了,上一副画然后停留1秒,只要覆盖的快我们看不到覆盖的过程,那么我们这个就是一副会动的画。
讲一下用到的函数:move(0,0);就是光标复位到0,0位
usleep(405000);这是一个以微秒为单位的延时函数。
void *refreshJieMian(void)
{
while(1)
{
move(0,0);
refresh();
gamePic();
moveSnake();
usleep(405000);
}
}
初始化贪吃蛇时加入定义方向
然后在蛇的身子的初始化函数里面,给dir赋个初始值
void InitSnake(void)
{
dir = RIGHT;
struct Snake *p = NULL;
head = (struct Snake *)malloc(sizeof(struct Snake));
head->hang = 2;
head->lie = 2;
head->next = NULL;
tail = head;
addNode();
addNode();
addNode();
}
转弯代码的优化
但做完以上操作之后我们发现,程序存在一点小bug,我们的贪吃蛇向右运动时左键触发,贪吃蛇立马掉头,我们要做的是复刻而不是升级所以,我们不需要这么运动,拿我们该这么优化呢。
很简单,我们只需要在将按键值赋值入dir函数中,修改一下就好了,还记得我们一开始宏定义UP等标志位时,相反的方向的数值是一个相反数吗,那么我们在赋值函数中判断一下这个传入的按键与运动状态dir的绝对值是否相等。
void turn(int direction)
{
if(direction != (0-dir))
{
dir = direction;
}
}
扫描贪吃蛇节点的代码
写到这里我才发现贪吃蛇身子在地图上扫描打印的代码忘记说了
基本思路是打印地图的时候在hang和lie++的时候我们判断是否与贪吃蛇身体的hang和lie相等,当然这个判断函数需要遍历贪吃蛇身体的链表。
判断扫描到的坐标是否是贪吃蛇身体节点的函数,需要遍历贪吃蛇身体的链表:
int hanSnakeNode(int i,int j)
{
struct Snake *p = head;
while(p != NULL)
{
if((p->hang == i)&&(p->lie == j))
{
return 1;
}
p = p->next;
}
return 0;
}
在地图中改变的代码我用特殊符号在其上下两行进行了标记
int hanSnakeNode(int i,int j)
{
struct Snake *p = head;
while(p != NULL)
{
if((p->hang == i)&&(p->lie == j))
{
return 1;
}
p = p->next;
}
return 0;
}
void gamePic(void)
{
int hang,lie;
for(hang = 0;hang <= 21;hang++)
{
if(hang==0)
{
for(lie=0;lie<20;lie++)
{
printw("--");
}
printw("\n");
}
if((hang>0)&&(hang<21))
{
for(lie=0;lie<=20;lie++)
{
if((lie==0)||(lie==20))
{
printw("|");
}
/********************************************************/
else if(hanSnakeNode(hang,lie))
{
printw("[]");
}
/*******************************************************/
else
{
printw(" ");
}
}
printw("\n");
}
if(hang==21)
{
for(lie=0;lie<20;lie++)
{
printw("--");
}
printw("\n");
}
}
printw("by_nbb");
}
到这来我们的贪吃蛇按键操作转弯和双线程的功能已经成型了。
4.食物随机出现与让贪吃蛇长长、死亡机制的机制
实现说说食物随机出现的函数,我们又称食物的初始化函数:
要说一个生成随机数的C语言函数rand,这个调用这个函数要调用stdilb.h的库
所以我们食物的结构体中的行坐标和列坐标由rand产生,即food->hang = rand%20;
为什么要加一个%20呢,因为rand会生成一个很大的随机数,而我们希望他的随机数在0-20(我们地图范围内),所以要给他取余20(其实我这个函数有点bug但懒得改了,如果你感兴趣可以自己修改一下,就是我的列坐标为0时,并不是打印空格,而是打印边界“|”,所以如果食物生成的坐标为0时会有bug,我的想法是,while判断到列坐标为0就运行重新赋值随机数即可,也没实际运行过,不知道会不会有未知bug)
食物初始化代码如下:
void Initfood(void)
{
int x,y;
if(food == NULL)
{
food = (struct Snake *)malloc(sizeof(struct Snake));
}
x = rand()%20;
y = rand()%20;
while(y == 0)
{
y = rand()%20;
}
food->hang = x;
food->lie = y;
}
还有就是判断食物坐标是否扫描到封装的函数:
int hasfood(int i,int j)
{
struct Snake *p = food;
if((p->hang == i)&&(p->lie == j))
{
return 1;
}
return 0;
}
食物坐标定义出来后我们要在地图上打印出来:
void gamePic(void)
{
int hang,lie;
for(hang = 0;hang <= 21;hang++)
{
if(hang==0)
{
for(lie=0;lie<20;lie++)
{
printw("--");
}
printw("\n");
}
if((hang>0)&&(hang<21))
{
for(lie=0;lie<=20;lie++)
{
if((lie==0)||(lie==20))
{
printw("|");
}
else if(hanSnakeNode(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_nbb");
}
贪吃蛇长长和食物机制的代码
然后我们将贪吃蛇的节点与食物坐标是否重合判断,如果重合我们在贪吃蛇移动的代码中不删除一个节点。
void moveSnake(void)
{
addNode();
if(hasfood(tail->hang,tail->lie))
{
Initfood();
}
else
{
deleNode();
}
if(ifSnakeDie())
{
InitSnake();
}
}
上面函数中的ifSnakeDie()是我们定义的贪吃蛇是否撞墙或者要到自己的死亡判定函数。
在该函数中我们判断贪吃蛇的tail节点坐标是否到达边界,和tail节点坐标与链表从head开始遍历链表成员看看是否有坐标点与tail的坐标点重合。
int ifSnakeDie(void)
{
struct Snake *p = head;
if((tail->lie == 0)||(tail->lie == 20)||(tail->hang == 21)||(tail->hang == 0))
{
return 1;
}
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 <stdio.h>
#include <unistd.h>
#include <time.h>
#define DOWM 1
#define UP -1
#define LIFT 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 = NULL;
int hanSnakeNode(int i,int j)
{
struct Snake *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)
{
struct Snake *p = food;
if((p->hang == i)&&(p->lie == j))
{
return 1;
}
return 0;
}
void addNode(void)
{
struct Snake *new;
new = (struct Snake *)malloc(sizeof(struct Snake));
if(dir == RIGHT)
{
new->hang = tail->hang;
new->lie = tail->lie+1;
}
else if(dir == LIFT)
{
new->hang = tail->hang;
new->lie = tail->lie-1;
}
else if(dir == DOWM)
{
new->hang = tail->hang+1;
new->lie = tail->lie;
}
else if(dir == UP)
{
new->hang = tail->hang-1;
new->lie = tail->lie;
}
new->next = NULL;
tail->next = new;
tail = new;
}
void deleNode(void)
{
struct Snake *p = head;
head = head->next;
free(p);
}
void Initfood(void)
{
if(food==NULL)
{
food = (struct Snake *)malloc(sizeof(struct Snake));
}
int x = rand()%20;
int y = rand()%20;
while(y==0)
{
y = rand()%20;
}
food->hang = y;
food->lie = x;
}
void InitSnake(void)
{
dir = RIGHT;
struct Snake *p = NULL;
while(head != NULL)
{
p = head;
head = head -> next;
free(p);
}
head = (struct Snake *)malloc(sizeof(struct Snake));
head->hang = 2;
head->lie = 2;
head->next = NULL;
tail = head;
addNode();
addNode();
addNode();
}
void InitCurses(void)
{
initscr();
keypad(stdscr,1);
noecho();
}
void moveSnake(void)
{
addNode();
if(hasfood(tail->hang,tail->lie))
{
Initfood();
}
else
{
deleNode();
}
if(ifSnakeDie())
{
InitSnake();
}
}
int ifSnakeDie(void)
{
struct Snake *p = head;
if((tail->lie == 0)||(tail->lie == 20)||(tail->hang == 21)||(tail->hang == 0))
{
return 1;
}
while(p->next != NULL)
{
if((p->hang == tail->hang)&&(p->lie == tail->lie))
{
return 1;
}
p = p->next;
}
return 0;
}
void gamePic(void)
{
int hang,lie;
for(hang = 0;hang <= 21;hang++)
{
if(hang==0)
{
for(lie=0;lie<20;lie++)
{
printw("--");
}
printw("\n");
}
if((hang>0)&&(hang<21))
{
for(lie=0;lie<=20;lie++)
{
if((lie==0)||(lie==20))
{
printw("|");
}
else if(hanSnakeNode(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_nbb");
}
void turn(int direction)
{
if(abs(direction) !=abs(dir))
{
dir = direction;
}
}
void *changeDir(void)
{
int key;
while(1)
{
key = getch();
switch(key)
{
case KEY_DOWN:
turn(DOWM);
break;
case KEY_UP:
turn(UP);
break;
case KEY_LEFT:
turn(LIFT);
break;
case KEY_RIGHT:
turn(RIGHT);
break;
}
}
}
void *refreshJieMian(void)
{ int i;
while(1)
{
move(0,0);
refresh();
gamePic();
moveSnake();
usleep(105000);
}
}
int main(void)
{
pthread_t t1;
pthread_t t2;
InitCurses();
InitSnake();
Initfood();
pthread_create(&t1,NULL,refreshJieMian,NULL);
pthread_create(&t2,NULL,changeDir,NULL);
while(1)
{
}
getch();
endwin();
}