在继上篇[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. [翻译]Writing Component Editors 编写组件的编辑器

    Writing Component Editors  编写组件的编辑器   All common control editors (opened from a control's context me ...

  2. vs2008快捷键极其技巧

    vs2008快捷键极其技巧 1. 工具: Microsoft Visual Studio 2008 Version 9.0.21022.8 RTM Microsoft .NET Framework V ...

  3. ArcGIS(批量)删除属性字段

    ArcGIS下删除属性字段有两种方式:① 单个删除:② 批量删除. 单个删除 批量删除 尽管如此,ArcGIS桌面软件在属性字段的编辑上并不太方便,所以我们自己做了一些工具辅助平时的内业处理工作.(* ...

  4. TortoiseSVN本地版本控制管理

    TortoiseSVN 是 Subversion 版本控制系统的一个免费开源客户端.下载地址:https://tortoisesvn.net/downloads.html. 安装好TortoiseSV ...

  5. TSQL--标示列、GUID 、序列

    --1. IDENTIY 列不能为空,不能设默认值,创建后不能使用ALTER TABLE TableName ALTER COLUMN修改,每张表只能有一个自增列--2. 查看当前值:SELECT I ...

  6. 通过hive向写elasticsearch的写如数据

    通过hive向写elasticsearch的写如数据 hive 和 elasticsearch 的整合可以参考官方的文档: ES-hadoop的hive整合 : https://www.elastic ...

  7. docker容器日志清理

    1.先查看磁盘空间 df -h 2.找到容器的containerId-json.log文件,并清理(治标不治本,log迟早还会大的) 查看各个容器的log文件大小 find /var/lib/dock ...

  8. ASP.NET Core学习总结(2)

    public class ControllerActionInvoker : ResourceInvoker, IActionInvoker 我们知道,ControllerActionInvoker实 ...

  9. PostMan工具使用之基础篇

    PostMan工具使用之基础篇 一.什么是PostMan Postman一款非常流行的API调试工具.(其他测试工具 Jmeter.soapUI) 二.下载及安装: 1.下载: 下载地址:https: ...

  10. 虚拟化安全 sandbox 技术分析

    原文链接:https://cloud.tencent.com/developer/news/215218 前言: libvirt-4.3搭配qemu-2.12使用,如果使用默认的编译选项,可能会让qe ...