一、游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列
本贪吃蛇游戏基于C语言开发,采用控制台窗口作为游戏界面,通过键盘方向 键控制蛇的移动,蛇吃到随机生成的食物后身体增长、分数增加 ,若蛇头触碰墙壁或自身身体则游戏结束 ,整体通过循环刷新界面 、坐标控制 、按键检测实现经典贪吃蛇的核心玩法,代码简洁易懂,适合 C 语言初学者学习图形控制、逻辑判断和简单游戏开发
二、游戏效果演示



三、贪吃蛇基本功能
使⽤C语⾔ 在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
四、需要的知识
C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等
五、Win32 API介绍
实现贪吃蛇会使⽤到的⼀些Win32 API知识
1.Win32 API
Windows 作为多任务操作系统,不仅负责统筹应用程序运行、内存分配与系统资源管理,更是一个庞大的系统服务平台。应用程序可通过调用平台提供的各类系统服务(每项服务对应一个功能函数),实现窗口创建、图形绘制、外设调用等操作。由于这些函数服务于应用程序开发,因此被称为应用程序编程接口,即 API 函数;而 WIN32 API 专指微软 Windows 32 位平台下的应用程序编程接口
2.控制台程序(Console)
平常运⾏起来的**⿊框程序** 其实就是控制台程序

可以使⽤cmd命令来设置控制台窗⼝的**⻓宽**:设置控制台窗⼝的⼤⼩,30⾏,100列
c
mode con cols=100 lines=30
参考:mode指令
还可以通过命令设置控制台窗⼝的名字
c
title 贪吃蛇
参考:title指令
这些指令能在控制台窗⼝执⾏的命令 ,也可以调⽤C语⾔函数 system来执⾏
c
include <stdio.h>
int main()
{
//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗⼝名称
system("title 贪吃蛇");
getchar();
return 0;
}

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

COORD类型的声明:
c
typedef struct _COORD
{
SHORT X;
SHORT Y;
} COORD, * PCOORD;
给坐标赋值:
c
COORD pos = { 10, 15 };
4.GetStdHandle
参考:GetStdHandle
GetStdHandle 是⼀个Windows API函数。⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备
c
HANDLE GetStdHandle(DWORD nStdHandle);
如:
c
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
5.GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩ 和可⻅性的信息
c
BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息
5.1.CONSOLE_CURSOR_INFO
c
typedef struct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
}
dwSize光标填充的字符单元格的百分⽐ 。 此值介于1到100之间 。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条bVisible游标的可⻅性。 如果光标可⻅,则此成员为TRUE
c
CursorInfo.bVisible = false; //隐藏控制台光标
6.SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性
c
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo)
如:
c
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
//获取控制台光标信息
GetConsoleCursorInfo(hOutput, &CursorInfo);
//隐藏控制台光标
CursorInfo.bVisible = false;
//设置控制台光标状态
SetConsoleCursorInfo(hOutput, &CursorInfo);
7.SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置
c
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD pos)
如:
c
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
SetPos:封装⼀个设置光标位置的函数
c
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
8. GetAsyncKeyState
获取按键情况,GetAsyncKeyState 的函数原型如下:
c
SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态 。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是 1,说明按键的状态是按下 ,如果最⾼是 0,说明按键的状态是抬起 ;如果最低位被置为1则说明,该按键被按过,否则为0
c
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
参考:虚拟键码 (Winuser.h) - Win32 apps
六、贪吃蛇游戏设计与分析
1.地图



