C语言实战 -- 经典贪吃蛇游戏(含完整源码)

简介: C语言实战 -- 经典贪吃蛇游戏(含完整源码)

🕹️前言

贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。在编程语⾔的教学中,我们经常以贪吃蛇为例,从设计到代码实现来检验我们的编程能⼒和逻辑能⼒。

💡一,技术要点

C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等

✏️二, 控制台设置

如果你们的Win11系统的控制台窗是这样显⽰,一定要重新设置控制台,不然无法进行贪吃蛇

调整⽅式:

保存后,重新打开cmd就⾏。

🌳三,Win32 API 介绍

本次实现贪吃蛇会使⽤到的⼀些Win32 API知识,接下来我进行介绍。

🦊🦊3.1 Win32 API

Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接

🐯🐯3.2 控制台程序

平常我们运⾏起来的⿊框程序其实就是控制台程序,我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列。

mode con cols=100 lines=30

也可以通过命令设置控制台窗⼝的名字

title 贪吃蛇

这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。例如:

#include <stdio.h>
#include <Windows.h>
//设置控制台的窗口大小,窗口名称
int main()
{
  system("mode con cols=100 lines=30");
  system("title 贪吃蛇");
  getchar();
  //system("pause");//暂停
  return 0;
}

🎮🎮3.3 控制台屏幕上的坐标 COORD

COORD是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。

给坐标赋值:

COORD pos = { 10, 15 };
• 1

✈️✈️3.4 获取设备句柄 GetStdHandle

GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。

HANDLE GetStdHandle(DWORD nStdHandle);
• 1

实例:

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

🎄🎄3.5 获取控制台光标信息 GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
//获取控制台光标信息 
GetConsoleCursorInfo(hOutput, &CursorInfo);

其中 CONSOLE_CURSOR_INFO是一个结构体,它包含了有关控制台光标的信息

CursorInfo.bVisible = false; //隐藏控制台光标 

🐳🐳3.6 设置光标可见性 etConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作 
CONSOLE_CURSOR_INFO CursorInfo;
//获取控制台光标信息 
GetConsoleCursorInfo(hOutput, &CursorInfo);
//隐藏控制台光标 
CursorInfo.bVisible = false; 
//设置控制台光标状态 
SetConsoleCursorInfo(hOutput, &CursorInfo);

🐉🐉3.7 设置光标位置 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。

SetPos:封装⼀个设置光标位置的函数

void SetPos(int x, int y)
{
  //获取设备句柄
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  //定位光标需要移动到的位置
  COORD pos = { x ,y };
  //将光标位置设置在到指定位置
  SetConsoleCursorPosition(handle, pos);
}

🦋🦋3.8 获取按键情况 GetAsyncKeyState

获取按键情况,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

#define KEY_PRESS(vk)   (GetAsyncKeyState(vk) & 0x1 ? 1:0)
int main()
{
  //检测数字按键是否按下
  while (1)
  {
    if (KEY_PRESS(0x30))
      printf("0\n");
    else if (KEY_PRESS(0x31))
      printf("1\n");
    else if (KEY_PRESS(0x32))
      printf("2\n"); 
    else if (KEY_PRESS(0x33))
      printf("3\n"); 
    else if (KEY_PRESS(0x34))
      printf("4\n"); 
    else if (KEY_PRESS(0x35))
      printf("5\n"); 
    else if (KEY_PRESS(0x36))
      printf("6\n"); 
    else if (KEY_PRESS(0x37))
      printf("7\n");
    else if (KEY_PRESS(0x38))
      printf("8\n"); 
    else if (KEY_PRESS(0x39))
      printf("9\n"); 
    
  }
  return 0;
}

🐍四,贪吃蛇游戏设计与分析

🔮🔮4.1 地图

我们最终的贪吃蛇⼤纲要是这个样⼦,那我们的地图如何布置呢?

这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。

控制台窗⼝的横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。

🚀🚀🚀4.1.1 <locale.h>本地化

#include <locale.h>
//切换本地信息模式
int main()
{
  char* loc;
  loc = setlocale(LC_ALL, NULL);
  printf("默认的本地信息:%s\n", loc);
  loc = setlocale(LC_ALL, "");
  printf("设置后的本地信息:%s\n", loc);
  return 0;
}

✈️✈️✈️4.1.2 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?

宽字符的字⾯量必须加上前缀“L”,否则C语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应wprintf() 的占位符为 %ls 。

