贪吃蛇代码学习心得(C语言)

贪吃蛇代码学习心得(C语言)

前言

以前自学C语言时,学了点控制台光标的相关知识,就一直想弄个小游戏出来,第一个想到的就是贪吃蛇了。上下左右控制方向,随机生成食物,吃上食物长度加一,碰到墙壁或咬到自己结束游戏......想到了很多,甚至当时兴奋的睡不着觉,可是自己一上手的时候,发现无从下手。最核心的如何让“蛇”动起来我都不知如何实现。我按照以前的按个键刷新屏幕的思路发现走不通。因为不按键的时候,蛇也是在动的,然后我就在网上看了些代码。当时没有静下心看明白,时隔几年再次学习C时,又想到了它。这次总算把它啃下来了。记录些心得,经验。

定义

#define U 1
#define D 2
#define L 3
#define R 4       //蛇的状态,U:上 ;D:下;L:左 R:右

typedef struct SNAKE //蛇身的一个节点
{
   int x;
   int y;
   struct SNAKE *next;
}snake;

//全局变量//
int score = 0, add = 10;//总得分与每次吃食物得分。
int status, sleeptime = 150,length = 4;//状态与每次运行的时间间隔与长度
snake *head, *food;//蛇头指针,食物指针
snake *q;//遍历蛇的时候用到的指针
int endGamestatus = 0; //游戏结束的情况,1:撞到墙;2:咬到自己;3:主动退出游戏。
  1. 定义4个宏,这个好理解,方便以后判断条件

  2. 蛇身采用的是链表的结构,其实发现这个结构对于这个游戏来说相当完美的结构。蛇是连续结构,一个连一个,但最重要的是头,可以通过头结点遍历整个蛇身。而且x,y坐标可以同时存储在结构体当中,比较方便。

  3. status和sleeptime是以后蛇动起来的关键,没有按键的情况下。

  4. 食物food和蛇身采用同一个结构体。

  5. 为了方便还引进了一个q指针指向头部。

函数解析

Pos();

void Pos(int x, int y)//设置光标位置
{
   COORD pos;
   HANDLE hOutput;
   pos.X = x;
   pos.Y = y;
   hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//返回标准的输入、输出或错误的设备的句柄,也就是获得输入、输出/错误的屏幕缓冲区的句柄
   SetConsoleCursorPosition(hOutput, pos);

   SetConsoleTextAttribute(hOutput,0x01|0x02); //设置颜色
}

输入x,y坐标参数,将光标设置在此处。这里面几个东西要记住,以后用到的多。句柄的作用,在网上查说是windows为了控制虚拟内存里的元素所用的标识符号,是内部的东西(就当死记住的东西好啦)。我又在最后加了一句源代码没有的,改变颜色,聊胜于无吧~如果能蛇身一种颜色,食物一种颜色,边框,计分板,随着蛇身变长还能变换。不知道能不能实现。

creatMap();

void creatMap()//创建地图
{
   int i;
   for (i = 0; i<58; i += 2)//打印上下边框
  {
       Pos(i, 0);
       printf("■");//一个方块占两个位置
       Pos(i, 26);
       printf("■");
  }
   for (i = 1; i<26; i++)//打印左右边框
  {
       Pos(0, i);
       printf("■");
       Pos(56, i);
       printf("■");
  }
}

initSnake();

void initSnake()//初始化蛇身
{
   snake* tail;
   int i;
   tail = (snake*)malloc(sizeof(snake));    //用头插初始化蛇身,以便创建完蛇身,头指针指向头部(第一个元素)
   tail->x = 24;
   tail->y = 5;
   tail->next = NULL;
   for(i=0;i<length;i++)
  {
       head = (snake*)malloc(sizeof(snake));
       head->next = tail;
       head->x = 24 + 2 * i;//一个蛇身占两个格子
       head->y = 5;
       tail = head;
  }
   q = head;
   while(q->next != NULL)
  {
       Pos(q->x,q->y);
       printf("■");
       q = q->next;
  }
}
  1. 头插法创建蛇身,这样头指针最后指向头部,最后将指针q指向蛇头。

  2. 我这里将源代码稍微改了下,初始化蛇身时就将q引进,而且输出蛇身时的while()里面的判断条件改了下,这下长度就是设置值了,源代码总是多一节。。。仔细分析,假设蛇身初始化长度为1,先创建了一个没有尾指针的tail,然后for()执行一次,head->next指向tail,tail再指向head,这里其实就有两个结点了,一个head和一个没有指向NULL的尾结点。如果按原代码的 tail != NULL,其实while()要执行两次,这样就比预想的长度多了一节。

