目录

  • 一、ncurse图形库的介绍
  • 二、ncurse上下左右键的获取
  • 三、地图规划
  • 四、显示贪吃蛇身子的第一个节点
  • 五、显示贪吃蛇完整身子
  • 六、贪吃蛇向右移动
  • 七、贪吃蛇撞墙死
  • 八、实现贪吃蛇四方向的风骚走位(引入Linux线程)
  • 九、贪吃蛇吃饭
  • 十、贪吃蛇撞墙和咬死自己代码优化
  • 十一、程序全部源码

游戏说明:Linux环境——基于Ncurse图形库的C语言小游戏

一、ncurse图形库的介绍

  贪吃蛇最为一款游戏,它使通过人操作键盘控制蛇身走位实现游戏效果的,所以按键响应必然少不了。在我们以前接触过的关于C语言按键响应方面的东西中,我们知道,C语言自带的 scanfscanfscanf 、 getchargetchargetchar 和 getsgetsgets 等库函数接收键盘输入的话,必须按下相应的键,再按下回车才能完成接收,很显然,这对于一款游戏是很不友好的,所以我们就要通过 ncursencursencurse 库来引入比较实用的按键响应功能,它封装了一个库,不需要按下回车,就能够接收键盘的各种响应。

   ncursencursencurse 的应用很广泛,尤其是再Linux内核开发方面,但是由于它图形交互界面不友好,早已淡出舞台,甚至体验感玩爆 ncursencursencurse 的C图形库GTK、C++图形库QT也趋于落伍,如今的嵌入式设备大部分都跑上了安卓系统。

如何使用 ncursencursencurse 呢?我们看下图,这是在Linux环境下的一段代码,解释了 ncursencursencurse 最基本的操作:

编译 ncursencursencurse 程序:(ncursencursencurse 非C自带的库函数,所以编译的时候要加上)

运行结果:

下面我们用它来接收一个按键:

#include<curses.h>int main()
{char c;initscr();c = getch();printw("you input:%c",c);getch();endwin();return 0;
}

当我们运行程序以后,按下 kkk 键,运行结果如图:

由此可见它对于接收键盘响应的强大之处。

二、ncurse上下左右键的获取

  在上面我们对于 ncursencursencurse 库有了大致的了解,但是,它如何捕获上下左右键来实现我们贪吃蛇游戏的效果呢?对于 cursecursecurse 来说,系统库函数为上下左右键安排了几个值,如下:

由于直接对上下左右键编码会对代码的理解很不友好,所以它的头文件对它们采用了宏定义。下面我们编写代码实现上下左右键的获取:

#include<curses.h>int main()
{int key; //char: 1byte  8bit    128     initscr();keypad(stdscr,1); //开启curses库中的按键获取while(1){key = getch();switch(key){case KEY_DOWN:printw("DOWN\n");break;case KEY_UP:printw("UP\n");break;case KEY_LEFT:printw("LEFT\n");break;case KEY_RIGHT:printw("RIGHT\n");break;}}endwin();return 0;
}

运行程序,当按下上下左右键后,运行结果为:

三、地图规划

  • 地图规划:

大小:20X20

地图竖直方向上的边界 : “|”
地图竖直方向上的边界 : “–”
贪吃蛇的身子:“[]”
贪吃蛇的食物:“##”

编程实现:

#include<curses.h>void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception
}void gamePic()
{int row;int col;for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}int main()
{initNcurse();gamePic();getch();endwin();return 0;
}

运行结果:

四、显示贪吃蛇身子的第一个节点

  在上面,我们通过循环的方式打印出了地图,这种打印类似于扫描,只有通过这种方式,贪吃蛇的身子才可以随时被插到地图中去。那么贪吃蛇身子如何显示呢?为了方便身子的移动和增加,在这里我们使用结构体和链表的形式去定义贪吃蛇身子:

  • 贪吃蛇身子的特点:
  1. 行坐标
  2. 列坐标
  3. 下一个节点的位置(地址/指针)
struct Snake
{int row;int col;struct Snake *next;
}
  • 贪吃蛇身子的显示:

如何显示蛇身子的一个节点:
设该节点:行坐标为2,列坐标为2

if(row == x.row && col == x.col){printw("[]");
}

编程实现:

#include<curses.h>struct Snake
{int row;int col;struct Snake *next;
};void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception
}void gamePic()
{int row;int col;for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(node1.row == row && node1.col == col){printw("[]");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}int main()
{initNcurse();gamePic();getch();endwin();return 0;
}

运行结果:

五、显示贪吃蛇完整身子

  贪吃蛇身子的显示关键在于多个节点如何构成一条蛇,关键在于节点坐标系的关系,如图:
横坐标不变,纵坐标累加,这样就能显示一条水平的蛇身,我们需要在动态绘制(循环扫描)地图的同时将蛇身打印出来,这里我们采用遍历蛇身链表的方式来实现蛇身的绘制。

将这里的判断条件进行函数的封装:(遍历链表)

int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}

蛇身初始化函数:(创建链表)

void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));new->row = tail->row;new->col = tail->col+1;new->next = NULL;tail->next = new;tail = new;
}void initSnake()
{head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 2;head->col = 2;tail = head;addNode();addNode();
}

相关代码整合后如下:

#include<curses.h>
#include<stdlib.h>struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception
}void gamePic()
{int row;int col;for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(node1.row == row && node1.col == col){printw("[]");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));new->row = tail->row;new->col = tail->col+1;new->next = NULL;tail->next = new;tail = new;
}void initSnake()
{head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 2;head->col = 2;tail = head;addNode();addNode();
}int main()
{initNcurse();gamePic();getch();endwin();return 0;
}

运行结果:

六、贪吃蛇向右移动

  上面我们实现了贪吃蛇蛇身的显示,但要实现贪吃蛇向右移动,我们必须对蛇身链表进行增删操作,即向右移动一步,蛇身向右增加一个节点,蛇尾删除一个节点:



所以我们需要封装删除节点函数:

void deleteNode()
{struct Snake *p; //p用来释放删除的节点空间p = head;head = head->next;free(p); //释放删除的节点
}

接下来封装蛇身向右移动函数:

void moveSnake()
{addNode();deleteNode();
}

主函数中需要不断检测按键输入,检测到右方向键输入以后需要调用蛇身移动函数并不断更新:
注意:这里需要在地图绘制函数中加上move(0,0),作用是让光标始终定位在起始位置,实现画面更新的覆盖:

主函数:

int main()
{int con;initNcurse();initSnake();gamePic();while(1){ //不断检测键盘输入con = getch();if(con == KEY_RIGHT){ moveSnake();gamePic(); //每移动一次,画面刷新,实现蛇身动态行走}}getch();endwin();return 0;
}

以上代码整合以后如下:

#include<curses.h>
#include<stdlib.h>struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(node1.row == row && node1.col == col){printw("[]");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));new->row = tail->row;new->col = tail->col+1;new->next = NULL;tail->next = new;tail = new;
}void initSnake()
{head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 2;head->col = 2;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p; //p用来释放删除的节点空间p = head;head = head->next;free(p); //释放删除的节点
}void moveSnake()
{addNode();deleteNode();
}int main()
{int con;initNcurse();initSnake();gamePic();while(1){ //不断检测键盘输入con = getch();if(con == KEY_RIGHT){ moveSnake();gamePic(); //每移动一次,画面刷新,实现蛇身动态行走}}getch();endwin();return 0;
}

运行结果:(不断按下向右键让蛇移动到中间)

七、贪吃蛇撞墙死

  贪吃蛇小游戏规定,蛇头撞墙蛇死,游戏重新开始。在我们绘制的蛇身中,蛇头作为链表尾,自然需要我们去判断蛇头是否于上下左右四个边界重合,当发生重合的时候,我们则需要重新初始化蛇身,游戏重新开始,编程如下:(代码中的 $tail$ 指的是链表尾,链表尾作为蛇头,链表头作为蛇尾)

void moveSnake()
{addNode();deleteNode();if(tail->row == 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}

当重新初始化蛇身的时候,势必会产生旧蛇身产生的内存垃圾,这时候就需要我们把旧蛇身所占用的内存空间释放掉,相关代码如下:

void initSnake()
{struct Snake *p = head; while(head != NULL){ //遍历旧蛇身,释放所有内存p = head;head = head->next;free(p);}head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}

以上代码全部整合以后如下:

#include<curses.h>
#include<stdlib.h>struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(node1.row == row && node1.col == col){printw("[]");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));new->row = tail->row;new->col = tail->col+1;new->next = NULL;tail->next = new;tail = new;
}void initSnake()
{struct Snake *p = head; while(head != NULL){ //遍历旧蛇身,释放所有内存p = head;head = head->next;free(p);}head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p; //p用来释放删除的节点空间p = head;head = head->next;free(p); //释放删除的节点
}void moveSnake()
{addNode();deleteNode();if(tail->row == 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}int main()
{int con;initNcurse();initSnake();gamePic();while(1){ //不断检测键盘输入con = getch();if(con == KEY_RIGHT){ moveSnake();gamePic(); //每移动一次,画面刷新,实现蛇身动态行走}}getch();endwin();return 0;

运行结果:

当蛇运行到边界:

当蛇撞墙以后:

八、实现贪吃蛇四方向的风骚走位(引入Linux线程)

  以上我们实现了贪吃蛇键盘控制移动并且撞墙复活,但是需要我们考虑的是贪吃蛇必须自己游走并且键盘控制转向,接下来我们一起来探讨以下这个问题:

首先我们考虑以下贪吃蛇的自由行走,这就需要我们在主函数中更改贪吃蛇按键检测相关代码:

int main()
{int con;initNcurse();initSnake();gamePic();while(1){moveSnake();gamePic();refresh(); //不断刷新界面usleep(100000); //给100ms延迟}getch();endwin();return 0;
}

接下来我们考虑一下贪吃蛇不断游走,键盘不断检测按键输入改变贪吃蛇走向,这无非是在上面代码的基础上加入我们首先介绍的键盘检测输入的代码,相关代码可以这样写:

int main()
{int con;initNcurse();initSnake();gamePic();while(1){moveSnake();gamePic();refresh(); //不断刷新界面usleep(100000); //给100ms延迟}while(1){key = getch();switch(key){case KEY_DOWN:printw("DOWN\n");break;case KEY_UP:printw("UP\n");break;case KEY_LEFT:printw("LEFT\n");break;case KEY_RIGHT:printw("RIGHT\n");break;}}getch();endwin();return 0;
}

很容易看出,代码中出现了两个 whilewhilewhile 循环,按照C程序逻辑,这个程序运行的结果只能是卡在其中一个 whilewhilewhile 循环中不能跳出,所以这就阻碍了游戏效果的实现。在这里我们引入线程的概念。(线程在之后的Linux系统编程中有详细讲解,在这里不做深入讨论,只将其用法)

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是独立调度和分派的基本单位。

既然这样,我们就有思路让这两个循环一起循环,就是创建两个线程,让着两个线程一起执行两个 whilewhilewhile 循环。在这里,我们将这两个循环进行函数封装,再分别传给两个线程,现在让我们看一下代码:

刷新界面函数:

void* refreshInterface()
{while(1){moveSnake();gamePic();refresh();usleep(100000);}
}

改变方向函数:

void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:printw("DOWN\n");break;case KEY_UP:printw("UP\n");break;case KEY_LEFT:printw("LEFT\n");break;case KEY_RIGHT:printw("RIGHT\n");break;}}}

主函数:

int main()
{pthread_t t1;  //线程描述符pthread_t t2;initNcurse();initSnake();gamePic();pthread_create(&t1, NULL, refreshInterface, NULL);   //创建线程t1,链接刷新界面函数pthread_create(&t2, NULL, changeDir, NULL); //创建线程t2,链接改变方向函数while(1); //卡住程序不退出getch();endwin();return 0;
}

接下来我们将方向改变和界面刷新结合在一起,这样的话我们就需要定义一个变量 dirdirdir 去记录方向的改变,借助方向的状态去实现转向。

定义方向变量 dirdirdir ,用来存储方向状态:(采用宏定义的形式使代码更易读)

#define UP    1
#define DOWN  2
#define LEFT  3
#define RIGHT 4int dir;  //全局变量,方便检测方向状态

键盘接收方向,改变状态变量:

void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:dir = DOWN;printw("DOWN\n");break;case KEY_UP:dir = UP;printw("UP\n");break;case KEY_LEFT:dir = LEFT;printw("LEFT\n");break;case KEY_RIGHT:dir = RIGHT;printw("RIGHT\n");break;}}
}

捕获方向的状态,改变节点增加方向:

void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));switch(dir){case UP:new->row = tail->row-1;new->col = tail->col;new->next = NULL;break;case DOWN:new->row = tail->row+1;new->col = tail->col;new->next = NULL;break;case LEFT:new->row = tail->row;new->col = tail->col-1;new->next = NULL;break;case RIGHT:new->row = tail->row;new->col = tail->col+1;new->next = NULL;break;}tail->next = new;tail = new;
}

