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;
}
目录
相关文章
|
3月前
|
C语言
C语言之斗地主游戏
该代码实现了一个简单的斗地主游戏,包括头文件引入、宏定义、颜色枚举、卡牌类、卡牌类型类、卡牌组合类、玩家类、游戏主类以及辅助函数等,涵盖了从牌的生成、分配、玩家操作到游戏流程控制的完整逻辑。
112 8
|
2天前
|
定位技术 C语言
c语言及数据结构实现简单贪吃蛇小游戏
c语言及数据结构实现简单贪吃蛇小游戏
|
3月前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
91 2
|
3月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
192 7
|
3月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
153 8
|
3月前
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
53 1
|
C语言
c语言打字母游戏源码
c语言打字母游戏源码
161 0
|
1月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
62 23
|
1月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
66 15
|
1月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
60 24