bieteSelf();

int biteSelf()//判断是否咬到了自己
{
   snake *self;
   self = head->next;
   while (self != NULL)
  {
       if (self->x == head->x && self->y == head->y)
      {
           return 1;
      }
       self = self->next;
  }
   return 0;
}

其实这个没什么好说的,本以为很好想,自己敲的时候才发现没那么容易。首先定义一个self指向head->next,然后遍历一遍,看和头部坐标有没有重合。这一个函数贯穿整个过程,每动一下,就要遍历一遍,其实计算量挺大的。(不过都计算机来说小儿科,都没有卡顿)

createFood();

void createFood()//随机出现食物
{
   snake *food_1;
   srand((unsigned)time(NULL));
   food_1 = (snake*)malloc(sizeof(snake));

   food_1->x = rand()% 52 + 2;
   while((food_1->x % 2) != 0)
  {
       food_1->x = rand()% 52 + 2;
  }
   food_1->y = rand() % 24+1;
  // q = head;
   while(q != NULL)//食物不能和身体重合
  {
       if(q->x ==food_1->x && q->y == food_1->y)
      {
           free(food_1);
           createFood();
      }
       q = q->next;
  }

   Pos(food_1->x,food_1->y);
   food = food_1;
   printf("○");
}
  1. 这里用了srand(),用于产生随机数。

  2. 因为蛇身的x轴方向占两个,所以food_1->x 必须为偶数,while()用来筛选。

cantCrossWall();

void cantCrossWall()//不能穿墙
{
   if (head->x == 0 || head->x == 56 || head->y == 0 || head->y == 26)
  {
       endGamestatus = 1;
       endGame();
  }
}

snakeMove();【核心代码】

void snakeMove()//蛇前进,上U,下D,左L,右R
{
   snake * nexthead;
   cantCrossWall();
   nexthead = (snake*)malloc(sizeof(snake));
   if(status == U)   //状态保持向上,即一直向上运动
  {
       nexthead->x = head->x;
       nexthead->y = head->y - 1;
       if(nexthead->x == food->x&&nexthead->y == food->y) //如果有食物
      {
           nexthead->next = head;
           head = nexthead;
           q= head;
           while(q->next != NULL)
          {
               Pos(q->x,q->y);
               printf("■");
               q = q->next;
          }
           score += add;
           createFood();
      }

       else             //没有食物
          {
               nexthead->next = head;
               head = nexthead;
               q= head;
               while(q->next->next != NULL)
              {
                   Pos(q->x,q->y);
                   printf("■");
                   q = q->next;
              }
               Pos(q->x,q->y);
               printf(" ");
               free(q->next);
               q->next = NULL;     //将指针置空

          }
  }
   if (status == D)
  {
       nexthead->x = head->x;
       nexthead->y = head->y + 1;
       if (nexthead->x == food->x && nexthead->y == food->y)  //有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           score = score + add;
           createFood();
      }
       else           //没有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q->next->next != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           Pos(q->x, q->y);
           printf(" ");
           free(q->next);
           q->next = NULL;
      }
  }
   if (status == L)
  {
       nexthead->x = head->x - 2;      //横向占两格
       nexthead->y = head->y;
       if (nexthead->x == food->x && nexthead->y == food->y)//有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           score = score + add;
           createFood();
      }
       else                                //没有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q->next->next != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           Pos(q->x, q->y);
           printf(" ");
           free(q->next);
           q->next = NULL;
      }
  }
   if (status == R)
  {
       nexthead->x = head->x + 2;      //横向占两格
       nexthead->y = head->y;
       if (nexthead->x == food->x && nexthead->y == food->y)//有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           score = score + add;
           createFood();
      }
       else                                         //没有食物
      {
           nexthead->next = head;
           head = nexthead;
           q = head;
           while (q->next->next != NULL)
          {
               Pos(q->x, q->y);
               printf("■");
               q = q->next;
          }
           Pos(q->x, q->y);
           printf(" ");
           free(q->next);
           q->next = NULL;
      }
  }
   if (biteSelf() == 1)       //判断是否会咬到自己
  {
       endGamestatus = 2;
       endGame();
  }
}
  1. 这里非常巧秒的引入了一个nexthead的指针来表示蛇的下一步,而蛇的下一步和最后按键又有关系,所以最开声明了status变量。然后根据if()判断条件来选择对应的走法。真的秒!

  2. 进入if()后,又分为两种情况,下一步有食物和下一步没有食物。若有食物,吃掉它,食物消失,长度加一;若没有,就保持原状态继续移动。

  3. 先来看看有食物:nexthead的坐标和food的坐标重合,就可以满足。然后使用头插法,创建头结点,最后再将q指向新的头结点,之后while()执行输出,这里判断条件为q->next !=NULL;如果按源代码的 q != NULL;长度又会多一节。原因还是初始化(假设长度为1)时,内存中期是有两个结点,而吃掉食物后,创建了一个新的结点,这时内存中有3个,而我们只需要打印两个。吃完之后,相应的分数增加,在生成新的食物。

  4. 没有食物:懂了上面的,很显然while这里要用 q->next->next!=NULL 来作判断条件,打印完蛇身,这时你会发现多了一节,(调试过程可以很明显的看出)因为开始的那个位置的图像还存留着,所以这是要用Pos来设置光标位置,while()走完时,q指向q->next,即头最开始位置(这里都假设蛇身长度为一),然后有空格覆盖,注意时两个空格!之后在释放掉 q->next,即初始化时只有数据域的结点,而q变为新的没有尾指针的结点image-20210224125633015

  5. 上下左右,四种状态,只要把坐标变化值稍作改变,其余都一样。需注意的是:x轴方向每次变化值为2。

  6. 最后,每走一步需要判断是否会咬到自己。