以上的代码基本上实现了贪吃蛇四个方向的走位,但是经过运行以后还存在一些问题,就是当贪吃蛇向右游走时,我们按下左方向键,贪吃蛇会向左走,这样与游戏的规则可能会发生一些冲突,因为蛇会咬死自己,所以我们对以上的代码进行以下优化,利用绝对值的方式避免相反方向的改变:

首先改一下宏定义:

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

封装一个判断贪吃蛇是否反向游走的函数:

void turnDir(int direction)
{if(abs(dir) != abs(direction)){dir = direction;}
}

更改一下方向改变的机制:

void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:turnDir(DOWN);printw("DOWN\n");break;case KEY_UP:turnDir(UP);printw("UP\n");break;case KEY_LEFT:turnDir(LEFT);printw("LEFT\n");break;case KEY_RIGHT:turnDir(RIGHT);printw("RIGHT\n");break;}}
}

由于考虑到这样优化后可能会出现程序崩溃,所以我们在 ncursencursencurse 初始化的时候添加 noecho()noecho()noecho() 函数来避免按键等无关信息的显示,使程序更健壮:

void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acceptionnoecho();
}

这样我们的代码就全部优化完毕,我们整合以下全部代码:

#include<curses.h>
#include<stdlib.h>#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;
int key;
int dir;int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception  noecho();
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(node1.row == row && node1.col == col){printw("[]");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));switch(dir){case UP:new->row = tail->row-1;new->col = tail->col;new->next = NULL;break;case DOWN:new->row = tail->row+1;new->col = tail->col;new->next = NULL;break;case LEFT:new->row = tail->row;new->col = tail->col-1;new->next = NULL;break;case RIGHT:new->row = tail->row;new->col = tail->col+1;new->next = NULL;break;}tail->next = new;tail = new;
}void initSnake()
{struct Snake *p = head; while(head != NULL){ //遍历旧蛇身,释放所有内存p = head;head = head->next;free(p);}head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p; //p用来释放删除的节点空间p = head;head = head->next;free(p); //释放删除的节点
}void moveSnake()
{addNode();deleteNode();if(tail->row == 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}void* refreshInterface()
{while(1){moveSnake();gamePic();refresh();usleep(100000);}}void turnDir(int direction)
{if(abs(dir) != abs(direction)){dir = direction;}
}void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:turnDir(DOWN);printw("DOWN\n");break;case KEY_UP:turnDir(UP);printw("UP\n");break;case KEY_LEFT:turnDir(LEFT);printw("LEFT\n");break;case KEY_RIGHT:turnDir(RIGHT);printw("RIGHT\n");break;}}}int main()
{pthread_t t1;pthread_t t2;initNcurse();initSnake();gamePic();pthread_create(&t1, NULL, refreshInterface, NULL);pthread_create(&t2, NULL, changeDir, NULL);while(1);getch();endwin();return 0;
}

运行结果:

初始状态:

按下向下键:

按下向左键:

按下向上键:

九、贪吃蛇吃饭

  关于贪吃蛇的食物,食物关心的是位置及符号,位置同样可以使用贪吃蛇节点结构体,食物我们使用 ‘##’ 去显示。

用贪吃蛇节点结构体定义食物结构体,食物初始化函数,食物坐标采用随机数取余的方式取地图坐标框内的坐标:

struct Snake food;void initFood()
{int x = rand()%20; //随机取20以内的数int y = rand()%20; //随机取20以内的数food.row = x;food.col = y;
}

初始化蛇身的同时,初始化食物:

void initSnake()
{dir = RIGHT;struct Snake *p = head;while(head != NULL){p = head;head = head->next;free(p);}initFood(); //食物在这里初始化head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}

定义食物检测函数,方便在地图刷新绘制过程中打印食物位置:

int hasFood(int i, int j)
{if(i == food.row && j == food.col){return 1;}return 0;
}

在地图绘制函数中增添打印食物的语句:

在蛇身移动函数中增加蛇头碰到食物以后蛇身增长的条件,这里我们采用不删除蛇尾的的方式去实现这个功能:(代码中的 $tail$ 指的是链表尾,链表尾作为蛇头,链表头作为蛇尾)

蛇头和食物坐标重合,食物消失初始化并且不删除蛇尾(等同于增加蛇长),不重合则按平时运行。

void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(tail->row == 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}

整合以上代码,实现贪吃蛇吃食物的功能:

#include<curses.h>
#include<stdlib.h>#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;
int key;
int dir;struct Snake food;void initFood()
{int x = rand()%20;int y = rand()%20;food.row = x;food.col = y;}int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}int hasFood(int i, int j)
{if(i == food.row && j == food.col){return 1;}return 0;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acception  noecho();
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n"); }if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(hasSnake(row, col)){printw("[]");}else if(hasFood(row, col)){printw("##");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));switch(dir){case UP:new->row = tail->row-1;new->col = tail->col;new->next = NULL;break;case DOWN:new->row = tail->row+1;new->col = tail->col;new->next = NULL;break;case LEFT:new->row = tail->row;new->col = tail->col-1;new->next = NULL;break;case RIGHT:new->row = tail->row;new->col = tail->col+1;new->next = NULL;break;}tail->next = new;tail = new;
}void initSnake()
{struct Snake *p = head; while(head != NULL){ //遍历旧蛇身,释放所有内存p = head;head = head->next;free(p);}initFood();head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p; //p用来释放删除的节点空间p = head;head = head->next;free(p); //释放删除的节点
}void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(tail->row == 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}void* refreshInterface()
{while(1){moveSnake();gamePic();refresh();usleep(100000);}}void turnDir(int direction)
{if(abs(dir) != abs(direction)){dir = direction;}
}void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:turnDir(DOWN);printw("DOWN\n");break;case KEY_UP:turnDir(UP);printw("UP\n");break;case KEY_LEFT:turnDir(LEFT);printw("LEFT\n");break;case KEY_RIGHT:turnDir(RIGHT);printw("RIGHT\n");break;}}}int main()
{pthread_t t1;pthread_t t2;initNcurse();initSnake();gamePic();pthread_create(&t1, NULL, refreshInterface, NULL);pthread_create(&t2, NULL, changeDir, NULL);while(1);getch();endwin();return 0;
}

运行结果:

吃食物前:

吃食物后:

十、贪吃蛇撞墙和咬死自己代码优化

  直到现在,我们要实现的功能基本都已经实现,但是还存在一些小问题,第一个问题就是蛇无法走到第一行边界,第二个问题就是蛇无法自己咬死自己,下面我们针对这两个问题进行解决:

首先,蛇无法走到上边界是因为我们定义的地图的第一行两行符号,一行是上边界,第二行就是第一对竖线,所以我们可以针对蛇撞墙复活的代码可以这样优化:

void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(tail->row < 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){initSnake();}
}

将蛇撞到上边界的条件缩小为tail->row < 0,这样蛇就可以走进上边界。

