在继上篇[C语言]贪吃蛇_结构数组实现大半年后,链表实现的版本也终于出炉了。两篇隔了这么久除了是懒癌晚期的原因外,对整个游戏流程的改进,模块的精简也花了一些时间(都是借口)。

优化模块的前沿链接:

·游戏流程结构的改进

·对输入的甄别与判断

·单链表元素移动

一、游戏流程

贪吃蛇游戏的原理很简单,即在一张地图内,有一条蛇和随机出现的食物,玩家操控蛇的移动,当蛇吃到了食物后,蛇长度增加。游戏过程中,蛇不能撞墙,也不能咬到自身。

反映到程序中,就是这样一张简略的流程图(结构数组实现):

在这个流程中,有许多的不足。当蛇已经存在并且接受了一个合法的输入时,根据下一步是否吃到食物来判断是否需要清除尾巴是合理的,但在控制台里,贪吃蛇每次循环移动其实都只需对两个位置进行操作:一个是接受操作后的蛇头,无论下一步在哪儿,这都是必须要打印的一个;另一个是蛇尾,这则需要根据蛇头是否吃到食物来决定去留。所以每次循环都重新打印所有节点是很多余的,因此需要改进。

我们可以这样改:在接受输入后,先把一定会移动的蛇头打印出来,再判断蛇尾的去留。最后在蛇(链表)各个节点中,依次赋得前一个节点的值。流程图移动模块如下:

按照这个流程图,蛇每次移动就只需要操作控制台上的两个节点了。另外可以将在控制台某坐标打印一个特殊符号抽象成一个函数:

  1. #define SPACE 0  
  2. #define NODE 1  
  3. #define FOOD 2  
  4. #define WALL 3  
  5.     
  6. void PrintIn(int size,int x,int y);  
  7.     
  8.     
  9. void PrintIn(int size,int x,int y)  
  10. {  
  11.     //size  
  12.     //清除节点:0    打印蛇身:1        
  13.     //打印食物:2    打印墙壁:3   
  14.     char *arr[4] = {" ","⊙","●","■"};  
  15.     Pos(x,y);  
  16.     printf("%s",arr[size]);  
  17. }  

二、初始化

1.初始化地图

[C语言]贪吃蛇_结构数组实现中我提到过,因为控制台一个字符的宽高所占像素点不同,所以再看控制台上想输出一个规整的正方形,就得让宽高之比为2:1。并且为了输出的正方形更完整,就需要使用一些占两个普通字符的特殊字符。

  1. #define WIDTH 60  
  2. #define HEIGHT 30  
  3.     
  4. void CreateMap(void);  
  5.     
  6. void CreateMap(void)  
  7. {  
  8.     int i;  
  9.     for(i=0;i<WIDTH;i+=2)// 上下30 宽   
  10.     {  
  11.         PrintIn(WALL,i,0);  
  12.         PrintIn(WALL,i,HEIGHT-1);  
  13.     }  
  14.     for(i=1;i<HEIGHT-1;i++)//左右 28+2 高   
  15.     {  
  16.         PrintIn(WALL,0,i);  
  17.         PrintIn(WALL,WIDTH-2,i);  
  18.     }  
  19. }  

2.初始化蛇

