前言:
通过持续数月的C语言系统学习,我们已经掌握了包括指针操作、结构体使用、文件IO等核心编程能力。为了检验学习成果并提升实战经验,在本篇技术博客中,我将带领大家开发一个具有里程碑意义的经典游戏项目 -- 贪吃蛇。
温馨提示:本篇博客为贪吃蛇游戏的前言准备。

一、贪吃蛇游戏效果演示
游戏效果演示:
二、贪吃蛇游戏设计
2.1 贪吃蛇游戏的最终目标
使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。
贪吃蛇游戏实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
• 退出游戏
2.2贪吃蛇游戏的思维导图
贪吃蛇游戏的思维导图如下图所示:

2.3贪吃蛇游戏的核心逻辑
核心逻辑:循环内依次执行输入处理→蛇移动→碰撞检测→状态显示→休眠:
2.3.1核心数据结构
采用链表存储蛇身:每个SnakeNode
节点包含坐标(x
,y
)和指向下一节点的指针,通过 "头增尾删" 实现蛇的移动(吃食物时只增不删,长度增长)。
2.3.2游戏流程循环
1. 初始化阶段
①控制台设置:调整窗口大小、标题,隐藏光标(提升视觉流畅度)。
②地图与蛇初始化:绘制边界(如上下左右的墙),生成初始蛇身(默认设置为 5 个节点,初始方向向右)。
③食物生成:随机生成坐标,确保不与蛇身重叠。
2. 运行循环(持续重复)
1.输入处理:
①监听键盘事件(方向键改方向、空格暂停 / 继续、F3 加速、F4 减速、ESC 退出)
②限制 "反向无效"(如当前向上时,按向下键不改变方向,避免瞬间自撞)。
2.蛇移动:
①按当前方向,在头部生成新节点(模拟 "前进")。
②若吃到食物(新头节点坐标与食物坐标重合):不删除尾部节点,蛇长度 + 1,重新生成食物并加分。
③若没吃到食物:删除尾部节点(保持长度不变),并清除尾部节点的屏幕显示。
3.碰撞:
①撞墙:新头节点坐标超出地图边界。
②自撞:新头节点坐标与自身其他节点(非头、非尾)坐标重合。
③若碰撞,设置 "游戏结束" 状态,退出循环。
④状态显示:在屏幕右侧显示分数、速度等级、游戏状态(正常 / 暂停)。
⑤休眠控制:通过
Sleep(速度)
控制移动频率(速度越快,休眠时间越短,蛇移动越敏捷)。
3. 结束与重玩
①游戏结束:释放蛇身链表的内存,显示 "Game Over"。
②重玩询问:提示 "是否重玩(Y/N)",根据输入决定是否重启 "初始化→运行循环"。
2.3.3关键机制细节
①移动的本质:链表的 "头插(前进)+ 尾删(保持长度)",视觉上呈现蛇的 "移动" 效果。
②食物系统:随机生成 + 避蛇身检测,保证食物可被吃到;吃食物后长度增长、分数增加,形成 "成长激励"。
③碰撞判定:通过坐标比对,快速判断 "撞墙" 或 "自撞",一旦触发则终止游戏循环。
④速度与策略:F3/F4 调整
Sleep
时长实现 "加速 / 减速",同时关联分数变化(加速加分、减速减分),让玩家在 "风险(速度快易撞)" 和 "收益(加分多)" 间做选择。
三、贪吃蛇游戏设计的技术栈
1. 编程语言
C 语言:游戏核心逻辑(如蛇的移动、碰撞检测、食物生成等)、数据结构定义、函数实现均使用 C 语言完成,包括结构体、枚举、指针、链表操作等 C 语言核心特性。
2. Windows API
游戏通过 Windows 系统提供的 API 实现控制台交互,主要涉及:
①控制台窗口控制:设置窗口大小,设置窗口标题。
②光标操作:隐藏和显示光标,定位光标位置(用于绘制蛇、食物、墙壁等元素)。
③键盘输入检测:实时获取键盘按键状态(如方向键、F3/F4、空格、ESC 等),实现对蛇的控制和游戏状态切换。
3. 数据结构
链表:
①蛇的身体通过链表连接,使用头插法添加新节点(蛇头移动)。
②通过遍历链表实现蛇身绘制、碰撞检测(撞自己)和内存释放。
结构体与枚举:
①存储蛇节点坐标,存储食物坐标和分数,整合蛇的核心信息(头节点、食物指针、方向、状态等)。
②用枚举定义蛇的移动方向(上下左右),用枚举定义游戏状态(正常运行、撞墙、撞自己、暂停等),使状态管理更清晰。
4. 控制台图形绘制
通过宽字符和光标定位在控制台绘制游戏元素:
①墙壁、蛇身、食物。
②游戏信息(分数、速度等级、操作提示)的文本绘制。
5. 游戏逻辑与状态管理
核心逻辑:
①蛇的移动:通过计算下一个节点坐标,结合方向枚举实现移动,并根据是否吃到食物决定是否增长蛇身或保持长度。
②碰撞检测:检测蛇头是否撞墙,检测蛇头是否撞到自身。
③分数与速度控制:吃食物增加分数,F3/F4 键调整速度(通过
_speed
控制休眠时间Sleep
),并关联分数变化。④状态检测:通过枚举管理游戏状态(正常运行、暂停、结束等),在循环中根据状态决定流程(继续运行、退出、重启等)。
6. 内存管理
①动态内存分配:使用
malloc
为蛇节点分配内存,避免栈内存溢出。②内存释放:通过遍历链表释放所有蛇节点内存,防止内存泄漏。
7. 标准库与工具
①C 标准库:
stdio.h
(输入输出)、stdlib.h
(内存分配、随机数)、time.h
(srand
初始化随机数种子,确保食物位置随机)、assert.h
(断言指针有效性,调试用)。②随机数生成:
rand()
结合time(0)
生成随机食物坐标,确保食物位置不与蛇身或墙壁重叠。
四、Windows API的详解
4.1 win32API
简单来说:Windows 是多作业系统,除了协调程序、分配内存、管资源,还像个 "服务站"------ 提供各种函数(服务)。应用程序调用这些函数,就能实现开窗口、画图形、用外设等操作,这类服务应用的函数叫 API;而 WIN32 API,就是 32 位 Windows 平台的这类编程接口。
4.2控制台主程序
平常我们运⾏起来的⿊框程序其实就是控制台程序,如下图所示:

4.2.1设置窗口大小
我们可以使用一些cmd指令来设置控制台的长宽,将控制台的长,宽设置为100 和 30
例如:通过这段指令:mode con cols=100 lines=30
4.2.2设置控制台名称
同时我们也可以设置,控制台的名称。
通过如下指令:title 贪吃蛇

4.2.3利用代码实现
当然我们也可以通过C语言代码,来实现控制台的大小和标题设置,通过system("指令")这个函数来实现
温馨提示:system("指令") 这个函数需要包含<windows.h>这个头文件
cpp
void test01()
{
system("mode con cols=130 lines=40");
system("title 贪吃蛇");
system("pause");
}
4.3控制台屏幕上的坐标
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
在控制台上的坐标系如下图所示:

COORD类型的声明:
cpp
typedef struct _COORD
{
SHORT X; // X坐标
SHORT Y; // Y坐标
} COORD, *PCOORD;
int main()
{
//例如给坐标赋值:
COORD pos = { 10, 15 };
return 0;
}
4.4通过句柄操作设备
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
简单来说就相当于一个手柄,通过该手柄就可以控制设备了,这里我们不需要过多与纠结其函数是如何实现,我们仅需要明白它的功能和如何调用就已经够用了。
GetStdHandle函数原型:
HANDLE GetStdHandle(DWORD nStdHandle);
它有三个参数:
1.STD_INPUT_HANDLE 获取标准输入设备
2.STD_OUTPUT_HANDLE 获取标准输出设备
3.STD_ERROR_HANDLE 获取标准错误设备
其中返回值HANDLE为一个void * 的指针,通过 typedef void *HANDLE 命名HANDLE。
这里我们只需要对控制台(标准输出)进行操作,所以我们仅需要用到获取标准输出设备,通过调用我们就可以进行操作控制台程序。
HANDLE GetStdHandle(STD_OUTPUT_HANDLE);
代码示例:
cppHANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
4.5获取控制台光标信息
GetConsoleCursorInfo函数原型:
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput
**参数二:指向存放光标信息的结构体:**PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
结构体_CONSOLE_CURSOR_INFO:主要用来存放控制台光标信息
typedef struct _CONSOLE_CURSOR_INFO
{
DWORD dwSize; //成员一 设置光标的大小
BOOL bVisible; //成员二 设置光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
成员一:dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条。
成员二:bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE
4.6设置控制台光标信息
SetConsoleCursorInfo函数:设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
函数原型为:
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput
**参数二:指向存放光标信息的结构体:**PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
4.7代码演示光标的设置
通过上面三个函数,我们就可以实现对光标大小和显示的操作
4.7.1设置光标大小
初始时光标的大小默认为25,如图所示:

代码示例:将默认的光标大小设置为100
cpp
//获得控制台窗口,进行使用
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
//定义储存控制台光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获得与houtput句柄相关的控制台光标的信息
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标的占比值
cursor_info.dwSize = 100;
//设置光标大小和光标可见度的函数
SetConsoleCursorInfo(houtput, &cursor_info);
如图所示:

4.7.2设置光标是否可见
如图所示,在默认状态下光标为可见状态:

代码示例:将光标设置为不可见状态
cpp
//获得控制台窗口,进行使用
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
//定义储存控制台光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获得与houtput句柄相关的控制台光标的信息
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标是否可见
cursor_info.bVisible = false;
//设置光标大小和光标可见度的函数
SetConsoleCursorInfo(houtput, &cursor_info);

4.8设置光标的位置
SetConsoleCursorPosition:设置指定控制台屏幕缓冲区中的光标位置
函数原型如下:
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput
**参数二:存放位置信息的坐标:**COORD pos
通过该函数,我们就可以设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中。
调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
4.8.1设置光标到指定的位置
cpp
//获得控制台窗口,进行使用
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
//定义储存控制台光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获得与houtput句柄相关的控制台光标的信息
GetConsoleCursorInfo(houtput, &cursor_info);
//设置控制台坐标
COORD pos = { 10, 20 };
//设置指定位置光标
SetConsoleCursorPosition(houtput, pos);
//进行暂停观察
getchar();

4.8.2封装设置光标位置的函数
cpp
//封装一个函数,用来设置光标位置
void set_pos(short x, short y)
{
HANDLE houtput= GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
4.9获取按键情况
GetAsyncKeyState:将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
函数原型如下:
SHORT GetAsyncKeyState(int vKey);
参数分析:键盘上按键的虚拟键值****int vKey
返回值分析:
1.GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后。
2.如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;
3.可以将返回值&0x1来进行检测:GetAsyncKeyState返回值的最低值是否为1
参考:虚拟键码表
代码示例1:定义宏判断按键是否被按下
cpp
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
代码示例2:检测数字键0~9是否被按下
cpp
//通过定义宏来判断
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1 ) ? 1 : 0 )
void test04()
{
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");
}
}
}
五、宽字符的打印
在贪吃蛇游戏中,我们采用宽字符进行界面渲染。游戏地图中的墙体使用宽字符□表示,蛇身使用●字符,食物则用★字符标识。与普通单字节字符不同,这些宽字符每个占据2个字节的存储空间。
对于宽字符的打印,需要进行本地化处理,通过如下函数进行本地化处理:
setlocale函数:进行本地化处理
函数原型如下所示:
char* setlocale (int category, const char* locale);
参数一:
• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
一般而言我们进行传入LC_ALL对所有类型进行修改。
参数二:
C标准仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)
温馨提示:使用该函数,需要包含<locale.h>头文件
宽字符打印的注意事项:
1.宽字符的字⾯量必须加上前缀"L",否则 C 语⾔会把字⾯量当作窄字符类型处理。
2.前缀"L"在单引号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;
3.在双引号前⾯,表⽰宽字符串,对应wprintf() 的占位符为 %ls
代码示例1:打印单个宽字符
cpp
#include <stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
char a = 'a';
char b = 'b';
printf("%c%c\n", a, b);
wchar_t wc1 = L'★';
wchar_t wc2 = L'我';
wprintf(L"%lc \n%lc", wc1, wc2);
return 0;
}
代码示例2:打印宽字符串
cpp
#include <stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
wprintf(L"Hello World\n");
wchar_t wstr[] = L"宽字符字符串";
wprintf(L"%ls",wstr);
return 0;
}
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。