其次关于蛇咬死自己,我们可以封装一个判断蛇是否死亡的函数,将撞墙和咬死自己两种情况整合在一起判断蛇是否死亡:

int ifSnakeDie()
{struct Snake *p;p = head;if(tail->row < 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){return 1;}while(p->next != NULL){if(p->row == tail->row && p->col == tail->col){return 1;}p = p->next;}return 0;
}

这里采用遍历链表的形式判断蛇头是否和蛇身重合从而判断是否死亡。(代码中的 $tail$ 指的是链表尾,链表尾作为蛇头,链表头作为蛇尾)

更改蛇身初始化函数:

void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(ifSnakeDie()){initSnake();}
}

相关代码整合如下:

#include<curses.h>
#include<stdlib.h>
#include<pthread.h>#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;
int key;
int dir;struct Snake food;void initFood()
{int x = rand()%20;int y = rand()%20;food.row = x;food.col = y;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acceptionnoecho();
}int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}int hasFood(int i, int j)
{if(i == food.row && j == food.col){return 1;}return 0;
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n");}if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(hasSnake(row, col)){printw("[]");}else if(hasFood(row, col)){printw("##");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");printw("By Shi Chenyang,Dir:%d\n",key);}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));switch(dir){case UP:new->row = tail->row-1;new->col = tail->col;new->next = NULL;break;case DOWN:new->row = tail->row+1;new->col = tail->col;new->next = NULL;break;case LEFT:new->row = tail->row;new->col = tail->col-1;new->next = NULL;break;case RIGHT:new->row = tail->row;new->col = tail->col+1;new->next = NULL;break;}tail->next = new;tail = new;
}void initSnake()
{dir = RIGHT;struct Snake *p = head;while(head != NULL){p = head;head = head->next;free(p);}initFood();head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p;p = head;head = head->next;free(p);
}int ifSnakeDie()
{struct Snake *p;p = head;if(tail->row < 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){return 1;}while(p->next != NULL){if(p->row == tail->row && p->col == tail->col){return 1;}p = p->next;}return 0;
}void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(ifSnakeDie()){initSnake();}
}void* refreshInterface()
{while(1){moveSnake();gamePic();refresh();usleep(100000);}}void turnDir(int direction)
{if(abs(dir) != abs(direction)){dir = direction;}
}void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:turnDir(DOWN);printw("DOWN\n");break;case KEY_UP:turnDir(UP);printw("UP\n");break;case KEY_LEFT:turnDir(LEFT);printw("LEFT\n");break;case KEY_RIGHT:turnDir(RIGHT);printw("RIGHT\n");break;}}}int main()
{pthread_t t1;pthread_t t2;initNcurse();initSnake();gamePic();pthread_create(&t1, NULL, refreshInterface, NULL);pthread_create(&t2, NULL, changeDir, NULL);while(1);getch();endwin();return 0;
}

运行结果:

咬死自己:

复活:

撞上边界:

复活:

十一、程序全部源码

