[C语言]贪吃蛇_结构数组实现

简介: 一、设计思路蛇身本质上就是个结构数组,数组里存储了坐标x、y的值,再通过一个循环把它打印出来,蛇的移动则是不断地刷新重新打印。所以撞墙、咬到自己只是数组x、y值的简单比较。 二、用上的知识点结构数组Windows API函数 三、具体实现先来实现静态页面,把地图、初始蛇身、食物搞定。

一、设计思路

蛇身本质上就是个结构数组,数组里存储了坐标x、y的值,再通过一个循环把它打印出来,蛇的移动则是不断地刷新重新打印。所以撞墙、咬到自己只是数组x、y值的简单比较。

 

二、用上的知识点

  1. 结构数组
  2. Windows API函数

 

三、具体实现

先来实现静态页面,把地图、初始蛇身、食物搞定。

这里需要用到Windows API的知识,也就是对控制台上坐标的修改

 

  1. //这段代码来自参考1
  2. void Pos(int x, int y)  
  3. {  
  4.     COORD pos;  
  5.     HANDLE hOutput;  
  6.     pos.X = x;  
  7.     pos.Y = y;  
  8.     hOutput = GetStdHandle(STD_OUTPUT_HANDLE);  
  9.     SetConsoleCursorPosition(hOutput, pos);  
  10. }  

    COORD是Windows API中定义的一种结构,表示在控制台上的坐标

  11. typedef struct _COORD {  
  12. SHORT X; // horizontal coordinate  
  13. SHORT Y; // vertical coordinate  
  14. } COORD;

    而代码中第八行则是获得屏幕缓冲区的句柄,第九行是直接修改光标位置的函数。

 

1.地图。

有了Pos()函数,打印一个框就不是问题了。假如我们用"-"作为上下边框,把"|"作为左右边框,这看起来没什么不妥,但其实我们已经掉进了坑里,直接上代码及实际效果图吧。

  1. //LONG==60
  2. //WIDTH==30
  3. void CreateMap()  
  4. {  
  5.     int i;  
  6.     for(i=0;i<LONG;i++)//上下两行   
  7.     {  
  8.         Pos(i,1);   
  9.         printf("-");  
  10.         Pos(i,WIDTH-1);  
  11.         printf("-");  
  12.     }  
  13.     for(i=2;i<WIDTH-1;i++)//左右两列   
  14.     {  
  15.         Pos(0,i);  
  16.         printf("|");   
  17.         Pos(LONG-1,i);  
  18.         printf("|");  
  19.     }  
  20. }  

    发现了问题吗?这是一条正常的蛇。。。那为什么看起来不正常呢?我们把边框都换成"#"来看看…

    这就清楚多了啊,要知道我们上下边框可是各有60个"#"的,长60宽30的长方形输出之后竟然成了个正方形。

    原因在这

    控制台上每个字符的长宽比例(像素点)是不同的,所以才会出现上图这种蛋疼的情况。

    解决方法其实也很简单,我们需要引入一些特殊符号,比如"●""■""⊙"等,这些字符的特点是它占据两个普通字符的位置

    所以上下边框就有60/2=30个符号,要让它仍然是个正方形的话,左右也可以设为30(28+2)个符号.

    代码及效果图如下

  21. void CreateMap()  
  22. {  
  23.     int i;  
  24.     for(i=0;i<LONG;i+=2)  
  25.     {  
  26.         Pos(i,0);  
  27.         printf("■");  
  28.         Pos(i,WIDTH-1);  
  29.         printf("■");  
  30.     }  
  31.     for(i=1;i<WIDTH-1;i++)  
  32.     {  
  33.         Pos(0,i);  
  34.         printf("■");  
  35.         Pos(LONG-2,i);  
  36.         printf("■");  
  37.     }  
  38. }  

    这样看就舒服多了,不过也让复杂度提升了一些,上边框每个符号的坐标分别是(0,0)(2,0)(4,0)…(2*n-2,0)这个在蛇的移动及食物的模块再提。

     

2.初始化一条蛇

因为蛇以及食物 本质上都是一个坐标,所以我们可以定义一个新的数据类型Node,每一个Node都是一个存储了两个变量(x、y)的结构体,再通过Node来定义蛇和食物。

  1. typedef struct node{  
  2.     int x;  
  3.     int y;  
  4. }Node;  
  5.     
  6.     
  7. Node snake[60];

    好了,我们现在定义了一条叫snake的蛇。为了这条蛇肥胖适中长宽比例一致,我们用"⊙"代表蛇的每一节。刚开始我们令蛇出现在地图中间位置,蛇头在右,共3个节点。所以我们需要求得每个节点的坐标。

  8.      void InitializeSnake()  
  9. {  
  10.     int i;  
  11.     for(i=0;i<3;i++)  
  12.     {  
  13.         snake[i].x = (LONG/2-i*2);//(30,15)(28,15)(26,15)  
  14.         snake[i].y = WIDTH/2;  
  15.         Pos(snake[i].x,snake[i].y);  
  16.         printf("");  
  17.     }  
  18. }  

    这样我们就在(30,15)(28,15)(26,15)三个坐标处确定了一条蛇。X坐标之间减2是因为""在X轴占两个基本值。

