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

目录
相关文章
|
1月前
|
存储 C语言 C++
【C语言数组】
【C语言数组】
|
9天前
|
存储 编译器 C语言
【C语言基础考研向】09 一维数组
数组是一种有序集合,用于存储相同类型的数据,便于统一操作与管理。例如,将衣柜底层划分为10个格子存放鞋子,便于快速定位。在C语言中,数组定义格式为 `类型说明符数组名[常量表达式];`,如 `int a[10];` 表示定义了一个包含10个整数的数组。数组初始化时可以直接赋值,也可以部分赋值,且数组长度必须固定。数组在内存中连续存储,访问时需注意下标范围,避免越界导致数据异常。数组作为参数传递时,传递的是首地址,修改会影响原数组。
|
9天前
|
存储 C语言
【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串
本文介绍了C语言中字符数组的初始化方法及其在函数间传递的注意事项。字符数组初始化有两种方式:逐个字符赋值或整体初始化字符串。实际工作中常用后者,如`char c[10]=&quot;hello&quot;`。示例代码展示了如何初始化及传递字符数组,并解释了为何未正确添加结束符`\0`会导致乱码。此外,还讨论了`scanf`函数读取字符串时忽略空格和回车的特点。
|
14天前
|
存储 编译器 程序员
C语言程序的基本结构
C语言程序的基本结构包括:1)预处理指令,如 `#include` 和 `#define`;2)主函数 `main()`,程序从这里开始执行;3)函数声明与定义,执行特定任务的代码块;4)变量声明与初始化,用于存储数据;5)语句和表达式,构成程序基本执行单位;6)注释,解释代码功能。示例代码展示了这些组成部分的应用。
28 10
|
12天前
|
C语言
C语言程序设计核心详解 第四章&&第五章 选择结构程序设计&&循环结构程序设计
本章节介绍了C语言中的选择结构,包括关系表达式、逻辑表达式及其运算符的优先级,并通过示例详细解释了 `if` 语句的不同形式和 `switch` 语句的使用方法。此外,还概述了循环结构,包括 `while`、`do-while` 和 `for` 循环,并解释了 `break` 和 `continue` 控制语句的功能。最后,提供了两道例题以加深理解。
|
12天前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
12天前
|
C语言
C语言程序设计核心详解 第三章:顺序结构,printf(),scanf()详解
本章介绍顺序结构的基本框架及C语言的标准输入输出。程序从`main()`开始依次执行,框架包括输入、计算和输出三部分。重点讲解了`printf()`与`scanf()`函数:`printf()`用于格式化输出,支持多种占位符;`scanf()`用于格式化输入,需注意普通字符与占位符的区别。此外还介绍了`putchar()`和`getchar()`函数,分别用于输出和接收单个字符。
|
12天前
|
存储 人工智能 C语言
C语言程序设计核心详解 第六章 数组_一维数组_二维数组_字符数组详解
本章介绍了C语言中的数组概念及应用。数组是一种存储同一类型数据的线性结构,通过下标访问元素。一维数组定义需指定长度,如`int a[10]`,并遵循命名规则。数组元素初始化可使用 `{}`,多余初值补0,少则随机。二维数组扩展了维度,定义形式为`int a[3][4]`,按行优先顺序存储。字符数组用于存储字符串,初始化时需添加结束符`\0`。此外,介绍了字符串处理函数,如`strcat()`、`strcpy()`、`strcmp()` 和 `strlen()`,用于拼接、复制、比较和计算字符串长度。
|
12天前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
1月前
|
算法 C语言
C语言------数组
这篇文章是关于C语言数组的实训,包括一维数组、二维数组和字符数组的定义、赋值、输入、输出方法,并通过实例代码演示了数组的使用和一些基本算法,如冒泡排序。
C语言------数组