

🔥@晨非辰Tong: 个人主页
👀专栏:《数据结构与算法入门指南》、《C++学习之旅》
💪学习阶段:C/C++、数据结构与算法初学者
⏳"人理解迭代,神理解递归。"
文章目录
- 引用
- 一、游戏背景
- 二、游戏效果演示
- 三、项目预期目标
-
- [3.1 包含的基本功能](#3.1 包含的基本功能)
- [3.2 项目涉及技术要点](#3.2 项目涉及技术要点)
- [四、Win32 API介绍](#四、Win32 API介绍)
-
- [4.1 控制台程序(Console)](#4.1 控制台程序(Console))
- [4.2 控制台屏幕上的坐标COORD](#4.2 控制台屏幕上的坐标COORD)
- [4.3 获取句柄:GetStdHandle](#4.3 获取句柄:GetStdHandle)
- [4.4 获取光标信息:GetConsoleCursorInfor](#4.4 获取光标信息:GetConsoleCursorInfor)
-
- [4.4.1 接收光标:CONSOLE_CURSOR_INFO](#4.4.1 接收光标:CONSOLE_CURSOR_INFO)
- [4.5 设置光标信息:SetConsoleCursorInfo](#4.5 设置光标信息:SetConsoleCursorInfo)
- [4.6 设置光标位置:SetConsoleCursorPosition](#4.6 设置光标位置:SetConsoleCursorPosition)
- [4.7 获取按键情况:GetAsyncKeyState](#4.7 获取按键情况:GetAsyncKeyState)
- 五、游戏设计与分析
-
- [5.1 地图设计](#5.1 地图设计)
-
- [5.1.1 <locale.h>本地化](#5.1.1 <locale.h>本地化)
- [5.1.2 类项](#5.1.2 类项)
- [5.1.3 setlocale函数](#5.1.3 setlocale函数)
- [5.1.4 宽字符的打印](#5.1.4 宽字符的打印)
- [4.2 蛇身和食物](#4.2 蛇身和食物)
- [4.3 结构设计](#4.3 结构设计)
- [4.4 总览:游戏流程](#4.4 总览:游戏流程)
- 五、核心逻辑实现
-
- [5.1 游戏主逻辑](#5.1 游戏主逻辑)
- [5.2 板块:游戏初始化](#5.2 板块:游戏初始化)
-
- [5.2.1 欢迎界面](#5.2.1 欢迎界面)
- [5.2.2 绘制地图](#5.2.2 绘制地图)
- [5.2.3 创建蛇](#5.2.3 创建蛇)
- [5.2.4 创建食物](#5.2.4 创建食物)
- [5.3 板块:游戏运行](#5.3 板块:游戏运行)
-
- [5.3.1 提示信息](#5.3.1 提示信息)
- [5.3.2 蛇身的移动](#5.3.2 蛇身的移动)
- [5.4 板块:游戏结束](#5.4 板块:游戏结束)
- 六、源码完整参考
- 总结
引用
掌握C语言的语法只是第一步,如何将分散的知识点融会贯通,构建出一个完整的项目,才是检验学习成效的关键。
贪吃蛇游戏,正是这样一个经典的"试金石"。它逻辑清晰、结构完整,几乎涵盖了C语言初级阶段的所有核心概念:从数据存储、流程控制,到函数封装与模块化设计。
本项目将引导你,使用C语言和Win32控制台API,从零开始实现。
这不仅是一次编程实践,更是一次对C语言学习成果的系统性检验与升华。让我们开始吧。
一、游戏背景
贪吃蛇是久负盛名的游戏,与俄罗斯方块、扫雷等游戏位列经典之位。
在C语言的学习中,将会以实现贪吃蛇项目来为C语言的学习进行收尾,也是借此检验掌握程度。(不会的赶紧去复习!!)
二、游戏效果演示
简易版贪吃蛇
三、项目预期目标
使用C语言在Windows环境的控制台中模拟实现简易版的贪吃蛇游戏。
3.1 包含的基本功能
- 贪吃蛇游戏地图绘制;
- 蛇吃食物(由上、下、左、右方向键控制蛇的功能);
- 蛇撞墙死亡;
- 蛇撞自身死亡;
- 计算得分;
- 蛇身的加速、减速;
- 暂停游戏;
3.2 项目涉及技术要点
友情提醒:实现贪吃蛇项目需要 C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API 等相关知识。
四、Win32 API介绍
Win32 API是Windows操作系统为应用程序提供的编程接口。它如同一个核心服务中心,应用程序通过调用这些预置的函数,便能请求系统完成窗口管理、图形绘制、文件操作等底层任务。
本游戏将在控制台环境中,运用其中几个关键API,实现光标控制、键盘响应等核心交互功能。
4.1 控制台程序(Console)
在电脑上打开一个叫 "命令提示符" 的应用(cmd)就是控制台程序。


在这个黑框框中,可以使用指令来设置控制台窗口的大小(长宽):通过设置行数、列数改变大小。
- 设置窗口大小
bash
mode con cols=120 lines=35
【详情参考:mode指令】
- 重命名窗口名称
bash
title 名称
【详情参考:title命令】
原版:
bash
title 贪吃蛇_雾忱星
改版:
当然,游戏实现指令肯定不是在cmd中一个一个输,而是在VS2022中去实现。
想要在VS2022上实现和命名控制台一样的作用,要做出以下设置(看情况!)
- 运行后,界面为如下显示,按步骤设置

- 设置效果为:

使用system函数,包含<stdlib.h>头文件
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
//设置窗口大小
system("mode con cols=30 lines=30");
//重命名
system("title 贪吃蛇");
getchar();//使用原因:防止程序直接结束,看不到效果
return 0;
}
4.2 控制台屏幕上的坐标COORD
【详情参考:COORD】
COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区的坐标,坐标系的原点(0,0)位于缓冲区顶部左侧单元格。

下面控制台的黑色光标就是一个坐标

COORD类型声明: 包含<windows.h> 头文件
c
typedef struct _COORD
{
SHORT X;
SHORT Y;
}
- 坐标赋值:
c
int main()
{
COORD pos = { 10, 10};
system("pause");
return 0;
}
但是,运行不会产生任何效果,因为还需要其他的内容配合进行,继续看。
4.3 获取句柄:GetStdHandle
【详情参考:GetStdHandle】包含<windows.h> 头文件
GetStdHandle是Windows API函数。用于从特定的标准设备(输入、输出、错误)中获取一个**句柄**(标识不同设备数值),使句柄可以操作设备。
我们可以将 "句柄"理解为"钥匙" ,可以用"钥匙"来操作相应的设备。前提是获取到相应设备的信息。
- 语法:
c
HANDLE GetStdHandle(DWORD nStdHandle);
- 参数:
| 值 | 含义 |
|---|---|
STD_INPUT_HANDLE ((DWORD)-10) |
标准输入设备。 最初,这是输入缓冲区CONIN$的控制台。 |
STD_OUTPUT_HANDLE ((DWORD)-11) |
标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$。 |
STD_ERROR_HANDLE ((DWORD)-12) |
标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$。 |
- 获取信息
- 获取标准输出设备的句柄
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//获取标准输出设备的句柄
int main()
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
return 0;
}
4.4 获取光标信息:GetConsoleCursorInfor
【详情参考 :GetConsoleCursorInfor】包含<windows.h> 头文件
作用:检索有关指定控制台屏幕缓冲区光标大小、可见性信息。
c
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
- 参数:
hConsoleOutput |
控制台屏幕缓冲区的句柄。 |
|---|---|
lpConsoleCursorInfo |
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息。 |
4.4.1 接收光标:CONSOLE_CURSOR_INFO
是一个结构体,存放控制台光标的信息。
【详情参考:CONSOLE_CURSOR_INFO】包含<windows.h> 头文件
- 语法:
c
typedef struct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- 参数:
dwSize |
由游标填充的字符单元格的百分比。 介于 1 和 100 之间。 光标外观变化,从完全填充单元格到显示为单元格底部的水平线。 |
|---|---|
bVisible |
游标的可见性。 如果游标可见,则此成员为 TRUE。 |
- 应用举例:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;//结构体变量
GetConsoleCursorInfo(houtput, &CursorInfo);
printf("%d\n", CursorInfo.dwSize);
return 0;
}


输出25代表光标占比,为完全显示的25%。
那么知道了如何获得信息,就要开始设置光标信息!
4.5 设置光标信息:SetConsoleCursorInfo
【详情参考:SetConsoleCursorInfo】包含<windows.h> 头文件
- 语法:
c
BOOL WINAPI SetConsoleCursorInfo
(
HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
- 参数:
hConsoleOutput |
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。 |
|---|---|
lpConsoleCursorInfo |
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构为控制台屏幕缓冲区的光标提供新的规范。 |
- 应用举例:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
//获取标准输出设备句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;//结构体变量
GetConsoleCursorInfo(houtput, &CursorInfo);
printf("%d\n", CursorInfo.dwSize);
//设置光标信息
CursorInfo.dwSize = 50;//将光标占比改为50
SetConsoleCursorInfo(houtput, &CursorInfo);
printf("%d\n", CursorInfo.dwSize);
return 0;
}
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
//获取标准输出设备句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义光标信息结构体变量
CONSOLE_CURSOR_INFO CursorInfo;
//获取和句柄相关的控制台上的光标信息,存在结构体中
GetConsoleCursorInfo(houtput, &CursorInfo);
printf("%d\n", CursorInfo.dwSize);
//设置光标显示
CursorInfo.bVisible = false;//不显示
//设置和句柄相关的控制台上的光标信息,存在结构体中
SetConsoleCursorInfo(houtput, &CursorInfo);
return 0;
}
改变显示true/false,包含<atdbool.h>头文件


4.6 设置光标位置:SetConsoleCursorPosition
【详情参考:SetConsoleCursorPosition】包含<windows.h> 头文件
- 语法:
c
BOOL WINAPI SetConsoleCursorPosition
(
HANDLE hConsoleOutput,
COORD dwCursorPosition
);
- 参数:
hConsoleOutput |
控制台屏幕缓冲区的句柄 |
|---|---|
dwCursorPosition |
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内 |
- 应用举例:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
//获取标准输出设备句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置坐标
COORD pos = { 10, 5 };
SetConsoleCursorPosition(houtput, pos);
getchar();
return 0;
}

- 封装成函数:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//封装成函数
void set_pos(short x, short y)
{
//获取标准输出设备句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置坐标
COORD pos = {x, y};
SetConsoleCursorPosition(houtput, pos);
}
int main()
{
set_pos(10, 20);
getchar();
return 0;
}
4.7 获取按键情况:GetAsyncKeyState
【详情参考:GetAsyncKeyState】包含<windows.h> 头文件
- 语法:
c
SHORT GetAsyncKeyState(int vKey);
- 参数:
vKey类型int,【虚拟键代码】。
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
- 返回值:
GetAsyncKeyState的返回值是short类型(2字节),在上⼀次调用GetAsyncKeyState函数后,如果返回的16位的short数据中:最高位(当前状态) 是1,说明按键的状态是按下,如果是0,说明按键的状态是抬起;最低位(历史状态) 被置为1则说明,该按键被按过,否则为0
- 应用示例:
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//获取按键情况
//虚拟键码
//宏函数
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
int main()
{
while (1)
{
if (KEY_PRESS(0x60))
{
printf("0\n");
}
else if (KEY_PRESS(0x61))
{
printf("1\n");
}
else if (KEY_PRESS(0x62))
{
printf("2\n");
}
else if (KEY_PRESS(0x63))
{
printf("3\n");
}
else if (KEY_PRESS(0x64))
{
printf("4\n");
}
else if (KEY_PRESS(0x65))
{
printf("5\n");
}
else if (KEY_PRESS(0x66))
{
printf("6\n");
}
else if (KEY_PRESS(0x67))
{
printf("7\n");
}
else if (KEY_PRESS(0x68))
{
printf("8\n");
}
else if(KEY_PRESS(0x69))
{
printf("9\n");
}
}
return 0;
}
五、游戏设计与分析
5.1 地图设计
(放图片)
在地图,设置墙体用宽字符 :□,打印蛇用宽字符 :●,打印食物用宽字符:★。
在这里 "宽字符"占2个字节 ,它的出现是 C语言为适应国际化做出的改变 。(C语言最初假定字符都是单字节,但是对于非英语国家就不适用)
C语言加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
5.1.1 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分 。
标准中依赖地区有以下几个部分:
- 数字量格式;
- 货币量格式;
- 字符集;
- 日期、时间表示形式;
5.1.2 类项
通过修改地区,程序可以改变它的行为来适应不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能不希望被修改 。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,指定⼀个类项:
- LC_COLLATE:影响字符串比较函数
strcol()/strxfrm()。 - LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响
printf()的数字格式。 - LC_TIME:影响时间格式
strftime()/wcsftime。 - LC_ALL:针对所有类项设置成特定语言环境。
【 每个类项详情,参考】
5.1.3 setlocale函数
【详情参考:setlocale】
函数用于修改当前地区,可以针对一个类项或者全部。
- 语法:
c
char* setlocale(int category, const char* locale);
- 参数:
category |
为前面展示的类项 |
|---|---|
locale |
可以是"C"(正常模式)/""(本地模式) |
(任意程序执行都默认为setlocale(LC_ALL, "C");)
- 返回值: 返回值是⼀个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针
NULL。
c
setlocale(LC_ALL, NULL)
上面可以用来查询当前地区,第二个参数为
NULL。
5.1.4 宽字符的打印
宽字符的字面量 必须加上前缀L ,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应占位符%lc;在双引号前,表示宽字符串,对应占位符为%ls。
- 宽字符类型:
wchar_t
c
#include <locale.h>
int main()
{
//设置地区
setlocale(LC_ALL, "");
wchar_t ch1 = L'●';
wchar_t ch2 = L'汉';
wchar_t ch3 = L'字';
printf("%c%c\n", 'a', 'b');
//注意打印时也要加L
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
return 0;
}

发现⼀个普通字符占⼀个字符的位置,但是⼀个汉字字符,占2个字符的,那么如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
4.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,连续5个节点。
注意: 蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半出现在墙体中,另外一般在墙外的现象,坐标不好对齐。
关于食物,在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇身重合,然后打印★。
4.3 结构设计
1.设计蛇身
可以知道蛇就是一个移动的链表,蛇身由节点组成。在地图中,蛇身以坐标形式定位且节点也要指向下一个节点,所以用结构体实现。
c
//蛇身节点
typedef struct SnakeNode
{
//坐标
int x;
int y;
struct SnakeNode* next;//指向下一个节点
}SnakeNode, * pSnakeNode;
- 维护贪吃蛇
对于蛇身,需要对以下几点维护:
| 维护整条蛇的指针(头) | 维护食物的指针 | 蛇的走向(上下左右) |
|---|---|---|
| 游戏状态(正常、撞墙、撞自己、正常退出) | 每个食物的分数 | 游戏总分数 |
| 每走一步休眠的时间 |
可见以上都是描述蛇的,所以用结构体实现:
c
/贪吃蛇数据
typedef struct Snake
{
pSnakeNode _pSnake;//维护整条蛇的指针
pSnakeNode _pFood;//维护食物的指针
enum DIRECTION _dir;//走向
enum GAME_STATUES _statues;//状态
int _food_weight;//一个食物的分数
int _score;//总分数
int _sleep_time;//休息时间,越短越快
}Snake, * pSnake;
- 对于方向、状态,清楚有什么,可以用枚举:
c
//枚举_蛇的走向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//枚举_游戏状态
enum GAME_STATUES
{
OK,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞自己
END_NOMAL//退出
};
4.4 总览:游戏流程

五、核心逻辑实现
5.1 游戏主逻辑
在开始,先设置本地区域setlocale(LC_ALL, "");,再进入总体实现:
- 游戏初始化:
GameInit();,完成相关数据初始化; - 游戏运行:
GameRunt();,实现运行游戏的主体函数; - 游戏结束:
GameEnd();,善后工作。
5.2 板块:游戏初始化
在这里需要完成:
- 控制台窗口大小、名称设置;
- 光标的隐藏;
- 打印欢迎界面、操作信息;
- 绘制地图;
- 创建蛇、食物。
c
void GameInit(pSnake ps)
{
//设置控制台窗口大小:110列40行大小
system("mode con cols=110 lines=40");
system("title @雾忱星的贪吃蛇");//命名程序
//隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获句柄
CONSOLE_CURSOR_INFO CursorInfo;//接受信息
GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false;//不可见
SetConsoleCursorInfo(houtput, &CursorInfo);//设置信息
//1.打印欢迎界面、操作信息
WelcomeToGame();
//2.打印地图
CreateMap();
//3.创建蛇
InitSnake(ps);
//4.创建食物
CreateFood(ps);
}

5.2.1 欢迎界面
进行简单的介绍以及必要的操作指引。
- 效果展示:


c
//1.欢迎界面
void WelcomeToGame()
{
//打印欢迎信息
SetPos(43, 16);
wprintf(L"【欢迎来到贪吃蛇小游戏】\n" );
SetPos(47, 20);
system("pause");//暂停
system("cls");//清屏
//操作说明
SetPos(47, 11);
wprintf(L"【游戏操作手册】\n");
SetPos(30, 15);
wprintf(L"1.【移动】:使用键盘上的↑ . ↓ . ← . → 分别控制蛇的移动\n");
SetPos(30, 17);
wprintf(L"2.【速度】:F3为加速,F4为减速\n");
SetPos(30, 19);
wprintf(L"3.【注意】:加速将会得到更高的分数!\n");
SetPos(30, 21);
system("pause");//暂停
system("cls");//清屏
}
- 注解: (注意宽字符的输出格式)
只需要找好合适位置,定位光标,将信息输出即可。
下方的"请按任意键继续. . ."的提示,由暂停指令system("pause");//暂停完成。
界面的切换,由清屏指令system("cls");//清屏完成。
5.2.2 绘制地图
打印宽字符,为贪吃蛇划分出一块地方运行。
格外注意坐标的计算:地图大小不能超过窗口大下、x坐标跨度为2、y坐标跨度为1。
- 效果展示:

c
//2.打印地图
//控制台窗口大小:110列40行大小
//注意两种打印格式:
//wprintf(L"%lc", L'□');正确,为宽字符
//wprintf(L"□");错误,为窄字符
#define WALL L'□'
void CreateMap()
{
//上边界
for (int i = 0; i < 39; i++)
{
wprintf(L"%lc", WALL);
}
//下边界
SetPos(0, 35);
for (int i = 0; i < 39; i++)
{
wprintf(L"%lc", WALL);
}
//左边界
for (int i = 1; i <= 34; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右边界
for (int i = 1; i <= 34; i++)
{
SetPos(76, i);
wprintf(L"%lc", WALL);
}
}
- 注解: 其中关键为宽字符的打印格式
可能习惯了printf()的格式,直接写了wprintf(L"□");这是不对的,输出的是窄字符。
5.2.3 创建蛇
基本信息在前面已经给过,现在将 使用链表 将节点进行连接并初始化各种属性。
- 效果展示:

c
//3.创建蛇
#define POS_X 24
#define POS_Y 5
#define BODY L'●'
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()");
return;
}
//设置节点坐标
cur->x = POS_X + (i * 2);//x坐标,因为宽字符占连个字符
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->_food_weight = 10;
ps->_score = 0;
ps->_sleep_time = 200;//ms
ps->_statues = OK;
}
- 注解:
生成节点需要申请对应的空间malloc,随便将蛇设置一个坐标,这里不要忘记将next置空,否则运行不成功。
这里用 "头插法" 将节点进行链接(当然尾插也可以),接着就在设置的坐标打印蛇身,并设置属性。
5.2.4 创建食物
随机生成食物坐标,但是不超过地图,不与蛇重合。
- 效果展示:

c
//4.创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//判断x是否是2的倍数
again:
do
{
x = rand() % 37 + 2;
y = rand() % 34 + 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 = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);//打印食物
ps->_pFood = pFood;//蛇的食物属性
}
}
- 注解:
使用函数srand((unsigned int)time(NULL));生成随机数,利用数学知识,保证在地图内。
a % b 的结果是 a 除以 b 的余数余数的范围永远是 0 到 b-1
循环判断是否与蛇重合,使用关键字goto,again回溯修正,保证正确性。
5.3 板块:游戏运行
此板块,先在地图右侧打印相关信息,提示玩家游戏状态。游戏根据蛇的状态,决定是否继续游戏。游戏中,根据按键情况做出相应操作。
- 虚拟键:
上 VK_UP |
下VK_DOWN |
左VK_LEFT |
右VK_RIGHT |
|---|---|---|---|
暂停VK_SPACE |
正常退出VK_ESCAPE |
加速VK_SHIFT |
减速VK_CAPITAL |
c
void GameRun(pSnake ps)
{
//打印窗口右侧信息
PrintHelpIno();
//蛇的操控
do
{
SetPos(80, 23);//得分
printf("【目前总分】:%d分", ps->_score);
SetPos(80, 25);
printf("【每个食物分数】:%d分", ps->_food_weight);
//检测按键_方向
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_SPACE))
{
//暂停
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出
ps->_status = END_NOMAL;
break;
}
else if (KEY_PRESS(VK_SHIFT))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_CAPITAL))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//蛇移动一次的过程
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
5.3.1 提示信息
- 效果展示:

c
void PrintHelpIno()
{
SetPos(93, 8);
printf("【请注意:】");
SetPos(80, 10);
printf("1.【状态】:蛇不能穿墙,不能要到自己!");
SetPos(80, 12);
wprintf(L"%ls", L"2.【移动】:使用键盘上的↑ . ↓ . ← . → ");
SetPos(80, 14);
wprintf(L"%ls", L"3.【速度】:SHIFT 为加速,Capslock 为减速");
SetPos(80, 16);
printf("4.【ESC】:退出游戏 ·【space】:暂停游戏");
SetPos(80, 20);
printf("【@雾忱星制作】");
}
5.3.2 蛇身的移动
对于移动函数,选择在下一位置提前创建节点,之后根据下一位置的情况进行分情况分析:
(节点的坐标根据位置关系进行加减)
c
//下一步食物?
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->_pFood->x) && (pn->y == ps->_pFood->y);
}
//吃食物
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
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);//再生成食物
}
//不是食物:释放最后的节点
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;
}
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 == 76 ||
ps->_pSnake->y == 0 || ps->_pSnake->y == 35)
{
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;
break;
}
cur = cur->next;
}
}
void SnakeMove(pSnake ps)
{
//创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove::malloc");
return;
}
//判断下一步的走向
switch (ps->_dir)
{
case UP:
{
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
}
break;
case DOWN:
{
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
}
break;
case LEFT:
{
pNextNode->x = ps->_pSnake->x - 2;
pNextNode->y = ps->_pSnake->y;
}
break;
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);
}
- 注解: 必须注意坐标的计算
判断下一步是否是食物: 节点坐标和创建的食物节点坐标对比
- 是食物:
链接食物节点,因为节点重复 ,将创建的下一节点释放、置空,再次打印蛇。 - 不是食物:
链接创建的下一节点,并且将最后一个节点释放、置空,这里要注意-->用空白字符将身体覆盖。
判断下一步是否撞墙: 节点坐标和墙的边界坐标对比,撞墙就将状态改为KILL_BY_WALL,游戏结束。
判断下一步是否撞自己: 遍历蛇身,将下一个节点坐标和蛇身的坐标对比,撞自己就将状态改为KILL_BY_SELF,游戏结束。
5.4 板块:游戏结束
当状态不再是OK时,在屏幕上打印结束信息,并释放所有节点。再询问是否再来一把游戏。
- 效果展示:

c
//结束游戏_善后
void GameEnd(pSnake ps)
{
SetPos(33, 14);
switch (ps->_status)
{
case END_NOMAL:
printf("【您主动结束了游戏!】");
break;
case KILL_BY_WALL:
printf("【蛇撞到了墙!】");
break;
case KILL_BY_SELF:
printf("【蛇撞到了自己!】");
break;
}
SetPos(0, 36);
//释放链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = NULL;
del = cur;
cur = cur->next;
free(del);
del->next = NULL;
}
}
六、源码完整参考
【源码存放:】https://gitee.com/tian-aochen/test_118/tree/master/test_12.6_贪吃蛇改/Snake
总结
html
🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:
本项目通过Win32控制台API,用C语言成功实现了经典的贪吃蛇游戏。
核心收获在于将零散的知识点------如结构体、数组、函数、循环控制------组织成一个可运行的整体。
从坐标处理到键盘响应,每一个步骤都强化了对底层基础的理解与控制力。这不仅巩固了C语言的学习效果,更重要的是建立起从构思到实现的完整项目开发思维。