y\x

26

27

28

29

30

31

15

 

3.随机出现食物

        先创建一个变量来存储食物的坐标

Node food; 

        得到它的坐标其实就是用随机值对长、宽取余,使值在区间(地图)范围内。

  1. void CreateFood()  
  2. {  
  3.     int i;  
  4.     srand((unsigned int)time(0));  
  5.     while(1)  
  6.     {  
  7.         do{  
  8.             food.x = rand()%(LONG-6)+2;  
  9.         }while(food.x%2!=0);  
  10.         food.y = rand()%(WIDTH-2)+1;  
  11.         for(i=0;i<3+length;i++)  
  12.             if(food.x==snake[i].x && food.y==snake[i].y)  
  13.             {  
  14.                 i=-1;  
  15.                 break;  
  16.             }  
  17.         if(i>=0)  
  18.         {  
  19.             Pos(food.x,food.y);  
  20.             printf("●");  
  21.             break;   
  22.         }  
  23.     }  
  24.     //AfterEatFood();   
  25. }  

    X的坐标值求法为rand()%(LONG-6)+2,因为食物""也是两个字符的位置,所以它可能的取值为(2,y)(4,y)…(56,y)上下变宽共30个字符,从0开始,每个+2,所以最后一个为(58,y)

    Rand()%(LONG)的取值范围为0~59而x=1,x=2,x=58,x=59是地图范围,所以得对LONG-6(60-6=54)取余,这样取值范围就是0~54,再加2,就成了2~56.又因为蛇的各节坐标及移动x坐标都是+2,所以食物的x坐标必须是偶数,这可以用一个do(…)while()搞定,先取值,再判断,不行就再取值

    Y的坐标稍微简单些,只要保证坐标值在1~28就行。

    另外求出了坐标之后要判断食物是否与蛇身重合,重合的话重新赋值。

 

搞完上面的,我们就有了一个基本的(静态)效果了,现在我们要让它动起来

    注:第86行是设置控制台窗口长、宽的系统函数。

 