pause();

void pause()//暂停
{
   while (1)       //使用无限循环达成
  {
       Sleep(300);
       if (GetAsyncKeyState(VK_SPACE))
      {
           break;
      }
  }
}

 

runGame();【核心代码】

void runGame()//控制游戏
{

   Pos(64, 15);
   printf("不能穿墙,不能咬到自己\n");
   Pos(64, 16);
   printf("用↑.↓.←.→分别控制蛇的移动.");
   Pos(64, 17);
   printf("F1 为加速,F2 为减速\n");
   Pos(64, 18);
   printf("ESC :退出游戏.space:暂停游戏.");
   Pos(64, 20);
   //printf("C语言研究中心 www.clang.cc");
   status = R;     //初始状态
   while (1)
  {
       Pos(64, 10);
       printf("得分:%d ", score);
       Pos(64, 11);
       printf("每个食物得分:%d分", add);
       if (GetAsyncKeyState(VK_UP) && status != D)
      {
           status = U;
      }
       else if (GetAsyncKeyState(VK_DOWN) && status != U)
      {
           status = D;
      }
       else if (GetAsyncKeyState(VK_LEFT) && status != R)
      {
           status = L;
      }
       else if (GetAsyncKeyState(VK_RIGHT) && status != L)
      {
           status = R;
      }
       else if (GetAsyncKeyState(VK_SPACE))
      {
           pause();
      }
       else if (GetAsyncKeyState(VK_ESCAPE))
      {
           endGamestatus = 3;
           break;
      }
       else if (GetAsyncKeyState(VK_F1))
      {
           if (sleeptime >= 50)
          {
               sleeptime = sleeptime - 30;
               add = add + 2;
               if (sleeptime == 320)
              {
                   add = 2;//防止减到1之后再加回来有错
              }
          }
      }
       else if (GetAsyncKeyState(VK_F2))
      {
           if (sleeptime<350)
          {
               sleeptime = sleeptime + 30;
               add = add - 2;
               if (sleeptime == 350)
              {
                   add = 1;  //保证最低分为1
              }
          }
      }

       Sleep(sleeptime);
       //getch();
       snakeMove();
  1. 用无限循环来达到游戏一直进行的效果,初始化状态为朝右。

  2. if (GetAsyncKeyState(VK_UP) && status != D) 这个判断条件来说明,如果按了上键并且状态不为向下,那么状态就为向上。第一个很好理解,第二个条件是因为,贪吃蛇这个游戏没有头尾互换这个操作。(当然你也可以做一个可以互换的)比如向前走着,就不能发出突然向后走的指令,要么尾变成头朝反方向走,要么直接判定咬到自己。

  3. Sleep()和snakeMove()两个函数就是蛇像动画一样动起来的原因了。若没有Sleep()或者非常低那么游戏一运行就结束了,电脑计算速度太快了,来不及给人脑反应。太高又如同幻灯片,没有连贯性。我感觉这个Sleep()就像游戏的FPS,间隔低了很流畅,高了很卡。哈哈,这个比喻很不恰当了。

endGame();

void endGame()//结束游戏
{
   system("cls");
   Pos(24, 12);
   if (endGamestatus == 1)
  {
       printf("对不起,您撞到墙了。游戏结束.");
  }
   else if (endGamestatus == 2)
  {
       printf("对不起,您咬到自己了。游戏结束.");
  }
   else if (endGamestatus == 3)
  {
       printf("您的已经结束了游戏。");
  }
   Pos(24, 13);
   printf("您的得分是%d\n\n\n", score);
char c;
while ((c = getchar()) != EOF && c != '\n');
   while (1)
  {
      printf("close?[y or n]\n");

       switch(getch())
      {
           case 'n':
        {
           system("cls");
           score = 0;
           add = 10;
           sleeptime = 150;
           length = 4;
           creatMap();
           initSnake();
           createFood();
           runGame();
           endGame();
        }
           case 'y':
        {
            exit(0);
        }
      }
  }
}    
  1. 根据endGamestatus来结束游戏,先清个屏。

  2. 源代码中只有y选项,我觉得很不爽,每次结束后,又得重新编译。于是我加了个重新开始的选择,(按n键)我定义了个char类型变量,来捕获按键,然后用while ((c = getchar()) != EOF && c != '\n');来清除缓冲区,因为为了测试稳定性,我在游戏过程中乱按字母键,出现了很多不人性化的地方,最终也是图片展示的这样,并没有达到预期的效果,就是游戏一结束就简简单单的提示是否结束游戏。所以请哪位大神在这里指点一二,感激不尽。

image-20210224132811303

initGame();gameStart();main();

void initGame()//开始界面
{
   Pos(40, 12);

   system("title 贪吃蛇小游戏");
   printf("欢迎来到贪食蛇游戏!");
   Pos(40, 25);
   system("pause");
   system("cls");
   Pos(25, 12);
   printf("用↑.↓.←.→分别控制蛇的移动, F1 为加速,2 为减速\n");
   Pos(25, 13);
   printf("加速将能得到更高的分数。\n");
   system("pause");
   system("cls");
}

void gameStart()//游戏初始化
{
   system("mode con cols=100 lines=30");
   initGame();
   creatMap();
   initSnake();
   createFood();
}

int main()
{
  gameStart();
  runGame();
  endGame();
  return 0;
}

这三个就放一起了,没什么好说的。

小结

总算断断续续啃下了贪吃蛇的代码,也是第一次写总结性博客,万事开头难吧,希望以后能更加熟练,技术也能进步。

展望

这个游戏,我还想之后做个加强版:

  1. 就最开始讲的,不同的元素用不同的颜色,或许还可以配上音效?BGM?

  2. 引进几种新的食物,比如奖励食物(吃了分加的多,速度不加),惩罚食物(中途不小心碰到,扣分加速一段时间(吃了拉肚子的食物,飞速找厕所))。

  3. 没吃几个食物速度就增加,相应的给予的分数也增加。

  4. 增加升级,关卡模式。

  5. 做个读/存档的功能,输入账号可以继续以前的数据来继续游戏。以前别的项目尝试过,但没有成功。

  6. 增加无敌模式,可以撞墙,从墙的另一边出来......

posted @ 2021-02-24 13:43  MADAOL  阅读(1561)  评论(0)    收藏  举报