1.总体程序
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#include <math.h>
#include <time.h>
struct Snake //贪吃蛇身子节点
{
char node; //节点序号
int row; //行坐标
int column; //列坐标
struct Snake *next; //下一个节点地址
};
struct Snake *head = NULL; //链表头(全局变量)
struct Snake *tail = NULL; //链表尾(全局变量):插入新节点后记住链表尾巴这个节点,方便尾插法操作
struct Snake food; //贪吃蛇食物(全局变量)
int key; //功能性按键的值(全局变量)
int dir; //贪吃蛇运动方向(全局变量)
#define UP 1 //符号常量,上
#define DOWN -1 //符号常量,下
#define LEFT 2 //符号常量,左
#define RIGHT -2 //符号常量,右
/* API1: 扫描贪吃蛇地图:可活动范围19×19 */
void gamePic();
/* API2: 判断是否显示贪吃蛇节点 */
char showSnakeNode(int row, int column);
/* API3: 初始化蛇的头(动态节点) */
void initSnakeHead();
/* API4: (吃食物后)创建新节点并用尾插法,从tail后面插入 */
void insertFromTail();
/* API5: 删除链表头(是上一节delNode函数的特殊情况) */
void delHead();
/* API6: 贪吃蛇按键移动,复活贪吃蛇,贪吃蛇吃食物长身体 */
void moveSnake();
/* API7: 新线程t1,不断刷新图形终端页面,用于贪吃蛇脱缰移动 */
void* refreshPage();
/* API8: 新线程t2,不断识别按键,用于改变贪吃蛇方向 */
void* changeDir();
/* API9: 当前dir与按键获取对比,判断是否给dir赋新值,防止贪吃蛇不合理走位和无效走位 */
void turn(int direction);
/* API10: 初始化贪吃蛇食物,考虑食物位置和贪吃蛇不要重叠 */
void initFood();
/* API11: 判断是否显示贪吃蛇食物,还可以用来判断贪吃蛇链表尾是否吃到食物 */
char showFood(int row, int column);
/* API12: 判断贪吃蛇是否死亡*/
char ifSnakeDie();
int main(int argc, char const *argv[])
{
pthread_t t1;
pthread_t t2;
initscr(); //进入curse图形终端
keypad(stdscr, 1);
noecho();
initFood(); //创建贪吃蛇食物
initSnakeHead(); //创建蛇头
insertFromTail(); //测试:给蛇加个尾巴
insertFromTail(); //测试:再给蛇加个尾巴
gamePic(); //扫描贪吃蛇地图
pthread_create(&t1, NULL, refreshPage, NULL); //新线程,贪吃蛇脱缰移动,while(1循环)
pthread_create(&t2, NULL, changeDir, NULL); //新线程,按键改变贪吃蛇移动
while(1); //防止主线程退出
endwin(); //退出curse图形终端
return 0;
}
void gamePic()
{
int row;
int column;
move(0,0); //Ncurse的光标定位函数
for(row=0; row<20; row++){
/* 地图第一部分 */
if (row == 0){
for(column=0; column<20; column++){
printw("--");
}
printw("\n");
}
/* 地图第二部分 */
if(row>=0 && row<=19){
for(column=0; column<=20; column++){
if(column==0 || column==20){
printw("|");
}else if(showSnakeNode(row, column)){ //每遍历到地图的一个空位置,就要判断贪吃蛇的身子是否显示
printw("[]");
}else if(showFood(row, column)){ //每遍历到地图的一个空位置,就要判断贪吃蛇食物是否显示
printw("##");
}else{
printw(" ");
}
}
printw("\n");
}
/* 地图第三部分 */
if(row == 19){
for(column=0; column<20; column++){
printw("--");
}
printw("\n");
printw("by lzh, key=0%3o, food.row=%d, food.column=%d\n", key, food.row, food.column);
}
}
}
char showSnakeNode(int row, int column)
{
struct Snake *p = head; //指向链表头
while(p != NULL){ //遍历链表,从头开始判断链表节点是否显示
if(p->row==row && p->column==column){
return 1;
}
p = p->next;
}
return 0;
}
void initSnakeHead()
{
dir = RIGHT; //给蛇一个初始的移动方向: 向右
struct Snake *deletedNode;
while(head != NULL){
deletedNode = head;
head = head->next;
free(deletedNode);
}
head = (struct Snake*)malloc(sizeof(struct Snake));
if(head == NULL){
printw("malloc error\n");
exit(-1);
}
head->node = 1;
head->next = NULL;
head->row = 1; //测试:蛇头从第1行出现
head->column = 1; //测试:蛇头从第1列出现
tail = head;
}
void insertFromTail()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
if(new == NULL){
printw("malloc error\n");
exit(-1);
}
new->node = tail->node + 1;
new->next = NULL;
switch(dir){ //根据全局变量dir确定新节点的行列坐标增减趋势
case UP:
new->row = tail->row - 1;
new->column = tail->column;
break;
case DOWN:
new->row = tail->row + 1;
new->column = tail->column;
break;
case LEFT:
new->row = tail->row;
new->column = tail->column - 1;
break;
case RIGHT:
new->row = tail->row;
new->column = tail->column + 1;
break;
}
tail->next = new;
/* 最后记住新的尾巴 */
tail = new;
}
void delHead()
{
struct Snake *p = NULL;
struct Snake *deletedNode = head;
head = head->next;
free(deletedNode);
p = head;
while(p != NULL){
p->node = p->node - 1;
p = p->next;
}
}
void moveSnake()
{
insertFromTail();
if(showFood(tail->row, tail->column)){ //如果碰到食物就不删除链表头
initFood();
}else{
delHead();
}
if(ifSnakeDie()){ //如果贪吃蛇死(撞墙或吃自己)了就重新开始游戏
initSnakeHead();
insertFromTail();
insertFromTail();
}
}
void* refreshPage()
{
while(1){ //贪吃蛇脱缰移动
moveSnake();
gamePic();
refresh(); //Ncurse的刷新函数
usleep(100000); //Ncurse的睡眠函数,单位微秒
}
}
void* changeDir()
{
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;
}
}
}
void turn(int direction)
{
if(abs(dir) != abs(direction)){
dir = direction;
}
}
void initFood()
{
int x, y;
char overlapOrNot = 0;
struct Snake *p;
srand((unsigned int)time(NULL));
x = rand()%19; //行坐标范围在0-18之间
y = rand()%19 + 1; //列坐标范围在1-19之间
/* while(1){ //防止食物出现的位置和贪吃蛇身子节点重合
p = head;
x = rand()%19; //行坐标范围在0-18之间
y = rand()%19 + 1; //列坐标范围在1-19之间
while(p != NULL){
if(x==p->row && y==p->column){
overlapOrNot = 1;
break;
}
p = p->next;
}
if(overlapOrNot == 0){
break;
}
} */
food.row = x;
food.column = y;
}
char showFood(int row, int column)
{
if(food.row==row && food.column==column){
return 1;
}
return 0;
}
char ifSnakeDie()
{
struct Snake *p;
p = head;
/* 撞墙判断,如果撞墙说明贪吃蛇死亡 */
if(tail->row<0 || tail->column==0 \
|| tail->row==20 || tail->column==20){
return 1;
}
/* 咬自己判断,如果贪吃蛇链表尾咬到其他部分,说明贪吃蛇死亡 */
while(p != tail){
if(p->row==tail->row && p->column==tail->column){
return 1;
}
p = p->next;
}
return 0;
}
2.Linux环境下的Ncurse图形库(<curses.h>)
1、为什么需要Ncurse:
- 常规方法打印贪吃蛇页面:
- C语言获取键盘输入函数的特点:就三个,getchar()、scanf()、gets() ,它们使用时输入完信息后都需要按住回车完成输入,否则这三个函数会一直卡在那里,实时性很差。但是贪吃蛇游戏里我们需要按下按键后就立马响应,比如wsad、上下左右,玩游戏时不可能通过按两个按键来改变蛇的方向,但是C语言自带的库函数不支持这样的功能,必须要借助Ncurse。
2、Ncurse的上下左右键值:
- ncurse通过一个八进制的值代表了上下左右键。但是这种方式可读性差,为了提高代码可读性,所以在ncurse的头文件中,通过宏定义的方式重新安排上下左右键值,注意键值是9位二进制,超过了128(8位有符号二进制),所以程序中不能用字符型变量来接收上下左右键值,可以用整型变量来接收:
#define KEY_DOWN 0402 /* down-arrow key */ #define KEY_UP 0403 /* up-arrow key */ #define KEY_LEFT 0404 /* left-arrow key */ #define KEY_RIGHT 0405 /* right-arrow key */
3、上下左右键的实时捕获:
- 头文件:#inlcude <curses.h>
vi /usr/include/curses.h 回车
- 头文件中查看功能性按键:
先进入:curses.h头文件 /KEY_UP 回车
- 想要使用这些功能性按键,就必须使用keypad函数:否则按这些非打印字符时会出现错误
keypad(stdscr, 1); //使用keypad函数从标准stdscr中接收键盘的功能键(快捷键),1表示接收
- 使用图形库的必要语句:在使用前加上initscr()函数,在使用后加上endwin()函数。这两个函数用于进入和退出curse图形终端页面。
- getch()函数:不需要回车就能获取一个字符
- printw()函数:扫描函数,用法和printf相同,注意不要用printf函数(测试有点问题)。
- 测试代码和演示结果:编译时,需要在后面加上 -lcurses(l的意思是link,-lcurses的意思是链接库)
/* 指令: gcc snake.c -lcusres 回车 运行 */ #include <curses.h> //1.头文件 int main(int argc, char const *argv[]) { int key; //2.不用char initscr(); //3.必要语句,初始化curse的图形终端页面,并进入 keypad(stdscr, 1); //4.开启键盘功能键 while(1){ key = getch(); //5.getch函数 printw("you input: 0%3o\n", key); //6.printw函数 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(); //7.必要语句,退出curse的图形终端页面 return 0; }
3.贪吃蛇地图绘制 gamePic()
1、地图规划:
- 地图大小:20×20 或 40×40
- 竖直边界:|
- 水平边界:--(认为两个短横线 -- 和一个竖线 | 等长)
- 逻辑:流程控制
2、地图编程实现:gamePic()
- 分成三部分打印:
#include <curses.h> void gamePic(); //API1: 扫描贪吃蛇地图 int main(int argc, char const *argv[]) { initscr(); //进入curse图形终端 keypad(stdscr, 1); gamePic(); //扫描贪吃蛇地图 getch(); endwin(); //退出curse图形终端 return 0; } void gamePic() { int row; int column; for(row=0; row<20; row++){ /* 打印地图第0行 */ if (row == 0){ for(column=0; column<20; column++){ printw("--"); } printw("\n"); for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else{ printw(" "); } } printw("\n"); } /* 打印地图1到18行 */ if(row>0 && row<19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else{ printw(" "); } } printw("\n"); } /* 打印地图第19行 */ if(row == 19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else{ printw(" "); } } printw("\n"); for(column=0; column<20; column++){ printw("--"); } printw("\n"); printw("by lzh"); } } }
- 地图代码可以优化:发现上面有重复部分,可以把第一部分和第三部分的左右两个|写到第二部分中
#include <curses.h> void gamePic(); //API1: 绘制贪吃蛇地图 int main(int argc, char const *argv[]) { initscr(); //进入curse图形终端 keypad(stdscr, 1); gamePic(); //扫描贪吃蛇地图 getch(); endwin(); //退出curse图形终端 return 0; } void gamePic() { int row; int column; for(row=0; row<20; row++){ /* 打印地图第1部分 */ if (row == 0){ for(column=0; column<20; column++){ printw("--"); } printw("\n"); } /* 打印地图第2部分 */ if(row>=0 && row<=19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else{ printw(" "); } } printw("\n"); } /* 打印地图第3部分 */ if(row == 19){ for(column=0; column<20; column++){ printw("--"); } printw("\n"); printw("by lzh"); } } }
4.贪吃蛇身子显示showSnakeNode()
1、身子规划:
- 目标:贪吃蛇地图的空余部分我们用空格来扫描出来,引入了贪吃蛇身子后就要用它们替换掉空格
- 贪吃蛇身体:[]
- 原理:结构体、链表
2、贪吃蛇身子节点:
-
struct Snake //贪吃蛇身子节点 { char node; //节点序号 int row; //行坐标 int column; //列坐标 struct Snake *next; //下一个节点地址 };
3、扫描显示贪吃蛇一个节点:
- 思路:不希望都扫描成空格,所以在贪吃蛇地图的第二部分中加入对贪吃蛇身子节点的行、列坐标的判断
- 代码(测试):
外部变量(测试): struct Snake x = {1,2,2,NULL}; ------------------------------------------------------------- API1: gamePic()函数 /* 地图第二部分 */ if(row>=0 && row<=19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else if(x.row==row && x.column==column){ //通过节点的行列坐标判断是否显示 printw("[]"); }else{ printw(" "); } } printw("\n"); } --------------------------------------------------------------
4、扫描显示贪吃蛇初始节点:
- 思路:只有连续的节点才可以看出一条蛇,先不考虑动态链表,假设贪吃蛇一开始就有三个固定节点。另外,为了进行链表中多个节点的判断,我们需要封装一个判断是否显示贪吃蛇节点的函数。
- 代码(测试):showSnakeNode()
外部变量(测试): /* 贪吃蛇固定的三节点(复活后默认三个节点) */ struct Snake x = {1,2,2,NULL}; struct Snake x2 = {2,2,3,NULL}; struct Snake x3 = {3,2,4,NULL}; ------------------------------------------------------------- main函数: x.next = &x2; //注意这2条赋值语句不能写在函数外部 x2.next = &x3; gamePic(); //绘制贪吃蛇地图 ------------------------------------------------------------- API1: gamePic()函数 /* 地图第二部分 */ if(row>=0 && row<=19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else if(showSnakeNode(row, column)){ //API2. 通过节点的行列坐标判断是否显示 printw("[]"); }else{ printw(" "); } } printw("\n"); } ------------------------------------------------------------- API2: showSnakeNode()函数 char showSnakeNode(int row, int column) { struct Snake *p = &x; //指向链表头,链表头永远不变 while(p != NULL){ //遍历链表,从头开始判断链表节点是否显示 if(p->row==row && p->column==column){ return 1; } p = p->next; } return 0; }
5.创建贪吃蛇的动态节点 initSnakeHead() 和 insertFromTail()
1、链表头和链表尾两个全局变量:
- 目的:各个函数都可以直接使用链表头和链表尾,尤其是在尾插法函数中,如果tail不是全局变量,那么每次进行新节点的插入,都得从head开始让节点p移动到链表尾部再插入新节点new。
全局变量: struct Snake *head = NULL; //链表头(全局变量) struct Snake *tail = NULL; //链表尾(全局变量):插入新节点后记住链表尾巴这个节点,方便尾插法操作
2、动态创建链表头:initSnakeHead()
- 思路:
f3. 封装创建链表头的API: void initSnakeHead(); f3.1 调用malloc函数为全局变量head开辟动态内存: head = (struct Snake*)malloc(sizeof(struct Snake)); f3.2 判断malloc是否成功,如果不成功则用exit(-1)结束整个程序 f3.3 初始化链表头的node: head->node = 1; f3.4 初始化链表头的指向: head->next = NULL; //因为此时蛇头就是蛇尾 f3.5 初始化链表头的row,代表蛇头从哪一行出现 f3.6 初始化链表头的column,代表蛇头从哪一列出现 f3.7 初始化蛇尾: tail = head;
- 代码:
API3: 创建链表头 void initSnakeHead() { head = (struct Snake*)malloc(sizeof(struct Snake)); if(head == NULL){ printw("malloc error\n"); exit(-1); } head->node = 1; head->next = NULL; head->row = 2; //测试:蛇头从第2行出现 head->column = 2; //测试:蛇头从第2列出现 tail = head; }
3、尾插法新建节点:insertFromTail()
- 思路:
f4. 封装创建新节点并用尾插法的API: void insertFromTail(); f4.1 调用malloc函数为新结点new开辟动态内存: struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake)); f4.2 判断malloc是否成功,如果不成功则用exit(-1)结束整个程序 f4.3 初始化新结点new的node,也就是tail往后一个: new->node = tail->node + 1; f4.4 初始化链表头的指向: new->next = NULL; //尾插法的新节点必须指向NULL f4.5 初始化链表头的row,代表新的蛇尾从哪一行出现 f4.6 初始化链表头的column,代表新的蛇尾从哪一列出现 f4.7 让原先链表的尾巴tail指向new: tail->next = new; f4.8 最后修改全局变量tail,记住新的链表尾巴: tail = new;
- 代码:
API4: 创建新节点并用尾插法 void insertFromTail() { struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake)); if(new == NULL){ printw("malloc error\n"); exit(-1); } new->node = tail->node + 1; new->next = NULL; new->row = tail->row; //测试:新节点的行和tail的一样 new->column = tail->column + 1; //测试:新节点的列比tail的列多1 tail->next = new; /* 最后记住新的尾巴 */ tail = new; }
6.贪吃蛇按键右移delHead(),moveSnake(),gamePic修改
1、向右移动的原理:
- 情景:蛇头从地图最左边出现,蛇尾的节点新加在蛇头右侧,接下来通过按键右移贪吃蛇。
- 原理:向右移动时,需要调用delHead()函数(是动态链表创建一节中delNode()函数的特殊情况),删除链表头head,再用insertFromTail()函数在尾部加一个列号,多一列的新节点。
2、删除链表头:delHead()
- 思路:
f5. 封装删除链表头的API: void insertFromTail(); f5.1 新定义一个代表节点的结构体指针变量p,用来保存链表头head: struct Test *p = head; f5.2 新定义一个代表被删节点的结构体指针变量deletedNode: struct Test *deletedNode; f5.3 删除的是链表头,所以用deletedNode记录: deletedNode = head; f5.4 修改链表头的指向,让head指向节点2: head = head->next; f5.5 用free函数释放掉被删节点: free(deletedNode); f5.6 将节点p移动到新链表头,接下来准备修改往后各个节点的node: p = head; f5.7 while循环,控制循环的变量p,当p!=NULL 时,进入循环,把后面所有节点的node-1 f5.7.1 从节点p开始,也就是从新链表头开始,修改node: p->node = p->node - 1; f5.7.2 修改代表节点的循环变量p,让p往后走: p = p->next;
- 代码:
API5: 删除链表头(是上一节delNode函数的特殊情况) void delHead() { struct Snake *p = NULL; struct Snake *deletedNode = head; head = head->next; free(deletedNode); p = head; while(p != NULL){ p->node = p->node - 1; p = p->next; } }
3、向右移动:moveSnake()
- 代码:
API6: 贪吃蛇移动 void moveSnake() { /* 向右走原理1:添加链表尾 */ insertFromTail(); /* 向右走原理2:删除链表头 */ delHead(); }
4、Ncurse光标移动函数:move(0, 0)
- 使用原因:在按下功能按键“右”时,我们必须再次调用gamePic()函数进行扫描,但是Ncurse图形终端页面里默认从上一次的光标位置开始扫描,所以第二次调用gamePic()函数扫描的图层不会覆盖掉第一次调用gamePic()扫描的图层,因此扫描前需要我们用move()函数将光标复位。为了方便使用,修改一下gamePic()函数,每次进入gamePic()函数时,都需要先写move(0,0)语句。
5、main函数中的测试代码:
int main(int argc, char const *argv[])
{
int key;
initscr(); //进入curse图形终端
keypad(stdscr, 1);
initSnakeHead(); //创建蛇头
insertFromTail(); //测试:给蛇加个尾巴
insertFromTail(); //测试:再给蛇加个尾巴
gamePic(); //扫描贪吃蛇地图
while(1){
key = getch();
if(key == KEY_RIGHT){ //贪吃蛇往右走,并且重新扫描地图
moveSnake();
gamePic();
}
}
endwin(); //退出curse图形终端
return 0;
}
7.贪吃蛇撞墙moveSnake修改/initSnakeHead修改
1、重新布置moveSnake函数:
- 原因:需要在贪吃蛇右移时,对贪吃蛇的链表尾巴tail中的行列坐标进行判断,如果说碰到上下左右边界时,那么我们就重新开始,也就是重新调用initSnakeHead()函数。注意我们在建立贪吃蛇地图的时候第0行是"--" 以及左右两个竖线,所以撞墙判断中第一个条件不能写成tail->row==0,否则贪吃蛇到不了顶部。
- 代码:
修改API6: 贪吃蛇移动 | 加入是否撞墙判断 void moveSnake() { /* 向右走原理1:添加链表尾 */ insertFromTail(); /* 向右走原理2:删除链表头 */ delHead(); /* 撞墙判断,如果撞墙游戏重新开始,但要注意之前的链表需要释放掉 */ if(tail->row<0 || tail->column==0 \ || tail->row==20 || tail->column==20){ initSnakeHead(); insertFromTail(); insertFromTail(); } }
2、重新布置initSnakeHead函数:
- 原因:贪吃蛇撞墙后,如果直接重新申请一个新的链表头,那么原先的链表其实还是存在的,但是head这个全局变量已经被修改了,所以容易造成内存泄漏,因此需要在initSnakeHead函数的开头释放原先链表
- 思路:
f3.1 新定义一个代表被删节点的结构体指针变量deletedNode: struct Test *deletedNode; f3.2 while循环,控制循环的变量是head,当head!=NULL 时,说明原先就存在链表,接下来进行全部释放 f3.2.1 总是从链表头开始删除,用deletedNode记录: deletedNode = head; f3.2.2 修改代表链表头的循环变量head的指向,让它指向下一个节点: head = head->next; f3.2.2 调用free函数释放掉被删节点deletedNode: free(deletedNode);
- 代码:
修改API3: 创建链表头 | 加入清空原先链表的操作 void initSnakeHead() { struct Snake *deletedNode; while(head != NULL){ deletedNode = head; head = head->next; free(deletedNode); } head = (struct Snake*)malloc(sizeof(struct Snake)); if(head == NULL){ printw("malloc error\n"); exit(-1); } head->node = 1; head->next = NULL; head->row = 2; //测试:蛇头从第2行出现 head->column = 2; //测试:蛇头从第2列出现 tail = head; }
8.贪吃蛇脱缰右移
1、Ncurse的刷新和睡眠函数:
- 使用原因:见6.5部分,在main函数中的贪吃蛇向右移动代码部分,我们希望贪吃蛇自己向右移动,所以在死循环while(1)中,我们去除掉对功能键的判断,直接执行goRight()函数以及gamePic()函数,但是测试发现图形刷新过快,导致我们根本看不见移动过程。所以需要同时使用refresh()函数,以及sleep()函数或usleep()函数人为控制Ncurse图形终端页面的刷新速率。
- usleep()函数:以微秒(10-6s)为单位。
2、main函数中的测试代码:
- 1
int main(int argc, char const *argv[]) { initscr(); //进入curse图形终端 keypad(stdscr, 1); initSnakeHead(); //创建蛇头 insertFromTail(); //测试:给蛇加个尾巴 insertFromTail(); //测试:再给蛇加个尾巴 gamePic(); //扫描贪吃蛇地图 while(1){ //贪吃蛇脱缰右移 moveSnake(); gamePic(); refresh(); //Ncurse的刷新函数 usleep(100000); //Ncurse的睡眠函数,单位微秒 } endwin(); //退出curse图形终端 return 0; }
9.Linux线程引入 refreshPage(),changeDir()
1、面临的问题:
- 问题1:在本节2.7部分和6.5部分中,我们用while(1)循环不断检测用户的功能性按键输入对蛇进行按键右移,在本节8.2部分中,我们还需要用while(1)循环不断刷新页面,让蛇脱缰右移。那么如何实现双while(1)循环的同时进行呢?
- 问题2:如何用上下左右这些按键来改变蛇的轨迹?
2、Linux线程概念:
- 错误示范:一个进程中的代码都是自上而下运行的,只有结束了上一句代码,才会执行下一句代码。所以两个while(1)一上一下写肯定不能达到我们的想要的效果。比如:
int main(int argc, char const *argv[]) { int key; initscr(); //进入curse图形终端 keypad(stdscr, 1); initSnakeHead(); //创建蛇头 insertFromTail(); //测试:给蛇加个尾巴 insertFromTail(); //测试:再给蛇加个尾巴 gamePic(); //扫描贪吃蛇地图 while(1){ //贪吃蛇脱缰右移 moveSnake(); gamePic(); refresh(); //Ncurse的刷新函数 usleep(100000); //Ncurse的睡眠函数,单位微秒 } while(1){ key = getch(); if(key == KEY_RIGHT){ //贪吃蛇往右走,并且重新扫描地图 moveSnake(); gamePic(); } } endwin(); //退出curse图形终端 return 0; }
- 回顾函数指针的用处,其中我们介绍了线程:
- 线程代码举例:通过pthread_create()函数创建新线程。编译时需要链接pthread库:在后面加上-lpthread
/* 指令: gcc test.c -lpthread 回车 运行 */ #include <stdio.h> #include <pthread.h> void* func1(void *arg) //注意创建的线程中,函数返回值必须是void* { while(1){ printf("this is func1\n"); sleep(1); } } void func2() { while(1){ printf("this is func2\n"); sleep(1); } } int main(int argc, char const *argv[]) //main函数就是一个线程,接下来创建新的线程 { pthread_t th1; //定义一个线程th1 pthread_create(&th1, NULL, func1, NULL); //创建线程:给谁创建?答:th1,所以传递线程th1的地址。 // 创建以后调用哪个函数?答:func1函数,所以传递函数的地址,即函数名 // 给这个线程传递什么操作数?答:无需操作数,所以传递NULL func2(); return 0; }
3、测试实现三线程: main() 以及 refreshPage() 以及 changeDir() 。仿照上述线程代码举例。
- refreshPage()函数:
API7: 新线程t1,不断刷新图形终端页面,用于贪吃蛇脱缰移动 void* refreshPage() { while(1){ //贪吃蛇脱缰右移 moveSnake(); gamePic(); refresh(); //Ncurse的刷新函数 usleep(100000); //Ncurse的睡眠函数,单位微秒 } }
- changeDir()函数:
API8: 新线程t2,不断识别按键,用于改变贪吃蛇方向 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; } } }
- main函数测试:尝试把用于获取功能性按键的变量key当做全局变量,在gamePic函数中打印key的值
/* 指令: gcc snake.c -lcurses -lpthread 回车 运行 */ int main(int argc, char const *argv[]) { pthread_t t1; pthread_t t2; initscr(); //进入curse图形终端 keypad(stdscr, 1); initSnakeHead(); //创建蛇头 insertFromTail(); //测试:给蛇加个尾巴 insertFromTail(); //测试:再给蛇加个尾巴 gamePic(); //扫描贪吃蛇地图 pthread_create(&t1, NULL, refreshPage, NULL); //新线程,贪吃蛇脱缰移动,while(1循环) pthread_create(&t2, NULL, changeDir, NULL); //新线程,按键改变贪吃蛇移动 while(1); //防止主线程退出 endwin(); //退出curse图形终端 return 0; }
10.贪吃蛇四方向风骚走位initSnakeHead 修改 / insertFromTail 修改 / changeDir修改
1、走位原理:根据让蛇在地图中移动起来的原理,贪吃蛇在进行四方位走位时(准确来说是三方位,因为我们不允许蛇掉头走,掉头走属于不合理的走位),也遵循的是在链表尾添加新节点,删除链表头的原理,但是新节点的行列坐标要根据贪吃蛇的移动方向来确定。
2、按键和方位两个全局变量:
- 目的:用dir来表示贪吃蛇当前的移动方位,用key表示用户的按键。这两个变量是实时性的,许多函数需要用到。另外为了让程序写的好看点,再宏定义四个代表方位的符号变量。
int key; //功能性按键的值(全局变量) int dir; //贪吃蛇运动方向(全局变量) #define UP 1 //符号常量,上 #define DOWN 2 //符号常量,下 #define LEFT 3 //符号常量,左 #define RIGHT 4 //符号常量,右
3、总体思路:判断现阶段我们要添加哪些函数,修改哪些函数。
- 蛇头一开始的运动方向dir在哪确定?答:initSnakeHead()函数(根据蛇头一开始出现在地图中的位置,这里是一开始出现在地图左上角,所以蛇头一开始向右运动)
- 在哪里根据全局变量dir确定新节点的行列坐标?答:insertFromTail()函数
- 在哪里根据功能性按键(全局变量key)来修改全局变量dir?答:changeDir()函数
- 从main函数的测试代码开始,再检查一下其他函数是否需要修改,发现moveSnake()函数、refreshPage()函数、delHead()函数、showSnakeNode()函数、gamePic()函数均不需要修改。
4、修改initSnakeHead()函数:
- 思路:在上次修改(7.2)的基础上,加一句即可
修改API3: 创建链表头 | 加入初始化蛇移动的方向 void initSnakeHead() { dir = RIGHT; //给蛇一个初始的移动方向: 向右 .............................. }
5、修改insertFromTail()函数:
- 思路:增加对蛇移动方向的判断
f4. 封装创建新节点并用尾插法的API: void insertFromTail(); f4.1 调用malloc函数为新结点new开辟动态内存: struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake)); f4.2 判断malloc是否成功,如果不成功则用exit(-1)结束整个程序 f4.3 初始化新结点new的node,也就是tail往后一个: new->node = tail->node + 1; f4.4 初始化链表头的指向: new->next = NULL; //尾插法的新节点必须指向NULL //f4.5 初始化链表头的row,代表新的蛇尾从哪一行出现 //f4.6 初始化链表头的column,代表新的蛇尾从哪一列出现 f4.7 让原先链表的尾巴tail指向new: tail->next = new; f4.8 最后修改全局变量tail,记住新的链表尾巴: tail = new; /* 修改如下:*/ f4.5 switch选择语句,对表达式dir进行判断, f4.5.1 当dir==UP时,说明贪吃蛇往上走 f4.5.1.1 新节点相对于链表尾巴的行坐标减1: new->row = tail->row - 1; f4.5.1.2 新节点相对于链表尾巴的列坐标不变: new->column = tail->column; f4.5.1.3 break跳出switch判断 f4.5.2 当dir==DOWN时,说明贪吃蛇往下走 f4.5.2.1 新节点相对于链表尾巴的行坐标加1: new->row = tail->row + 1; f4.5.2.2 新节点相对于链表尾巴的列坐标不变: new->column = tail->column; f4.5.2.3 break跳出switch判断 f4.5.3 当dir==LEFT时,说明贪吃蛇往左走 f4.5.3.1 新节点相对于链表尾巴的行坐标不变: new->row = tail->row; f4.5.3.2 新节点相对于链表尾巴的列坐标减1: new->column = tail->column - 1; f4.5.3.3 break跳出switch判断 f4.5.4 当dir==RIGHT时,说明贪吃蛇往右走 f4.5.4.1 新节点相对于链表尾巴的行坐标不变: new->row = tail->row; f4.5.4.2 新节点相对于链表尾巴的列坐标加1: new->column = tail->column + 1; f4.5.4.3 break跳出switch判断
- 代码:
修改API4: 创建新节点并用尾插法 | 加入对蛇方位的判断再决定新节点new的行列坐标 void insertFromTail() { struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake)); if(new == NULL){ printw("malloc error\n"); exit(-1); } new->node = tail->node + 1; new->next = NULL; switch(dir){ //根据全局变量dir确定新节点的行列坐标增减趋势 case UP: new->row = tail->row - 1; new->column = tail->column; break; case DOWN: new->row = tail->row + 1; new->column = tail->column; break; case LEFT: new->row = tail->row; new->column = tail->column - 1; break; case RIGHT: new->row = tail->row; new->column = tail->column + 1; break; } tail->next = new; /* 最后记住新的尾巴 */ tail = new; }
6、修改changeDir()函数:
- 代码:要注意refreshPage()和changeDir()这两个线程并不是有规律的交替出现的,而是通过抢夺硬件资源完成的,所以不要在两个while循环里都打印东西,否则会导致页面刷新错误。
API8: 新线程t2,不断识别按键,用于改变贪吃蛇方向 | 加入对全局变量dir的修改 void* changeDir() { while(1){ key = getch(); switch(key){ case KEY_DOWN: dir = DOWN; break; case KEY_UP: dir = UP; break; case KEY_LEFT: dir = LEFT; break; case KEY_RIGHT: dir = RIGHT; break; } } }
11.绝对值方式解决不合理或无效走位turn(),changeDir修改
1、原理:
- 情景:假设当前贪吃蛇在往右走,但是我按下了左按键,这是个不合理走位,此时我们希望我们按下向左键后,程序是没有响应的。
- 特点:不合理走位方向总是与当前移动方向相反;无效走位方向总是与当前移动方向相同。
- 思路:考虑到全局变量dir的传递路线:“changeDir()函数 → insertFromTail()函数”。如果出现了不合理走位,我们需要将它无视,所以在changeDir()函数和insertFromTail()函数之间我们有必要添加一个用来判断是否把新获取的代表按键方向的符号常量赋值给dir的函数,也就是按下按键后不着急立马给dir赋新值。
- 实现方法:先把按键方向相反的符号常量的值设置成绝对值相同的正负数。
#define UP 1 //符号常量,上 #define DOWN -1 //符号常量,下 #define LEFT 2 //符号常量,左 #define RIGHT -2 //符号常量,右
2、判断是否给dir赋新值:turn()函数
- 思路:
f9. 封装判断是否给dir赋新值的API: void turn(int direction); 形参direction从changeDir()函数中传递过来,turn()函数是changeDir()和insertFromTail()的中间过程 f9.1 判断当前代表移动方向的全局变量dir的绝对值是否不等于按键代表的方位direction f9.1.1 如果是,说明当前获取按键的不会造成不合理走位或者无效走位 f9.1.1.1 那么,给全局变量dir赋新值: dir = direction; f9.1.2 否则,就什么也不做,代表用户输入的是无效按键或者反向按键。
- 代码:
API9: 当前dir与按键获取对比,判断是否给dir赋新值 void turn(int direction) { if(abs(dir) != abs(direction)){ dir = direction; } }
3、修改changeDir()函数:
- 代码:
void* changeDir() { 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; } } }
4、可能出现的问题:有时候我们的Ncurses图形终端页面会扫描出乱七八糟的东西,我们可以在初始化Ncurses后,调用Ncurses的noecho()函数,它的作用是屏蔽掉无关信息(比如功能键的信息)。
12.贪吃蛇吃食物initFood(),showFood(),gamePic修改 / moveSnake修改
1、食物规划:
- 贪吃蛇食物:##
- 贪吃蛇位置:随机在贪吃蛇地图上出现,食物被吃掉后,新食物的出现位置尽量不要和蛇身重叠。
- 原理:贪吃蛇的食物也可以用结构体表示,食物的行列坐标是个随机数,由于地图可活动范围是19×19,并且根据gamePic中创建的地图范围,可以知道食物行坐标的范围是0~18,食物列坐标的范围是1~19。
struct Snake food; //贪吃蛇食物(全局变量)
2、C语言随机数函数的使用:
- 方式一:每次进入程序随机序列都一样。
/* rand函数在stdlib.h头文件中被声明 */ rand(); //创造一个很大的随机数
- 方式二:每次进入程序随机序列都不一样。
/* srand和rand函数在stldlib.h头文件中被声明、time函数在time.h头文件中被声明 */ srand((unsigned int)time(NULL)); //time函数用于产生格林威治时间到现在的秒数 //srand用于设置随机种子,它的实参是unsigned int类型的 rand();
3、初始化贪吃蛇食物:initFood()
- 思路:
f10. 封装初始化贪吃蛇食物的API: void initFood(); f10.1 新定义一个代表节点的结构体指针变量p,用来保存链表头head: struct Test *p = head; f10.2 设置随机种子: srand((unsigned int)time(NULL)); f10.3 while死循环,接下来不断产生新的食物坐标,直到新食物的坐标不和贪吃蛇身子重合 f10.3.1 将节点p复位到链表头head: p = head; f10.3.2 根据食物行坐标的范围是0~18,通过随机数取余给代表食物行坐标的变量x赋值: x = rand()%19; f10.3.3 根据食物列坐标的范围是1~19,通过随机数取余给代表食物列坐标的变量y赋值: y = rand()%19 + 1; f10.3.4 while循环,控制循环的变量p,当p!=NULL 时,进入循环,遍历贪吃蛇所有节点坐标 f10.3.4.1 判断食物坐标x是否等于p->row,并且食物坐标y是否等于p->column f10.3.4.1.1 如果是,那么说明食物的坐标和身子重合了,需要重新给x、y赋值 f10.3.4.1.1.1 修改代表食物和贪吃蛇身子覆盖的循环变量overlapOrNot: overlapOrNot = 1; //用1代表重叠,用0代表不重叠,初始化成0 f10.3.4.1.1.2 用break提前退出f10.3.4的循环,准备进行下一次判断 f10.3.4.1.2 修改代表节点的循环变量p,让p往后走: p = p->next; f10.3.5 通过变量overlapOrNot判断食物和贪吃蛇身子是否重叠 f10.3.5.1 如果不是,说明x、y的赋值是合理的, f10.3.5.1.1 那么,直接用break提前退出f10.3的死循环 f10.4 修改代表食物的全局变量food的行坐标: food.row = x; f10.5 修改代表食物的全局变量food的列坐标: food.column = y;
- 代码:经过测试后存在的BUG是加入了while(1)循环来防止食物出现位置和蛇身重合后,程序可能会卡死,留给读者优化。
API10: 初始化贪吃蛇食物 void initFood() { int x, y; char overlapOrNot = 0; struct Snake *p; srand((unsigned int)time(NULL)); x = rand()%19; //行坐标范围在0-18之间 y = rand()%19 + 1; //列坐标范围在1-19之间 /* while(1){ //防止食物出现的位置和贪吃蛇身子节点重合 p = head; x = rand()%19; //行坐标范围在0-18之间 y = rand()%19 + 1; //列坐标范围在1-19之间 while(p != NULL){ if(x==p->row && y==p->column){ overlapOrNot = 1; break; } p = p->next; } if(overlapOrNot == 0){ break; } } */ food.row = x; food.column = y; }
4、贪吃蛇食物显示:showFood()
- 代码:和showSnakeNode()函数差不多
API11: 判断是否显示贪吃蛇食物,还可以用来判断贪吃蛇链表尾是否吃到食物 char showFood(int row, int column) { if(food.row==row && food.column==column){ return 1; } return 0; }
5、修改gamePic()函数:
- 代码:在贪吃蛇地图的第二部分,通过API11,增加对是否显示贪吃蛇食物的判断。
--------------------------------------------------------------------------------------- API1: gamePic()函数 /* 地图第二部分 */ if(row>=0 && row<=19){ for(column=0; column<=20; column++){ if(column==0 || column==20){ printw("|"); }else if(showSnakeNode(row, column)){ //每遍历到地图的一个空位置,就要判断贪吃蛇的身子是否显示 printw("[]"); }else if(showFood(row, column)){ //每遍历到地图的一个空位置,就要判断贪吃蛇食物是否显示 printw("##"); }else{ printw(" "); } } printw("\n"); } ----------------------------------------------------------------------------------------
6、修改moveSnake()函数:
- 代码:在上次修改(7.1)中,通过API11,加上对蛇是否碰到食物进行判断。逻辑是碰到食物就不删除链表头,只增加链表尾,形象的说就是贪吃蛇变长了。
修改API6: 贪吃蛇移动 | 加入是否撞墙判断 | 加入蛇是否碰到食物的判断 void moveSnake() { insertFromTail(); if(showFood(tail->row, tail->column)){ //如果碰到食物就不删除链表头 initFood(); }else{ delHead(); } /* 撞墙判断,如果撞墙游戏重新开始,但要注意之前的链表需要释放掉 */ if(tail->row<0 || tail->column==0 \ || tail->row==20 || tail->column==20){ initSnakeHead(); insertFromTail(); insertFromTail(); } }
13.贪吃蛇咬自己找死ifSnakeDie(),moveSnake修改
1、判断贪吃蛇是否死亡:ifSnakeDie()。贪吃蛇死亡有两种情况,除了我们一开始讲的碰壁,还有一种情况是咬到自己,也就是链表尾碰到了贪吃蛇身子的其他节点,那么我们不妨将这两种情况写在一起,一同作为moveSnake()函数中是否重新调用initSnakeHead()函数的判据。
- 思路:
f12. 封装判断贪吃蛇是否死亡的API: char ifSnakeDie(); //函数返回值1代表死亡,0代表不死亡 f12.1 新定义一个代表节点的结构体指针变量p,用来保存链表头head: struct Test *p = head; f12.2 撞墙判断,撞墙就返回1,提前结束函数 //内在逻辑:如果能够顺利通过f12.2,说明没有撞墙,接下来对是否咬到自己进行判断 f12.3 while循环,控制循环的变量p,当p!=tail 时,进入循环,遍历除了链表尾之外所有节点坐标 f12.3.1 判断节点p的坐标是否等于链表尾tail的坐标 f12.3.1.1 如果是,说明蛇撞到自己,代表贪吃蛇死亡 f12.3.1.1.1 返回1 f12.3.2 修改代表节点的循环变量p,让p往后走: p = p->next; f12.4 如果能够顺利通过f12.2和f12.3,那么说明贪吃蛇没死,就返回0
- 代码:
API12: 判断贪吃蛇是否死亡 char ifSnakeDie() { struct Snake *p; p = head; /* 撞墙判断,如果撞墙说明贪吃蛇死亡 */ if(tail->row<0 || tail->column==0 \ || tail->row==20 || tail->column==20){ return 1; } /* 咬自己判断,如果贪吃蛇链表尾咬到其他部分,说明贪吃蛇死亡 */ while(p != tail){ if(p->row==tail->row && p->column==tail->column){ return 1; } p = p->next; } return 0; }
2、修改moveSnake()函数:
- 代码:在上次修改(12.6)中,加入对贪吃蛇是否咬到自己的判断。
修改API6: 贪吃蛇移动 | 加入是否撞墙判断 | 加入蛇是否碰到食物的判断 | 加入是否咬到自己判断 void moveSnake() { insertFromTail(); if(showFood(tail->row, tail->column)){ //如果碰到食物就不删除链表头 initFood(); }else{ delHead(); } if(ifSnakeDie()){ //如果贪吃蛇死(撞墙或吃自己)了就重新开始游戏 initSnakeHead(); insertFromTail(); insertFromTail(); } }