1.1.setlocale 函数
参考:setlocale
c
char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和 ""(本地模式)。
在任意程序执⾏开始,都会隐藏式执⾏调⽤:
c
setlocale(LC_ALL, "C");
当地区设置为"C"时,设置为C语⾔默认的模式,这时库函数按正常⽅式执⾏。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤""作为第2个参数,调⽤setlocale函
数就可以切换到本地模式,这种模式下程序会适应本地环境。
⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等
c
setlocale(LC_ALL, "");//切换到本地环境
setlocale 的返回值是⼀个字符串指针,表⽰已经设置好的格式。如果调⽤失败,则返回空指针 NULL 。
setlocale() 可以⽤来查询当前地区,这时第⼆个参数设为 NULL 就可以了。
c
#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;
}
1.2.宽字符打印
在游戏地图上,我们打印墙体使⽤宽字符 :□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★普通的字符是占1个字节的 ,这类宽字符是占⽤2个字节
宽字符的字⾯量必须加上前缀 L ,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀 L在单引号前⾯,表⽰宽字符,宽字符的打印使⽤ wprintf ,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls
2.蛇⾝和⻝物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处
注意 :
蛇的每个节点的x坐标必须是2个倍数 ,否则可能会出现蛇的**⼀个节点有⼀半⼉出现在墙体中** ,另外⼀般在墙外的现象,坐标不好对⻬
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★
3.结构设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:
c
// 贪吃蛇节点结构体
// 作用:表示贪吃蛇身体的每一个小方块
typedef struct SnakeNode
{
// 节点的横坐标(在控制台/游戏界面的 X 位置)
int x;
// 节点的纵坐标(在控制台/游戏界面的 Y 位置)
int y;
// 指针:指向下一个身体节点,形成链表结构
struct SnakeNode* next;
}
// 结构体别名:直接用 SnakeNode 表示节点
SnakeNode,
// 指针别名:pSnakeNode 等价于 struct SnakeNode*
* pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
c
// 贪吃蛇游戏总控结构体
// 作用:管理整个游戏的所有数据(蛇、食物、分数、状态、方向等)
typedef struct Snake
{
// 指向蛇头节点的指针,用来维护整条蛇的链表结构
pSnakeNode _pSnake;
// 指向食物节点的指针,管理游戏中食物的位置
pSnakeNode _pFood;
// 蛇头当前移动的方向(上/下/左/右),默认向右
enum DIRECTION _Dir;
// 游戏当前状态(正常运行/游戏结束/暂停等)
enum GAME_STATUS _Status;
// 游戏当前获得的总分数
int _Score;
// 每个食物的分值,默认每个食物 10 分
int _foodWeight;
// 蛇每移动一步的休眠时间(控制游戏速度)
int _SleepTime;
}
// 结构体别名:直接用 Snake 表示整个游戏对象
Snake,
// 指针别名:pSnake 等价于 struct Snake*
* pSnake;
蛇的⽅向,可以⼀⼀列举,使⽤枚举
c
// 方向枚举
// 作用:定义蛇头的四个移动方向
enum DIRECTION
{
UP, // 向上
DOWN, // 向下
LEFT, // 向左
RIGHT // 向右(默认方向)
};
游戏状态,可以⼀⼀列举,使⽤枚举
c
//游戏状态
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
END_NOMAL//正常结束
};
4.游戏流程设计
4.1.游戏开始 - GameStart
- 设置游戏窗口的大小
- 设置窗口的名字
- 隐藏屏幕光标
- 打印欢迎界面 -
WelcomeToGame - 创建地图 -
CreateMap - 初始化蛇身 -
nitSnake - 创建食物 -
CreateFood
4.2.游戏运行 - GameRun
- 右侧打印帮助信息 -
PrintHelpInfo - 打印当前已获得分数和每个食物的分数
- 获取按键情况 -
KEY_PRESS - 根据按键情况移动蛇 -
SnakeMove
4.3.SnakeMove(蛇移动子流程)
- 根据蛇头的坐标和方向,计算下一个节点的坐标
- 判断下一个节点是否是食物 -
NextIsFood - 是食物就吃掉 -
EatFood - 不是食物,吃掉食物,尾巴删除一节 -
NoFood - 判断是否撞墙 -
KillByWall - 判断是否装上自己 -
KillBySelf
4.4.游戏结束 - GameEnd
- 告知游戏结束的原因
- 释放蛇身节点
七、有戏逻辑实现分析
1.游戏主逻辑
程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑。
主逻辑分为3个过程:
- 游戏开始
GameStart完成游戏的初始化 - 游戏运⾏
GameRun完成游戏运⾏逻辑的实现 - 游戏结束
GameEnd完成游戏结束的说明,实现资源释放
c
#include "Snake.h"
// 游戏核心流程函数
void game()
{
// 接收用户是否再来一局的输入字符
char ch = 0;
// 循环:支持游戏重开
do
{
// 清屏,保证每局游戏界面干净
system("cls");
// 定义贪吃蛇游戏结构体变量,初始化为0
Snake snake = { 0 };
// 游戏初始化:创建蛇、食物、设置初始状态
GameStart(&snake);
// 游戏运行:移动、按键、碰撞、吃食物等核心逻辑
GameRun(&snake);
// 游戏结束:释放内存、销毁蛇、清理界面
GameEnd(&snake);
// 将光标定位到界面下方,提示用户是否重开
SetPos(76, 29);
printf("是否再来一局(Y/N):");
// 获取用户输入
ch = getchar();
// 清空输入缓冲区,防止残留字符影响下一次输入
while (getchar() != '\n');
}
// 用户输入Y/y,重新开始一局
while (ch == 'Y' || ch == 'y');
// 游戏完全退出,将光标归位
SetPos(0, 53);
}
// 主函数:程序入口
int main()
{
// 设置本地语言环境,解决中文乱码问题
setlocale(LC_ALL, "");
// 设置随机数种子,保证食物随机生成
srand((unsigned)time(NULL));
// 启动游戏逻辑
game();
// 程序正常结束
return 0;
}
2. 游戏开始GameStart
这个模块完成游戏的初始化任务:
- 控制台窗⼝⼤⼩的设置
- 控制台窗⼝名字的设置
- ⿏标光标的隐藏
- 打印欢迎界⾯
- 创建地图
- 初始化蛇
- 创建第⼀个⻝物
c
//游戏的初始化
//功能:启动游戏时的所有准备工作(窗口、光标、地图、蛇、食物)
void GameStart(pSnake ps)
{
//设置控制台窗口大小:宽180列,高55行
system("mode con cols=180 lines=55");
//设置控制台窗口标题名称:贪吃蛇
system("title 贪吃蛇");
//获取标准输出句柄(用于操作控制台光标)
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义光标信息结构体
CONSOLE_CURSOR_INFO CurserInfo;
//获取当前控制台光标信息
GetConsoleCursorInfo(houtput, &CurserInfo);
//设置光标不可见(游戏界面更美观)
CurserInfo.bVisible = false;
//将设置应用到控制台
SetConsoleCursorInfo(houtput, &CurserInfo);
//打印游戏欢迎界面与操作说明
WelcomeToGame();
//绘制游戏地图边框
CreateMap();
//初始化蛇的身体、方向、状态等信息
InitSnake(ps);
//随机创建第一个食物
CreateFood(ps);
}
2.1.打印欢迎界⾯
WelcomeToGame函数实现逻辑:
该函数用于展示游戏启动前的欢迎界面与操作说明,通过光标定位函数在控制台指定位置打印欢迎语,暂停等待玩家按键确认后清屏;接着再次定位光标打印详细操作说明,包括方向键控制、加速减速规则等内容,再次暂停等待玩家按键后清屏,完成游戏前的引导流程,为玩家提供清晰的操作指引
c
//打印界面和功能介绍
//功能:展示游戏欢迎语 + 操作说明,按任意键继续
void WelcomeToGame()
{
//设置光标位置:x=82,y=27
SetPos(82,27);
//打印游戏欢迎语(宽字符,支持中文)
wprintf(L"欢迎来到贪吃蛇小游戏\n");
//设置光标位置:x=82,y=30
SetPos(82, 30);
//暂停,提示按任意键继续
system("pause");
//清屏
system("cls");
//设置光标位置:x=50,y=27
SetPos(50, 27);
//打印游戏操作说明:方向键控制、F3加速、F4减速
wprintf(L"用 ↑ ↓ ← → 来控制蛇的移动,按 F3 加速,按 F4 减速,加速能得到更高的分数\n");
//设置光标位置:x=80,y=30
SetPos(80, 30);
//暂停,按任意键开始游戏
system("pause");
//清屏,进入游戏界面
system("cls");
}
2.2.创建地图
创建地图就是将墙打印出来,因为是宽字符打印 ,所有使⽤wprintf函数,打印格式串前使⽤L
c
// 墙:定义地图边框的显示符号
// L'□' 表示宽字符方块,用于绘制游戏地图的围墙
#define WALL L'□'
创建地图函数CreateMap
该函数用于绘制贪吃蛇游戏的闭合地图围墙,通过循环与光标定位分四部分完成:首先从左上角开始横向打印上边界围墙,接着定位到控制台底部横向打印下边界围墙,再分别沿最左侧与最右侧纵向打印左右边界围墙,最终形成一个封闭的矩形游戏区域,限制蛇的移动范围
c
//绘制地图
//功能:使用 WALL 符号绘制游戏的上下左右围墙
void CreateMap()
{
// 绘制【上围墙】,从坐标(0,0)开始,横向打印70个墙方块
for (int i = 0; i < 70; i++)
{
wprintf(L"%lc", WALL);
}
// 绘制【下围墙】,先将光标定位到 (0, 50),再横向打印70个墙方块
SetPos(0, 50);
for (int i = 0; i < 70; i++)
{
wprintf(L"%lc", WALL);
}
// 绘制【左围墙】,纵向打印,从y=1到y=49,x坐标固定为0
for (int i = 1; i < 50; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
// 绘制【右围墙】,纵向打印,从y=1到y=49,x坐标固定为138
for (int i = 1; i < 50; i++)
{
SetPos(138, i);
wprintf(L"%lc", WALL);
}
}
2.3.初始化蛇⾝
创建5个节点,然后将每个节点存放在链表中进⾏管理
创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,每个⻝物的分数
- 游戏状态是:OK
- 蛇的移动速度:200毫秒
- 蛇的默认⽅向:RIGHT
- 初始成绩:0
- 每个⻝物的分数:10
- 蛇的初始位置:(70,30)
蛇⾝打印的宽字符:
c
// 蛇身的符号
// L'●' 表示实心圆,作为贪吃蛇身体节点的显示图案
#define BODY L'●'
蛇的初始位置:(70,30)
c
//蛇的初始位置
#define POS_X 70
#define POS_Y 30
初始化蛇⾝函数:InitSnake
c
//初始化蛇
//功能:创建蛇的初始身体(5个节点),设置位置、打印蛇身、初始化游戏属性
void InitSnake(pSnake ps)
{
// 定义当前节点指针
pSnakeNode cur = NULL;
// 创建 5 个节点的蛇身(默认长度)
for (int i = 0; i < 5; i++)
{
// 开辟节点内存
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
// 开辟失败报错并退出程序
perror("InitSnake()::malloc() failed");
exit(-1);
}
cur->next = NULL;
// 设置节点坐标:x 依次递增,y 保持不变(水平向右)
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
// 头插法插入节点(保证蛇头在最右侧)
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->_direction = RIGHT; // 默认向右移动
ps->_score = 0; // 初始分数 0
ps->_food_weight = 10; // 每个食物 10 分
ps->_sleep_time = 200; // 移动间隔 200ms(控制速度)
ps->_status = OK; // 游戏状态正常
}
2.4.创建⼀个⻝物
- 先随机⽣成⻝物的坐标
- x坐标必须是2的倍数
- ⻝物的坐标得在墙体内部
- ⻝物的坐标不能和蛇⾝每个节点的坐标重复
- 创建⻝物节点,打印⻝物
⻝物打印的宽字符:
c
//食物的符号
#define FOOD L'★'
创建⻝物的函数:CreateFood
c
// 功能:在地图随机位置创建食物,且不与蛇身重叠
void CreateFood(pSnake ps)
{
// 定义食物坐标变量
int x = 0;
int y = 0;
// 标签:坐标冲突时跳回此处重新生成
again:
// 随机生成食物坐标,并保证 x 坐标为偶数(与蛇身对齐)
do
{
// 随机生成 x 坐标:范围 2 ~ 137
x = (rand() % 135) + 2;
// 随机生成 y 坐标:范围 1 ~ 49
y = (rand() % 49) + 1;
} while (x % 2 != 0);
// 遍历蛇身,检测食物坐标是否与蛇身冲突
pSnakeNode cur = ps->_pSnake;
while (cur)
{
// x、y 坐标与蛇身节点完全相同,说明冲突
if (x == cur->x && y == cur->y)
{
// 跳回重新生成坐标
goto again;
}
// 遍历下一个蛇身节点
cur = cur->next;
}
// 动态分配食物节点内存
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
// 内存分配失败,打印错误信息
perror("CreateFood()::malloc() failed");
// 退出程序
exit(-2);
}
// 给食物节点赋值 x 坐标
pFood->x = x;
// 给食物节点赋值 y 坐标
pFood->y = y;
// 初始化食物节点 next 指针为空
pFood->next = NULL;
// 光标定位到食物坐标
SetPos(x, y);
// 打印食物图案
wprintf(L"%lc", FOOD);
// 将创建好的食物节点保存到游戏结构体
ps->_pFood = pFood;
}