4.让蛇动起来

     蛇每次移动背后发生的事就是数组里的值改变,再在每个坐标位置打印蛇身。

    为了让蛇一直动,我们就需要一个循环    

  1. while(1)  
  2. {  
  3.     //获得输入,改变坐标  
  4.     //在每个坐标处输出   

首先,我们需要确定方向,而这需要两个变量,一个是输入值(可能是任意值),另一个则是确定方向的变量。

这里介绍一个函数

  1. int kbhit(void);  
  2. // 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0

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

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

再对ch做判断,如果是符合情况(不能往后走等)的输入,则开始执行switch改变坐标

  1. if(ch=='w'&&direction!='s')  
  2.     direction = ch;  
  3. else if(ch=='s'&&direction!='w')  
  4.     direction = ch;  
  5. else if(ch=='a'&&direction!='d')  
  6.     direction = ch;  
  7. else if(ch=='d'&&direction!='a')  
  8.     direction = ch;  
  9. else if(ch==' ')  
  10.     continue

这里设置空格是暂停,而为了让蛇一开始就移动,我们把direction设置为d(往右)。

 

在方向确定了之后,再用一个switch语句进行坐标判断

  1. switch(direction)  
  2. {  
  3.     case 'w':  
  4.         if(snake[0].x==food.x && snake[0].y-1==food.y)  
  5.         {  
  6.             length++;  
  7.             score+=10;  
  8.             snake[2+length].x = snake[2+length-1].x;  
  9.             snake[2+length].y = snake[2+length-1].y;  
  10.             for(i=length+3-2;i>0;i--)  
  11.             {  
  12.                 snake[i].x = snake[i-1].x;  
  13.                 snake[i].y = snake[i-1].y;  
  14.             }  
  15.             CreateFood();  
  16.         }  
  17.         else  
  18.         {  
  19.             Pos(snake[2+length].x,snake[2+length].y);  
  20.             printf(" ");  
  21.             for(i=length+3-1;i>0;i--)  
  22.             {  
  23.                 snake[i].x = snake[i-1].x;  
  24.                 snake[i].y = snake[i-1].y;  
  25.             }  
  26.         }  
  27.         snake[0].y -=1;  
  28.         break;  
  29.     case 's':  
  30.         //。。。   
  31.     case 'a':  
  32.         //。。。   
  33.     case 'd':  
  34.         //。。。   
  35. }  

    对蛇头的下一步做判断,如果吃到了食物的话,则先对分数等全局变量进行处理,再把snake[2+length-1](吃到食物后的倒数第二个变量)的值赋值给snake[2+length](此时新加的尾节)。

    再从倒数第二节开始,把前一节的坐标值赋给后一节,直到第二节得到了之前蛇头坐标。在食物被吃了之后,再调用随机出现食物函数。

如果没有吃到食物的话,先到之前最后一节的坐标处,输入空格,算是销毁它再对各节重新赋值。在蛇头后每节都赋值完成之后,根据输入值单独对蛇头赋值,如输入是'w',则往上,所以蛇头纵坐标减一。

对其余输入也是同样的道理,在snake数组各值都更新之后,再用一个函数把它打印出来。

这样移动部分就实现了,现在只需处理一些小模块就行。

 

5.移动后的处理。

这一部分相对简单,即对判断蛇是否撞墙、是否咬到自身,再对这种情况做处理,我们用两个函数搞定它

  1. int ThroughWall()  
  2. {  
  3.     if(snake[0].x==0 || snake[0].x==58 ||  
  4.         snake[0].y==0 || snake[0].y==29)  
  5.         {  
  6.             Pos(25,15);  
  7.             printf("撞墙 游戏结束");  
  8.             return 1;  
  9.         }  
  10.         Pos(0,WIDTH);  
  11.         printf(" ");  
    1. int BiteItself()  
    2. {  
    3.     int i;  
    4.     for(i=3;i<=2+length;i++)  
    5.         if((snake[0].x==snake[i].x) && (snake[0].y==snake[i].y))  
    6.         {  
    7.             Pos(25,15);  
    8.             printf("咬到自己 游戏结束");  
    9.             return 1;  
    10.         }  
    11. }  

当返回值为1时,游戏也就GG了。

  1. if(ThroughWall()==1)  
  2. {  
  3.     Pos(25,WIDTH);  
  4.     system("pause");   
  5.     exit(0);   
  6. }  
  7. if(BiteItself()==1)  
  8. {  
  9.     Pos(25,WIDTH);  
  10.     system("pause");  
  11.     exit(0);   
  12. }

最后再加一行Sleep()函数,对刷新时间(每次重新打印的时间间隔)做处理。speed是一个变量,在每次吃到食物后递减。

Sleep(speed);

 

 

源代码在这:结构数组实现_贪吃蛇源码

 

四、总结与反思。

    首先从蛇的结构上来说,结构数组的实现直接无视了"效率"这个词,数组占用大量空间且有容量限制,并不是一种好办法。

    其次是BUG的问题,在ThroughWall()函数中,在对蛇头坐标进行判断时在蛇头移动到(x,1)位置时,游戏直接结束,且没有任何提示。

但诡异的是,在判断后加入 Pos(0,WIDTH);printf(" "); 这两行不相干的语句后,这个问题解决了,而我对这两行语句的原有目的则只是想把闪烁不停光标放到地图外面去。

    还有就是while()循环里代码行太多,特别是switch-case 里各项,蛇身的移动(结构数组个元素坐标值的变换)应该抽象成一个move()函数。

 

五、其他。

    这是对我第一份代码(snakeV1.0)的重构,程序结构上有较大变化

    重构期间研究了链表实现_贪吃蛇源码,在结构上采用了里面的部分思想。

 

    个人空空如也的github:MagicXyxxx的github,今后会不定期更新一些乱七八糟的玩意儿,开心就好。

目录
相关文章
|
14天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
57 16
|
27天前
|
存储 编译器 C语言
【c语言】数组
本文介绍了数组的基本概念及一维和二维数组的创建、初始化、使用方法及其在内存中的存储形式。一维数组通过下标访问元素,支持初始化和动态输入输出。二维数组则通过行和列的下标访问元素,同样支持初始化和动态输入输出。此外,还简要介绍了C99标准中的变长数组,允许在运行时根据变量创建数组,但不能初始化。
37 6
|
1月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 人工智能 BI
C语言:数组的分类
C语言中的数组分为一维数组、多维数组和字符串数组。一维数组是最基本的形式,用于存储一系列相同类型的元素;多维数组则可以看作是一维数组的数组,常用于矩阵运算等场景;字符串数组则是以字符为元素的一维数组,专门用于处理文本数据。
|
1月前
|
存储 C语言
C语言:一维数组的不初始化、部分初始化、完全初始化的不同点
C语言中一维数组的初始化有三种情况:不初始化时,数组元素的值是随机的;部分初始化时,未指定的元素会被自动赋值为0;完全初始化时,所有元素都被赋予了初始值。
|
1月前
|
存储 数据管理 编译器
揭秘C语言:高效数据管理之数组
揭秘C语言:高效数据管理之数组
|
1月前
|
C语言 C++
保姆式教学C语言——数组
保姆式教学C语言——数组
17 0
保姆式教学C语言——数组
|
1月前
|
C语言
数组栈的实现(C语言描述)
本文介绍了如何在C语言中使用数组来实现栈的数据结构,包括栈的创建、入栈、出栈、获取栈顶元素、检查栈是否为空、获取栈的大小以及销毁栈等操作,并提供了相应的函数实现。
27 1
|
1月前
|
C语言
顺序表数组法构建(C语言描述)
如何使用C语言通过数组方法构建有序顺序表,包括顺序表的创建、插入、删除和打印等。
18 2
|
1月前
|
C语言 定位技术 API
【C语言】实践:贪吃蛇小游戏(附源码)(二)
【C语言】实践:贪吃蛇小游戏(附源码)
【C语言】实践:贪吃蛇小游戏(附源码)(二)