继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

[C语言]链表实现贪吃蛇及部分模块优化

蝴蝶不菲
关注TA
已关注
手记 364
粉丝 81
获赞 379

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

 

优化模块的前沿链接:

        ·游戏流程结构的改进

        ·对输入的甄别与判断

        ·单链表元素移动

 

一、游戏流程

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

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

    https://img2.mukewang.com/5b6d970700014df203060558.jpg

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

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

    https://img4.mukewang.com/5b6d971e000138ae03460466.jpg

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

  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. 2.  // 检查当前是否有键盘输入,若有则返回一个非0值,否则返回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()函数用来更新一个链表(蛇),使前一个节点的值赋给后一个节点,对这个只需要两个临时变量就可以。

https://img1.mukewang.com/5b6d97500001c6b506480281.jpg

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

https://img1.mukewang.com/5b6d975f0001a85806240258.jpg

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

  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、主函数截图:

https://img4.mukewang.com/5b6d978600019d2503440441.jpg

    3、运行截图:

https://img3.mukewang.com/5b6d979c0001408806070441.jpg

原文出处:https://www.cnblogs.com/magicxyx/p/9456533.html

打开App,阅读手记
3人推荐
发表评论
随时随地看视频慕课网APP