#include<curses.h>
#include<stdlib.h>
#include<pthread.h>#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2struct Snake
{int row;int col;struct Snake *next;
};struct Snake *head;
struct Snake *tail;
int key;
int dir;struct Snake food;void initFood()
{int x = rand()%20;int y = rand()%20;food.row = x;food.col = y;
}void initNcurse()
{initscr();keypad(stdscr,1); //keyboard acceptionnoecho();
}int hasSnake(int i, int j)
{struct Snake *p;p = head;while(p != NULL){if(i == p->row && j == p->col){return 1;}p = p->next;}return 0;
}int hasFood(int i, int j)
{if(i == food.row && j == food.col){return 1;}return 0;
}void gamePic()
{int row;int col;move(0,0);for(row=0;row<20;row++){if(row == 0){for(col=0;col<20;col++){printw("--");}printw("\n");}if(row>=0 && row<20){for(col=0;col<=20;col++){if(col==0 || col==20){printw("|");}else if(hasSnake(row, col)){printw("[]");}else if(hasFood(row, col)){printw("##");}else{printw("  ");}}printw("\n");}if(row == 19){for(col=0;col<20;col++){printw("--");}printw("\n");printw("By Shi Chenyang,Dir:%d\n",key);}}
}void addNode()
{struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));switch(dir){case UP:new->row = tail->row-1;new->col = tail->col;new->next = NULL;break;case DOWN:new->row = tail->row+1;new->col = tail->col;new->next = NULL;break;case LEFT:new->row = tail->row;new->col = tail->col-1;new->next = NULL;break;case RIGHT:new->row = tail->row;new->col = tail->col+1;new->next = NULL;break;}tail->next = new;tail = new;
}void initSnake()
{dir = RIGHT;struct Snake *p = head;while(head != NULL){p = head;head = head->next;free(p);}initFood();head = (struct Snake*)malloc(sizeof(struct Snake));head->next =NULL;head->row = 1;head->col = 1;tail = head;addNode();addNode();
}void deleteNode()
{struct Snake *p;p = head;head = head->next;free(p);
}int ifSnakeDie()
{struct Snake *p;p = head;if(tail->row < 0 || tail->col == 0 || tail->row == 20 || tail->col == 20){return 1;}while(p->next != NULL){if(p->row == tail->row && p->col == tail->col){return 1;}p = p->next;}return 0;
}void moveSnake()
{addNode();if(hasFood(tail->row, tail->col)){initFood();}else{deleteNode();}if(ifSnakeDie()){initSnake();}
}void* refreshInterface()
{while(1){moveSnake();gamePic();refresh();usleep(100000);}}void turnDir(int direction)
{if(abs(dir) != abs(direction)){dir = direction;}
}void* changeDir()
{while(1){key = getch();switch(key){case KEY_DOWN:turnDir(DOWN);printw("DOWN\n");break;case KEY_UP:turnDir(UP);printw("UP\n");break;case KEY_LEFT:turnDir(LEFT);printw("LEFT\n");break;case KEY_RIGHT:turnDir(RIGHT);printw("RIGHT\n");break;}}}int main()
{pthread_t t1;pthread_t t2;initNcurse();initSnake();gamePic();pthread_create(&t1, NULL, refreshInterface, NULL);pthread_create(&t2, NULL, changeDir, NULL);while(1);getch();endwin();return 0;
}

以上就是关于Linux环境下的C语言贪吃蛇小游戏的全部学习流程,多多交流多多分享。

一辈子很短,努力的做好两件事就好;第一件事是热爱生活,好好的去爱身边的人;第二件事是努力学习,在工作中取得不一样的成绩,实现自己的价值,而不是仅仅为了赚钱;