3. 游戏运⾏GameRun
该函数是贪吃蛇游戏的核心运行循环,首先打印右侧操作帮助信息,随后进入循环:实时刷新显示总分数与当前食物分数;通过宏检测玩家按键,实现上下左右方向控制(禁止反向)、空格暂停 / 继续、F3 加速、F4 减速、ESC 退出游戏等功能;每次循环调用蛇移动函数让蛇前进一格,并根据设置的休眠时间控制移动速度;只要游戏状态保持正常,循环就持续执行,直到撞墙、自撞或主动退出才终止循环,结束游戏运行
需要的虚拟按键:
- 上:
VK_UP - 下:
VK_DOWN - 左:
VK_LEFT - 右:
VK_RIGHT - 空格:
VK_SPACE - ESC:
VK_ESCAPE - F3:
VK_F3 - F4:
VK_F4
c
在这里插入代码片// 功能:游戏运行核心逻辑
void GameRun(pSnake ps)
{
// 打印游戏右侧的操作帮助信息
PrintHelpInfo();
// 游戏主循环:只要状态正常就持续运行
do
{
// 定位光标并实时打印【总分数】
SetPos(145, 24);
printf("总分数:%d", ps->_score);
// 定位光标并实时打印【当前食物的分数】
SetPos(145, 26);
printf("当前食物的分数:%2d", ps->_food_weight);
// 检测 上 键:不能直接向下掉头
if (KEY_PRESS(VK_UP) && ps->_direction != DOWN)
{
ps->_direction = UP;
}
// 检测 下 键:不能直接向上掉头
else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
{
ps->_direction = DOWN;
}
// 检测 左 键:不能直接向右掉头
else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
{
ps->_direction = LEFT;
}
// 检测 右 键:不能直接向左掉头
else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
{
ps->_direction = RIGHT;
}
// 检测 空格 键:暂停/继续游戏
else if (KEY_PRESS(VK_SPACE))
{
//调用暂停函数
Pause();
}
// 检测 F3 键:加速(速度上限60ms)
else if (KEY_PRESS(VK_F3))
{
// 加速
if (ps->_sleep_time >= 60)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
// 检测 F4 键:减速(分数下限2)
else if (KEY_PRESS(VK_F4))
{
// 减速
if (ps->_food_weight >= 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
// 检测 ESC 键:正常退出游戏
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL;
}
// 执行蛇走一步的完整逻辑(移动/吃食物/碰撞检测)
SnakeMove(ps);
// 按设置的时间休眠,控制蛇的移动速度
Sleep(ps->_sleep_time);
// 游戏状态为 OK 时,持续循环
} while (ps->_status == OK);
}
3.1.KEY_PRESS
检测按键状态,封装了⼀个宏
c
//检测按键
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)
3.2.PrintHelpInfo
该函数用于在游戏界面右侧固定位置打印操作帮助信息,通过光标定位函数将提示文字依次显示在指定坐标处,内容包含游戏规则、方向控制、加速减速、退出及暂停方式,让玩家在游戏过程中随时查看操作说明,提升游戏体验
c
// 功能:在游戏界面右侧打印操作帮助信息
void PrintHelpInfo()
{
// 定位光标到 (145, 6),打印游戏规则:不能穿墙、不能咬到自己
SetPos(145, 6);
wprintf(L"%ls",L"不能穿墙,不能咬到自己");
// 定位光标到 (145, 8),打印:方向键控制移动
SetPos(145, 8);
wprintf(L"%ls", L"用 ↑ ↓ ← → 来控制蛇的移动");
// 定位光标到 (145, 10),打印:F3加速、F4减速
SetPos(145, 10);
wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
// 定位光标到 (145, 12),打印:加速分数更高
SetPos(145, 12);
wprintf(L"%ls", L"加速能得到更高的分数");
// 定位光标到 (145, 14),打印:按ESC退出游戏
SetPos(145, 14);
wprintf(L"%ls", L"按 Esc 退出游戏");
// 定位光标到 (145, 16),打印:空格暂停/开始
SetPos(145, 16);
wprintf(L"%ls", L"按 空格 开始/暂停游戏");
}

3.3.蛇⾝移动SnakeMove
先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标,确定了下⼀个位置后,看下⼀个位置是否是⻝物NextIsFood,是⻝物就做吃⻝物处理EatFood,如果不是⻝物则做前进⼀步的处理NoFood
蛇⾝移动后,判断此次移动是否会造成撞墙KillByWall或者撞上⾃⼰蛇⾝KillBySelf,从⽽影响游戏的状态
c
// 功能:蛇走一步的完整过程
void SnakeMove(pSnake ps)
{
// 创建一个新节点,表示蛇即将到达的下一个位置
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
// 判断节点内存分配是否成功
if (pNextNode == NULL)
{
// 分配失败,打印系统错误信息
perror("SnakeMove()::malloc() failed");
// 退出程序
exit(-3);
}
// 根据蛇当前的移动方向,设置下一个节点的坐标
switch(ps->_direction)
{
// 向上:x不变,y减1
case UP:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
// 向下:x不变,y加1
case DOWN:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
break;
// 向左:x减2,y不变(保证与蛇身对齐)
case LEFT:
pNextNode->x = ps->_pSnake->x - 2;
pNextNode->y = ps->_pSnake->y;
break;
// 向右:x加2,y不变(保证与蛇身对齐)
case RIGHT:
pNextNode->x = ps->_pSnake->x + 2;
pNextNode->y = ps->_pSnake->y;
break;
}
// 检测下一个坐标处是不是食物
if (NextIsFood(pNextNode, ps))
{
// 是食物,执行吃食物逻辑
EatFood(pNextNode, ps);
}
else
{
// 不是食物,执行正常移动逻辑
NoFood(pNextNode, ps);
}
// 检测蛇头是否撞到围墙
KillByWall(ps);
// 检测蛇头是否撞到自己的身体
KillBySelf(ps);
}
3.3.1.NextIsFood
该函数用于判断蛇的下一个移动位置是否为食物,通过对比下一个节点坐标与食物节点的 x、y 坐标是否完全相等,返回布尔值结果,为蛇移动时执行吃食物逻辑提供判断依据
c
// 功能:检测蛇的下一个位置是否是食物
// 参数:pn - 蛇下一个位置的节点,ps - 游戏结构体指针
// 返回值:是食物返回true,不是返回false
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
// 对比下一个节点坐标与食物坐标,完全相等则表示是食物
return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}
3.3.2.EatFood
该函数用于处理蛇吃到食物后的逻辑,采用头插法将食物节点直接变为新蛇头,释放临时节点避免内存泄漏;遍历整条蛇身重新打印,更新总分数;最后调用创建食物函数,在地图上随机生成新的食物,完成吃食物、长身体、加分、刷新食物的完整流程
c
// 功能:蛇吃到食物,身体增长、加分、生成新食物
void EatFood(pSnakeNode pn, pSnake ps)
{
// 头插法:将食物节点作为新蛇头,插入链表头部
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
// 释放临时创建的下一个位置节点,避免内存泄漏
free(pn);
pn = NULL;
// 遍历蛇身链表,重新打印整条蛇
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
// 总分数增加当前食物的分值
ps->_score += ps->_food_weight;
// 在地图上重新创建一个新的食物
CreateFood(ps);
}
3.3.3.NoFood
该函数用于处理蛇未吃到食物的正常移动逻辑,将新节点作为蛇头插入链表头部;遍历链表到倒数第二个节点并打印蛇身,将最后一个节点位置打印为空格实现擦除效果,释放最后一个节点内存并断开链接,模拟蛇前进、尾部消失的移动效果
c
// 功能:蛇下一步位置不是食物,正常向前移动
void NoFood(pSnakeNode pn, pSnake ps)
{
// 新节点作为蛇头,插入链表头部
pn->next = ps->_pSnake;
ps->_pSnake = pn;
// 从头遍历,直到倒数第二个节点
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
// 定位并打印当前节点的蛇身
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
// 指针后移
cur = cur->next;
}
3.3.4.KillByWall
该函数用于检测蛇头是否撞到游戏地图的围墙,判断蛇头坐标是否触碰上下左右任意一面围墙,若撞墙则将游戏状态设置为撞墙死亡,触发游戏结束
c
// 功能:检测蛇头是否撞到围墙,撞墙则游戏结束
void KillByWall(pSnake ps)
{
// 判断蛇头坐标是否触碰 左/右/上/下 任意一面围墙
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 138 || ps->_pSnake->y == 0 || ps->_pSnake->y == 50)
{
// 设置游戏状态为:撞墙死亡
ps->_status = KILL_BY_WALL;
}
}
3.3.5.KillBySelf
该函数用于检测蛇头是否撞到自身身体,从蛇头的下一个节点开始遍历整条蛇身,逐一对比身体节点坐标与蛇头坐标,若坐标重合则判定蛇撞到自己,将游戏状态设置为自撞死亡,触发游戏结束
c
// 功能:检测蛇头是否撞到自己的身体,自撞则游戏结束
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;
break;
}
// 继续遍历下一个身体节点
cur = cur->next;
}
}
4.游戏结束
该函数用于游戏结束后的善后处理,先清空控制台屏幕,根据游戏结束状态(正常退出、撞到自己、撞墙)打印对应的结束提示语;随后遍历蛇身链表,逐个释放所有节点内存,避免内存泄漏,完成游戏全部清理工作
c
// 功能:游戏结束,打印结果并释放内存
void GameEnd(pSnake ps)
{
// 清空控制台屏幕
system("cls");
// 根据游戏结束状态,打印不同的提示信息
switch (ps->_status)
{
// 正常主动退出
case END_NORMAL:
SetPos(78, 27);
printf("主动结束游戏\n");
break;
// 撞到自己身体
case KILL_BY_SELF:
SetPos(79, 27);
printf("撞到自己,游戏结束\n");
break;
// 撞到围墙
case KILL_BY_WALL:
SetPos(80, 27);
printf("撞到墙,游戏结束\n");
break;
}
八、所有代码
test.c
c
// 包含自定义的贪吃蛇头文件,提供游戏相关的函数和结构体声明
#include "Snake.h"
// 游戏主逻辑函数,封装整个贪吃蛇游戏的流程控制
void game()
{
// 定义字符变量ch,用于接收用户是否再来一局的输入(Y/N)
char ch = 0;
// do-while循环:先执行一次游戏,再根据用户输入判断是否重开
do
{
// 调用系统命令cls,清空控制台屏幕,保证每局游戏界面干净
system("cls");
// 定义贪吃蛇结构体变量snake,初始化为0(清空所有成员变量)
Snake snake = { 0 };
// 调用游戏初始化函数,传入snake的地址,完成蛇身、地图、食物等初始设置
GameStart(&snake);
// 调用游戏运行函数,传入snake的地址,处理游戏核心逻辑:移动、吃食物、撞墙、撞身体等
GameRun(&snake);
// 调用游戏结束函数,传入snake的地址,做善后处理:释放内存、重置数据等
GameEnd(&snake);
// 调用设置光标位置函数,将控制台光标移动到(76,29)坐标处
SetPos(76, 29);
// 在光标位置输出提示语,询问玩家是否再来一局
printf("是否再来一局(Y/N):");
// 获取用户输入的一个字符,赋值给ch
ch = getchar();
// 循环读取缓冲区剩余字符,直到读到换行符\n,清空输入缓冲区,避免影响下一次输入
while (getchar() != '\n');
// 循环条件:如果用户输入Y或y,就重新开始一局游戏
} while (ch == 'Y' || ch == 'y');
// 游戏完全退出前,将光标移动到(0,53)坐标处
SetPos(0, 53);
}
// 程序入口主函数
int main()
{
// 设置本地语言环境,适配控制台显示中文、特殊字符等,解决乱码问题
setlocale(LC_ALL, "");
// 设置随机数生成器的种子,以当前系统时间为种子,保证食物随机生成位置不重复
srand((unsigned)time(NULL));
// 调用game函数,启动贪吃蛇游戏
game();
// 主函数正常结束,返回0给操作系统
return 0;
}
Snake.h
c
// 防止头文件被重复包含(多次引用头文件时,只编译一次)
#pragma once
// 标准输入输出头文件,提供 printf、scanf、getchar 等输入输出函数
#include <stdio.h>
// 本地化设置头文件,用于设置控制台语言环境,解决中文乱码问题
#include <locale.h>
// Windows系统API头文件,提供控制台操作、按键检测、光标定位等功能
#include <windows.h>
// C语言标准布尔类型头文件,提供 bool、true、false
#include <stdbool.h>
// 标准库头文件,提供内存分配(malloc/free)、随机数(rand)等功能
#include <stdlib.h>
// 时间头文件,提供 time 函数,用于生成随机数种子
#include <time.h>
// 控制台输入头文件,提供 _getch 等无回显按键读取函数
#include <conio.h>
// 宏定义:墙壁的符号(宽字符,适配中文控制台) L表示宽字符
#define WALL L'□'
// 宏定义:蛇身的符号
#define BODY L'●'
// 宏定义:食物的符号
#define FOOD L'★'
// 宏定义:蛇出生的初始X坐标
#define POS_X 70
// 宏定义:蛇出生的初始Y坐标
#define POS_Y 30
// 宏定义:检测键盘按键是否按下
// GetAsyncKeyState:WindowsAPI,检测虚拟按键状态 &1 表示按键按下
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)
// 枚举类型:蛇的移动方向
enum DIRECTION
{
UP, // 向上 0
DOWN, // 向下 1
LEFT, // 向左 2
RIGHT // 向右 3
};
// 枚举类型:游戏运行状态
enum GAME_STATUS
{
OK, // 游戏正常运行
KILL_BY_WALL, // 游戏结束:撞墙
KILL_BY_SELF, // 游戏结束:撞到自己身体
END_NORMAL // 游戏正常退出(玩家主动退出)
};
// 结构体类型定义:蛇身的每一个节点
typedef struct SnakeNode
{
int x; // 节点的X坐标(横向)
int y; // 节点的Y坐标(纵向)
struct SnakeNode* next; // 指针:指向下一个蛇身节点,形成链表结构
}SnakeNode, * pSnakeNode; // 重命名:SnakeNode=节点结构体,pSnakeNode=节点指针
// 结构体类型定义:贪吃蛇游戏整体管理结构
typedef struct Snake
{
pSnakeNode _pSnake; // 指针:指向蛇头(整条蛇的链表头节点)
pSnakeNode _pFood; // 指针:指向当前食物的节点
enum DIRECTION _direction; // 当前蛇的移动方向
enum GAME_STATUS _status; // 当前游戏状态(正常/撞墙/撞自己/退出)
int _food_weight; // 单个食物的分数
int _score; // 玩家当前总得分
int _sleep_time; // 蛇移动间隔时间(值越小移动越快,越大越慢)
}Snake,* pSnake; // 重命名:Snake=游戏结构体,pSnake=游戏结构体指针
// 函数声明:游戏初始化(创建蛇、食物、地图、初始状态)
void GameStart(pSnake ps);
// 函数声明:打印游戏欢迎界面
void WelcomeToGame();
// 函数声明:设置控制台光标坐标(x横向,y纵向)
void SetPos(short x, short y);
// 函数声明:绘制游戏地图(围墙)
void CreateMap();
// 函数声明:初始化蛇的身体(创建初始蛇头+蛇身)
void InitSnake(pSnake ps);
// 函数声明:随机创建食物(不能出现在墙上/蛇身上)
void CreateFood(pSnake ps);
// 函数声明:游戏主运行逻辑(按键、移动、碰撞检测)
void GameRun(pSnake ps);
// 函数声明:蛇向前移动一步的核心逻辑
void SnakeMove(pSnake ps);
// 函数声明:检测蛇的下一个位置是否是食物,返回true/false
bool NextIsFood(pSnakeNode pn, pSnake ps);
// 函数声明:蛇吃到食物(增长身体、加分、生成新食物)
void EatFood(pSnakeNode pn, pSnake ps);
// 函数声明:蛇下一步不是食物(正常移动,不增长)
void NoFood(pSnakeNode pn, pSnake ps);
// 函数声明:检测蛇是否撞墙,撞墙则修改游戏状态
void KillByWall(pSnake ps);
// 函数声明:检测蛇是否撞到自己身体,撞到则修改游戏状态
void KillBySelf(pSnake ps);
// 函数声明:游戏结束善后(释放链表内存、重置状态)
void GameEnd(pSnake ps);
Snake.c
c
// 包含自定义贪吃蛇头文件,导入结构体、枚举、宏、函数声明
#include "Snake.h"
// 功能:设置控制台光标到指定(x,y)坐标位置
// 参数x:横向列坐标,参数y:纵向行坐标
void SetPos(short x, short y)
{
// 定义控制台输出设备句柄变量,初始化为空指针
HANDLE houtput = NULL;
// 获取控制台标准输出窗口的句柄,赋值给houtput
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 定义COORD坐标结构体变量pos,存入x、y坐标
COORD pos = { x,y };
// 调用Windows控制台API,移动光标到pos坐标位置
SetConsoleCursorPosition(houtput, pos);
}
// 功能:打印游戏欢迎界面、操作介绍、分步引导
void WelcomeToGame()
{
// 将光标移动到坐标(82,27)位置
SetPos(82,27);
// 宽字符打印欢迎贪吃蛇游戏字样
wprintf(L"欢迎来到贪吃蛇小游戏\n");
// 光标移动到(82,30)
SetPos(82, 30);
// 控制台暂停,等待用户按任意键继续
system("pause");
// 清空整个控制台屏幕
system("cls");
// 光标移动到(50,27)
SetPos(50, 27);
// 打印游戏按键操作说明
wprintf(L"用 ↑ ↓ ← → 来控制蛇的移动,按 F3 加速,按 F4 减速,加速能得到更高的分数\n");
// 光标移动到(80,30)
SetPos(80, 30);
// 再次暂停等待按键
system("pause");
// 再次清屏,进入正式游戏界面
system("cls");
}
// 功能:绘制游戏四周围墙地图
void CreateMap()
{
// 循环打印上方围墙,一共70个围墙符号
for (int i = 0; i < 70; i++)
{
// 宽字符输出围墙符号□
wprintf(L"%lc", WALL);
}
// 将光标移动到下方围墙起始坐标(0,50)
SetPos(0, 50);
// 循环打印下方围墙,70个围墙符号
for (int i = 0; i < 70; i++)
{
// 输出围墙符号
wprintf(L"%lc", WALL);
}
// 循环绘制左侧围墙,纵向从第1行到第49行
for (int i = 1; i < 50; i++)
{
// 光标移动到左侧围墙对应坐标(0,i)
SetPos(0, i);
// 输出围墙符号
wprintf(L"%lc", WALL);
}
// 循环绘制右侧围墙,纵向从第1行到第49行
for (int i = 1; i < 50; i++)
{
// 光标移动到右侧围墙对应坐标(138,i)
SetPos(138, i);
// 输出围墙符号
wprintf(L"%lc", WALL);
}
}
// 功能:初始化贪吃蛇,创建初始5节蛇身链表
// 参数ps:贪吃蛇整体结构体指针
void InitSnake(pSnake ps)
{
// 定义临时节点指针cur,用于创建蛇身节点,初始为空
pSnakeNode cur = NULL;
// 循环5次,创建初始5节蛇身体
for (int i = 0; i < 5; i++)
{
// 动态内存分配,申请一个蛇节点大小空间,强转为节点指针
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
// 判断内存是否申请失败
if (cur == NULL)
{
// 打印内存分配失败错误信息
perror("nitSnake()::malloc() failed");
// 异常退出程序,返回错误码-1
exit(-1);
}
// 新节点后继指针置空
cur->next = NULL;
// 设置节点x坐标,初始位置向右依次偏移2格
cur->x = POS_X + 2 * i;
// 设置节点y坐标,保持初始行不变
cur->y = POS_Y;
// 判断当前蛇头是否为空(链表为空)
if (ps->_pSnake == NULL)
{
// 链表为空,当前新节点直接作为蛇头
ps->_pSnake = cur;
}
// 链表不为空,使用头插法插入新节点
else
{
// 新节点指向原来的蛇头
cur->next = ps->_pSnake;
// 更新蛇头为当前新节点
ps->_pSnake = cur;
}
}
// 将cur重新指向蛇头,准备遍历打印蛇身
cur = ps->_pSnake;
// 遍历整条蛇链表
while (cur)
{
// 光标移动到当前蛇节点坐标
SetPos(cur->x, cur->y);
// 打印蛇身符号●
wprintf(L"%lc", BODY);
// cur向后移动,遍历下一个节点
cur = cur->next;
}
// 设置蛇默认移动方向为向右
ps->_direction = RIGHT;
// 初始化总分数为0
ps->_score = 0;
// 设置单个食物基础分值为10
ps->_food_weight = 10;
// 设置蛇初始移动间隔时间200毫秒
ps->_sleep_time = 200;
// 设置游戏初始状态为正常运行
ps->_status = OK;
}
// 功能:随机生成食物,食物不能在蛇身上、不能在墙上
void CreateFood(pSnake ps)
{
// 定义食物x坐标变量,初始0
int x = 0;
// 定义食物y坐标变量,初始0
int y = 0;
// 定义跳转标签,坐标冲突时回到此处重新生成食物
again:
// 循环随机生成坐标,直到x为偶数(和蛇对齐)
do
{
// 随机生成x坐标范围2~136
x = (rand() % 135) + 2;
// 随机生成y坐标范围1~49
y = (rand() % 49) + 1;
} while (x % 2 != 0);
// cur指向蛇头,开始遍历判断食物是否在蛇身上
pSnakeNode cur = ps->_pSnake;
// 遍历整条蛇链表
while (cur)
{
// 判断食物坐标和当前蛇节点坐标重合
if (x == cur->x && y == cur->y)
{
// 坐标冲突,跳转到again重新生成
goto again;
}
// cur向后遍历下一个蛇节点
cur = cur->next;
}
// 动态分配食物节点内存
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
// 判断内存分配是否失败
if (pFood == NULL)
{
// 打印错误信息
perror("CreateFood()::malloc() failed");
// 异常退出,错误码-2
exit(-2);
}
// 给食物节点赋值x坐标
pFood->x = x;
// 给食物节点赋值y坐标
pFood->y = y;
// 食物节点后继指针置空
pFood->next = NULL;
// 光标移动到食物坐标
SetPos(x, y);
// 打印食物符号★
wprintf(L"%lc", FOOD);
// 游戏结构体保存食物节点指针
ps->_pFood = pFood;
}
// 功能:游戏全部初始化总函数(窗口、光标、欢迎页、地图、蛇、食物)
void GameStart(pSnake ps)
{
// 设置控制台窗口大小:180列宽,55行高
system("mode con cols=180 lines=55");
// 设置控制台窗口标题为贪吃蛇
system("title 贪吃蛇");
// 获取控制台输出句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 定义光标信息结构体变量
CONSOLE_CURSOR_INFO CurserInfo;
// 获取当前控制台光标属性信息
GetConsoleCursorInfo(houtput, &CurserInfo);
// 设置光标不可见
CurserInfo.bVisible = false;
// 应用新的光标设置
SetConsoleCursorInfo(houtput, &CurserInfo);
// 调用欢迎界面打印函数
WelcomeToGame();
// 调用绘制地图围墙函数
CreateMap();
// 调用初始化蛇身函数
InitSnake(ps);
// 调用创建第一个食物函数
CreateFood(ps);
}
// 功能:在界面右侧打印游戏帮助说明文字
void PrintHelpInfo()
{
// 光标定位,打印游戏规则
SetPos(145, 6);
wprintf(L"%ls",L"不能穿墙,不能咬到自己");
// 光标定位,打印方向按键说明
SetPos(145, 8);
wprintf(L"%ls", L"用 ↑ ↓ ← → 来控制蛇的移动");
// 光标定位,打印F3F4加速减速说明
SetPos(145, 10);
wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
// 光标定位,打印加速加分规则
SetPos(145, 12);
wprintf(L"%ls", L"加速能得到更高的分数");
// 光标定位,打印ESC退出说明
SetPos(145, 14);
wprintf(L"%ls", L"按 Esc 退出游戏");
// 光标定位,打印空格暂停说明
SetPos(145, 16);
wprintf(L"%ls", L"按 空格 开始/暂停游戏");
}
// 功能:游戏暂停函数,按空格继续
void Pause()
{
// 死循环等待按键
while (1)
{
// 休眠200毫秒,减少CPU占用
Sleep(200);
// 判断空格键是否按下
if (KEY_PRESS(VK_SPACE))
{
// 按下空格,跳出循环,结束暂停
break;
}
}
}
// 功能:判断蛇下一步位置是不是食物
// 返回true:是食物,false:不是食物
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
// 判断下一步节点坐标==食物坐标
return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}
// 功能:蛇吃到食物时执行:蛇变长、加分、刷新食物
void EatFood(pSnakeNode pn, pSnake ps)
{
// 食物节点作为新蛇头,头插法
ps->_pFood->next = ps->_pSnake;
// 更新蛇头为食物节点
ps->_pSnake = ps->_pFood;
// 释放临时申请的下一步节点内存
free(pn);
// 指针置空,防止野指针
pn = NULL;
// cur指向蛇头,准备重新打印整条蛇
pSnakeNode cur = ps->_pSnake;
// 遍历整条蛇
while (cur)
{
// 光标移动到蛇节点位置
SetPos(cur->x, cur->y);
// 打印蛇身符号
wprintf(L"%lc", BODY);
// 遍历下一个节点
cur = cur->next;
}
// 总分加上当前食物分值
ps->_score += ps->_food_weight;
// 重新生成新食物
CreateFood(ps);
}
// 功能:蛇下一步没有食物,正常移动,不变长
void NoFood(pSnakeNode pn, pSnake ps)
{
// 新节点作为蛇头,头插
pn->next = ps->_pSnake;
// 更新蛇头
ps->_pSnake = pn;
// cur指向蛇头
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 == 138 || ps->_pSnake->y == 0 || ps->_pSnake->y == 50)
{
// 设置游戏状态为撞墙死亡
ps->_status = KILL_BY_WALL;
}
}
// 功能:检测蛇头是否撞到自己身体
void KillBySelf(pSnake ps)
{
// cur从蛇头下一个节点开始遍历(身体部分)
pSnakeNode cur = ps->_pSnake->next;
// 遍历整条身体链表
while (cur)
{
// 判断蛇头坐标和身体节点重合
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
// 设置状态为撞到自己死亡
ps->_status = KILL_BY_SELF;
// 跳出循环
break;
}
// 遍历下一个身体节点
cur = cur->next;
}
}
// 功能:蛇每移动一步的完整逻辑
void SnakeMove(pSnake ps)
{
// 动态申请一个新节点,代表蛇即将走到的下一个位置
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
// 判断内存申请是否失败
if (pNextNode == NULL)
{
// 打印错误信息
perror("SnakeMove()::malloc() failed");
// 异常退出,错误码-3
exit(-3);
}
// 根据当前蛇方向,计算下一步坐标
switch(ps->_direction)
{
case UP:
// 向上移动:x不变,y减1
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
case DOWN:
// 向下移动:x不变,y加1
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
break;
case LEFT:
// 向左移动:x减2,y不变
pNextNode->x = ps->_pSnake->x - 2;
pNextNode->y = ps->_pSnake->y;
break;
case RIGHT:
// 向右移动:x加2,y不变
pNextNode->x = ps->_pSnake->x + 2;
pNextNode->y = ps->_pSnake->y;
break;
}
// 判断下一步是不是食物
if (NextIsFood(pNextNode, ps))
{
// 是食物:执行吃食物逻辑
EatFood(pNextNode, ps);
}
else
{
// 不是食物:执行普通移动逻辑
NoFood(pNextNode, ps);
}
// 检测是否撞墙
KillByWall(ps);
// 检测是否撞自己
KillBySelf(ps);
}
// 功能:游戏主循环运行函数(按键、移动、计分、速度控制)
void GameRun(pSnake ps)
{
// 打印右侧帮助信息
PrintHelpInfo();
// 游戏正常运行时持续循环
do
{
// 光标定位,打印当前总分数
SetPos(145, 24);
printf("总分数:%d", ps->_score);
// 光标定位,打印当前单个食物分值
SetPos(145, 26);
printf("当前食物的分数:%2d", ps->_food_weight);
// 按下上键,且当前不是向下,禁止180°掉头
if (KEY_PRESS(VK_UP) && ps->_direction != DOWN)
{
// 修改方向为向上
ps->_direction = UP;
}
// 按下下键,且当前不是向上
else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
{
// 修改方向为向下
ps->_direction = DOWN;
}
// 按下左键,且当前不是向右
else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
{
// 修改方向为向左
ps->_direction = LEFT;
}
// 按下右键,且当前不是向左
else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
{
// 修改方向为向右
ps->_direction = RIGHT;
}
// 按下空格键
else if (KEY_PRESS(VK_SPACE))
{
// 调用暂停函数
Pause();
}
// 按下F3加速键
else if (KEY_PRESS(VK_F3))
{
// 速度大于等于60才可以加速
if (ps->_sleep_time >= 60)
{
// 移动间隔减少30ms,速度变快
ps->_sleep_time -= 30;
// 单个食物分数+2
ps->_food_weight += 2;
}
}
// 按下F4减速键
else if (KEY_PRESS(VK_F4))
{
// 食物分数大于等于2才可以减速
if (ps->_food_weight >= 2)
{
// 移动间隔增加30ms,速度变慢
ps->_sleep_time += 30;
// 单个食物分数-2
ps->_food_weight -= 2;
}
}
// 按下ESC退出键
else if (KEY_PRESS(VK_ESCAPE))
{
// 设置状态为主动正常退出
ps->_status = END_NORMAL;
}
// 调用蛇移动一步函数
SnakeMove(ps);
// 按照设置时间休眠,控制移动速度
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
// 功能:游戏结束,打印结束原因,释放蛇身全部内存
void GameEnd(pSnake ps)
{
// 清空屏幕
system("cls");
// 根据游戏结束状态打印对应提示
switch (ps->_status)
{
case END_NORMAL:
// 光标定位,打印主动退出
SetPos(78, 27);
printf("主动结束游戏\n");
break;
case KILL_BY_SELF:
// 光标定位,打印撞到自己
SetPos(79, 27);
printf("撞到自己,游戏结束\n");
break;
case KILL_BY_WALL:
// 光标定位,打印撞到墙
SetPos(80, 27);
printf("撞到墙,游戏结束\n");
break;
}
// cur指向蛇头,准备释放链表
pSnakeNode cur = ps->_pSnake;
// 遍历整条蛇链表释放内存
while (cur)
{
// del保存当前要删除节点
pSnakeNode del = cur;
// cur向后移动
cur = cur->next;
// 释放当前节点内存
free(del);
}
}