[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,今后会不定期更新一些乱七八糟的玩意儿,开心就好。

目录
相关文章
|
24天前
|
传感器 算法 安全
【C语言】两个数组比较详解
比较两个数组在C语言中有多种实现方法,选择合适的方法取决于具体的应用场景和性能要求。从逐元素比较到使用`memcmp`函数,再到指针优化,每种方法都有其优点和适用范围。在嵌入式系统中,考虑性能和资源限制尤为重要。通过合理选择和优化,可以有效提高程序的运行效率和可靠性。
75 6
|
27天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
53 5
|
27天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
1月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
1月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
1月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
57 4
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
89 16
|
2月前
|
存储 编译器 C语言
【c语言】数组
本文介绍了数组的基本概念及一维和二维数组的创建、初始化、使用方法及其在内存中的存储形式。一维数组通过下标访问元素,支持初始化和动态输入输出。二维数组则通过行和列的下标访问元素,同样支持初始化和动态输入输出。此外,还简要介绍了C99标准中的变长数组,允许在运行时根据变量创建数组,但不能初始化。
59 6
|
2月前
|
存储 人工智能 BI
C语言:数组的分类
C语言中的数组分为一维数组、多维数组和字符串数组。一维数组是最基本的形式,用于存储一系列相同类型的元素;多维数组则可以看作是一维数组的数组,常用于矩阵运算等场景;字符串数组则是以字符为元素的一维数组,专门用于处理文本数据。
|
2月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。