【c语言】简单贪吃蛇的实现

简介: 【c语言】简单贪吃蛇的实现



一、游戏说明

  • 贪吃蛇地图绘制
  • 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇身加速、减速
  • 暂停游戏

二、地图坐标

我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:

三、头文件

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#define Case break;case
#define WALL L'□'
#define BODY L'●'
#define FOOD L'☆'
//默认的起始坐标
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
  int x;
  int y;
  struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;
enum GAME_STATUS
  //游戏状态
{
  OK = 1,//正常运行
  ESC,//按了ESC键退出,正常退出
  KILL_BY_WALL,//撞墙
  KILL_BY_SELF//撞到自身
};
//行走的方向
enum DIRECTION
  //方向
{
  UP = 1,
  DOWN,
  LEFT,
  RIGHT
};
//贪吃蛇
typedef struct Snake
{
  pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头
  pSnakeNode pFood;//指向食物的指针
  int Score;//当前累积的分数
  int FoodWeight;//一个食物的分数,默认每个食物10分
  int SleepTime;//每走一步休眠时间?
  //蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
  enum GAME_STATUS status;//游戏当前的状态
  enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右
  //...
}Snake,* pSnake;
//typedef struct Snake* pSnake;
//定位控制台的光标位置
void SetPos(int x, int y);
//游戏开始的准备
void GameStart(pSnake ps);
//打印欢迎界面
void welcomeToGame();
//绘制地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇移动的函数- 每次走一步
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);
//游戏结束的资源释放
void GameEnd(pSnake ps);

四、蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。

注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。

关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

五、数据结构设计

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信

息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行。

#define WALL L'□'  墙
#define BODY L'●'  蛇身
#define FOOD L'☆' 食物

蛇节点结构如下:

//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
  int x;
  int y;
  struct SnakeNode* next;
    //是一个指向下一个 SnakeNode 类型节点的指针,用于构建链表来表示蛇的身体。
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;

封装一个Snake的结构来维护整条贪吃蛇:

pSnakeNode pSnake:这是一个指向 SnakeNode 类型的指针,代表蛇的头部。通常,贪吃蛇的实现会用一个链表来表示蛇的身体,其中每个节点(SnakeNode)代表蛇身体的一部分,而 pSnake 指向这个链表的第一个节点,即蛇头。

pSnakeNode pFood:这是一个指向 SnakeNode 类型的指针,代表食物的位置。在贪吃蛇游戏中,食物会被随机放置在游戏区域内,当蛇吃到食物时,这个食物会被移除,并且蛇的身体会增长。

enum GAME_STATUS status;:这是一个枚举类型,表示游戏当前的状态。具体的枚举值没有在代码中给出,但可能包括“游戏中”、“游戏结束”等状态。

enum DIRECTION dir;:这是一个枚举类型,表示蛇当前移动的方向。具体的枚举值也没有在代码中给出,但通常包括“向上”、“向下”、“向左”、“向右”等方向。

typedef struct Snake
{
  pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头
  pSnakeNode pFood;//指向食物的指针
  int Score;//当前累积的分数
  int FoodWeight;//一个食物的分数,默认每个食物10分
  int SleepTime;//每走一步休眠时间?
  //蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
  enum GAME_STATUS status;//游戏当前的状态
  enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右
  //...
}Snake,* pSnake;
//typedef struct Snake* pSnake;

蛇的方向,可以一一列举,使用枚举:

//行走的方向
enum DIRECTION
  //方向
{
  UP = 1,
  DOWN,
  LEAF,
  RIGHT
};

游戏状态,可以一一列举,使用枚举:

enum GAME_STATUS
  //游戏状态
{
  OK = 1,//正常运行
  ESC,//按了ESC键退出,正常退出
  KILL_BY_WALL,//撞墙
  KILL_BY_SELF//撞到自身
};

六、Snake.c

5.1、游戏开始函数

void GameStart(pSnake ps)
{
  //设置控制台的信息,窗口大小,窗口名
  system("mode con cols=120 lines=40");
  system("title 贪吃蛇");
  //隐藏光标
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  CONSOLE_CURSOR_INFO CursorInfo;
  GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
  CursorInfo.bVisible = false;
  SetConsoleCursorInfo(handle, &CursorInfo);//设置光标信息
  //打印欢迎信息
  welcomeToGame();
  //绘制地图
  CreateMap();
  //初始化蛇
  InitSnake(ps);
  //创建食物
  CreateFood(ps);
}

