贪吃蛇小游戏主要运用了链表和线程实现游戏的运行,三要素分别是:地图->蛇身移动、增加、撞墙和咬自己->在地图范围内随机生成食物。接下来分步实现:
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 4
int 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 -2
void 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]范围内的随机数。文章来源:https://www.toymoban.com/news/detail-506186.html
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; //每增加一个食物,得分+1
noecho();
}
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();
}
到这里贪吃蛇小游戏就已经成功了,时不时写一个贪吃蛇有助于加强操作链表的记忆,欢迎交流分享,完整代码在专栏中可以找到。文章来源地址https://www.toymoban.com/news/detail-506186.html
到了这里,关于【C语言】贪吃蛇实现思路详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!