在初始化蛇之前,我们得给蛇一个定义:蛇应该是一个链表,其中每个节点都包含了一个坐标。所以有如下定义:

  1. typedef struct {  
  2.     int x;  
  3.     int y;  
  4. }Place;     //坐标   
  5.     
  6. typedef struct node{  
  7.     Place place;  
  8.     struct node *next;  
  9. }Node;      //节点   
  10.     
  11. typedef struct snake{  
  12.     Node *head;  
  13.     int size;   //长度   
  14. }Snake;     //指向一条蛇   
  15.      

    因此当我们声明

  16. Snake snake;  

    时,我们其实就声明了一条蛇。

    好了,现在可以给蛇赋予节点了。原理也很简单,在链表尾部加三个节点就好。我们规定蛇头在右,共有三个节点,位置居中,所以蛇头的坐标应该为(28,14),后两个节点依次为(26,14)、(24,14)。

  17. bool InitializeSnake(Snake *psnake)  
  18. {  
  19.     Node *pnew;  
  20.     Node *scan;  
  21.          
  22.     for(int i = 0;i<3;i++)  
  23.     {  
  24.         scan = (psnake->head);  
  25.         pnew = (Node *)malloc(sizeof(Node));  
  26.         if(pnew == NULL)  
  27.         {  
  28.             printf("pnew == NULL");  
  29.             system("pause");  
  30.             return false;   
  31.         }  
  32.         pnew->place.x = 28-2*i;  
  33.         pnew->place.y = 14;  
  34.         pnew->next = NULL;  
  35.         psnake->size++;  
  36.         PrintIn(NODE,pnew->place.x,pnew->place.y);  
  37.         if(scan == NULL)  
  38.              psnake->head = pnew;  
  39.         else  
  40.         {  
  41.             while(scan->next != NULL)  
  42.                 scan = scan->next;  
  43.             scan->next = pnew;  
  44.         }  
  45.     }  
  46.     return true;  
  47. }  

3.初始化食物

食物可用一个全局变量来表示,该变量存储一个坐标值。因此可用上之前定义的Place结构。

  1. typedef Place Food;  
  2.     
  3. Food food = {0,0};  

    而坐标值的范围只要保证两点就好:在地图内;不与蛇身重合。

  4. void CreateFood(void)  
  5. {   
  6.     int flag = 0;  
  7.     srand((unsigned int)time(0));  
  8.     while(1)  
  9.     {  
  10.         do{  
  11.             food.x = rand()%(WIDTH-5)+2;  
  12.         }while(food.x%2!=0);  
  13.         food.y = rand()%(HEIGHT-2)+1;  
  14.         Node *scan = snake.head;  
  15.         while(scan !=NULL)  
  16.         {  
  17.             if(scan->place.x == food.x &&  
  18.                 scan->place.y == food.y)  
  19.                 {  
  20.                     flag = -1;  
  21.                     break;  
  22.                 }  
  23.             scan = scan->next;  
  24.         }  
  25.         if(flag>=0)  
  26.         {  
  27.             PrintIn(FOOD,food.x,food.y);  
  28.             break;  
  29.         }  
  30.     }  
  31. //    AfterEatFood();  
  32. }  

二、蛇的移动——输入的甄别

蛇的移动本质很简单,就是不断更新蛇的位置,并打印。所以我们需要一个循环:

  1. while(true)    
  2. {    
  3.  //。。。  
  4. }   

    其次我们需要接收输入,用来控制游戏进行

这里介绍一个函数

  1. 1.  int kbhit(void);    
  2. 值,否则返回0  

这是一个非阻塞函数,有键按下时返回非0,但此时按键码仍然在键盘缓冲队列中。所以在确定键盘有响应之后,再用一个char变量将输入从缓冲区中调出来。

  1. 1.  if(kbhit())    
  2. 2.      ch = getch();    

    现在我们规定游戏中'w' 's' 'a' 'd'控制方向,空格暂停,所以对于用户的输入,我们需要判断是否合法。我用了一个数组+循环来代替一连串的if:

  3. char ch,direction = ' ';  
  4. char charr[5] = {'w','s','a','d',' '};  
  5. int flag = 0;  
  6. if(kbhit())  
  7.     ch = getch();  
  8. for(int i = 0;i<5;i++)   //判断输入是否为规定的五个字符   
  9. {  
  10.     if(ch == charr[i])  
  11.     {  
  12.         flag = 1;  
  13.         break;  
  14.     }  
  15. }  

    当我们得到的输入合法时,我们仍需判断现在的输入方向是否与之前的方向相反,毕竟在我设计的这个游戏里,蛇身可不能折叠往自己身上碾过去。

    在我用数组实现的那个版本里,我用了一大串if-else来避免相反的输入,这虽然简单,却很无脑。所以我用一个更简单的方法代替了它。在我们规定为正确输入的五个字符中,ASCII码分别为a:97,d:100,w:119,s:115,space:32,其中ad是冲突的一对,ws是冲突的一对。ad的差值为±3,ws的差值为±4,空格直接暂停,因此不予考虑。所以我们只需要判断,如果输入ch的值与方向direction的差值为±3或者±4,那么就可以断定输入不合法,丢弃。

  16. if(flag == 1)   //确认输入正常   
  17. {  
  18.     if(!(direction-ch==4||direction-ch==-4||direction-ch==3||direction-ch==-3))  
  19.     {   //排除与方向相反的输入   
  20.         direction = ch;  
  21.     }  
  22.     else if(ch == ' ')  
  23.         continue;  
  24. }  

    之前版本10行的事情,现在有意义的代码只有5行。