#include <stdio.h>
#include<locale.h>
int main()
{
  setlocale(LC_ALL, "");
  wchar_t ch1 = L'中';
  wchar_t ch2 = L'国';
  wchar_t ch3 = L'★';
  wchar_t ch4 = L'●';
  wprintf(L"%lc\n", ch1);
  wprintf(L"%lc\n", ch2);
  wprintf(L"%lc\n", ch3);
  wprintf(L"%lc\n", ch4);
  printf("ab\n");
  return 0;
}

普通字符和宽字符打印出宽度的展⽰如下:

🎄🎄🎄4.1.3 地图坐标

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

🌭🌭4.2 蛇⾝和⻝物

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

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

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

🐥🐥4.3 数据结构设计

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:

//维护一个蛇身的节点
//定义蛇身的节点: 蛇身坐标+下一个节点的指针
typedef struct SnakeNode
{
  int x;
  int y;
  struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇

//贪吃蛇 -- 整个游戏中的维护:
// 蛇头+食物+方向+当前总分数+一个食物的分数+蛇的状态+蛇的速度
typedef struct Snake
{
  pSnakeNode pSnake;//维护整条蛇的指针
  pSnakeNode pFood;//指向食物的指针,蛇会吃掉食物,所以类似蛇的节点类型,增长蛇身
  int Score;//当前累计的分数
  int FoodWeight;//一个食物的分数
  int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
  enum GAME_STATUS status;//游戏当前的状态
  enum DIRECTION dir;//蛇当前走的方向
}Snake ,* pSnake;

蛇的⽅向,可以⼀⼀列举,使⽤枚举:

/蛇的方向
enum DIRECTION
{
  UP=1,
  DOWN,
  LEFT,
  RIGHT
};

游戏状态,可以⼀⼀列举,使⽤枚举:

//游戏的状态
enum GAME_STATUS
{
  OK = 1,//游戏正常进行
  ESC,//按ESC键退出
  KILL_BY_WALL,//撞墙
  KILL_BY_SELF//自己撞自己
};

⛱️⛱️4.4 游戏流程设计

💎5. 核⼼逻辑实现分析

🎋🎋5.1 游戏主逻辑

程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑。

主逻辑分为3个过程:

游戏开始(GameStart)完成游戏的初始化

游戏运⾏(GameRun)完成游戏运⾏逻辑的实现

游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

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

🌵🌵5.2 游戏开始(GameStart)

这个模块完成游戏的初始化任务:

控制台窗⼝⼤⼩的设置

控制台窗⼝名字的设置

⿏标光标的隐藏

打印欢迎界⾯

创建地图

初始化第蛇

创建第⼀个⻝物

void GameStart(pSnake ps)
{
  //设置控制台的信息:窗口大小,窗口名
  system("mode con cols=100 lines=30");
  system("title 贪吃蛇");
  //隐藏光标
  //获取句柄
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  //获取控制台光标信息
  CONSOLE_CURSOR_INFO cursor_info = { 0 };
  GetConsoleCursorInfo(handle, &cursor_info);
  //隐藏光标
  cursor_info.bVisible = false;
  //将改变后的光标进行设置
  SetConsoleCursorInfo(handle, &cursor_info);
  //打印欢迎信息
  WelcomeToGame();
  
  //绘制地图
  CreateMap();
  //初始化蛇
  InitSnake(ps);
  //创建食物
  CreateFood(ps);
}

🌹🌹🌹5.2.1 打印欢迎界⾯

在游戏正式开始之前,做⼀些功能提醒:

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

🌳🌳🌳5.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使⽤wprintf函数,打印格式串前使⽤L,打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

墙体打印的宽字符:

#define WALL L'□' 

易错点:就是坐标的计算

上:(0,0)到(56,0)

下:(0,26)到(56,26)

左:(0,1)到(0,25)

右:(56,1)到(56,25)

创建地图函数CreateMap:

void CreateMap()
{
  int i = 0;
  //上
  SetPos(0, 0);
  for (i = 0; i <= 56; i += 2)
  {
    wprintf(L"%lc", WALL);
  }
  //下
  SetPos(0, 26);
  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.2.3 初始化蛇⾝

蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。

创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。

蛇的初始位置从(24,5)开始

再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,每个⻝物的分数。

游戏状态是:OK

蛇的移动速度:200毫秒

蛇的默认⽅向:RIGHT

初始成绩:0

每个⻝物的分数:10

蛇⾝打印的宽字符:

#define BODY L'●'  

初始化蛇⾝函数:InitSnake

void InitSnake(pSnake ps)
{
  //初始化5个蛇身节点
  pSnakeNode cur = NULL;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    cur = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (cur == NULL)
    {
      perror("InitSnake:malloc():");
      return;
    }
    cur->x = POS_X + 2 * i;
    cur->y = POS_Y;
    cur->next = NULL;
    //用头插,把5个节点链在一起
    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 = 180;//毫秒,就是2秒
  ps->status = OK;
}

🍒🍒🍒5.2.4 创建第⼀个⻝物

先随机⽣成⻝物的坐标

x坐标必须是2的倍数

⻝物的坐标不能和蛇⾝每个节点的坐标重复

创建⻝物节点,打印⻝物

⻝物打印的宽字符:

#define FOOD L'★'

创建⻝物的函数:CreateFood

void CreateFood(pSnake ps)
{
  //1.食物是随机出现的,坐标就是随机的
  //2.坐标必须在墙内
  //3.坐标不能在蛇身上
  int x = 0;
  int y = 0;
again:
  do
  {
    x = rand() % 53 + 2;
    y = rand() % 24 + 1;
  } while (x % 2 != 0);//生成的x坐标必须为偶数,蛇头的坐标是2的倍数,方便蛇头吃
  
  //坐标和蛇的身体的每个节点做比较,遍历比较
  pSnakeNode cur = ps->pSnake;
  while (cur)
  {
    if (x == cur->x && y == cur->y)
    {
      goto again;//当出现重合时,重新回去生成坐标
    }
    cur = cur->next;
  }
  //创建食物
  pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
  if (pFood == NULL)
  {
    perror("CreateFood::malloc()\n");
    return;
  }
  pFood->x = x;
  pFood->y = y;
  ps->pFood = pFood;
  SetPos(x, y);//定位在坐标的位置,产生食物
  wprintf(L"%lc", FOOD);
}

🍓🍓5.3 游戏运⾏(GameRun)

游戏运⾏期间,右侧打印帮助信息,提⽰玩家,坐标开始位置(64,15)

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

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

戏。

需要的虚拟按键的罗列:

• 上:VK_UP

• 下:VK_DOWN

• 左:VK_LEFT

• 右:VK_RIGHT

• 空格:VK_SPACE

• ESC:VK_ESCAPE

• F3:VK_F3

• F4:VK_F4

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

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;//每加速一次,休眠时间减少30毫秒
        ps->FoodWeight += 2;//每加速一次,食物分数加2分
      }
    }
    else if (KEY_PRESS(VK_F4))
    {
      //减速:休眠时间要变长
      if (ps->FoodWeight>2)
      {
        ps->SleepTime += 30;//每减速一次,休眠时间增加30毫秒
        ps->FoodWeight -= 2;//每减速一次,食物分数减2分
      }
    }
    //睡眠一下
    Sleep(ps->SleepTime);
    //蛇走的过程
    SnakeMove(ps);
  } while (ps->status == OK);
}

