【Linux】基于Ncurse图形库的贪吃蛇(C语言)
ncurses(new curses)是一个程序库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面。在ubuntu低版本中运行不容易乱码,在高版本中gcc时,删去原先a.out多编译几次,并等待一段时间,也不会出现乱码。//判断地图上的点是否是蛇身节点。vi /usr/include/curses.h 可查看ncurse的宏定义。运行效果链接:https://v.douyin.c
目录
一、Ncurse图形库
ncurses(new curses)是一个程序库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面。它是一个虚拟终端中的“类GUI”应用软件工具箱。它还优化了屏幕刷新方法,以减少使用远程shell时遇到的延迟。
在ubuntu系统上安装库文件:
apt-get install libncurses5-dev
如何使用ncurse?
#include <curses.h>
int main()
{
initscr();//ncurse 界面的初始化函数
printw("This is curses window\n");//在ncurse模式下的printf
getch();//等待用户输入,如果没有这句话,程序就退出了,看不到运行的结果,也就是看不到上面那句话
endwin();//程序退出,调用改函数来恢复shell终端的显示,如果没有这句话,shell终端字乱码,坏掉
return 0;
}
贪吃蛇项目中还会用到的一些ncurses库中的函数(代码如下):
#include<curses.h> // Linux下图形界面库头文件
noecho(); // 不回显用户输入的内容
keypad(); // 允许用户终端的键盘,允许getch()函数获取功能键
move(x,y); //move(int x, int y);移动光标到x,y
refresh(); //更新终端屏幕
为什么要用ncurse?
ncurse是终端下实现简单图形界面的不二选择,按键响应快速。
编译curses文件
gcc xxx.c -lcurses
ncurse的上下左右键
vi /usr/include/curses.h 可查看ncurse的宏定义
二、重要步骤
1、准备工作
准备一些初始化函数以及用到的全局变量。
创建蛇身节点结构体和结构体变量食物food,并且定义全局变量key,dir保存值:
//定义贪吃蛇节点结构体
struct Snake
{
int hang;//行
int lie;//列
struct Snake *next;//下一个节点
};
struct Snake *head = NULL;//定义蛇尾(链表头)
struct Snake *tail = NULL;//定义蛇头(链表尾)
int key;//记录键入的值 changeDirection()函数中使用到
int dir;//记录方向的值 addNode()函数中使用到
struct Snake food;//定义结构体变量食物
封装一些ncurse界面的初始函数:
//函数封装初始化Ncurse界面
void initNcurse()
{
initscr();//ncurse界面的初始化函数
keypad(stdscr,TRUE);//从标准stdscr中接受功能键,TRUE代表是否接收
noecho();//大多数的交互式应用程序在初始化时会调用noecho()函数,用于在进行控制操作时不显示输入的控制字符。
}
2、打印输出游戏界面(地图、蛇身、食物)
设计游戏界面样式,运用for循环嵌套打印输出游戏地图、蛇身以及食物。
先准备两个函数:
int judgeFood(int i,int j);//判断蛇头是否吃到食物
int judgeSnakeNode(int i,int j);//判断地图上的点是否是蛇身节点
如果传过来的i和j与行和列相等,则通过gamePic()打印输出。
//判断地图上的点是否有食物
int judgeFood(int i,int j)
{
if(food.hang == i && food.lie == j){
return 1;
}
return 0;
}
//判断地图上的点是否是蛇身节点
int judgeSnakeNode(int i,int j)
{
struct Snake *p;
p = head;
while(p != NULL){
if(p->hang == i && p->lie == j){
return 1;
}
p = p->next;
}
return 0;
}
游戏界面设计(20X20):
食物“##”;
蛇身“[]”;
地图第1行就是hang=0;
gamePic()打印输出游戏界面:
//打印输出游戏界面
void gamePic()
{
int hang;
int lie;
move(0,0);//将光标定位到0行0列
for(hang=0;hang<=19;hang++){
if(hang == 0){
printw(" Gluttonous snake\n");
for(lie=0;lie<=20;lie++){
printw("--");
}
printw("\n");
}
if(hang >= 0 || hang <= 19){
for(lie=0;lie<=21;lie++){
if(lie == 0 || lie == 21){
printw("|");
}else if(judgeSnakeNode(hang,lie)){//判断蛇身节点,打印蛇身
printw("[]");
}else if(judgeFood(hang,lie)){//判断食物节点,打印食物
printw("##");
}else{
printw(" ");
}
}
printw("\n");
}
if(hang == 19){
for(lie=0;lie<=20;lie++){
printw("--");
}
printw("\n");
printw("By Apibro,food.hang=%d,food.lie=%d\n",food.hang+1,food.lie);//打印作者及当前食物坐标(hang+1)
}
}
}
3、蛇的移动及食物生成
首先要初始化蛇身initSnake(),蛇需要方向,下面再阐述,蛇(蛇头就是链表尾tail)的移动通过moveSnake()来实现,移动过程中通过addNode()在蛇头增加一节点(尾插法),与此同时在蛇尾删除一节点deleteNode(),移动过程中如果吃到食物(judgeFood()判断是否吃到食物)则增加一节,增加节点通过addNode()来实现,食物的随机生成通过initFoot()来实现,移动过程中如果撞墙或则蛇身(judgeSnakeDeath()判断蛇是否死亡)则死亡,游戏重新开始。
蛇的移动:
//蛇的移动(蛇头增加新节点,删除蛇尾节点)
void moveSnake()
{
addNode();//增加新节点
if(judgeFood(tail->hang,tail->lie)){
initFoot();//随机食物生成
}else{
deleteNode();//删除蛇尾(头结点)
}
if(judgeSnakeDeath()){
initSnake();//死亡,重新初始化蛇身,游戏重新开始
}
}
其余需要用到的函数:
//随机食物生成
void initFoot()
{
int x = rand()%20+1;
int y = rand()%20+1;
x = x-1;//为了第0行显示成第1行
food.hang = x;
food.lie = y;
}
//增加新节点(通过方向判断加在哪)
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
switch(dir){//根据键入的值来增加节点,修改tail所指结点中hang,lie并将修改后的值赋值给新节点
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->next = new;
tail = new;
}
//初始化蛇身
void initSnake()
{
struct Snake *p;
dir = RIGHT;//蛇头初始方向为RIGHT
while(head != NULL){//判断蛇是否为空,清理内存
p = head;
head = head->next;
free(p);
}
initFoot();//随机食物生成
head = (struct Snake*)malloc(sizeof(struct Snake));
head->hang = 1;
head->lie = 2;
head->next = NULL;
tail = head;//初始时尾指针指向头
addNode();
addNode();
addNode();//增加3节点,设置初始蛇身长度
}
//删除蛇尾(头结点)
void deleteNode()
{
struct Snake *p;
p = head;
head = head->next;
free(p);//释放p=head原先头节点
}
//判断蛇是否死亡
int judgeSnakeDeath()
{
struct Snake *p;
p = head;
if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 21){//当tail节点中数据到达最大边界return 1
return 1;
}
while(p->next != NULL){
if(p->hang == tail->hang && p->lie == tail->lie){//当tail节点中数据与蛇身一致时return 1
return 1;
}
p = p->next;
}
return 0;
}
4、游戏界面刷新
游戏界面固定,按回车才会变,所以需要不停打印刷新,通过moveSnack()和gamePic()函数就实现游戏界面的刷新,使贪吃蛇动起来,但此时方向固定为初始方向。
封装刷新界面函数refreshInterface();如下:
//界面刷新
void* refreshInterface()
{
while(1){
moveSnake();//移动
gamePic();//打印输出
refresh();//更新终端屏幕
usleep(100000);//控制蛇的运动速度,以微秒为单位,100毫秒睡眠一次,执行挂起不动
}
}
5、扫描键入的值判断方向
有了键盘输入,就可以改变蛇的初始方向,dir可以改变,蛇也就有了方向。
宏定义及封装函数如下:
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2
//通过绝对值判断相反方向不触发
void turn(int direction)
{
if(abs(dir) != abs(direction)){
dir = direction;
}
}
//扫描键入的值判断方向
void* changeDirection()
{
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;
}
}
}
6、多个while并存(线程解决)
在贪吃蛇运动过程中,我们需要改变蛇的移动方向,这是就需要不停扫描键盘输入的值来判断方向,同时还需要不停的刷新界面,为了多个while循环并存这里需要引入linux线程。
线程的基本用法:
参考网页:Linux多线程相关的函数
在Linux中使用线程
#include <pthread.h> // 头文件
pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;
如:pthread_t t1; //多线程定义
pthread_create(&t1,NULL,refreshInterface,NULL);
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
参数4:线程主函数执行期间所使用的参数,如要传多个参数, 可以用结构封装。
使用多线程的函数必须返回指针型,如void *refreshInterface()
注:gcc xxx.c -lcurses -lpthead //编译需要连接pthead库
三、注意事项及完整代码
注意事项:
在ubuntu低版本中运行不容易乱码,在高版本中gcc时,删去原先a.out多编译几次,并等待一段时间,也不会出现乱码。
完整代码:
#include <curses.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2
//定义贪吃蛇节点结构体
struct Snake
{
int hang;//行
int lie;//列
struct Snake *next;//下一个节点
};
struct Snake *head = NULL;//定义蛇尾(链表头)
struct Snake *tail = NULL;//定义蛇头(链表尾)
int key;//记录键入的值 changeDirection()函数中使用到
int dir;//记录方向的值 addNode()函数中使用到
struct Snake food;//定义结构体变量食物
//函数封装初始化Ncurse界面
void initNcurse()
{
initscr();//ncurse界面的初始化函数
keypad(stdscr,TRUE);//从标准stdscr中接受功能键,TRUE代表是否接收
noecho();//大多数的交互式应用程序在初始化时会调用noecho()函数,用于在进行控制操作时不显示输入的控制字符。
}
//随机食物生成
void initFoot()
{
int x = rand()%20+1;
int y = rand()%20+1;
x = x-1;//为了第0行显示成第1行
food.hang = x;
food.lie = y;
}
//判断地图上的点是否有食物
int judgeFood(int i,int j)
{
if(food.hang == i && food.lie == j){
return 1;
}
return 0;
}
//判断地图上的点是否是蛇身节点
int judgeSnakeNode(int i,int j)
{
struct Snake *p;
p = head;
while(p != NULL){
if(p->hang == i && p->lie == j){
return 1;
}
p = p->next;
}
return 0;
}
//打印输出游戏界面
void gamePic()
{
int hang;
int lie;
move(0,0);//将光标定位到0行0列
for(hang=0;hang<=19;hang++){
if(hang == 0){
printw(" Gluttonous snake\n");
for(lie=0;lie<=20;lie++){
printw("--");
}
printw("\n");
}
if(hang >= 0 || hang <= 19){
for(lie=0;lie<=21;lie++){
if(lie == 0 || lie == 21){
printw("|");
}else if(judgeSnakeNode(hang,lie)){//判断蛇身节点,打印蛇身
printw("[]");
}else if(judgeFood(hang,lie)){//判断食物节点,打印食物
printw("##");
}else{
printw(" ");
}
}
printw("\n");
}
if(hang == 19){
for(lie=0;lie<=20;lie++){
printw("--");
}
printw("\n");
printw("By Apibro,food.hang=%d,food.lie=%d\n",food.hang+1,food.lie);//打印作者及当前食物坐标
}
}
}
//增加新节点(通过方向判断加在哪)
void addNode()
{
struct Snake *new = (struct Snake*)malloc(sizeof(struct Snake));
switch(dir){//根据键入的值来增加节点,修改tail所指结点中hang,lie并将修改后的值赋值给新节点
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->next = new;
tail = new;
}
//初始化蛇身
void initSnake()
{
struct Snake *p;
dir = RIGHT;//蛇头初始方向为RIGHT
while(head != NULL){//判断蛇是否为空,清理内存
p = head;
head = head->next;
free(p);
}
initFoot();//随机食物生成
head = (struct Snake*)malloc(sizeof(struct Snake));
head->hang = 1;
head->lie = 2;
head->next = NULL;
tail = head;//初始时尾指针指向头
addNode();
addNode();
addNode();//增加3节点,设置初始蛇身长度
}
//删除蛇尾(头结点)
void deleteNode()
{
struct Snake *p;
p = head;
head = head->next;
free(p);//释放p=head原先头节点
}
//判断蛇是否死亡
int judgeSnakeDeath()
{
struct Snake *p;
p = head;
if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 21){//当tail节点中数据到达最大边界return 1
return 1;
}
while(p->next != NULL){
if(p->hang == tail->hang && p->lie == tail->lie){//当tail节点中数据与蛇身一致时return 1
return 1;
}
p = p->next;
}
return 0;
}
//蛇的移动(蛇头增加新节点,删除蛇尾节点)
void moveSnake()
{
addNode();//增加新节点
if(judgeFood(tail->hang,tail->lie)){
initFoot();//随机食物生成
}else{
deleteNode();//删除蛇尾(头结点)
}
if(judgeSnakeDeath()){
initSnake();//死亡,重新初始化蛇身,游戏重新开始
}
}
//界面刷新
void* refreshInterface()
{
while(1){
moveSnake();//移动
gamePic();//打印输出
refresh();//更新终端屏幕
usleep(100000);//控制蛇的运动速度,以微秒为单位,100毫秒睡眠一次,执行挂起不动
}
}
//通过绝对值判断相反方向不触发
void turn(int direction)
{
if(abs(dir) != abs(direction)){
dir = direction;
}
}
//扫描键入的值判断方向
void* changeDirection()
{
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()
{
pthread_t t1;
pthread_t t2;
initNcurse();
initSnake();
gamePic();
pthread_create(&t1, NULL, refreshInterface, NULL);
pthread_create(&t2, NULL, changeDirection, NULL);
while(1);
getch();
endwin();
return 0;
}
四、运行效果
运行效果链接:https://v.douyin.com/6ooTQgt/
最后谢谢阅读,笔者乃小白,如有错误之处还请指正。
更多推荐
所有评论(0)