三、蛇的移动

为了方便对移动的坐标进行操作,我们声明一个数组,用来存储不同方向下坐标的变化:

  1. int dir_value[2][4] = {  
  2.     {0,0,-2,2},  
  3.     {-1,1,0,0}  
  4. };  

    不同下标分别对于w s a d,因为长度60的WIDTH其实只有30个单位,所以x值一次加2。

1、画面上的移动

由于蛇身每个节点都一个样,所以没有必要每次循环都把所有的节点重新输出一遍,只需要更新头节点和尾节点就好。在游戏中,无论是撞墙、还是其他情况,蛇只要移动了,那么他头节点的坐标一定会改变,因此我们可以在移动后先把新的蛇头打印出来。至于蛇尾,如果蛇移动后并没有吃到食物,蛇尾则删除,吃到了的话蛇尾则保留。所以在打印了头部之后再判断头部是否吃到食物,再对蛇尾进行处理。

  1. switch(direction)  
  2.         {  
  3.             case 'w':  
  4.                 PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]);    //打印头部  
  5.                 if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)  
  6.                 {  
  7.                     //AddNode(&snake);  //尾插法  
  8.                     //CreateFood();  
  9.                 }  
  10.                 else     //没有吃到  
  11.                 {  
  12.                     Node *tail = GetTail(&snake);  
  13.                     PrintIn(SPACE,tail->place.x,tail->place.y);     //画面上消除尾部节点  
  14.                 }  
  15. //...  
  16. }  

2、画面外的移动

在内存中,我们则需要更新各个节点的坐标。如果吃到了食物,则加入一个节点(我用的尾插法),并将前一节点的值赋给后一节点。先前的头节点坐标值赋给第二节点,头节点则根据输入,更新新的坐标值。没有吃到的话,也直接赋值,尾节点坐标值因为下一步就要更新,所以可丢弃不管,只需得到前一节点坐标就好。

  1. case 'w':  
  2.                 PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]);  
  3.                 if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)  
  4.                 {  
  5.                     AddNode(&snake);    //尾插法  
  6.                     CreateFood();  
  7.                 }  
  8.                 else  
  9.                 {  
  10.                     Node *tail = GetTail(&snake);   //得到尾节点  
  11.                     PrintIn(SPACE,tail->place.x,tail->place.y);  
  12.                 }  
  13.                 RenewSnake(&snake);   //链表各节点值的跟新  
  14.                 snake.head->place.x += dir_value[0][0];  //蛇头更新  
  15.                 snake.head->place.y += dir_value[1][0];  
  16.                 break;  

    其中RenewSnake()函数用来更新一个链表(蛇),使前一个节点的值赋给后一个节点,对这个只需要两个临时变量就可以。

从这简单的流程图可看出一点端倪,现在我们把步骤完善一下。

因此我们得到了一些普适性的方法,代码如下:

  1. void RenewSnake(Snake *psnake)  
  2. {  
  3.     int x_index[2] = {0,0},y_index[2] = {0,0};  
  4.     Node *scan = psnake->head;  
  5.         
  6.     int i = 1;  
  7.     x_index[i%2] = scan->place.x;  
  8.     y_index[i%2] = scan->place.y;  
  9.         
  10.     for(i = 1;i<psnake->size;i++)  
  11.     {     
  12.         x_index[(i+1)%2] = scan->next->place.x;  
  13.         y_index[(i+1)%2] = scan->next->place.y;  
  14.             
  15.         scan->next->place.x = x_index[i%2];  
  16.         scan->next->place.y = y_index[i%2];  
  17.             
  18.         scan = scan->next;  
  19.     }  
  20. }  

    同理,其余三个方向也是如此。