🧬🧬🧬5.3.1 检测按键状态 KEY_PRESS

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

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

💰💰💰5.3.2 打印帮助信息 PrintHelpInfo

void PrintHelpInfo()
{
  SetPos(62, 15);
  printf("1.不能穿墙,不能咬到自己!");
  SetPos(62, 16);
  printf("2.用↑.↓ . ← . →来控制蛇的移动!");
  SetPos(62, 17);
  printf("3.F3是加速,F4是减速.");
  SetPos(62, 18);
  printf("4.ESC:退出游戏.space:暂停游戏.");
}

🎲🎲🎲5.3.3 蛇⾝移动(SnakeMove)

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

确定了下⼀个位置后,看下⼀个位置是否是⻝物(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;
    break;
  case LEFT:
    pNext->x = ps->pSnake->x - 2;
    pNext->y = ps->pSnake->y;
    break;
  case RIGHT:
    pNext->x = ps->pSnake->x + 2;
    pNext->y = ps->pSnake->y;
    break;
  }
  //下一个坐标处是否是食物,是,返回1,不是,返回0
  if (NextIsFood(ps, pNext))
  {
    //是食物就吃掉,蛇身变长
    EatFood(ps, pNext);
  }
  else
  {
    //不是食物就正常走一步
    NotEatFood(ps, pNext);
  }
  //检测是否撞墙
  KillByWall(ps);
  //检测是否撞到自己
  KillBySeif(ps);
}
🍭🍭🍭🍭5.3.3.1 判断下一个位置是否是食物 NextIsFood
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
  if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
    return 1;//下一个位置是食物
  else
    return 0;
}
🌭🌭🌭🌭5.3.3.2 吃食物 EatFood
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);
}