定位控制台的光标位置

//定位控制台的光标位置
void SetPos(int x, int y)
{
  //获得设备句柄
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  //根据句柄设置光标的位置
  COORD pos = { x,y };
  SetConsoleCursorPosition(handle, pos);
}

欢迎来到贪吃蛇游戏

void welcomeToGame()
{
  //欢迎信息
  SetPos(35, 10);
  printf("欢迎来到贪吃蛇小游戏\n");
  SetPos(38, 20);
  system("pause");
  system("cls");
  //功能介绍信息
  SetPos(15, 10);
  printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速\n");
  SetPos(15, 11);
  printf("加速能得到更高的分数");
  SetPos(38, 20);
  system("pause");
  system("cls");
}

创建一个地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L

打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

void CreateMap()
{
  int i = 0;
  //上
  SetPos(0, 0);
  for (i = 0; i <= 56; i += 2)
  {
    wprintf(L"%lc", WALL);
  }
  //下
  SetPos(0, 25);
  for (i = 0; i <= 56; i += 2)
  {
    wprintf(L"%lc", WALL);
  }
  //左
  for (i = 1; i <= 25; i++)
  {
    SetPos(0, i);
    wprintf(L"%lc", WALL);
  }
  //右
  for (i = 1; i <= 25; i++)
  {
    SetPos(56, i);
    wprintf(L"%lc", WALL);
  }
}

初始化创建蛇身的节点

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。

创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。

结构体成员:记录它们的坐标:(x,y),和记录下一个位置的前驱结构体指针:next。

void InitSnake(pSnake ps)
{
  //创建五个蛇身的节点
  pSnakeNode cur = NULL;
  int i = 0;
  for (i = 0; i < 5; ++i)
  {
    cur = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (cur == NULL)
    {
      perror("InsitSnkae():malloc()");
      return;
    }
    cur->x = POS_X + 2 * i;
    cur->y = POS_Y;
    cur->next = NULL;
    //头插法
    if (ps->pSnake == NULL)
    {
      ps->pSnake = cur;
    }
    else {
      cur->next = ps->pSnake;
      ps->pSnake = cur;
    }
  }
  //打印蛇身
  cur = ps->pSnake;
  while (cur)
  {
    SetPos(cur->x, cur->y);
    wprintf(L"%lc", BODY);
    cur = cur->next;
  }
  //贪吃蛇的其他信息
  ps->dir = RIGHT;
  ps->FoodWeight = 10;
  ps->pFood = NULL;
  ps->Score = 0;
  ps->SleepTime = 200;
  ps->status = OK;
}

创建第一个食物

  • 先随机生成食物的坐标
  • x坐标必须是2的倍数
  • 食物的坐标不能和蛇身每个节点的坐标重复
  • 创建食物节点,打印食物
void CreateFood(pSnake ps)
{
  int x = 0;//x范围: 2~54 -> 0~52 + 2 -> rand()%53 + 2
  int y = 0;//y范围: 1~25 -> 0~24 + 1 -> rand()%24 + 1
again:
  do {
    x = rand() % 53 + 2;
    y = rand() % 24 + 1;
  } while (x % 2 != 0);
  
  //坐标和蛇的身体的每个几点的坐标比较
  pSnakeNode cur = ps->pSnake;
  while (cur)
  {
    if (x == cur->x && y == cur->y)
    {
      goto again;
    }
    cur = cur->next;
  }
  //创建食物
  pSnakeNode pFood = malloc(sizeof(SnakeNode));
  if (pFood == NULL)
  {
    perror("CreateFood:malloc()");
    return;
  }
  pFood->x = x;
  pFood->y = y;
  ps->pFood = pFood;
  SetPos(x, y);
  wprintf(L"%lc", FOOD);
}

5.2、游戏运行函数

游戏运行期间,右侧打印帮助信息,提示玩家

根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。

如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

确定了蛇的方向和速度,蛇就可以移动了。

