一、设计思路

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

二、用上的知识点

结构数组Windows API函数

三、具体实现

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

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

//这段代码来自参考1
void Pos(int x, int y)
{
 COORD pos;
 HANDLE hOutput;
 pos.X = x;
 pos.Y = y;
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 SetConsoleCursorPosition(hOutput, pos);
}

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

typedef struct _COORD {
SHORT X; // horizontal coordinate
SHORT Y; // vertical coordinate
} COORD;

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

1.地图。

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

//LONG==60
//WIDTH==30
void CreateMap()
{
 int i;
 for(i=0;i<LONG;i++)//上下两行
 {
 Pos(i,1);
 printf("-");
 Pos(i,WIDTH-1);
 printf("-");
 }
 for(i=2;i<WIDTH-1;i++)//左右两列
 {
 Pos(0,i);
 printf("|");
 Pos(LONG-1,i);
 printf("|");
 }
}


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

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

原因在这

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

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

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

代码及效果图如下

void CreateMap()
{
 int i;
 for(i=0;i<LONG;i+=2)
 {
 Pos(i,0);
 printf("■");
 Pos(i,WIDTH-1);
 printf("■");
 }
 for(i=1;i<WIDTH-1;i++)
 {
 Pos(0,i);
 printf("■");
 Pos(LONG-2,i);
 printf("■");
 }
}

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

2.初始化一条蛇

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

typedef struct node{
 int x;
 int y;
}Node;


Node snake[60];

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

 void InitializeSnake()
{
 int i;
 for(i=0;i<3;i++)
 {
 snake[i].x = (LONG/2-i*2);//(30,15)(28,15)(26,15)
 snake[i].y = WIDTH/2;
 Pos(snake[i].x,snake[i].y);
 printf("⊙");
 }
}

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

3.随机出现食物

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

Nodefood;

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

void CreateFood()
{
 int i;
 srand((unsigned int)time(0));
 while(1)
 {
 do{
  food.x = rand()%(LONG-6)+2;
 }while(food.x%2!=0);
 food.y = rand()%(WIDTH-2)+1;
 for(i=0;i<3+length;i++)
  if(food.x==snake[i].x && food.y==snake[i].y)
  {
  i=-1;
  break;
  }
 if(i>=0)
 {
  Pos(food.x,food.y);
  printf("●");
  break;
 }
 }
 //AfterEatFood();
}

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.让蛇动起来

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

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

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

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

这里介绍一个函数

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

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

if(kbhit())
 ch = getch(); 

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

if(ch=='w'&&direction!='s')
 direction = ch;
else if(ch=='s'&&direction!='w')
 direction = ch;
else if(ch=='a'&&direction!='d')
 direction = ch;
else if(ch=='d'&&direction!='a')
 direction = ch;
else if(ch==' ')
 continue;


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

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

switch(direction)
{
 case 'w':
 if(snake[0].x==food.x && snake[0].y-1==food.y)
 {
  length++;
  score+=10;
  snake[2+length].x = snake[2+length-1].x;
  snake[2+length].y = snake[2+length-1].y;
  for(i=length+3-2;i>0;i--)
  {
  snake[i].x = snake[i-1].x;
  snake[i].y = snake[i-1].y;
  }
  CreateFood();
 }
 else
 {
  Pos(snake[2+length].x,snake[2+length].y);
  printf(" ");
  for(i=length+3-1;i>0;i--)
  {
  snake[i].x = snake[i-1].x;
  snake[i].y = snake[i-1].y;
  }
 }
 snake[0].y -=1;
 break;
 case 's':
 //。。。
 case 'a':
 //。。。
 case 'd':
 //。。。
}

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

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

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

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

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

5.移动后的处理。

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

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

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

if(ThroughWall()==1)
{
 Pos(25,WIDTH);
 system("pause");
 exit(0);
}
if(BiteItself()==1)
{
 Pos(25,WIDTH);
 system("pause");
 exit(0);
}

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

Sleep(speed);

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

四、总结与反思。

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

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

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

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

五、其他。

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

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

个人空空如也的github:MagicXyxxx的github

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

02-06 03:09