预备的两个知识
一、ncurses
什么是ncurses:
-
ncurses是一个程序库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面。它是一个虚拟终端中的“类GUI”应用软件工具箱。它还优化了屏幕刷新方法,以减少使用远程shell时遇到的延迟。
摘自:ncurses是什么
-
ncurses(new curses)是一套编程库,它提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。ncurses名字中的n`味着“new”,因为它是curses的自由软件版本。由于AT&T“臭名昭著”的版权政策,人们不得不在后来用ncurses去代替它。
-
-
curses是一个在Linux/Unix下广泛应用的图形函数库, 作用是可以在终端内绘制简单的图形用户界面。
-
curses可以让我们在Linux下编出好看的图形。
-
curses的名字起源于"cursor optimization", 即光标优化。现在几乎所有的Linux系统都带了curses函数库, curses也加入了对鼠标的支持, 一些菜单和面板的处理。可以说curses是Linux终端图形界面编程的不二选择(比如著名的vi就是基于curses编的)
摘自:C语言curses
-
为什么本次做贪吃蛇小游戏需要ncurse
:获取键盘的输入更加快速,响应也更加快速。如果不用ncurse,用其他获取输入函数(如scanf()
、get()
等)来控制蛇转向时,如果需要输入方向后再按回车键就太慢了
ncurses小例子:获取键盘上摁下的按键
#include <curses.h>
int main(void)
{
char c;
initscr();
while(1)
{
c = getch();
printw("\nwhat you input:%c\n",c);
}
endwin();
return 0;
}
- 使用ncurses需要添加头文件
curses.h
initscr()
:初始化窗口对象和 ncurses 代码,返回代表整个屏幕的窗口对象(简单理解:初始化屏幕使之开始进入curses图形化工作方式)endwin()
:用于结束curses, 恢复原来的屏幕
代码写好后需要用ncurses库编译,因此使用gcc编译文件时需要输入 gcc test.c -lcurses
参考:C语言curses
运行程序如下:
运行上面代码编译好的程序,如果摁下↑
会显示如下:
现在使其能正常输出显示功能键
查看curses.h
,在终端输入:vi /usr/include/curses.h
,按回车即可进入查看。接着可以输入/KEY_UP
,按下回车,即可查看这些功能性按键(如F1
、F2
、↑
、↓
等)
为了使上面的小例子在摁下↑
后也能显示出来,就要使用一个函数keypad()
,这个函数设置了可以在stdscr中接受键盘的功能键
stdscr是什么:
curses是一个在Linux/Unix下广泛应用的图形函数库,作用是可以在终端内绘制简单的图形用户界面。
curses使用两个数据结构映射终端屏幕:stdscr和curscr。stdscr是“标准屏幕”(逻辑屏幕),在curses函数库产生输出时就刷新,是默认输出窗口(用户不会看到该内容)。curscr是“当前屏幕”(物理屏幕),在调用refresh函数时,函数库会将curscr刷新为stdscr的样子。个人简单理解:就是个比终端还要厉害的输出屏幕,像个画布,可用于绘制各种东西
上面代码增加keypad()
后应该如下:
#include <curses.h>
int main(void)
{
char c;
initscr();
keypad(stdscr,1);
while(1)
{
c = getch();
printw("\nwhat you input:%c\n",c);
}
endwin();
return 0;
}
keypad(stdscr,1)
:第一个参数意思是从stdscr中接收功能键,第二个参数表示是否接收,1代表接收
编译运行后,摁下↑
:
看到依然显示不太正常,这是因为变量c
是char
型,它只占1个字节(byte),也就是8位(bit),在无符号的情况下最多它只能表达到128。而↑
的“值”可能就超过这个数值,因此变量c
存储的信息不完整
使用int
型变量,继续修改代码成如下:
#include <curses.h>
int main(void)
{
int key;
initscr();
keypad(stdscr,1);
while(1)
{
key = getch();
printw("\nwhat you input:%d\n",key);
}
endwin();
return 0;
}
编译运行,摁下↑
:
看起来能正常输出一些内容了,但是这个值和头文件curses.h里面定义的功能键的值不一样:
这是因为这里面的0403是八进制,它刚好等于十进制的259
可以利用curses.h宏定义的名称,修改成如下:
#include <curses.h>
int main(void)
{
int key;
initscr();
keypad(stdscr,1);
while(1)
{
key = getch();
switch(key)
{
case KEY_UP:
printw("UP\n");
break;
case KEY_DOWN:
printw("DOWN\n");
break;
case KEY_LEFT:
printw("LEFT\n");
break;
case KEY_RIGHT:
printw("RIGHT\n");
break;
default:
printw("\n%c\n",key);
}
}
endwin();
return 0;
}
编译运行后摁下↑
就能输出:UP了
二、线程
线程的起源:《线程是什么》
程序、进程和线程:程序、进程和线程
其它关于线程的文章:
- 《【Linux】线程》ps.此篇包含线程的概念、使用pthread线程库进行编程、以及对linux线程的补充。适合初次学习线程时看
- 《Linux中的线程 》ps.此篇包含线程的概念、各种名词解释、各种比较、使用线程的目的。也适合初次学习线程时看
- 【Linux】线程ps.此篇内容很多,专有名词很多,有点难懂,可在线程学习后期再看
个人理解:当一个程序有两个线程,就好比有了两个通道,通道一有通道一的代码,通道二有通道二的代码,两个通道都会“同时”运行
摘录上面提到的文章中一些比较重要的东西:
- linux没有真正的线程,是用进程来模拟的
- 进程是承担分配系统资源的基本实体。线程是CPU调度的基本单位。
- 线程存在于进程中,共享进程的资源
创建线程
创建一个新线程要使用pthread_create()
,使用前要添加头文件pthread.h
原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数:
thread
:线程标识符地址。
attr
:手动指定新线程的属性。一般将其设置为 NULL,表示新建线程遵循默认属性。
start_routine
:以函数指针的方式指明新建线程需要执行哪个函数
arg
:向 start_routinue() 函数的形参传递数据。一般将其设置为 NULL,表示不传递任何数据。
线程小例子
使程序里的两个while(1)
都运行
#include <stdio.h>
#include <pthread.h>
void* func1()
{
while(1)
{
printf("This is func1!\n");
sleep(1);
}
}
void* func2()
{
while(1)
{
printf("This is func2!\n");
sleep(1);
}
}
int main(void)
{
pthread_t th1;
pthread_create(&th1, NULL, func1, NULL);
func2();
return 0;
}
sleep(1);
用于将目前动作延迟一段时间,避免打印的东西刷屏
输出结果:
- 线程之间不一定是交替运行的
- 在多核中,多线程是同时运行的,在单核中,线程之间会争夺cpu等硬件资源
一个简单的贪吃蛇小游戏
绘制地图边框
地图竖直方向上的边界:|
地图水平方向上的边界:--
地图大小:20x20
#include <curses.h>
void gamePic()
{
int row = 0;
int col = 0;
for(row=0; row<20; row++)
{
if(row==0)//绘制上边框
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<20; col++)//绘制左右边框
{
if(col==0 || col==19)
printw("|");
else
printw(" ");//两个空格
}
if(row==19)//绘制下边框
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
}
int main(void)
{
initscr();
keypad(stdscr,1);
gamePic();
getch();//让程序停一下,方便显示画面
endwin();
return 0;
}
编译运行后画面如下:
发现上下的虚线框会突出来一点,这时需要把右边框向右再移动一点,只需修改绘制左右边框的代码:
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else
printw(" ");
}
再次编译运行后画面如下:
画不会动的蛇
接下来先画出不会动的贪吃蛇身子,思路如下:
贪吃蛇的身子是由一个个结点[]
组成,每个[]
都是一个结构体,里面存有自身的坐标row
、col
,还存有下一个[]
的地址。每个[]
串成一个链表。
设置两个全局指针变量head
和tail
,分别指着蛇的头结点和尾结点,以方便对蛇身进行操作
struct Snake
{
int row;
int col;
struct Snake *next;
}
struct Snake *head = NULL;
struct Snake *tail = NULL;
考虑到后面会使蛇会动起来,而且吃到食物后会增加长度,因此每个[]
均由malloc()
动态生成,以方便管理结点删除与增加
自定义一个函数initSnake()用以初始化蛇身。游戏开始时蛇身有3个结点,都在第二行。第一、第二、第三节点的坐标分别在第十七、十八、十九列
void initSnake()
{
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 2;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
其中,addNode()
用于添加一个身体节点,此处暂不考虑在哪个方向上添加,只需用此函数画出上述3个结点。添加节点方式是在头节点左边添加一个新节点:
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->row = head->row;
new->col = head->col-1;
new->next = head;
head = new;
}
画出贪吃蛇在gamePic()
完成,只需在绘制画面部分的代码修改成如下:
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
其中,hasSnakeBody(row, col)
用于判断在row和col这一点是否存在蛇的身子,如果存在就返回1:
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
至此,文件完整的代码如下:
#include <curses.h>
#include <stdlib.h>
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
void addNode()//增加一个蛇节点
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->row = head->row;
new->col = head->col-1;
new->next = head;
head = new;
}
void initSnake()//初始化蛇
{
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)//判断当前位置是否存在蛇身
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
void gamePic()//绘制画面
{
int row = 0;
int col = 0;
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
}
int main(void)
{
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
getch();//防止程序直接退出
endwin();
return 0;
}
编译运行结果如下:
使贪吃蛇摁一下动一下
先只实现摁一下←
,蛇就向左动一下
在主函数中,设定一个变量con
,用于存储键盘按下的按键值。使用while(1)
对不断监测键盘。当监测到键盘摁下←
的时候,使蛇身往左走一步。然后绘制此时的图案
主函数中的代码如下:
int main(void)
{
int con;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
while(1)
{
con = getch();
if(con == KEY_LEFT)
{
moveSnake();
gamePic();
}
}
getch();//防止程序直接退出
endwin();
return 0;
}
其中,moveSnake()是将蛇身往左移动一格,以三个节点的身子为例,实现移动蛇身一格思路如下:
- 从左往右数,在第一个节点前增加一个新节点
- 原第二个、原第三个节点保持不变
- 删除最后一个节点
代码如下:
void moveSnake()
{
addNode();
deleteNode();
}
deleteNode()
用于删除蛇身最后面的节点。先设一个指针变量,使其通过while循环后指向链表倒数第二个结点,然后用free(tail)
删除蛇身最后一个节点:
void deleteNode()
{
struct Snake *p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
编译运行,摁下←
会发现画面如下:
发现界面有些混乱,因为在摁下←
前,光标在底框最后处,在绘制蛇身向左移动一格后的画面时就紧接着光标的位置继续绘制了。为了使新产生的图能够覆盖掉原先的图,需要在画图的gamePic()
函数中添加move(0,0);
,让光标在每次画图前都先移动到(0,0)处:
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
......
}
添加完后编译运行,摁下←
即可使蛇身向左移动一格
至此,完整代码如下:
#include <curses.h>
#include <stdlib.h>
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->row = head->row;
new->col = head->col-1;
new->next = head;
head = new;
}
void initSnake()
{
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
void moveSnake()
{
addNode();
deleteNode();
}
int main(void)
{
int con;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
while(1)
{
con = getch();
if(con == KEY_LEFT)
{
moveSnake();
gamePic();
}
}
endwin();
return 0;
}
贪吃蛇碰墙后重新开始
实现方法:在每次移动蛇身后对蛇头(头结点)进行判断,当蛇头的row值等于0或20,或者蛇头的col值等于0或20都算撞到墙了,撞墙了就用initSnake()
重新开始。因此moveSnake()
函数修改成如下:
void moveSnake()
{
addNode();
deleteNode();
if(head->row==0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
然后直接编译运行,不断摁下←
,使蛇身向左移动,当蛇头撞到左边的墙后就会重新开始
虽然现在就已经能实现碰墙后重新开始了,但是有一个问题,在调用initSnake()
时,会开辟新的空间用来形成蛇身(也就是再弄多一条新的链表)。而撞墙后的这条蛇(也就是旧的链表)在系统内存中依然占着空间。如果每次撞墙都直接使用initSnake()
而不释放掉撞墙的蛇所占的内存空间,就会造成系统内存不断被占用,导致内存空间不足,程序就有可能会崩掉。
重新开始前(或者说是重新造一条蛇前)需要先释放掉旧蛇所占的内存空间,只需要先对全局变量head进行判断,看当前是否已经存在一条蛇(也就是当前是否已经存在一个链表)
initSnake()
函数前面增加如下代码:
void initSnake()
{
struct Snake *p;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
......
}
至此,完整代码如下:
#include <curses.h>
#include <stdlib.h>
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->row = head->row;
new->col = head->col-1;
new->next = head;
head = new;
}
void initSnake()
{
struct Snake *p;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
void moveSnake()
{
addNode();
deleteNode();
if(head->row==0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
int main(void)
{
int con;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
while(1)
{
con = getch();
if(con == KEY_LEFT)
{
moveSnake();
gamePic();
}
}
endwin();
return 0;
}
使蛇身自动向左走
在主函数中的while(1)
里面,修改代码至如下:
int main(void)
{
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs=1s)
}
endwin();
return 0;
}
其中,refresh()
与usleep(100000)
组合在一起就是每0.1秒刷新一次屏幕。编译运行即可让蛇自动向左移动
蛇身自动向左走的同时监测按键
监测键盘按键和使蛇自行移动都各自需要一个while(1)
来实现,如果要使蛇不断自行移动还要不间断地监测键盘,就需要用两个while(1)
,要在程序里使两个while(1)
都同时运行,就需要用到线程
使用线程相关操作需要先增加头文件pthread.h
主函数代码修改至如下:
int main(void)
{
pthread_t t1;//创建线程变量t1
pthread_t t2;//创建线程变量t1
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshScr, NULL);
pthread_create(&t2, NULL, changeDir, NULL);
while(1);//防止主线程退出
endwin();
return 0;
}
pthread_create()
有四个参数:
thread
:线程标识符地址。
attr
:手动指定新线程的属性。一般将其设置为 NULL,表示新建线程遵循默认属性。
start_routine
:以函数指针的方式指明新建线程需要执行哪个函数
arg
:向 start_routinue()
函数的形参传递数据。一般将其设置为 NULL,表示不传递任何数据。
语句pthread_create(&t1, NULL, refreshScr, NULL);
创建了一个线程,它执行refreshScr()
函数。refreshScr()
函数用于让蛇自行向左移动。
refreshScr()
函数代码如下:
void* refreshScr()
{
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs = 1s)
}
}
语句pthread_create(&t2, NULL, changeDir, NULL);
创建了另一个线程,它执行changeDir()
函数。changeDir()
函数在此处用于监测键盘,并输出所摁下的方向键。
changeDir()
函数代码如下:
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;
}
}
}
其中,key
为全局变量
至此,完整代码如下:
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
int key;
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->row = head->row;
new->col = head->col-1;
new->next = head;
head = new;
}
void initSnake()
{
struct Snake *p;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
void moveSnake()
{
addNode();
deleteNode();
if(head->row==0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
void* refreshScr()
{
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs = 1s)
}
}
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(void)
{
pthread_t t1;
pthread_t t2;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshScr, NULL);
pthread_create(&t2, NULL, changeDir, NULL);
while(1);
endwin();
return 0;
}
编译运行后,摁下←
,界面如下:
此时蛇的身子能自动向左移动,并且摁下方向键↑
、↓
、←
、→
均能识别出来并输出在屏幕上
使用键盘改变蛇移动的方向
增加一个全局变量dir,用于存储蛇当前移动的方向
在initSnake()
里,将dir初始化成RIGHT,并把该语句放在靠前的位置
在changeDir()
里检测键盘方向键以改变dir的值。changeDir()
代码修改至如下:
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;
}
}
}
其中,DOWN
、UP
、LEFT
、RIGHT
的值如下:
#define UP 1
#define DOWN 2
#define LEFT 3
#define RIGHT 4
然后在addNode()
里根据dir的值创建新节点
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->next = NULL;
switch(dir)
{
case UP:
new->row = head->row-1;
new->col = head->col;
break;
case DOWN:
new->row = head->row+1;
new->col = head->col;
break;
case LEFT:
new->row = head->row;
new->col = head->col-1;
break;
case RIGHT:
new->row = head->row;
new->col = head->col+1;
break;
}
new->next = head;
head = new;
}
至此,完整代码如下:
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#define UP 1
#define DOWN 2
#define LEFT 3
#define RIGHT 4
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
int key;
int dir;
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->next = NULL;
switch(dir)
{
case UP:
new->row = head->row-1;
new->col = head->col;
break;
case DOWN:
new->row = head->row+1;
new->col = head->col;
break;
case LEFT:
new->row = head->row;
new->col = head->col-1;
break;
case RIGHT:
new->row = head->row;
new->col = head->col+1;
break;
}
new->next = head;
head = new;
}
void initSnake()
{
struct Snake *p;
dir = LEFT;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
void moveSnake()
{
addNode();
deleteNode();
if(head->row==0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
void* refreshScr()
{
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs = 1s)
}
}
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;
}
}
}
int main(void)
{
pthread_t t1;
pthread_t t2;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshScr, NULL);
pthread_create(&t2, NULL, changeDir, NULL);
while(1);
endwin();
return 0;
}
编译运行,摁下方向键即可操纵蛇身移动
如果编译后运行发现像下图这样一团糟,可参考最后的补充一栏解决
到目前为止,关于蛇身移动,还存在的问题有:
- 蛇能原地调头走
- 地图最上面的一行不能行走,有一堵“隐形墙”
使蛇不能直接反方向移动
更改UP
和DOWN
的值为相反数,LEFT
和RIGHT
的值为相反数
#define UP 1
#define DOWN -1
#define LEFT -2
#define RIGHT 2
摁下方向键来改变dir的值的时候应该先进行判断,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;
}
}
}
其中,turn()
函数用于判断是否允许更改全局变量dir。turn()
函数代码如下:
void turn(int direction)
{
if(abs(dir) != abs(direction))
dir = direction;
}
abs()
是取括号内的数的绝对值。
-
如果传进来的参数(也就是监测到键盘所按下的方向键)的绝对值,与当前dir的绝对值不相等,dir的值就可以更改为键盘所按下的方向键的值
-
如果传进来的参数(也就是监测到键盘所按下的方向键)的绝对值,与当前dir的绝对值相等,说明键盘摁下的值要么和蛇当前前进的是方向一样的,要么和蛇当前前进的方向是相反的,就不对dir的值进行改变
解决地图最上面一行不能行走的问题:
moveSnake()
中,对蛇头进行撞墙判断的if语句修改至如下即可:
if(head->row<0 || head->col==0 || head->row==20 || head->col==20)
只是将head->row==0
修改成head->row<0
原因:在gamePic()
绘制边界时,row=0的时候就绘制了下图红框部分(上面的虚线加左右两个竖杆):
因此红框这一行都是属于row=0,而原先的判断条件head->row==0
就使得这一行不能行走
增加食物,蛇吃了后变长一格
添加一个struct snake
类型的结构体变量food,并定义一个initFood()
来初始化食物的位置
struct Snake food;
void initFood()
{
static int x = 2;
static int y = 2;
food.row = x;
food.col = y;
x+=2;
y+=2;
}
在这里,设定食物的初始位置在(2,2),每吃一次后食物的x和y坐标各增加2
在绘制界面的时候增加一个判断,如果是食物的坐标就打印"##",gamePic()
中绘制食物的代码修改至如下:
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else if(hasFood(row,col))
printw("##");
else
printw(" ");
}
其中hasFood(row, col)
用于判断在当前row和col这个点,是否存在食物,如果存在则返回1,如果不存在则返回0。hasFood()
代码如下:
int hasFood(int i, int j)
{
if(foo.row==i && food.col==j)
return 1;
return 0;
}
initFood()
在initSnake()
中调用,放在靠前的位置即可
实现蛇吃到食物后身子变长:
在moveSnake()
中对蛇头进行判断,如果当前蛇头的节点的row值和col值,等于食物的row值和col值,说明吃到了食物,蛇身移动的时候就不用删除最后一个节点了;否则就删除尾节点。用hasFood()
来判断当前头结点的位置是否有存在食物即可。moveSnake()
修改代码至如下:
void moveSnake()
{
addNode();
if(hasFood(head->row, head->col))
initFood();
else
deleteNode();
if(head->row<0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
至此,完整代码如下:
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#define UP 1
#define DOWN -1
#define LEFT -2
#define RIGHT 2
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
int key;
int dir;
struct Snake food;
void initFood()
{
static int x = 2;
static int y = 2;
food.row = x;
food.col = y;
x+=2;
y+=2;
}
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->next = NULL;
switch(dir)
{
case UP:
new->row = head->row-1;
new->col = head->col;
break;
case DOWN:
new->row = head->row+1;
new->col = head->col;
break;
case LEFT:
new->row = head->row;
new->col = head->col-1;
break;
case RIGHT:
new->row = head->row;
new->col = head->col+1;
break;
}
new->next = head;
head = new;
}
void initSnake()
{
struct Snake *p;
dir = LEFT;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
initFood();
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
int hasFood(int i, int j)
{
if(food.row==i && food.col==j)
return 1;
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else if(hasFood(row,col))
printw("##");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
void moveSnake()
{
addNode();
if(hasFood(head->row, head->col))
initFood();
else
deleteNode();
if(head->row<0 || head->col==0 || head->row==20 || head->col==20)
initSnake();
}
void* refreshScr()
{
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs = 1s)
}
}
void turn(int direction)
{
if(abs(dir) != abs(direction))
dir = direction;
}
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;
}
}
}
int main(void)
{
pthread_t t1;
pthread_t t2;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshScr, NULL);
pthread_create(&t2, NULL, changeDir, NULL);
while(1);
endwin();
return 0;
}
运行编译后,当蛇吃到食物后自身会长一格,食物会依次出现在(2,2),(4,4),(6,6)……直到消失(坐标超过了边界)
此时会多出一个问题:蛇变长后,如果咬到自己并不会死
使食物出现的位置随机
要让食物的坐标值是随机的,可使用rand()
来产生一个随机数
地图的宽20,要得到一个0≤x<20的数,可以用rand()%20
修改initFood()
代码至如下:
void initFood()
{
int x = rand()%20;
int y = rand()%20;
food.row = y;
food.col = x;
}
编译运行,即可使食物在随机位置出现
但是又会多出一个问题,运行多几次后有时会发现食物消失了,在界面上没出现食物
这是因为按照上面rand()%20
的写法,食物的col
值,也就是x
值,能取到0。
看绘制界面的时候的代码:
for(row=0;row<20;row++)
{
if(row == 0)
for(col=0;col<20;col++)
printw("--");
printw("\n");
for(col=0;col<=20;col++)
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else if(hasFood(row,col))
printw("##");
else
printw(" ");
if(row == 19)
{
printw("\n");
for(col=0;col<20;col++)
printw("--");
}
}
绘制左边竖线的时候col
等于0,绘制右边竖线的时候col
等于20,所以左边框就有可能会盖住食物。因此食物的col
的取值范围应该是[1,19]
生成[0,19]的整数:
a = rand()%20;
生成[1,20]的整数:a = 1 + rand()%20;
生[n,m]的整数:n + rand()%(m-n+1);
继续修改initFood()
代码至如下:
void initFood()
{
int x = 1 + (rand()%19);
int y = rand()%20;
food.row = y;
food.col = x;
}
编译运行,即可让食物正常出现
蛇咬到自己就会死
蛇是否咬到自己可放在moveSnake()
中去判断,moveSnake()
代码修改至如下:
void moveSnake()
{
addNode();
if(hasFood(head->row, head->col))
initFood();
else
deleteNode();
if(ifSnakeDie())
initSnake();
}
其中,ifSnakeDie()
用于判断蛇是否撞界或咬到自己,如果撞界或咬到自己就返回1。代码如下:
int ifSnakeDie()
{
struct Snake *p;
p = head->next;
if(head->row<0 || head->col==0 || head->row==20 || head->col==20)//判断是否撞界
return 1;
while(p != NULL)//判断是否咬到自己
{
if(p->row == head->row && p->col == head->col)
return 1;
p = p->next;
}
return 0;
}
至此,简单的贪吃蛇小游戏即完成
完整代码
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#define UP 1
#define DOWN -1
#define LEFT -2
#define RIGHT 2
struct Snake
{
int row;
int col;
struct Snake *next;
};
struct Snake *head = NULL;
struct Snake *tail = NULL;
int key;
int dir;
struct Snake food;
void initFood()
{
int x = 1 + (rand()%19);
int y = rand()%20;
food.row = y;
food.col = x;
}
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
new->next = NULL;
switch(dir)
{
case UP:
new->row = head->row-1;
new->col = head->col;
break;
case DOWN:
new->row = head->row+1;
new->col = head->col;
break;
case LEFT:
new->row = head->row;
new->col = head->col-1;
break;
case RIGHT:
new->row = head->row;
new->col = head->col+1;
break;
}
new->next = head;
head = new;
}
void initSnake()
{
struct Snake *p;
dir = LEFT;
while(head != NULL)
{
p = head;
head = head->next;
free(p);
}
initFood();
head = (struct Snake *)malloc(sizeof(struct Snake));
head->row = 1;
head->col = 19;
head->next = NULL;
tail = head;
addNode();
addNode();
}
int hasSnakeBody(int y, int x)
{
struct Snake *p = head;
while(p!=NULL)
{
if(y==p->row && x==p->col)
return 1;
p = p->next;
}
return 0;
}
int hasFood(int i, int j)
{
if(food.row==i && food.col==j)
return 1;
return 0;
}
void gamePic()
{
int row = 0;
int col = 0;
move(0,0);
for(row=0; row<20; row++)
{
if(row==0)
for(col=0; col<20; col++)
printw("--");
printw("\n");
for(col=0; col<=20; col++)
{
if(col==0 || col==20)
printw("|");
else if(hasSnakeBody(row,col))
printw("[]");
else if(hasFood(row,col))
printw("##");
else
printw(" ");
}
if(row==19)
{
printw("\n");
for(col=0; col<20; col++)
printw("--");
}
}
printw("\n");
}
void deleteNode()
{
struct Snake *p;
p = head;
while(p->next != tail)
p = p->next;
free(tail);
tail = p;
tail->next = NULL;
}
int ifSnakeDie()
{
struct Snake *p;
p = head->next;
if(head->row<0 || head->col==0 || head->row==20 || head->col==20)
return 1;
while(p != NULL)
{
if(p->row == head->row && p->col == head->col)
return 1;
p = p->next;
}
return 0;
}
void moveSnake()
{
addNode();
if(hasFood(head->row, head->col))
initFood();
else
deleteNode();
if(ifSnakeDie())
initSnake();
}
void* refreshScr()
{
while(1)
{
moveSnake();
gamePic();
refresh();
usleep(100000);//=0.1s (1000000μs = 1s)
}
}
void turn(int direction)
{
if(abs(dir) != abs(direction))
dir = direction;
}
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;
}
}
}
int main(void)
{
pthread_t t1;
pthread_t t2;
initscr();
keypad(stdscr,1);
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshScr, NULL);
pthread_create(&t2, NULL, changeDir, NULL);
while(1);
endwin();
return 0;
}
补充
有时编译后运行后,代码没有问题,但是界面会一团糟,例如下图:
这属于是Ncurse的坑,在确保代码没有问题的情况下,可以再次编译运行一次,如果不行就再编译运行一次…
或者可以在refreshScr()
中,在moveSnake()
语句下一行处添加noecho()
。noecho()
意思是不要打印一些无关的信息(比如功能键)的信息出来。
如果添加了这个也不管用就只能再进行编译试多几次…