void GameRun(pSnake ps)
{
  //打印帮助信息
  PrintHelpInfo();
  //检测按键
  do
  {
    //当前的分数情况
    SetPos(62, 10);
    printf("总分:%5d\n", ps->Score);
    
    SetPos(62, 11);
    printf("食物的分支:%02d\n", ps->FoodWeight);
    //检测按键
    //上、下、左、右、ESC、空格、F3、F4
    if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
    {
      ps->dir = UP;
    }
    else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
    {
      ps->dir = DOWN;
    }
    else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
    {
      ps->dir = LEFT;
    }
    else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
    {
      ps->dir = RIGHT;
    }
    else if (KEY_PRESS(VK_ESCAPE))
    {
      ps->status = ESC;
      break;
    }
    else if (KEY_PRESS(VK_SPACE))
    {
      //游戏要暂停
      pause();//暂停和开始
    } 
    else if (KEY_PRESS(VK_F3))
    {
      if (ps->SleepTime >= 80)
      {
        ps->SleepTime -= 30;
        ps->FoodWeight += 2;
      }
    }
    else if (KEY_PRESS(VK_F4))
    {
      if (ps->FoodWeight > 2)
      {
        ps->SleepTime += 30;
        ps->FoodWeight -= 2;
      }
    }
    
    //走一步
    SnakeMove(ps);
    //睡眠一下
    Sleep(ps->SleepTime);
    
  }while(ps->status == OK);
  
}

检测按键状态,我们封装了一个宏

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

打印帮助信息

//打印帮助信息
void PrintHelpInfo()
{
  SetPos(62, 15);
  printf("1. 不能穿墙. 不能咬到自己");
  SetPos(62, 16);
  printf("2. 用 ↑ . ↓ . ← . → 来控制蛇的移动");
  SetPos(62, 17);
  printf("3. F3是加速,F4是减速");
  SetPos(62, 19);
  printf(" ");
}

暂停函数

void pause()
{
  while (1)
  {
    Sleep(100);
    if (KEY_PRESS(VK_SPACE))
    {
      break;
    }
    
  }
}

下一个是否是食物

//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
  if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
    return 1;
  else
    return 0;
}

下一步要走的位置处就是食物,就吃掉食物

//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)
{
  pNext->next = ps->pSnake;
  ps->pSnake = pNext;
  pSnakeNode cur = ps->pSnake;
  //打印蛇身
  while (cur)
  {
    SetPos(cur->x, cur->y);
    wprintf(L"%lc", BODY);
    cur = cur->next;
  }
  ps->Score += ps->FoodWeight;
  //释放旧的食物
  free(ps->pFood);
  //新建食物
  CreateFood(ps);
}

如果下一步不是食物

将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,放弃掉蛇身的最后一个节点

//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
  //头插法
  pNext->next = ps->pSnake;
  ps->pSnake = pNext;
  //释放尾结点
  pSnakeNode cur = ps->pSnake;
  while (cur->next->next)
  {
    SetPos(cur->x, cur->y);
    wprintf(L"%lc", BODY);
    cur = cur->next;
  }
  //将尾节点的位置打印成空白字符
  SetPos(cur->next->x, cur->next->y);
  printf("  ");
  free(cur->next);
  cur->next = NULL;//易错
}

检测是否撞墙

判断蛇头的坐标是否和墙的坐标冲突

//检测是否撞墙
void KillByWall(pSnake ps)
{
  if (ps->pSnake->x == 0 ||
    ps->pSnake->x == 56 ||
    ps->pSnake->y == 0 ||
    ps->pSnake->y == 25)
  {
    ps->status = KILL_BY_WALL;
  }
  
}

检测是否撞自己

判断蛇头的坐标是否和蛇身体的坐标冲突

//检测是否撞自己
void KillBySelf(pSnake ps)
{
  pSnakeNode cur = ps->pSnake->next;//从第二个节点开始
  while (cur)
  {
    if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
    {
      ps->status = KILL_BY_SELF;
      return;
    }
    cur = cur->next;
  }
}

蛇移动的函数

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。

确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理

(EatFood),如果不是食物则做前进一步的处理(NoFood)。

蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

