C++贪吃蛇游戏代码学习笔记(附完整实现解析)
C++贪吃蛇游戏代码学习笔记(附完整实现解析)
一、游戏核心概述
1.1 游戏原理
贪吃蛇游戏是经典的即时交互游戏,核心逻辑为:通过键盘控制蛇的移动方向,蛇在固定地图内追逐食物,吃到食物后身体变长、分数增加且移动速度加快,若蛇头触碰边界或自身身体则游戏结束。
本实现基于C++控制台开发,借助Windows API控制光标位置与隐藏,通过时间戳控制蛇的移动频率,采用结构化设计封装地图、蛇、坐标等核心要素,逻辑清晰且易扩展。
1.2 核心依赖库
| 库名 | 核心用途 |
|---|---|
iostream | 控制台输入输出(绘制地图、显示分数等) |
windows.h | 控制控制台光标(隐藏、移动)、获取标准输出句柄 |
chrono | 时间戳计算,控制蛇的移动频率(毫秒级精度) |
conio.h | 检测键盘输入(_kbhit())、获取按键(_getch()) |
ctime/cstdlib | 生成随机数种子(srand()),用于食物随机生成 |
sstream/cstring | 分数转换为字符串(实时更新分数显示) |
二、核心数据结构设计
代码采用结构体封装游戏核心要素,实现数据与操作的解耦,符合模块化编程思想,主要包含以下4种核心结构:
2.1 坐标结构体(pos)
struct pos { int x; // 横向坐标(对应地图列数) int y; // 纵向坐标(对应地图行数) }; 解析:作为基础数据结构,用于表示蛇头、蛇身、食物在地图上的位置,所有与“位置”相关的操作(移动、绘制、碰撞检测)均依赖此结构。
2.2 地图结构体(map)
enum blocktype { FOOD = 1, // 食物标识 EMPTY = 0 // 空区域标识 }; struct map { blocktype block[H][L]; // 地图二维数组(H行L列,宏定义H=27,L=52) bool hasfood; // 标记地图上是否存在食物(避免重复生成) }; 关键说明:
- 用枚举
blocktype定义地图格子类型,使代码语义更清晰,避免魔法值; - 二维数组
block存储整个地图的状态,每个元素表示对应位置是食物还是空区域; hasfood作为食物生成的“开关”,仅当地图无食物时才触发生成逻辑,提升效率。
2.3 蛇结构体(snake)
const int fangxiang[4][2] = { {-1,0},//向上(y减1,x不变) {1,0},//向下(y加1,x不变) {0,-1},//向左(x减1,y不变) {0,1} };//向右(x加1,y不变) struct snake { pos aspos[H * L]; // 存储蛇身所有节的位置(最大长度为地图总格子数) int sfangxiang; // 当前移动方向(对应fangxiang数组下标:0-上,1-下,2-左,3-右) int schangdu; // 蛇的当前长度 int movelasttime; // 上一次移动的时间戳(毫秒) int movepinglu; // 移动频率(毫秒/次,值越小速度越快) }; 关键说明:
- 方向数组
fangxiang:将方向与坐标变化绑定,通过下标快速获取移动时的坐标偏移量,简化方向控制逻辑; aspos数组:按“头在前、尾在后”的顺序存储蛇身位置,aspos[0]固定为蛇头;- 移动控制相关属性:
movelasttime与movepinglu配合,实现蛇移动频率的精确控制,避免移动过快或过慢。
三、核心功能模块解析
代码按“初始化→绘制→交互控制→逻辑判断→主循环”的流程组织,各模块职责单一,相互配合完成游戏运行。
3.1 初始化模块
负责游戏启动时的环境准备,包括地图、蛇的初始化及光标隐藏,为游戏运行奠定基础。
3.1.1 地图初始化(intimap)
void intimap(map& Map) { for (int y = 0; y < H; y++) { for (int u = 0; u < L; u++) { Map.block[y][u] = blocktype::EMPTY; // 所有格子初始化为空 } } Map.hasfood = false; // 初始无食物 } 解析:双重循环遍历地图二维数组,将所有位置设为空区域,同时标记无食物状态,确保游戏初始环境干净。
3.1.2 蛇初始化(intisnake)
void intisnake(snake& asnake) { asnake.sfangxiang = 3; // 初始方向向右(符合玩家操作习惯) asnake.schangdu = 3; // 初始长度为3 // 初始位置:地图居中,横向排列(头在右,尾在左) asnake.aspos[0] = { L / 2, H / 2 }; asnake.aspos[1] = { L / 2 - 1, H / 2 }; asnake.aspos[2] = { L / 2 - 2, H / 2 }; asnake.movelasttime = 0; // 初始移动时间戳为0 asnake.movepinglu = 200; // 初始移动频率:200毫秒/次 } 解析:初始化蛇的核心状态,包括方向、长度、初始位置和移动参数。初始位置设为地图居中,提升游戏体验;方向默认向右,符合多数玩家的操作直觉。
3.1.3 光标隐藏(hidecursor)
void hidecursor() { HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 获取标准输出句柄 CONSOLE_CURSOR_INFO curInfo = { 1, FALSE }; // 光标信息:大小1,隐藏 SetConsoleCursorInfo(hOutput, &curInfo); // 设置光标状态 } 解析:通过Windows API隐藏控制台光标,避免光标闪烁影响游戏画面的连贯性,提升视觉体验,是控制台游戏开发的常用优化手段。
3.2 绘制模块
负责将地图、蛇、食物、分数等游戏元素渲染到控制台,是玩家与游戏交互的视觉载体,核心为“精准定位+内容输出”。
3.2.1 地图绘制(drawmap)
void drawmap(map& tmap) { system("cls"); // 清空控制台(避免上一帧画面残留) // 绘制上边框 cout << "┌"; for (int i = 0; i < L; i++) cout << "─"; cout << "┐" << endl; // 绘制中间区域(边框+空区域) for (int p = 0; p < H; p++) { cout << "│"; // 绘制左边框 for (int a = 0; a < L; a++) { if (tmap.block[p][a] == blocktype::EMPTY) cout << " "; // 空区域输出空格 } cout << "│" << endl; // 绘制右边框 } // 绘制下边框 cout << "└"; for (int i = 0; i < L; i++) cout << "─"; cout << "┘" << endl; } 关键说明:
- 用
system("cls")清空控制台,确保每帧画面干净,避免残留; - 采用Unicode边框字符(┌、─、┐等),使地图边框更美观,区别于传统的*或#边框;
- 中间区域仅绘制边框和空区域,蛇和食物通过后续独立绘制函数渲染,实现“分层绘制”,逻辑更清晰。
3.2.2 单个元素绘制(drawunit/drawunit2)
void drawunit(pos& p, const char unit[]) { COORD coord; // Windows API坐标结构 HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); // 坐标偏移:边框占1行1列,需将地图坐标转换为控制台坐标 coord.X = p.x + 1; coord.Y = p.y + 1; SetConsoleCursorPosition(houtput, coord); // 移动光标到目标位置 cout << unit; // 输出元素(蛇身用■,食物用●) } 解析:游戏绘制的核心工具函数,通过SetConsoleCursorPosition精准移动光标到目标位置,再输出对应元素。关键在于“坐标偏移”——地图坐标(0,0)对应控制台的(1,1),因为边框占用了第一行和第一列。
3.2.3 蛇绘制(drawsnake)
void drawsnake(snake& snk) { for (int g = 0; g < snk.schangdu; ++g) { drawunit(snk.aspos[g], "■"); // 遍历蛇身所有节,绘制为■ } } 解析:遍历蛇身位置数组aspos,调用drawunit将每一节蛇身绘制为“■”,实现蛇的可视化。由于蛇头和蛇身采用相同样式,通过位置(aspos[0])区分蛇头。
3.3 核心逻辑模块
是游戏的“大脑”,包含蛇的移动、碰撞检测、食物生成、分数计算等核心规则,决定游戏的运行状态。
3.3.1 蛇的移动(movesnake)
void movesnake(snake& snk) { // 蛇身跟随:从尾部到头部,依次复制前一节的位置(避免覆盖) for (int f = snk.schangdu - 1; f >= 1; f--) { snk.aspos[f] = snk.aspos[f - 1]; } // 蛇头移动:根据当前方向更新坐标(使用方向数组的偏移量) snk.aspos[0].y += fangxiang[snk.sfangxiang][0]; snk.aspos[0].x += fangxiang[snk.sfangxiang][1]; } 关键逻辑(重点):
- 蛇身跟随规则:必须从尾部开始向前更新位置,若从头部开始会导致所有节都复制蛇头位置,蛇身“消失”。核心思想是“每一节都移动到前一节的旧位置”;
- 蛇头移动规则:通过方向数组获取坐标偏移量,直接更新蛇头位置,无需遍历,效率为O(1)。
时间复杂度:O(n)(n为蛇的长度),仅与蛇身长度相关,效率较高。
3.3.2 碰撞检测(checkbianyuan + 吃食物检测)
碰撞检测是游戏结束和状态更新的核心触发条件,分为“边界碰撞”“自身碰撞”和“食物碰撞”三类。
// 边界与自身碰撞检测:返回true表示安全,false表示游戏结束 bool checkbianyuan(pos& p,snake &snk) { // 1. 边界碰撞:蛇头超出地图范围 if (p.x < 0 || p.x >= L || p.y < 0 || p.y >= H) { return false; } // 2. 自身碰撞:蛇头与除自身外的蛇身重叠 for (int i = 1; i < snk.schangdu; i++) { if (snk.aspos[0].x == snk.aspos[i].x && snk.aspos[0].y == snk.aspos[i].y) { return false; } } return true; } // 食物碰撞检测:返回true表示吃到食物 bool checkeatfood(snake& snk, map& Map, pos& tail) { pos head = snk.aspos[0]; // 蛇头位置为食物时触发吃食物逻辑 if (Map.block[head.y][head.x] == blocktype::FOOD) { snk.aspos[snk.schangdu++] = tail; // 蛇身变长(尾部追加一节) Map.block[head.y][head.x] = blocktype::EMPTY; // 清除食物标记 Map.hasfood = false; // 允许生成新食物 drawunit(tail, "■"); // 绘制新增的蛇尾 return true; } return false; } 关键说明:
- 自身碰撞检测时,跳过蛇头(
i从1开始),避免蛇头与自身比较导致误判; - 吃食物时,利用移动前记录的蛇尾位置(
tail)追加蛇身,模拟“蛇身变长”的效果,同时清除食物标记,触发新食物生成。
3.3.3 食物生成(checkfoodgenerate)
void checkfoodgenerate(snake& snk, map& Map) { if (!Map.hasfood) { // 仅当无食物时生成 while (true) { // 生成地图范围内的随机坐标 int x = rand() % L; int y = rand() % H; bool isSnake = false; // 检查随机位置是否在蛇身上(避免食物生成在蛇体内) for (int i = 0; i < snk.schangdu; i++) { if (snk.aspos[i].x == x && snk.aspos[i].y == y) { isSnake = true; break; } } if (!isSnake) { // 位置合法(不在蛇身上) Map.block[y][x] = blocktype::FOOD; Map.hasfood = true; pos z = { x, y }; drawunit(z, "●"); // 绘制食物 return; } } } } 关键逻辑:
- 随机坐标生成:通过
rand() % L和rand() % H确保坐标在地图范围内; - 位置合法性校验:循环检测随机位置是否在蛇身上,若在则重新生成,避免食物“嵌入”蛇身导致无法被吃;
- 生成开关控制:
hasfood确保同一时间地图上只有一个食物,符合游戏规则。
3.3.4 方向控制(controlfangxiang)
void controlfangxiang(snake& snk) { if (_kbhit()) { // 检测是否有键盘输入(无输入则不执行) switch (_getch()) { // 获取按键字符 case 'w': // 向上:不能从向下反向(避免蛇直接回头撞自身) if (snk.sfangxiang != 1) snk.sfangxiang = 0; break; case 's': // 向下:不能从向上反向 if (snk.sfangxiang != 0) snk.sfangxiang = 1; break; case 'a': // 向左:不能从向右反向 if (snk.sfangxiang != 3) snk.sfangxiang = 2; break; case 'd': // 向右:不能从向左反向 if (snk.sfangxiang != 2) snk.sfangxiang = 3; break; } } } 核心优化:
添加“反向限制”——蛇不能直接向相反方向移动(如当前向下时不能直接向上),避免玩家误操作导致蛇头立即碰撞自身,提升游戏的容错性和体验。
3.4 游戏主循环
主循环是游戏的“心脏”,不断循环执行“输入检测→状态更新→画面渲染”流程,直到游戏结束条件触发。
int main() { map Map; snake snk; int score = 0; // 分数(全局累加) int i = 4; srand((unsigned int)time(NULL)); // 初始化随机种子(仅一次,避免食物位置重复) hidecursor(); // 隐藏光标 intimap(Map); // 初始化地图 drawmap(Map); // 绘制初始地图 intisnake(snk); // 初始化蛇 drawsnake(snk); // 绘制初始蛇 // 分数和游戏结束提示的固定位置 pos scorePos = { L/2-5, 0 }; // 分数显示在顶部居中 pos gameOverPos = { L/2-4, H/2 };// 游戏结束提示在屏幕居中 // 游戏主循环 while (true) { // 1. 控制蛇移动(按频率移动,失败则游戏结束) if (!checksnakemove(snk, Map, score,i)) break; // 2. 实时更新分数显示 stringstream ss; ss << "得分: " << score; char scoreChar[100]; strcpy_s(scoreChar, ss.str().c_str()); drawunit(scorePos, scoreChar); // 3. 检测方向输入 controlfangxiang(snk); // 4. 检测并生成食物 checkfoodgenerate(snk, Map); } // 游戏结束:显示提示 drawunit(gameOverPos, "游戏结束"); while (true) {} // 保持窗口(避免程序立即退出) return 0; } 主循环核心流程:
- 初始化阶段:完成地图、蛇、随机种子等初始化,绘制初始画面;
- 循环阶段: 通过
checksnakemove控制蛇的移动频率,移动失败则退出循环(游戏结束); - 将分数转换为字符串并实时更新显示;
- 检测方向键输入,更新蛇的移动方向;
- 检测食物状态,无食物则生成新食物。
- 结束阶段:显示“游戏结束”提示,通过无限循环保持控制台窗口,避免程序立即关闭。
四、关键优化与易错点总结
4.1 核心优化点
- 随机种子优化:
srand()放在main函数开头仅初始化一次,避免每次生成食物时重复初始化,导致食物位置集中或重复; - 移动频率控制:通过
chrono库的时间戳计算移动间隔,实现毫秒级精确控制,比Sleep()更灵活(不阻塞输入检测); - 方向反向限制:避免蛇直接反向移动撞自身,提升容错性;
- 分层绘制:地图、蛇、食物、分数分别绘制,逻辑清晰,便于维护和扩展。
4.2 常见易错点
- 坐标偏移错误:绘制时未考虑边框的1行1列偏移,导致蛇或食物“超出”地图边框;
- 蛇身移动顺序错误:从头部开始更新蛇身位置,导致蛇身覆盖,表现为“蛇身消失”;
- 随机种子重复初始化:在
checkfoodgenerate中调用srand(),导致同一秒内生成的食物位置相同; - 碰撞检测时机错误:移动前检测蛇头位置,未考虑移动后的新位置,导致碰撞判断延迟;
- 分数更新不及时:未实时刷新分数显示,导致玩家无法及时获取得分反馈。
五、扩展与改进方向
- 增加游戏难度等级:设置不同初始速度和加速幅度,满足不同玩家需求;
- 添加特殊食物效果:如“减速食物”“加分食物”“无敌时间”等,提升游戏趣味性;
- 记录最高分:将最高分存储到文件,游戏结束时对比并更新,增加游戏粘性;
- 优化画面效果:为蛇头和蛇身设置不同颜色(通过Windows API控制控制台文字颜色),提升视觉区分度;
- 添加暂停功能:支持空格键暂停/继续游戏,提升操作灵活性。