【C语言】C语言小项目—贪吃蛇相关推荐

  1. C语言小项目--贪吃蛇游戏

    一.贪吃蛇小游戏简介: 用上下左右控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分,而且蛇的身子会越吃越长,身子越长玩的难度就越大,不能碰墙,也不能咬到自己的身体,等到了一定的分数,就能过关. ...

  2. 0基础C语言实战项目-贪吃蛇小游戏

    大家好啊,如果有一样是编程0基础的大一本科生,相信大家现在都对自己学习了一段时间的程序设计课程还没有过实战经验: 最近也是考试刚完,持着练手的心态我编写了这样一个经典的小游戏-贪吃蛇: 由于是第一次发 ...

  3. C语言与C++基础编写贪吃蛇项目1

    C语言与C++基础编写贪吃蛇项目1 第一阶段: 简单的移动的贪吃蛇编写. 小白编写,勿喷. 一.总体项目功能: 1.添加背景音乐或者动作音效 2.有欢迎界面,游戏选项等界面 3.地图范围内有障碍物,可 ...

  4. 【从入门到入土系列】C语言制作小游戏-贪吃蛇:Copy+运行即可另附注释

    系列文章 本系列持续更新中,欢迎您的访问! 系列简介 本系列由唐文疏撰写,负责记录博主的学习生涯中的一点一滴.独乐乐不如众乐乐,故此分享给大家.欢迎大家一起讨论.学习和批评指点. 博主只是一个普普通通 ...

  5. 项目: 贪吃蛇(C语言)

    项目:贪吃蛇 一.项目描述和最终效果展示 二.生成一个静态的蛇 三.玩家控制小蛇移动 四.增加判断失败功能 五.增加吃食物 分数统计 暂停游戏 等功能 一.项目描述和最终效果展示 项目描述: 玩家通过 ...

  6. C语言学习历程--小项目篇(1)

    C语言学习历程–小项目篇–基于winpcap的UDP数据发送 开发环境介绍 1.操作系统:windows10(基于x64处理器).IDE:vs 2019(微软官网个人免费版).winpcap安装包及开 ...

  7. 【Java闭关修炼】SpringBoot项目-贪吃蛇对战小游戏-配置git环境和项目创建

    [Java闭关修炼]SpringBoot项目-贪吃蛇对战小游戏-配置git环境和项目创建 项目的逐步细分 配置git环境 创建项目后端 前后端不分离写法-url访问路径解析资源 安装vue vue文件 ...

  8. 【Java闭关修炼】SpringBoot项目-贪吃蛇对战小游戏-配置Mysql与注册登录模块2

    [Java闭关修炼]SpringBoot项目-贪吃蛇对战小游戏-配置Mysql与注册登录模块2 传统的登录验证模式 JWT登录验证方式 下载安装依赖 创建JWTUTIL JwtAuthenticati ...

  9. 面向对象编程java小游戏_JavaScript面向对象编程小游戏---贪吃蛇代码实例

    1 面向对象编程思想在程序项目中有着非常明显的优势: 1- 1 代码可读性高.由于继承的存在,即使改变需求,那么维护也只是在局部模块 1-2 维护非常方便并且成本较低. ​2 这个demo是采用了面向 ...

  10. 用pygame做一个简单的python小游戏---贪吃蛇

    用pygame做一个简单的python小游戏-贪吃蛇 贪吃蛇游戏博客链接:(方法一样,语言不一样) c++贪吃蛇:https://blog.csdn.net/weixin_46791942/artic ...

最新文章

  1. JS基础类型和引用类型
  2. Javaweb学习笔记——(五)——————DOMXML目录
  3. android编程读取sd卡txt文件,如何读取SD卡中的txt文件?
  4. VTK:Filtering之SurfaceFromUnorganizedPointsWithPostProc
  5. 深度学习-Tensorflow2.2-tf.data输入模块{2}-tf.data输入实例-10
  6. html超链接点不了_HTML、CSS、JS都有哪些区别?不看必悔
  7. numpy方法读取加载mnist数据集
  8. js字符串的字典序_27. 字符串的排列
  9. 使用管道和rm命令遇到的问题
  10. Java实现mysql的读写分离
  11. linux下组态软件,linux组态软件入门使用
  12. <EDEM CFD案例01>EDEM2018 + FLUENT19.2 Coupling Interface Compiling
  13. 怎么清楚计算机硬盘搜索记录,win7系统怎么清除搜索记录_windows7删除计算机搜索记录的方法...
  14. conventional-changelog 参数含义
  15. 双活数据中心概念及优缺点介绍
  16. Kotlin object的三种用法
  17. GlobalSign是什么,其中的ssl证书类型有哪些
  18. 安卓开发仿有道词典和谷歌翻译词典软件在线输入翻译源码下载
  19. ESD静电二极管端口信号防护应用产品型号——SM712
  20. 【04】SAP ABAP性能优化 - 如何选用内表类型(STANDARD, SORTED, HASHED)?

热门文章

  1. android+省电播放器,真的能省电?五款Android省电应用实测
  2. Ubuntu安装JDK教程
  3. FM1208CPU卡读写函数说明
  4. destoon短信接口更换成和其他运营商通道并存
  5. 特种作业人员题库及答案
  6. 菜鸟教程学习JAVA 01
  7. 高等代数——大学高等代数课程创新教材(丘维声)——2.1笔记+习题
  8. python导出pdf_是程序员,就用python导出pdf
  9. 西门子V90 PN伺服EPOS模式+FB284功能库使用示例教程(图文)
  10. 使用FlyMcu ISP清除 STM32F411CE 芯片卡死