⏰⏰⏰⏰5.3.3.3 不是食物 NoFood

将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,释放掉蛇⾝的最后⼀个节点。

易错点:这⾥最容易错误的是,释放最后⼀个结点后,还得将指向在最后⼀个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。

void NotEatFood(pSnake ps, pSnakeNode pNext)
{   
  //不是食物
  //用头插法,先把pNext节点挂上去
  pNext->next = ps->pSnake;
  ps->pSnake = pNext;
  //再把尾结点删除,保持蛇的原长
  pSnakeNode cur = ps->pSnake;
  while (cur->next->next != NULL)
  {
    //重新打印蛇身
    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;//易错
}

💰💰💰💰5.3.3.4 检测是否撞墙 KillByWall

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

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

🐳🐳🐳🐳5.3.3.5 检测是否撞到自己 KillBySelf

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

//检测是否撞到自己
void KillBySeif(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;
  }
}

🚀🚀5.4 游戏结束

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

void GameEnd(pSnake ps)
{
  SetPos(18, 12);
  //说明清楚游戏结束的状态
  switch(ps->status)
  {
  case ESC:
    printf("退出游戏!\n");
    break;
  case KILL_BY_WALL:
    printf("很遗憾,撞墙了,游戏结束!\n");
    break;
  case KILL_BY_SELF:
    printf("很遗憾,撞到自己了,游戏结束!\n");
    break;
  }
  //是否贪吃蛇链表资源
  pSnakeNode cur = ps->pSnake;
  while (cur)
  {
    pSnakeNode del = cur->next;
    free(cur);
    cur = del;
  }
  free(ps->pFood);
  ps = NULL;
}

🎮6. 完整源代码

完整代码实现,分3个⽂件实现。

test.c

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