void SnakeMove(pSnake ps)
{
  pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
  if (pNext == NULL)
  {
    perror("SnakeMove():malloc()");
    return;
  }
  pNext->next = NULL;
  switch (ps->dir)
  {
  case UP:
    pNext->x = ps->pSnake->x;
    pNext->y = ps->pSnake->y - 1;
    break;
  Case DOWN:
    pNext->x = ps->pSnake->x;
    pNext->y = ps->pSnake->y + 1;
  Case LEFT:
    pNext->x = ps->pSnake->x - 2;
    pNext->y = ps->pSnake->y;
  Case RIGHT:
    pNext->x = ps->pSnake->x + 2;
    pNext->y = ps->pSnake->y;
    break;
  }
  //下一个坐标是否是食物
  if (NextIsFood(ps, pNext))
  {
    //是食物就吃掉
    EatFood(ps, pNext);
  }
  else {
    //不是食物就正常一步
    NotEatFood(ps, pNext);
  }
  //检测撞墙
  KillByWall(ps);
  
  //检测是否撞自己
  KillBySelf(ps);
}

七、游戏结束的资源释放

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。

//游戏结束的资源释放
void GameEnd(pSnake ps)
{
  SetPos(20, 15);
  switch (ps->status)
  {
  case ESC:
    printf("主动退出游戏,正常退出\n");
  Case KILL_BY_WALL:
    printf("很遗憾,撞墙了,游戏结束\n");
  Case KILL_BY_SELF:
    printf("很遗憾,撞到自己了,游戏结束\n");
    break;
  }
  
  //释放贪吃蛇的链表资源
  pSnakeNode cur = ps->pSnake;
  pSnakeNode del = NULL;
  while (cur)
  {
    del = cur;
    cur = cur->next;
    free(del);
  }
  free(ps->pFood);
  ps = NULL;
}

八、Test.c

void test()
{
  //创建贪食蛇
  Snake snake = { 0 };
  //GameStart(&snake);//游戏开始前的初始化
  //GameRun();//玩游戏的过程
  //GameEnd();//善后的工作
  int ch = 0;
  do
  {
    Snake snake = { 0 };
    GameStart(&snake);//游戏开始前的初始化
    GameRun(&snake);//玩游戏的过程
    GameEnd(&snake);//善后的工作
    SetPos(15, 20);
    printf("再来一局吗?(Y/N):");
    ch = getchar();
    getchar();// 清理\n
  } while (ch == 'Y' || ch == 'y');
}
int main()
{
  //修改适配本地的环境
  setlocale(LC_ALL, "");
  test();//贪吃蛇游戏的测试
  SetPos(0, 30);
  return 0;
}

今天就先到这了!!!

看到这里了还不给博主扣个:

⛳️ 点赞☀️收藏 ⭐️ 关注!

你们的点赞就是博主更新最大的动力!

有问题可以评论或者私信呢秒回哦。

相关文章
|
4天前
|
API 定位技术 C语言
贪吃蛇---C语言---详解
贪吃蛇---C语言---详解
|
4天前
|
API 定位技术 C语言
C语言项目实战——贪吃蛇
贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。 在编程语言的学习中,我将以贪吃蛇为例,从设计到代码来展示一个C语言项目实战,进而再一步提升读者对C语言的理解和认知。
88 0
|
4天前
|
C语言
C语言之实现贪吃蛇小游戏篇(2)
C语言之实现贪吃蛇小游戏篇(2)
26 1
|
7月前
|
C语言 C++
【c语言】贪吃蛇
【c语言】贪吃蛇
66 0
|
4天前
|
C语言
|
4天前
|
C语言 计算机视觉
C语言贪吃蛇(有详细注释)
C语言贪吃蛇(有详细注释)
11 0
|
4天前
|
存储 定位技术 API
C语言实现贪吃蛇【完整版】
C语言实现贪吃蛇【完整版】
|
4天前
|
C语言
C语言实战演练之贪吃蛇游戏
C语言实战演练之贪吃蛇游戏
|
4天前
|
存储 定位技术 API
贪吃蛇-c语言版本
贪吃蛇-c语言版本
|
5月前
|
API 定位技术 C语言
贪吃蛇代码实现与剖析(C语言)(下)
贪吃蛇代码实现与剖析(C语言)(下)