四、移动后的操作

在这个游戏中,我们需要这么几个变量:

  1. int length = -1;  
  2. int score = -10;  
  3. int speed = 250;  

其中,length其实可以不需要。我们需要在吃到食物后进行一系列的操作,如加分,重新生成食物等等。所以在移动时的判断里加入一些函数。

  1. if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)  
  2. {  
  3.     AddNode(&snake);    //尾插法  
  4.     CreateFood();  
  5. }  

    生成食物还需要加分等操作,所以我们可以把加分等操作的函数(AfterEatFood();)放到该函数末尾。不过这样的话,游戏开始生成的第一个食物就需要注意了,因此我们的两个全局变量都是负值。

  6. void AfterEatFood()  
  7. {  
  8.     Pos(WIDTH+20,HEIGHT-20);  
  9.     printf("%d = %d",++length,snake.size);  
  10.     Pos(WIDTH+16,HEIGHT-18);  
  11.     if(speed>150)  
  12.         score += 10;  
  13.     else  
  14.         score += 20;  
  15.     printf("%d",score);  
  16.     if(speed>100)  
  17.         speed-=5;  
  18.     Pos(WIDTH+16,HEIGHT-16);  
  19.     printf("%d",speed);  
  20. }  

    在蛇移动后,我们还需判断蛇是否撞墙或者咬到自身。撞墙是蛇头与边界坐标的比较,咬到自身则可以用一个循环。

  21. if(ThroughWall(&snake) == true)  
  22. {  
  23.     Pos(0,30);  
  24.     system("pause");  
  25.     exit(0);   
  26. }  
  27. if(BiteItself(&snake)==true)  
  28. {  
  29.     Pos(0,30);  
  30.     system("pause");  
  31.     exit(0);   
  32. }         
  1. bool ThroughWall(Snake *psnake)  
  2. {  
  3.     if(psnake->head->place.x == 0 || psnake->head->place.x == WIDTH-2 ||  
  4.         psnake->head->place.y == 0 || psnake->head->place.y == HEIGHT-1)  
  5.         {  
  6.             Pos(25,15);  
  7.             printf("撞墙,游戏结束!");  
  8.             return true;  
  9.         }  
  10.     else  
  11.     {  
  12.         Pos(0,HEIGHT);  
  13.         printf(" ");    //将闪烁不停的光变放到地图外面---迷之操作=。=   
  14.         return false;  
  15.     }  
  16. }  
  17.     
  18. bool BiteItself(Snake *psnake)  
  19. {  
  20.     Node *scan = psnake->head;  
  21.         
  22.     while(scan->next != NULL)  
  23.     {  
  24.         scan = scan->next;  
  25.         if(scan->place.x == psnake->head->place.x &&  
  26.             scan->place.y == psnake->head->place.y)  
  27.         {  
  28.             Pos(25,15);  
  29.             printf("咬到自身,游戏结束!");  
  30.             return true;  
  31.         }  
  32.     }  
  33.     return false;  
  34. }  

    最后在循环末尾加入Sleep,控制游戏的节奏。

  35. Sleep(speed);  

五、附注

1、源代码地址:贪吃蛇链表实现源码

2、主函数截图:

3、运行截图:

[C语言]链表实现贪吃蛇及部分模块优化的更多相关文章

  1. 【C语言项目】贪吃蛇游戏(上)

    目录 00. 目录 01. 开发背景 02. 功能介绍 03. 欢迎界面设计 3.1 常用终端控制函数 3.2 设置文本颜色函数 3.3 设置光标位置函数 3.4 绘制字符画(蛇) 3.5 欢迎界面函 ...

  2. 【C语言项目】贪吃蛇游戏(下)

    目录 00. 目录 07. 游戏逻辑 7.5 按下ESC键结束游戏 7.6 判断是否撞到墙 7.7 判断是否咬到自己 08. 游戏失败界面设计 8.1 游戏失败界面边框设计 8.2 撞墙失败界面 8. ...

  3. C语言之贪吃蛇

    利用链表的贪吃蛇,感觉自己写的时候还是有很多东西不熟悉, 1.预编译 2.很多关于系统的头文件也不是很熟悉 3.关于内存 第一个是.h头文件 #ifndef _SNAKE_H_H_H #define ...

  4. JavaScript版—贪吃蛇小组件

    最近在学习JavaScript,利用2周的时间看完了<JavaScript高级编程>,了解了Js是一门面向原型编程的语言,没有像C#语言中的class,也没有私有.公有.保护等访问限制的级 ...

  5. 贪吃蛇(C语言版)链表实现

    贪吃蛇 gitee:贪吃蛇C语言版: Snake 蛇的结构 typedef struct Snake { int x; int y; struct Snake *next; }; 游戏开始欢迎界面 / ...

  6. 小项目特供 贪吃蛇游戏(基于C语言)

    C语言写贪吃蛇本来是打算去年暑假写的,结果因为ACM集训给耽搁了,因此借寒假的两天功夫写了这个贪吃蛇小项目,顺带把C语言重温了一次. 是发表博客的前一天开始写的,一共写了三个版本,第一天写了第一版,第 ...

  7. c语言贪吃蛇详解3.让蛇动起来

    c语言贪吃蛇详解3.让蛇动起来 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识点. 上次 ...

  8. 贪吃蛇小游戏-----C语言实现

    1.分析 众所周知,贪吃蛇游戏是一款经典的益智游戏,有PC和手机等多平台版本,既简单又耐玩.该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,蛇不能撞墙,也不能装到自己,否则游戏结束.玩过贪吃蛇的 ...

  9. C/C++编程笔记:C语言贪吃蛇源代码控制台(二),分数和食物!

    接上文<C/C++编程笔记:C语言贪吃蛇源代码控制台(一),会动的那种哦!>如果你在学习C语言开发贪吃蛇的话,零基础建议从上一篇开始哦!接下来正式开始吧! 三.蛇的运动 上次我已经教大家画 ...

随机推荐

  1. Android-画板

    在上一篇博客,Android-图像原理/绘制原理,讲解到绘图原理中,画布 + 画笔

  2. Provider 模式

    Provider 模式:为一个API进行定义和实现的分离. 常见场景:DBPrider切换,第3方集成API切换 以发邮件为例: Email Provider Config: public abstr ...

  3. Redis配置参数汇总

    ==配置文件全解=== ==基本配置daemonize no 是否以后台进程启动databases 16 创建database的数量(默认选中的是database 0) save 900 1 #刷新快 ...

  4. asp.net——XML格式导出Excel

    下面介绍一种导出Excel的方法: 此方法不需要在服务器上安装Excel,采用生成xml以excel方式输出到客户端,可能需要客户机安装excel,所以也不会有乱七八糟的权限设定,和莫名其妙的版本问题 ...

  5. Android / iOS 招聘

    1. 面试题 https://github.com/ChenYilong/iOSInterviewQuestions 2. 一些不错的idea CDI - Développeur iOS/Androi ...

  6. delphi 在字符串中输出单引号'

    在程序代码里,用单引号 引起来的两个单引号,经过编译后是一个单引号.'''ok''':编译后表示'ok';

  7. Python3.5 学习四

    装饰器 定义:本质是函数,装饰其他函数,即为其他函数添加附加功能的 原则: 1 不能修改被装饰函数的源代码 2 不能改变被装饰函数的调用方式(对于被装饰函数来说完全透明,不会受影响) 实现装饰器功能的 ...

  8. vsftpd服务器配置虚拟用户

    添加宿主用户 新建系统用户vsftpd,用户目录为/home/wwwroot, 用户登录终端设为/bin/false(即使之不能登录系统) useradd vsftpd -d /home/wwwroo ...

  9. Javascript对象的几种创建方式

    (1) 工厂模式 Function(){ Var child = new object() Child.name = “欲泪成雪” Child.age=”20” Return child; } Var ...

  10. 初识Mybatis框架

    mybatis框架  主要是对数据库进行操作的 编写sql语句 使我们对数据库的crud操作更加简洁方便!! 1.使用mybatis框架 进行第一个项目 查询数据库 并返回数据 :(简单) (1)搭建 ...