目录
今天想和大家分享一个特别适合C语言初学者的项目——贪吃蛇游戏的实现。相信很多同学在学习C语言时都遇到过这样的困惑:学了语法、学了结构,但不知道该怎么把它们用起来。别担心,这个项目就是为你量身定制的!它能帮助你巩固C语言知识,还能让你体验到编程的乐趣。下面,就让我手把手带你完成这个经典小游戏吧!

为什么选择贪吃蛇?
贪吃蛇和俄罗斯方块、扫雷一样,都是经典游戏中的"元老级"角色。它看似简单,却包含了丰富的编程知识点,特别适合作为C语言学习后的第一个实战项目。通过实现贪吃蛇,你可以:
• 巩固C语言基础:函数、结构体、枚举、指针等知识都会用到
• 理解数据结构:链表的实际应用场景
• 接触系统API:学习如何与操作系统交互
• 培养编程思维:从设计到实现的完整流程
最重要的是,当你看到自己写的代码变成一个可玩的游戏时,那种成就感会让你爱上编程!记得我第一次做出贪吃蛇时,兴奋得一晚上没睡着呢!😄

一、项目目标与准备
我们要实现什么?
我们的目标是:使用C语言在Windows控制台中实现一个完整的贪吃蛇游戏,包含以下功能:
• 🐍 蛇的移动控制(上下左右方向键)
• 🍎 蛇吃食物后身体变长
• 🧱 蛇撞墙或撞到自己时游戏结束
• 📊 实时显示得分
• ⚡ 蛇身加速、减速功能
• ⏸️ 暂停游戏功能
技术栈准备
实现这个项目,我们需要掌握:
• C语言基础:函数、结构体、枚举、指针、动态内存管理
• Win32 API:控制台操作相关API
• 链表:用于表示蛇的身体
别担心,我会详细解释每个知识点,即使你对某些概念还不太熟悉也没关系!
二、控制台编程基础
在开始写游戏之前,我们先要了解如何在控制台中进行精细的屏幕操作。这部分是很多初学者的"拦路虎",但其实掌握了就很简单。
1. 控制台窗口设置
首先,我们需要设置控制台窗口的大小和标题,让游戏界面看起来更专业:
// 设置控制台窗口大小:100列,30行
system("mode con cols=100 lines=30");
// 设置窗口标题
system("title 贪吃蛇");
这里用到了system()函数,它可以执行DOS命令。就像你打开命令提示符(cmd)输入命令一样,system()让我们的程序也能执行这些命令。
小贴士:在实际开发中,尽量少用
system(),因为它效率较低且有安全风险。但在这个小项目中,它是最快捷的方式。
2. 控制台坐标系统
控制台窗口有自己的一套坐标系统,理解这个对游戏开发至关重要:

控制台坐标系统示意图
- X轴:横向,从左到右递增
- Y轴:纵向,从上到下递增
注意:控制台中的一个普通字符占1个位置,但宽字符(如中文、特殊符号)占2个位置。这就是为什么在贪吃蛇中,我们的地图坐标要特别注意对齐。
3. 光标控制
默认情况下,控制台会显示一个闪烁的光标,这对游戏体验很不友好。我们需要隐藏它:
// 获取标准输出的句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
// 隐藏光标
CursorInfo.bVisible = false;
// 设置光标状态
SetConsoleCursorInfo(hOutput, &CursorInfo);
我们来演示对比一下:

使用这段代码后:

这段代码可能看起来有点复杂,我就简单解释一下:
想象控制台是一个画布,
HANDLE hOutput就是我们拿在手里的画笔。CONSOLE_CURSOR_INFO则是画笔的属性设置本,我们通过GetConsoleCursorInfo查看当前设置,然后修改bVisible(是否可见)属性,最后用SetConsoleCursorInfo应用新设置。
不理解也没有关系,直接使用即可
4. 设置光标位置
为了让蛇和食物出现在正确的位置,我们需要能够精确控制输出位置:
void SetPos(short x, short y) {
COORD pos = {x, y};
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOutput, pos);
}
COORD是一个结构体,表示坐标点。SetConsoleCursorPosition就是我们的"定位器",告诉系统下次输出应该从哪个位置开始。
演示:

同样的会使用即可
5. 宽字符处理
贪吃蛇中我们会用到一些特殊符号(如蛇身●、食物★、墙体□),这些是宽字符,需要特别处理:
#include <locale.h>
// 设置本地化,支持宽字符
setlocale(LC_ALL, "");
// 定义宽字符
wchar_t ch = L'●';
// 使用wprintf打印宽字符
wprintf(L"%c", ch);
这里有个关键点:C语言默认使用ASCII编码,只支持单字节字符。当我们需要显示中文或特殊符号时,就需要使用宽字符(wchar_t)和宽字符函数(如wprintf)。
专业解释:ASCII编码只能表示128个字符,对于中文等需要更多符号的语言就不够用了。宽字符使用Unicode编码,可以表示更多字符。
setlocale(LC_ALL, "")的作用是让程序适应本地环境,支持宽字符显示。
简单的理解这样操作以后就可以打印特殊符号了
6. 按键检测
游戏需要实时响应用户按键,这里我们使用GetAsyncKeyState:
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
// 检测是否按下了上箭头键
if(KEY_PRESS(VK_UP)) {
// 处理向上移动
}
GetAsyncKeyState会返回一个16位的值,最高位表示按键状态(按下/抬起),最低位表示是否被按过。我们的宏KEY_PRESS就是提取最低位来判断按键是否被触发。
三、游戏设计与数据结构
现在,我们进入游戏设计的核心部分。一个好的数据结构设计能让代码清晰、易于维护。
1. 地图设计
我们的地图是一个27行×58列的区域(可根据需要调整),四周是墙体:

创建地图的代码:
void CreateMap() {
int i = 0;
// 上边墙 (0,0)-(56,0)
SetPos(0, 0);
for(i = 0; i < 58; i += 2) {
wprintf(L"%c", ‘■’);
}
// 下边墙 (0,26)-(56,26)
SetPos(0, 26);
for(i = 0; i < 58; i += 2) {
wprintf(L"%c", ‘■’);
}
// 左边墙
for(i = 1; i < 26; i++) {
SetPos(0, i);
wprintf(L"%c", ‘■’);
}
// 右边墙
for(i = 1; i < 26; i++) {
SetPos(56, i);
wprintf(L"%c", ‘■’);
}
}
注意:因为宽字符占2个位置,所以墙体坐标要以2为步长(i += 2),否则会出现错位。
2. 蛇身表示:为什么用链表?
这是很多初学者的疑问:为什么蛇身要用链表而不是数组?
这里简单说明一下,之后会在《数据结构杂谈》系列的文章中详细介绍,敬请期待哦!
想象一下,贪吃蛇吃食物后身体会变长,这个长度是动态变化的。如果用数组,我们需要预先分配足够大的空间,而且插入新节点(吃食物后)需要移动大量元素,效率很低。
而链表天生适合这种长度动态变化的场景:
- 添加节点(吃食物)只需在头部插入
- 移动时只需调整头尾
- 内存使用更高效
蛇身节点的结构定义:
typedef struct SnakeNode {
int x; // 蛇身节点的x坐标
int y; // 蛇身节点的y坐标
struct SnakeNode* next; // 指向下一个节点的指针
} SnakeNode, *pSnakeNode;
3. 游戏状态管理
我们需要跟踪游戏的各种状态,这里使用枚举类型非常合适:
// 蛇的移动方向
enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT
};
// 游戏状态
enum GAME_STATUS {
OK, // 正常运行
KILL_BY_WALL, // 撞墙
KILL_BY_SELF, // 撞到自己
END_NOMAL // 正常结束(主动退出)
};
枚举让代码更具可读性。想象一下,如果用数字0、1、2表示方向,以后回头看代码时可能都忘了哪个数字对应哪个方向。而用UP、DOWN这样的名字,一目了然!
4. 完整游戏数据结构
最后,我们定义一个结构体来管理整个游戏状态:
typedef struct Snake {
pSnakeNode _pSnake; // 维护整条蛇的指针
pSnakeNode _pFood; // 维护食物的指针
enum DIRECTION _Dir; // 蛇头的方向
enum GAME_STATUS _Status;// 游戏状态
int _Socre; // 当前得分
int _foodWeight; // 每个食物的分数
int _SleepTime; // 每走一步的休眠时间(控制速度)
} Snake, *pSnake;
这个结构体就像游戏的"大脑",保存了游戏运行所需的所有关键信息。
四、核心功能实现详解
1. 游戏初始化
游戏开始时,我们需要完成一系列准备工作:
void GameStart(pSnake ps) {
// 设置控制台窗口
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
// 隐藏光标
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
// 打印欢迎界面
WelcomeToGame();
// 创建地图
CreateMap();
// 初始化蛇
InitSnake(ps);
// 创建食物
CreateFood(ps);
}
其中,InitSnake函数 初始化蛇的初始状态(长度为5,向右移动):
void InitSnake(pSnake ps) {
pSnakeNode cur = NULL;
int i = 0;
// 创建5个蛇身节点(头插法)
for(i = 0; i < 5; i++) {
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
cur->next = NULL;
cur->x = POS_X + i * 2; // x坐标以2为步长(宽字符对齐)
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"%c", BODY);
cur = cur->next;
}
// 初始化游戏数据
ps->_SleepTime = 200; // 初始速度
ps->_Socre = 0; // 初始分数
ps->_Status = OK; // 游戏状态
ps->_Dir = RIGHT; // 初始方向
ps->_foodWeight = 10; // 食物分值
}
学长经验:这里使用头插法构建链表,是因为贪吃蛇的移动逻辑中,新节点总是添加在头部(蛇头前方)。头插法让这个操作非常高效。
2. 蛇的移动逻辑
这是游戏的核心!蛇的移动看似简单,实际上包含多个关键步骤:
void SnakeMove(pSnake ps) {
// 1. 创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
// 2. 根据当前方向计算下一个位置
switch(ps->_Dir) {
case UP:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
// 其他方向类似...
}
// 3. 检查下一个位置是否是食物
if(NextIsFood(pNextNode, ps)) {
EatFood(pNextNode, ps); // 是食物,吃掉
} else {
NoFood(pNextNode, ps); // 不是食物,正常移动
}
// 4. 检测碰撞
KillByWall(ps);
KillBySelf(ps);
}
吃食物的处理
当蛇头到达食物位置时,我们需要:
- 将新节点插入蛇头前方(不需要删除尾部节点)
- 增加分数
- 生成新的食物
void EatFood(pSnakeNode psn, pSnake ps) {
// 头插法:新节点成为新的蛇头
psn->next = ps->_pSnake;
ps->_pSnake = psn;
// 重新绘制整条蛇
pSnakeNode cur = ps->_pSnake;
while(cur) {
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
// 增加分数
ps->_Socre += ps->_foodWeight;
// 释放旧食物,创建新食物
free(ps->_pFood);
CreateFood(ps);
}
普通移动的处理
当蛇没有吃到食物时,移动逻辑是:
- 将新节点插入蛇头前方
- 删除蛇尾节点(保持长度不变)
void NoFood(pSnakeNode psn, pSnake ps) {
// 头插法:新节点成为新的蛇头
psn->next = ps->_pSnake;
ps->_pSnake = psn;
// 绘制除尾部外的所有节点
pSnakeNode cur = ps->_pSnake;
while(cur->next->next) {
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
// 清除尾部并释放内存
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
关键点:这里我们只绘制到倒数第二个节点,然后清除最后一个节点并释放内存。这样就实现了"移动"效果——蛇头前进一格,蛇尾跟着前移一格。
碰撞检测
最后,我们需要检测两种碰撞:
- 撞墙检测:蛇头坐标是否与墙体坐标重合
- 自撞检测:蛇头坐标是否与身体其他部分重合
int 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;
return 1;
}
return 0;
}
int KillBySelf(pSnake ps) {
pSnakeNode cur = ps->_pSnake->next;
while(cur) {
if((ps->_pSnake->x == cur->x) &&
(ps->_pSnake->y == cur->y)) {
ps->_Status = KILL_BY_SELF;
return 1;
}
cur = cur->next;
}
return 0;
}
3. 游戏主循环
游戏的主循环负责处理用户输入、更新游戏状态和渲染画面:
void GameRun(pSnake ps) {
PrintHelpInfo(); // 打印右侧帮助信息
do {
// 显示当前得分
SetPos(64, 10);
printf("得分:%d", ps->_Socre);
printf(" 每个食物得分:%d分", ps->_foodWeight);
// 检测按键并更新方向
if(KEY_PRESS(VK_UP) && ps->_Dir != DOWN) {
ps->_Dir = UP;
}
// 其他方向类似...
// 暂停功能
else if(KEY_PRESS(VK_SPACE)) {
pause();
}
// 退出游戏
else if(KEY_PRESS(VK_ESCAPE)) {
ps->_Status = END_NOMAL;
break;
}
// 加速功能
else if(KEY_PRESS(VK_F3)) {
if(ps->_SleepTime >= 50) {
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
// 减速功能
else if(KEY_PRESS(VK_F4)) {
if(ps->_SleepTime < 350) {
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if(ps->_SleepTime == 350) {
ps->_foodWeight = 1;
}
}
}
// 控制移动速度
Sleep(ps->_SleepTime);
// 执行移动逻辑
SnakeMove(ps);
} while(ps->_Status == OK); // 游戏继续条件
}
学长小技巧:
Sleep(ps->_SleepTime)控制蛇的移动速度。值越小,速度越快。通过F3/F4键调整这个值,就能实现加速/减速功能。
五、常见问题与解决方案
在实现过程中,你可能会遇到一些问题,这里分享几个常见问题的解决方案:
1. 字符显示错位
问题:墙体或蛇身显示不整齐,出现"半截"现象。
原因:宽字符占2个位置,但坐标计算时没有考虑这一点。
解决方案:
- 蛇身和食物的x坐标必须是2的倍数
- 绘制墙体时,循环步长应为2(
i += 2)
2. 蛇移动过快或过慢
问题:蛇移动速度不合适,游戏体验差。
解决方案:
- 调整
_SleepTime的初始值(200毫秒是个不错的起点) - 实现加速/减速功能,让玩家可以自定义速度
3. 内存泄漏
问题:长时间游戏后程序崩溃。
原因:没有正确释放动态分配的内存。
解决方案:
- 吃食物时释放旧食物节点:
free(ps->_pFood) - 游戏结束时释放整条蛇:遍历链表并释放每个节点
4. 按键响应不灵敏
问题:快速按键时,蛇的反应跟不上。
原因:GetAsyncKeyState的使用方式不当。
解决方案:
- 确保在每次循环中都检测按键状态
- 使用
KEY_PRESS宏正确提取按键信息
六、完整可运行的代码分享给大家
为什么需要完整代码?
很多同学在学习过程中会遇到这样的问题:
- 理解了原理,但不知道如何组织代码
- 遇到编译错误不知道如何解决
- 想快速看到效果,验证自己的理解
所以,今天我特意整理了经过测试的完整代码,你可以直接复制粘贴,编译运行。如果在实现过程中遇到问题,也可以对照完整代码进行排查。
完整代码结构
我们的贪吃蛇项目采用模块化设计,分为三个文件:
- test.cpp - 主程序文件,包含main函数和游戏流程控制
- snake.h - 头文件,包含数据结构定义和函数声明
- snake.cpp - 实现文件,包含所有功能函数的具体实现
这种组织方式让代码结构清晰,便于理解和维护,也是实际项目开发中常用的方式。
1. test.cpp(主程序文件)
#include "Snake.h"
#include <locale.h>
void test() {
int ch = 0;
srand((unsigned int)time(NULL));
do {
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar(); // 清理\n
} while (ch == 'Y');
SetPos(0, 27);
}
int main() {
// 修改当前地区为本地模式,为了支持中文宽字符的打印
setlocale(LC_ALL, "");
// 测试逻辑
test();
return 0;
}
2. snake.h(头文件)
#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
// 方向
enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT
};
// 游戏状态
enum GAME_STATUS {
OK, // 正常运行
KILL_BY_WALL, // 撞墙
KILL_BY_SELF, // 咬到自己
END_NOMAL // 正常结束
};
#define WALL L'□'
#define BODY L'●' // ★○●◇◆□■
#define FOOD L'★' // ★○●◇◆□■
// 蛇的初始位置
#define POS_X 24
#define POS_Y 5
// 蛇身节点
typedef struct SnakeNode {
int x;
int y;
struct SnakeNode* next;
} SnakeNode, *pSnakeNode;
typedef struct Snake {
pSnakeNode _pSnake; // 维护整条蛇的指针
pSnakeNode _pFood; // 维护食物的指针
enum DIRECTION _Dir; // 蛇头的方向默认是向右
enum GAME_STATUS _Status;// 游戏状态
int _Socre; // 当前获得分数
int _foodWeight; // 每个食物10分
int _SleepTime; // 每走一步休眠时间
} Snake, *pSnake;
// 游戏开始前的初始化
void GameStart(pSnake ps);
// 游戏运行过程
void GameRun(pSnake ps);
// 游戏结束
void GameEnd(pSnake ps);
// 设置光标的坐标
void SetPos(short x, short y);
// 欢迎界面
void WelcomeToGame();
// 打印帮助信息
void PrintHelpInfo();
// 创建地图
void CreateMap();
// 初始化蛇
void InitSnake(pSnake ps);
// 创建食物
void CreateFood(pSnake ps);
// 暂停响应
void pause();
// 下一个节点是食物
int NextIsFood(pSnakeNode psn, pSnake ps);
// 吃食物
void EatFood(pSnakeNode psn, pSnake ps);
// 不吃食物
void NoFood(pSnakeNode psn, pSnake ps);
// 撞墙检测
int KillByWall(pSnake ps);
// 撞自身检测
int KillBySelf(pSnake ps);
// 蛇的移动
void SnakeMove(pSnake ps);
3. snake.cpp(实现文件)
#include "Snake.h"
// 设置光标的坐标
void SetPos(short x, short y) {
COORD pos = { x, y };
HANDLE hOutput = NULL;
// 获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
void WelcomeToGame() {
SetPos(40, 15);
printf("欢迎来到贪吃蛇小游戏");
SetPos(40, 25); // 让按任意键继续的出现的位置好看点
system("pause");
system("cls");
SetPos(25, 12);
printf("用 ↑. ↓. ←. →分别控制蛇的移动, F3为加速,F4为减速\n");
SetPos(25, 13);
printf("加速将能得到更高的分数。\n");
SetPos(40, 25); // 让按任意键继续的出现的位置好看点
system("pause");
system("cls");
}
void CreateMap() {
int i = 0;
// 上(0,0)-(56, 0)
SetPos(0, 0);
for (i = 0; i < 58; i += 2) {
wprintf(L"%c", WALL);
}
// 下(0,26)-(56, 26)
SetPos(0, 26);
for (i = 0; i < 58; i += 2) {
wprintf(L"%c", WALL);
}
// 左 //x是0,y从1开始增长
for (i = 1; i < 26; i++) {
SetPos(0, i);
wprintf(L"%c", WALL);
}
// x是56,y从1开始增长
for (i = 1; i < 26; i++) {
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
void InitSnake(pSnake ps) {
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->next = NULL;
cur->x = POS_X + i * 2;
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"%c", BODY);
cur = cur->next;
}
// 初始化贪吃蛇数据
ps->_SleepTime = 200;
ps->_Socre = 0;
ps->_Status = OK;
ps->_Dir = RIGHT;
ps->_foodWeight = 10;
}
void CreateFood(pSnake ps) {
int x = 0;
int y = 0;
again:
// 产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
do {
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake; // 获取指向蛇头的指针
// 食物不能和蛇身冲突
while (cur) {
if (cur->x == x && cur->y == 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;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", FOOD);
ps->_pFood = pFood;
}
}
void PrintHelpInfo() {
// 打印提示信息
SetPos(64, 15);
printf("不能穿墙,不能咬到自己\n");
SetPos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3为加速,F4为减速\n");
SetPos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
}
void pause() // 暂停
{
while (1) {
Sleep(300);
if (KEY_PRESS(VK_SPACE)) {
break;
}
}
}
// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
int NextIsFood(pSnakeNode psn, pSnake ps) {
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
void EatFood(pSnakeNode psn, pSnake ps) {
// 头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
// 打印蛇
while (cur) {
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
free(ps->_pFood);
CreateFood(ps);
}
// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps) {
// 头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
// 打印蛇
while (cur->next->next) {
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
// 最后一个位置打印空格,然后释放节点
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
// pSnake ps维护蛇的指针
int 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;
return 1;
}
return 0;
}
// pSnake ps维护蛇的指针
int KillBySelf(pSnake ps) {
pSnakeNode cur = ps->_pSnake->next;
while (cur) {
if ((ps->_pSnake->x == cur->x) &&
(ps->_pSnake->y == cur->y)) {
ps->_Status = KILL_BY_SELF;
return 1;
}
cur = cur->next;
}
return 0;
}
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);
}
void GameStart(pSnake ps) {
// 设置控制台窗口的大小,30行,100列 //mode为DOS命令
system("mode con cols=100 lines=30");
// 设置cmd窗口名称
system("title 贪吃蛇");
// 获取标准输出的句柄(用来标识不同设备的数值)
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo); // 获取控制台光标信息
CursorInfo.bVisible = false; // 隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo); // 设置控制台光标状态
// 打印欢迎界面
WelcomeToGame();
// 打印地图
CreateMap();
// 初始化蛇
InitSnake(ps);
// 创造第一个食物
CreateFood(ps);
}
void GameRun(pSnake ps) {
// 打印右侧帮助信息
PrintHelpInfo();
do {
SetPos(64, 10);
printf("得分:%d", ps->_Socre);
printf(" 每个食物得分:%d分", ps->_foodWeight);
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_F3)) {
if (ps->_SleepTime >= 50) {
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4)) {
if (ps->_SleepTime < 350) {
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if (ps->_SleepTime == 350) {
ps->_foodWeight = 1;
}
}
}
// 蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
void GameEnd(pSnake ps) {
pSnakeNode cur = ps->_pSnake;
SetPos(24, 12);
switch (ps->_Status) {
case END_NOMAL:
printf("您主动退出游戏\n");
break;
case KILL_BY_SELF:
printf("您撞上自己了,游戏结束!\n");
break;
case KILL_BY_WALL:
printf("您撞墙了,游戏结束!\n");
break;
}
// 释放蛇身的节点
while (cur) {
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
扩展建议
当你成功运行基础版本后,可以尝试以下扩展:
-
添加音效:使用Windows的
PlaySoundAPI添加吃食物、游戏结束等音效 -
保存最高分:使用文件操作保存和读取最高分记录
-
多种食物类型:实现不同分数、不同效果的食物
等等
最后的话
看到这里,相信你已经掌握了用C语言实现贪吃蛇的完整过程。编程最重要的不是记住代码,而是理解思路和解决问题的方法。这个项目虽然不大,但它涵盖了C语言的许多核心概念:
- 结构体与链表:管理复杂数据
- 枚举类型:提高代码可读性
- 动态内存管理:合理使用和释放内存
- 系统API调用:与操作系统交互
- 游戏循环设计:状态管理和逻辑控制
学长寄语:当你看到自己写的代码变成一个可玩的游戏时,那种成就感会让你明白:所有的努力都是值得的!编程就像搭积木,开始时你可能只认识几块基础积木(语法),但随着经验积累,你会学会如何将它们组合成各种有趣的形状(项目)。贪吃蛇只是你编程之旅的第一站,前方还有更多精彩的风景等着你!
最后,动手去做吧! 不要害怕犯错,调试的过程本身就是最好的学习。期待看到你实现的贪吃蛇,以及你对它的各种创意扩展!
互动时间:你成功实现贪吃蛇了吗?遇到了什么有趣的问题?欢迎在评论区分享你的经验和心得!

854