snake.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <stdbool.h>
#include <locale.h>
#define WALL L'□' //墙体
#define BODY L'●' //蛇身
#define FOOD L'★' //食物
//蛇默认的起始位置
#define POS_X 24
#define POS_Y 5 
//游戏的状态
enum GAME_STATUS
{
  OK = 1,//游戏正常进行
  ESC,//按ESC键退出
  KILL_BY_WALL,//撞墙
  KILL_BY_SELF//自己撞自己
};
//蛇的方向
enum DIRECTION
{
  UP=1,
  DOWN,
  LEFT,
  RIGHT
};
//维护一个蛇身的节点
//定义蛇身的节点: 蛇身坐标+下一个节点的指针
typedef struct SnakeNode
{
  int x;
  int y;
  struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//贪吃蛇 -- 整个游戏中的维护:
// 蛇头+食物+方向+当前总分数+一个食物的分数+蛇的状态+蛇的速度
typedef struct Snake
{
  pSnakeNode pSnake;//维护整条蛇的指针
  pSnakeNode pFood;//指向食物的指针,蛇会吃掉食物,所以类似蛇的节点类型,增长蛇身
  int Score;//当前累计的分数
  int FoodWeight;//一个食物的分数
  int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
  enum GAME_STATUS status;//游戏当前的状态
  enum DIRECTION dir;//蛇当前走的方向
}Snake ,* pSnake;
//定位函数
void SetPos(int x, int y);
//获取按键情况,检测按键是否按过,1为按过,0为没2按过
#define KEY_PRESS(vk)   (GetAsyncKeyState(vk) & 0x1 ? 1:0)
//游戏开始的准备环节
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 KillBySeif(pSnake ps);
//游戏结束释放资源
void GameEnd(pSnake ps);

snake.c

#define _CRT_SECURE_NO_WARNINGS 
#include "Snake.h"
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(18, 10);
  printf("用↑.↓ . ← . →来控制蛇的移动,F3是加速,F4是减速。");
  SetPos(18, 11);
  printf("加速能够得到更高的分数!");
  SetPos(38, 20);
  system("pause");
  system("cls");
}
void CreateMap()
{
  int i = 0;
  //上
  SetPos(0, 0);
  for (i = 0; i <= 56; i += 2)
  {
    wprintf(L"%lc", WALL);
  }
  //下
  SetPos(0, 26);
  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);
  }
}
void InitSnake(pSnake ps)
{
  //初始化5个蛇身节点
  pSnakeNode cur = NULL;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    cur = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (cur == NULL)
    {
      perror("InitSnake:malloc():");
      return;
    }
    cur->x = POS_X + 2 * i;
    cur->y = POS_Y;
    cur->next = NULL;
    //用头插,把5个节点链在一起
    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 = 180;//毫秒,就是2秒
  ps->status = OK;
}
void CreateFood(pSnake ps)
{
  //1.食物是随机出现的,坐标就是随机的
  //2.坐标必须在墙内
  //3.坐标不能在蛇身上
  int x = 0;
  int y = 0;
again:
  do
  {
    x = rand() % 53 + 2;
    y = rand() % 24 + 1;
  } while (x % 2 != 0);//生成的x坐标必须为偶数,蛇头的坐标是2的倍数,方便蛇头吃
  
  //坐标和蛇的身体的每个节点做比较,遍历比较
  pSnakeNode cur = ps->pSnake;
  while (cur)
  {
    if (x == cur->x && y == cur->y)
    {
      goto again;//当出现重合时,重新回去生成坐标
    }
    cur = cur->next;
  }
  //创建食物
  pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
  if (pFood == NULL)
  {
    perror("CreateFood::malloc()\n");
    return;
  }
  pFood->x = x;
  pFood->y = y;
  ps->pFood = pFood;
  SetPos(x, y);//定位在坐标的位置,产生食物
  wprintf(L"%lc", FOOD);
}
void GameStart(pSnake ps)
{
  //设置控制台的信息:窗口大小,窗口名
  system("mode con cols=100 lines=30");
  system("title 贪吃蛇");
  //隐藏光标
  //获取句柄
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  //获取控制台光标信息
  CONSOLE_CURSOR_INFO cursor_info = { 0 };
  GetConsoleCursorInfo(handle, &cursor_info);
  //隐藏光标
  cursor_info.bVisible = false;
  //将改变后的光标进行设置
  SetConsoleCursorInfo(handle, &cursor_info);
  //打印欢迎信息
  WelcomeToGame();
  
  //绘制地图
  CreateMap();
  //初始化蛇
  InitSnake(ps);
  //创建食物
  CreateFood(ps);
}
void PrintHelpInfo()
{
  SetPos(62, 15);
  printf("1.不能穿墙,不能咬到自己!");
  SetPos(62, 16);
  printf("2.用↑.↓ . ← . →来控制蛇的移动!");
  SetPos(62, 17);
  printf("3.F3是加速,F4是减速.");
  SetPos(62, 18);
  printf("4.ESC:退出游戏.space:暂停游戏.");
}
void Pause()
{
  while (1)
  {
    Sleep(100);
    if (KEY_PRESS(VK_SPACE))
    {
      break;
    }
  }
}
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);
}
void NotEatFood(pSnake ps, pSnakeNode pNext)
{   
  //不是食物
  //用头插法,先把pNext节点挂上去
  pNext->next = ps->pSnake;
  ps->pSnake = pNext;
  //再把尾结点删除,保持蛇的原长
  pSnakeNode cur = ps->pSnake;
  while (cur->next->next != NULL)
  {
    //重新打印蛇身
    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 == 26)  //下
  {
    ps->status = KILL_BY_WALL;
  }
}
//检测是否撞到自己
void KillBySeif(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;
  }
}
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;
    break;
  case LEFT:
    pNext->x = ps->pSnake->x - 2;
    pNext->y = ps->pSnake->y;
    break;
  case RIGHT:
    pNext->x = ps->pSnake->x + 2;
    pNext->y = ps->pSnake->y;
    break;
  }
  //下一个坐标处是否是食物,是,返回1,不是,返回0
  if (NextIsFood(ps, pNext))
  {
    //是食物就吃掉,蛇身变长
    EatFood(ps, pNext);
  }
  else
  {
    //不是食物就正常走一步
    NotEatFood(ps, pNext);
  }
  //检测是否撞墙
  KillByWall(ps);
  //检测是否撞到自己
  KillBySeif(ps);
}
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;//每加速一次,休眠时间减少30毫秒
        ps->FoodWeight += 2;//每加速一次,食物分数加2分
      }
    }
    else if (KEY_PRESS(VK_F4))
    {
      //减速:休眠时间要变长
      if (ps->FoodWeight>2)
      {
        ps->SleepTime += 30;//每减速一次,休眠时间增加30毫秒
        ps->FoodWeight -= 2;//每减速一次,食物分数减2分
      }
    }
    //睡眠一下
    Sleep(ps->SleepTime);
    //蛇走的过程
    SnakeMove(ps);
  } while (ps->status == OK);
}
void GameEnd(pSnake ps)
{
  SetPos(18, 12);
  //说明清楚游戏结束的状态
  switch(ps->status)
  {
  case ESC:
    printf("退出游戏!\n");
    break;
  case KILL_BY_WALL:
    printf("很遗憾,撞墙了,游戏结束!\n");
    break;
  case KILL_BY_SELF:
    printf("很遗憾,撞到自己了,游戏结束!\n");
    break;
  }
  //是否贪吃蛇链表资源
  pSnakeNode cur = ps->pSnake;
  while (cur)
  {
    pSnakeNode del = cur->next;
    free(cur);
    cur = del;
  }
  free(ps->pFood);
  ps = NULL;
}
目录
相关文章
|
6天前
|
定位技术 API C语言
C语言——实现贪吃蛇小游戏
本文介绍了一个基于Windows控制台的贪吃蛇游戏的实现方法。首先,需调整控制台界面以便更好地显示游戏。接着,文章详细描述了如何使用Win32 API函数如`COORD`、`GetStdHandle`、`GetConsoleCursorInfo`等来控制控制台的光标和窗口属性。此外,还介绍了如何利用`GetAsyncKeyState`函数实现键盘监听功能。文中还涉及了`&lt;locale.h&gt;`库的使用,以支持本地化字符显示。
19 1
C语言——实现贪吃蛇小游戏
|
17天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_二叉排序树(二叉搜索树)_构建_删除_插入操作详解
这份二叉排序树习题集涵盖了二叉搜索树(BST)的基本操作,包括构建、查找、删除等核心功能。通过多个具体示例,如构建BST、查找节点所在层数、删除特定节点及查找小于某个关键字的所有节点等,帮助读者深入理解二叉排序树的工作原理与应用技巧。此外,还介绍了如何将一棵二叉树分解为两棵满足特定条件的BST,以及删除所有关键字小于指定值的节点等高级操作。每个题目均配有详细解释与代码实现,便于学习与实践。
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_循环单链表和循环双链表
本文档详细介绍了用C语言实现循环单链表和循环双链表的相关算法。包括循环单链表的建立、逆转、左移、拆分及合并等操作;以及双链表的建立、遍历、排序和循环双链表的重组。通过具体示例和代码片段,展示了每种算法的实现思路与步骤,帮助读者深入理解并掌握这些数据结构的基本操作方法。
|
19天前
|
算法 C语言 开发者
C语言手撕实战代码_单链表
本文档详细介绍了使用C语言实现单链表的各种基本操作和经典算法。内容涵盖单链表的构建、插入、查找、合并及特殊操作,如头插法和尾插法构建单链表、插入元素、查找倒数第m个节点、合并两个有序链表等。每部分均配有详细的代码示例和注释,帮助读者更好地理解和掌握单链表的编程技巧。此外,还提供了判断子链、查找公共后缀等进阶题目,适合初学者和有一定基础的开发者学习参考。
|
1月前
|
SQL 缓存 自然语言处理
实战案例1:基于C语言的Web服务器实现。
实战案例1:基于C语言的Web服务器实现。
111 15
|
1月前
|
存储 编译器 数据处理
【编程秘籍】解锁C语言数组的奥秘:从零开始,深入浅出,带你领略数组的魅力与实战技巧!
【8月更文挑战第22天】数组是C语言中存储同类型元素的基本结构。本文从定义出发,详述数组声明、初始化与访问。示例展示如何声明如`int numbers[5];`的数组,并通过下标访问元素。初始化可在声明时进行,如`int numbers[] = {1,2,3,4,5};`,编译器自动计算大小。初始化时未指定的元素默认为0。通过循环可遍历数组,数组名视为指向首元素的指针,方便传递给函数。多维数组表示矩阵,如`int matrix[3][4];`。动态数组利用`malloc()`分配内存,需用`free()`释放以避免内存泄漏。掌握这些技巧是高效数据处理的基础。
55 2
|
1月前
|
算法 编译器 C语言
【C语言篇】猜数字游戏(赋源码)
rand函数会返回⼀个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的⼤⼩是依赖编译器上实现的,但是⼤部分编译器上是32767。
|
1月前
|
存储 数据可视化 数据安全/隐私保护
【C语言】C语言-成绩管理系统(管理员+教师+学生 源码)【独一无二】
【C语言】C语言-成绩管理系统(管理员+教师+学生 源码)【独一无二】