【C语言】贪吃蛇实现思路详解
贪吃蛇小游戏主要运用了链表和线程实现游戏的运行,三要素分别是:地图->蛇身移动、增加、撞墙和咬自己->在地图范围内随机生成食物。接下来分步实现:
1. 地图
1.1 ncurse图形库库
1.2 接收功能键
1.3 通过ncurse绘制地图
2. 蛇身
2.1 静态构造蛇身
2.2 动态构造蛇身
2.3 蛇身的移动
2.4 控制方向
2.5 完善蛇的死亡方式
3. 食物
1. 地图
1.1 ncurse图形库库
在讲地图之前,先简单介绍一下ncurse图形库,在C语言库函数中常用获取按键响应的方式主要有:scanf()、getchar()、gets()等,但是必须按键后回车才能完成接收,为了蛇身自主移动方便通过按键控制其方向,我们就要引入ncurse库实现不需要回车响应就能完成按键的接收。
当然,有人会说ncurse早就out了,更甚于完爆它的GTK、c++图形库QT也逐渐落伍,现在大部分嵌入式设备也都开始安卓系统,所以在这里我们只是简单引用一下通过它实现对链表的操作不做过多了解。
那怎么使用ncurse呢?
Ubuntu下输入指令安装:
sudo apt-get install libncurses5-dev
#include<curses.h> //调用库函数;int main()
{initscr();//初始化ncurse界面;printw("we are into ncurse\n");//相当于printf;getch();等待用户输入,如果没这句话程序会直接退出,看不到运行结果;endwin();程序退出,通过它来恢复shell终端的显示,如果没这句话,shell终端会乱码甚至崩掉;return 0;
}
运行结果 :
接下来再接收一个按键:
#include<curses.h>int main()
{int n;initscr();n = getch();printw("your input is :%d\n",n);getch();endwin();return 0;
}
运行结果:
按a,a的ASCII码是97,接收成功;
1.2 接收功能键
那如何获取↑ ↓ ← →功能键呢?
#include<curses.h>int main()
{int key;initscr();keypad(stdscr,1);//接收功能键,1表示是while(1){key = getch();printw("your input is :%d\n",key);}getch();endwin();return 0;
}
结果如下依次输入↑ ↓ ← →:
获取到使用功能键的值,方便接下来通过它来控制小蛇,O的K
1.3 通过ncurse绘制地图
很显然我们的地图可以看作一个二维数组,既然是二维数组,我们就可以用for()进行遍历打印,先构建一个20×30的数组打印地图边界(用#表示):
#include<curses.h>int main()
{int i,j;initscr();for(i=0;i<20;i++){for(j=0;j<30;j++){printw("#");}printw("\n");}getch();endwin();return 0;
}
运行结果:
地图我们只留下边框就好,中间部分需要去除一下:
#include<curses.h>
void initCury()
{initscr();keypad(stdscr,1);/*keypad设置了在stdscr中可以接收键盘的功能键,如:↑ ↓ ← → F1等*/
}void mapGame()
{int i,j;for(i=0;i<20;i++){if(i == 0){for(j=0;j<30;j++){printw("#");}printw("\n");}//第零行,地图上侧,全部打印#if(i>=0 && i<=18){for(j=0;j<=29;j++){if(j == 0 || j == 29){printw("#");}else{printw(" ");}}printw("\n");}//第1~18行,列只有为0和29的时候才打印#if(i == 19){for(j=0;j<=29;j++){printw("#");}}//第19行,同第0行}printw("\n");
}void main()
{initCury();//将初始化ncurse封装mapGame();//将地图打印封装getch();endwin();
}
运行结果:
地图 O的K。
2. 蛇身
2.1 静态构造蛇身
我们知道蛇身始终在地图内,这里用@表示,先静态定义几个蛇身以作演示:
#include<curses.h>struct Snake{int hang;int lie;struct Snake *next;
};int score = 10;struct Snake initSnake1 = {0,14,NULL};//蛇尾,表头
struct Snake initSnake2 = {1,14,NULL};
struct Snake initSnake3 = {2,14,NULL};
struct Snake initSnake4 = {3,14,NULL};//蛇头void initCury()
{initscr();keypad(stdscr,1);
}int hasNode(int x,int y)
{struct Snake *p;p = &initSnake1;//定义一个局部变量将链表头传过来while(p->next != NULL){if(p->hang < x && p->lie == y){ //判断该坐标是否有蛇身return 1;}p = p->next;}return 0;
}void mapGame()
{int i,j;for(i=0;i<20;i++){if(i == 0){for(j=0;j<=29;j++){printw("#");}printw("\n");}if(i>=0 && i<=18){for(j=0;j<=29;j++){if(j == 0 || j == 29){printw("#");}else if(hasNode(i,j)){printw("@");/*如果蛇身坐标等于所遍历的坐标,则打印蛇身@ */}else{printw(" ");}}}}printw("\n");}if(i == 19){for(j=0;j<=29;j++){printw("#");}}}
}void main()
{initSnake1.next = &initSnake2;initSnake2.next = &initSnake3;initSnake3.next = &initSnake4;initCury();mapGame();getch();endwin();
}
2.2 动态构造蛇身
静态蛇身不利于蛇身移动,我们来动态开辟一个蛇身:
struct Snake{int hang;int lie;struct Snake *next;
};//将蛇身定义为一个结构体方便移动和增加struct Snake *head;
struct Snake *tail;
/*将head和tail定义为全局变量*/
void initSnake()
{head = (struct Snake*)malloc(sizeof(struct Snake));//malloc动态开辟一个head空间head->hang = 0;head->lie = 15;head->next = NULL;//定义一个表头初始坐标tail = head;
}
2.3 蛇身的移动
蛇身打印好那怎么让他还是动呢?这里采取的方法是将将链表头移到表尾:
既然移动要删除头节点并且向表尾增加一个节点,我们先来构造增加和删除节点的函数:
void addBody()
{struct Snake *newBody = (struct Snake*)malloc(sizeof(struct Snake));//动态开辟一个新空间存储删掉的节点newBody->hang = tail->hang+1;//由于初始方向我们给定向下,所以行坐标+1,纵坐标不变newBody->lie = tail->lie;newBody->next = NULL;tail->next = newBody;tail = newBody;//重新使表尾为新节点
}void delBody()
{struct Snake *p;p = head;head = head->next;//将链表头指向下一个节点,表头就独立出来了free(p);//释放掉旧表头
}
那如何使用构造出来的两个函数呢?
void moveBody()
{addBody();delBody();//通过不断增加表尾释放表头使蛇向下移动if(tail->hang==0||tail->lie==0||tail->hang==20||tail->lie==30){/*判断如果蛇撞墙,重新初始化蛇身,意味着游戏重新开始*/initSnake();}
}
用while(1)使用这个函数蛇身就能不断向下移动
void main()
{int conKey;//定义一个局部变量存储按键initCury();initSnake();mapGame();while(1){conKey=getch();if(conKey==KEY_DOWN){//KEY_DOWN是ncurse中的宏参数,即↓键moveBody();mapGame();refresh();//每次移动坐标都要刷新一下才能显示移动的效果}}getch();endwin();
}
我们玩贪吃蛇不能靠我们自己按键来移动吧,所以要构造一个函数帮我们刷新:
void refreshPage()
{while(1){moveBody();mapGame();refresh();usleep(150000);//延时150毫秒}
}
2.4 控制方向
调用它我们就能不使用方向键让蛇自己向下移动,接下来就是通过方向键改变蛇的方向了,首先需要宏定义一下方向,其次需要定义一个全局变量dir来表示蛇运动的方向,并将值赋为DOWN也就是初始向下,然后通过按键来控制蛇的方向:
#define UP 1
#define DOWN 2
#define LEFT 3
#define RIGHT 4int dir;//定义dir(方向)全局变量,dir初始方向为DOWN,在initSnake()中初始化void addBody()
{struct Snake *newBody = (struct Snake*)malloc(sizeof(struct Snake));newBody->hang = tail->hang+1;newBody->lie = tail->lie;newBody->next = NULL;switch(dir){ //控制蛇头方向:比如按↑,蛇头移动到链表尾上方case UP:newBody->hang = tail->hang-1;newBody->lie = tail->lie;break;case DOWN:newBody->hang = tail->hang+1;newBody->lie = tail->lie;break;case LEFT:newBody->hang = tail->hang;newBody->lie = tail->lie-1;break;case RIGHT:newBody->hang = tail->hang;newBody->lie = tail->lie+1;break;}tail->next = newBody;tail = newBody;
}void contKey()
{while(1){key = getch();switch(key){case KEY_UP:printw("UP\n");dir = UP;break;case KEY_DOWN:printw("DOWN\n");dir = DOWN;break;case KEY_LEFT:printw("LEFT\n");dir = LEFT;break;case KEY_RIGHT:printw("RIGHT\n");dir = RIGHT;break;}}
}150,1-8 89%
写到这里运行的话就会崩掉,因为刷新界面和控制方向存在同步关系,为了使得移动刷新的同时操作蛇改变方向就要用到线程来解决:
线程的创建:
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
pthread_t *restrict tidp:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。
const pthread_attr_t *restrict attr:通过该参数可以定制线程的属性,比如可以指定新建线程栈的大小、调度策略等。如果创建线程无特殊的要求,该值也可以是NULL,表示采用默认属性,通常都用NULL。
oid *(*start_rtn)(void *):线程需要执行的函数。创建线程,是为了让线程执行一定的任务。线程创建成功之后, 该线程就会执行start_routine函数,该函数之于线程,就如同main函数之于主线程。
void *restrict arg:传递给start_routine函数的实参,当不需要传递任何数据时,将arg赋值为NULL即可。
创建三个线程同时进行:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* fun1()
{while(1){printf("this is fun1\n");sleep(1);}
}void* fun2()
{while(1){printf("this is fun2\n");sleep(1);}
}
void* fun3()
{while(1){printf("this is fun3\n");sleep(1);}
}int main()
{pthread_t t1;pthread_t t2;pthread_t t3;pthread_create(&t1,NULL,fun1,NULL);pthread_create(&t2,NULL,fun2,NULL);pthread_create(&t3,NULL,fun3,NULL);while(1);return 0;
}
现在三个while就可以同时运行了,创建两个线程同时运行refreshPage()和contKey():
void main()
{pthread_t t1;pthread_t t2;initCury();initSnake();mapGame();pthread_create(&t1,NULL,refreshPage,NULL);pthread_create(&t2,NULL,contKey,NULL);while(1);getch();endwin();
}
现在就可以控制蛇上下左右移动了,但是有个bug,如果蛇头方向为下但是按上键,会发现蛇会直接往上移动,简直开了挂!这里就要用到一个函数:abs()
abs()函数用于求整数的绝对值,比如abs(-1)和abs(1)的返回值都是1;
根据abs()函数我们就可将方向进行define,按键绝对值相同时不发生方向的改变:
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2void turn(int direction)
{if(abs(dir) != abs(direction)){ /*将目前方向传入,判断按键方向绝对值是否等于目前方向,如果不等于,改变其方向,(即:dir=1, 方向向上,按↓时dir=-1,但abs(-1)=1=原先的dir,方向不发生改变,只有按←和→时,abs(dir)变为2,方向才会发生改变)*/dir = direction;}
}void* contKey()
{while(1){key = getch();switch(key){case KEY_UP:printw("key:UP\n");turn(UP);break;case KEY_DOWN:printw("key:DOWN\n");turn(DOWN);break;case KEY_LEFT:printw("key:LEFT\n");turn(LEFT);break;case KEY_RIGHT:printw("key:RIGHT\n");turn(RIGHT);break;}}noecho();//屏蔽掉控制字符(如组合键操作)
}
现在蛇身的控制就已经写完了。
2.5 完善蛇的死亡方式
蛇的死亡方式有两种:撞墙和咬自己,现在封装一个函数来完美实现蛇的死亡用于mapGame()的判断。
int snakeKilled()
{struct Snake *p;p = head;if(tail->hang<0||tail->lie==0||tail->hang==20||tail->lie==30){ //撞墙return 1;}while(p->next!=NULL){if(p->hang==tail->hang && p->lie==tail->lie){ /*判断tail(蛇头)是否与链表节点相等,相等就是咬到自己*/return 1;}p = p->next;}return 0;
}
在moveBody()中根据该函数返回值判断蛇是否死亡就好,如果死亡重新initSnake()就实现死亡后重新开始游戏。
3. 食物
在贪吃蛇中食物是随机生成的,这里要用到rand()函数:
rand():C语言中用来产生一个随机数的函数.使用方法是rand ()% (n-m+1)+m,这个式子表示产生 [m,n]范围内的随机数。
struct Snake food;
int score=0;void* initFood()
{int x = rand()%20;//行:随机在0~20行生成食物x坐标int y = rand()%30;//列:随即在0~30列生成食物y坐标while(x==0 || y==0 || x==20 || y==30){ //避免食物在墙体内部,重新随机生成食物x = rand()%20;y = rand()%30;}food.hang = x;food.lie = y;score+=1; //每增加一个食物,得分+1noecho();
}int hasFood(int x,int y)
/*将墙内坐标传入,如果食物坐标与传入坐标相等,在mapGame()中打印$表示食物*/
{if(food.hang == x && food.lie == y){return 1;}return 0;
}void mapGame()
{int i,j;move(0,0);for(i=0;i<=19;i++){if(i == 0){for(j=0;j<=30;j++){printw("#");}printw("\n");}// 0 if(i>=0 || i<=19){for(j=0;j<=30;j++){if(j == 0 || j == 30){printw("#");}else if(hasNode(i,j)){printw("@");}else if(hasFood(i,j)){printw("$");}else{printw(" ");}}printw("\n");}if(i == 19){for(j=0;j<=30;j++){printw("#");}}}printw("\nScore: %d$\n",score);printw("food.x:%d, food.y:%d\n",food.hang,food.lie);noecho();
}
到这里贪吃蛇小游戏就已经成功了,时不时写一个贪吃蛇有助于加强操作链表的记忆,欢迎交流分享,完整代码在专栏中可以找到。
【C语言】贪吃蛇实现思路详解相关推荐
- c++实现经典游戏贪吃蛇(超详解)
经典版: 要求: 1碰到四周和自身游戏结束: 2迟到食物蛇身增长: 3食物吃得越多游戏越快: 4通过w,a,s,d可控制蛇头的方向: 5蛇身颜色不一致. 源码(需要easy-x) // 贪吃蛇.cpp ...
- C语言贪吃蛇游戏代码,贪吃蛇C语言代码实现大全
一.C语言贪吃蛇代码实现前言 设计贪吃蛇游戏的主要目的是让大家夯实C语言基础,训练编程思维,培养解决问题的思路,领略多姿多彩的C语言. 贪吃蛇是非常经典的一款游戏,本次我们模拟在控制台实现贪吃蛇游戏, ...
- C语言贪吃蛇详解4,c语言贪吃蛇详解4.食物的投放与蛇的变长
c语言贪吃蛇详解4.食物的投放与蛇的变长 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识 ...
- C语言处理贪吃蛇游戏蛇的长度,c语言贪吃蛇详解4.食物的投放与蛇的变长
c语言贪吃蛇详解4.食物的投放与蛇的变长 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识 ...
- c语言贪吃蛇详解5.GameOver功能与显示成绩
c语言贪吃蛇详解5.GameOver功能与显示成绩 以前我们已经做出来了一个能吃东西变长的蛇.不过它好像不会死... 现在就来实现一下game over的功能吧. 写个函数判断蛇是否撞到自己或者撞到墙 ...
- c语言贪吃蛇添加排行榜,c语言贪吃蛇排行榜_...12年4月编程语言排行榜 C语言荣归宝座...
12年4月编程语言排行榜 C语言荣归宝座 JPG,902x531,131KB,424_250 C语言在目前的编程语言排行榜上占据头名的位置-全屏显示课程章节 JPG,500x267,232KB,467 ...
- 784-C语言rand和srand用法详解
C语言rand和srand用法详解 在实际编程中,我们经常需要生成随机数,例如,贪吃蛇游戏中在随机的位置出现食物,扑克牌游戏中随机发牌. 在C语言中,我们一般使用 <stdlib.h> 头 ...
- 50行的python游戏代码_50行代码实现贪吃蛇(具体思路及代码)
[下载文档: 50行代码实现贪吃蛇(具体思路及代码).txt ] (友情提示:右键点上行txt文档名->目标另存为) 50行代码实现贪吃蛇(具体思路及代码) 最近一直在准备用来面试的几个小de ...
- 关于日期正则表达式的思路详解
1 概述 首先需要说明的一点,无论是Winform,还是Webform,都有很成熟的日历控件,无论从易用性还是可扩展性上看,日期的选择和校验还是用日历控件来实现比较好. 前几天在CSDN ...
最新文章
- 我国将明确侵犯个人信息定罪及量刑标准
- Spectral clustering 谱聚类讲解及实现
- python celery异步_【Python】Celery异步处理
- [渝粤教育] 西北大学 仪器分析 参考 资料
- 从地理围栏看物联网安防
- springmvc重定向到另一个项目_springmvc怎么重定向,从一个controller跳到另外一个controller...
- latex 箭头_Pandoc上手以及如何使得latex文件转换为Docx文件(MAC)
- ip地址在c语言中长度是多少_c语言中(++i)+(++i)+(++i)究竟等于多少?
- C++ HOOK实现全局键盘钩子的详细过程
- 微信小程序或微信网页里关注公众号
- android 7.0低电耗Doze模式
- 网络广告创意与制作技巧
- iOS 应用安装包瘦身
- Efficient Protocols for Set Membership and Range Proof 学习笔记
- 图表xAxis文字竖排显示
- ESXi 6.7.0含RTL8168驱动安装包(亲测可用)
- Win7更换锁屏壁纸
- idea Maven阿里云仓库和阿里spring代理仓库配置
- 和平精英分数计算机制,和平精英加分机制怎么算 加分玩法详解
- WebSocket断开原因