开发环境
选择了linux平台,可以帮助我在编写代码的同时了解linux下一些基本的命令行操作。然后图形绘制选则使用curses进行绘制,缺点当然多且比较落后,但是好处是简单比scanf这些好用(因为不需要用到回车键实现输入),适合我这种嵌入式小小白。
curses库的介绍
curses是一个在Linux/Unix下广泛应用的图形函数库,作用是可以在终端内绘制简单的图形用户界面。
curses使用两个数据结构映射终端屏幕,stdscr和curscr。stdscr是“标准屏幕”(逻辑屏幕),在curses函数库产生输出时就刷新,是默认输出窗口(用户不会看到该内容)。curscr是“当前屏幕”(物理屏幕),在调用refresh函数是,函数库会将curscr刷新为stdscr的样子。
Ubuntu18.04系统没有自带curses库,需要手动安装。
安装方法:
sudo apt-get install libncurses5-dev
环境搭建
首先得先添加一个头文件
#include <curses.h>
其次在代码的开头结尾必须要有initscr和endwin这两个函数。keypad函数帮助打开keypad模式,他可以把一些键盘上常用的功能键写成key定义,增强代码可读性和简化编写流程降低难度。例如把上方向键定义成KEY_UP而不是几个记不住的数或者字符。
int main()
{
initscr();
keypad(stdscr,1);
/*
代码部分
*/
endwin();
return 0;
}
至此准备工作基本完成,可以开始构思怎么实现游戏的代码。
代码编写
地图绘制
目标是绘制一块矩形,给贪吃蛇活动的空间(包括空地和墙壁)。用两个for循环嵌套即可实现,注意在curses里printf要换成printw函数。
代码
void gamePic()
{
int hang;
int lie;
for(hang=0;hang<22;hang++){ //列数行数自定是个矩形就行
for(lie=0;lie<20;lie++){
if(hang==0||hang==21){
printw("--");
}
else{
if(lie==0 || lie==19)
printw("||");
else
printw(" ");
}
}
printw("\n");
}
画出这么一个矩形
![](https://img-blog.csdnimg.cn/img_convert/41b82051bf4c8b6207085839ed0544e1.png)
至此地图绘制完毕。
贪吃蛇
贪吃蛇身子绘制
关键是创建一个链表来存放贪吃蛇身体各部分的细节包括这部分的行位置和列位置,考虑到后面贪吃蛇的移动进食为了方便使用动态链表来实现。
首先创建一个结构体
struct Snake
{
int hang;//行位置
int lie;//列位置
struct Snake *next;
};
头和尾声明两个结构体指针后,把身体这段代码封装一下
struct Snake* head;
struct Snake* tail;
void initSnake()
{
head=(struct Snake*)malloc(sizeof(struct Snake*));
head->hang=2;
head->lie=2;
head->next=NULL;
tail=head;
}
提到链表了肯定离不开链表的增加与删除,毕竟是相比数组的显著优点嘛,贪吃蛇身子作为链表构建同样也是离不开的。
增加新结点
void addNode()//尾插法增加新结点
{
struct Snake* new;
new=(struct Snake*)malloc(sizeof(struct Snake*));
tail->next=new;
new->hang=tail->hang;
new->lie=tail->lie+1;
new->next=NULL;
tail=new;
}
删除旧结点
void deleteNode()
{
struct Snake*p;
p=head;
head=head->next;
free(p);
}
然后把initSnake函数略微修改
void initSnake()
{
head=(struct Snake*)malloc(sizeof(struct Snake*));
head->hang=2;
head->lie=2;
head->next=NULL;
tail=head;
addNode();
addNode();
}
主函数里调用该函数就得到了有头有尾的有3个结点的贪吃蛇身体。
把贪吃蛇链表显示到屏幕上
重写地图绘制的gamePic函数即可,还有就是在画完边界后填空前多加一个elseif判断该点存不存在贪吃蛇身体,把判断函数命名为isSnakeNode()。还有就是绘制完后记得让光标回到起点进而实现覆盖方便后面的动态图像显示。
int isSnakeNode(int x,int y)
{
struct Snake *p=head;
while(p!=NULL){
if(p->hang==x && p->lie==y)
return 1;
else
p=p->next;
}
return 0;
}
void gamePic()
{
int hang;
int lie;
move(0,0);// 光标回到起点
for(hang=0;hang<22;hang++){
for(lie=0;lie<20;lie++){
if(hang==0||hang==21){
printw("--");
}
else{
if(lie==0)
printw("||");
else if(lie==19)
printw("||");
else if(isSnakeNode(hang,lie))
printw("[]");
else
printw(" ");
}
}
printw("\n");
}
}
得到静止的贪吃蛇
![](https://img-blog.csdnimg.cn/img_convert/13cfb427060762b22dd0ca3c2e5b5d93.png)
贪吃蛇自动向右前进
重新写一个moveSnake函数,尾插一个新结点,删除头结点,就实现了向某个方向的移动一步。移动完后判断链表尾巴有没有碰到地图边缘,碰到的话就重新生成贪吃蛇。
void moveSnake()
{
addNode();
deleteNode();
if(tail->lie==19 ||tail->lie==0||tail->hang==0|| tail->hang==21)initSnake();
}
然后因为撞墙的贪吃蛇身体malloc的部分没有释放掉,可能导致内存泄露,所以不能简单重新生成头尾指针重指向那么简单,得把之前的那条贪吃蛇链表全部释放掉,故重写initSnake函数加个判断。
void initSnake()
{
struct Snake* p;
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();
}
在主函数里把moveSnake放在循环里每个0.1s调用一次moveSnake就实现向右自动前进
int main()
{
initscr();
keypad(stdscr,1);
initSnake();
while(1){
moveSnake();
gamePic();
refresh();
usleep(100000);
}
endwin();
return 0;
}
贪吃蛇自动前进优化
上面的代码只能让贪吃蛇自动向右,那么其他方向怎么办呢?可以创建一个全局变量dir负责保存当前方向,向别的方向前进的区别是addNode里新增结点的行列位置相比链表尾巴位置是怎么变,重写addNode函数如下
#define UP 1
#define DOWN 2
#define LEFT 3
#define RIGHT 4
void addNode()
{
struct Snake* new=(struct Snake *)malloc(sizeof(struct Snake*));
tail->next=new;
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;
}
new->next=NULL;
tail=new;
}
这样贪吃蛇就有了向别的方向自动前进的能力,只要改变dir值就行,为后面检测按键通过按键改变贪吃蛇方向的功能铺垫。
按键控制贪吃蛇方向
按键检测
可以用getch()函数负责检测用户输入了什么方向键,在主函数中调用changeDir函数即可不断检测并通过改变全局变量dir的值来影响新结点的生成位置。
void changeDir()
{
while(1){
key=getch();
switch(key){
case KEY_UP:
dir=UP;
printw("UP");
break;
case KEY_DOWN:
dir=DOWN;
printw("DOWN");
break;
case KEY_LEFT:
dir=LEFT;
printw("LEFT");
break;
case KEY_RIGHT:
dir=RIGHT;
printw("RIGHT");
break;
}
}
}
按键检测与自动前进发生冲突?
综合以上代码,我们惊讶地发现在主函数里一方面要去不断地运行moveSnake的那些代码让贪吃蛇不断前进(哪个方向无所谓),另一方要不停地检测我们有没有输入新的方向键。那么可以像下面一样写在一个while循环里面吗?
int main()
{
initscr();
keypad(stdscr,1);
initSnake();
while(1){
moveSnake();
gamePic();
refresh();
usleep(100000);
changeDir();//按键检测
}
endwin();
return 0;
}
答案显然是否定的,因为按键检测函数changeDir()里的getch()检测函数,在检测不到按键输入时便会堵塞,整个程序都会因此卡在这里,moveSnake()得不到第二次调用自然是一条静止的贪吃蛇。同理在main()里写两个while循环第二个循环则会一直运行不到,我们需要同时运行两个循环,这里就需要引入线程来解决问题。
解决两个while(1)不能同时的运行方法——线程引入
添加线程所需头文件
#include <pthread.h>
参考代码
void* refreshjiemian()
{
while(1){
moveSnake();
gamePic();
refresh();
usleep(100000);
}
}
void* changeDir()
{
while(1){
key=getch();
switch(key){
case KEY_UP:
dir=UP;
printw("UP");
break;
case KEY_DOWN:
dir=DOWN;
printw("DOWN");
break;
case KEY_LEFT:
dir=LEFT;
printw("LEFT");
break;
case KEY_RIGHT:
dir=RIGHT;
printw("RIGHT");
break;
}
}
}
int main()
{
pthread_t t1;
pthread_t t2;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1,NULL,refreshjiemian,NULL);//创建线程1
pthread_create(&t2,NULL,changeDir,NULL);//创建线程2
while(1);
endwin();
return 0;
}
编译代码
gcc snakedemo.c -lcurses -lpthread
到这里基本就实现可按键控制方向自由行走的贪吃蛇啦。
贪吃蛇食物
食物初始化
struct Snake food;
void initFood()
{
int x=rand()%20+1;
int y=rand()%19+1;
food.hang=x;
food.lie=y;
}
int isFood(int x,int y)
{
if(food.hang==x && food.lie==y)
return 1;
else
return 0;
}
重写地图绘制函数
void gamePic()
{
int hang;
int lie;
move(0,0);
for(hang=0;hang<22;hang++){
for(lie=0;lie<20;lie++){
if(hang==0||hang==21){
printw("--");
}
else{
if(lie==0)
printw("||");
else if(lie==19)
printw("||");
else if(isSnakeNode(hang,lie))
printw("[]");
else if(isFood(hang,lie))
printw("ww");
else
printw(" ");
}
}
printw("\n");
}
printw("key=%d food:%d %d",key,food.hang,food.lie);
}
吃下去贪吃蛇变长
不删结点不就相当于变长了吗,于是可以重写moveSnake函数。
void moveSnake()
{
addNode();
if(tail->hang==food.hang && tail->lie==food.lie)
initFood();
else
deleteNode();
}
自己咬自己直接重新开始游戏
int isSnakeDie()
{
struct Snake *p;
p=head;
if(tail->lie==19 ||tail->lie==0||tail->hang==0|| tail->hang==21)
return 1;
else
while(p->next!=NULL){
if(p->hang==tail->hang && p->lie==tail->lie)
return 1;
p=p->next;
}
return 0;
}
void moveSnake()
{
addNode();
if(tail->hang==food.hang && tail->lie==food.lie)
initFood();
else
deleteNode();
if(isSnakeDie())initSnake();
}
完整代码
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#define UP -1
#define DOWN 1
#define LEFT -2
#define RIGHT 2
void initNurse()
{
initscr();
keypad(stdscr,1);
}
struct Snake
{
int hang;
int lie;
struct Snake *next;
};
struct Snake food;
struct Snake *head=NULL;
struct Snake *tail=NULL;
int key;
int dir;
int isSnakeNode(int x,int y)
{
struct Snake *p=head;
while(p!=NULL){
if(p->hang==x && p->lie==y)
return 1;
else
p=p->next;
}
return 0;
}
int isFood(int x,int y)
{
if(food.hang==x && food.lie==y)
return 1;
else
return 0;
}
int isSnakeDie()
{
struct Snake *p;
p=head;
if(tail->lie==19 ||tail->lie==0||tail->hang==0|| tail->hang==21)
return 1;
else
while(p->next!=NULL){
if(p->hang==tail->hang && p->lie==tail->lie)
return 1;
p=p->next;
}
return 0;
}
void addNode()
{
struct Snake* new=(struct Snake *)malloc(sizeof(struct Snake*));
tail->next=new;
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;
}
new->next=NULL;
tail=new;
}
void initFood()
{
int x=rand()%20+1;
int y=rand()%19+1;
food.hang=x;
food.lie=y;
}
void initSnake()
{
dir=RIGHT;
struct Snake* p;
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();
}
void gamePic()
{
int hang;
int lie;
move(0,0);
for(hang=0;hang<22;hang++){
for(lie=0;lie<20;lie++){
if(hang==0||hang==21){
printw("--");
}
else{
if(lie==0)
printw("||");
else if(lie==19)
printw("||");
else if(isSnakeNode(hang,lie))
printw("[]");
else if(isFood(hang,lie))
printw("ww");
else
printw(" ");
}
}
printw("\n");
}
printw("key=%d food:%d %d",key,food.hang,food.lie);
}
void deleteNode()
{
struct Snake*p;
p=head;
head=head->next;
free(p);
}
void moveSnake()
{
addNode();
if(tail->hang==food.hang && tail->lie==food.lie)
initFood();
else
deleteNode();
if(isSnakeDie())initSnake();
}
void* refreshjiemian()
{
while(1){
moveSnake();
gamePic();
refresh();
usleep(100000);
}
}
void turn (int direction)
{
if(abs(direction)!=abs(dir)){
dir=direction;
}
}
void* changeDir()
{
while(1){
key=getch();
switch(key){
case KEY_UP:
turn(UP);
printw("UP");
break;
case KEY_DOWN:
turn(DOWN);
printw("DOWN");
break;
case KEY_LEFT:
turn(LEFT);
printw("LEFT");
break;
case KEY_RIGHT:
turn(RIGHT);
break;
}
}
}
int main()
{
pthread_t t1;
pthread_t t2;
initNurse();
initSnake();
initFood();
gamePic();
pthread_create(&t1,NULL,refreshjiemian,NULL);
pthread_create(&t2,NULL,changeDir,NULL);
while(1);
endwin();
return 0;
}
总结
从0开始确实有难度,我们新手还是先从1开始吧。