资讯动态

C++贪吃蛇游戏开发:从零实现控制台游戏与核心算法解析

发布时间:2026/8/10 14:07:19 来源:尧图企业网站定制
1. 项目概述为什么选择Dev-C和贪吃蛇如果你在找一门编程语言入门或者想重温一下大学时在机房敲代码的感觉C和贪吃蛇这个组合绝对是经典中的经典。我当年就是从这玩意儿开始第一次感受到了“创造”一个能跑起来的程序的快乐。现在各种IDE集成开发环境花里胡哨功能强大但对于初学者尤其是想纯粹理解代码逻辑和编译过程的朋友来说Dev-C这种轻量级、近乎“裸奔”的IDE反而更能让你聚焦于代码本身。Dev-C是个老家伙了界面复古但胜在简单直接。它没有现代IDE那些智能提示、代码补全的“保姆级”服务这恰恰逼着你去记住每一个函数名、每一个头文件。写贪吃蛇这种控制台小游戏用不到复杂的图形库核心就是逻辑控制——蛇怎么动、食物怎么生成、撞墙和撞自己怎么判断。这个过程能把C里最基础也最重要的几个概念串起来循环、条件判断、数组或者链表、函数封装还有那么一点点面向对象的雏形比如把蛇抽象成一个对象。网上源码很多但“复制粘贴”和自己“从零敲出来”是两码事。前者你得到的只是一个能运行的黑框框后者你能清楚地知道每一行代码为什么在那里出了问题该去哪找。这篇内容我就带你走一遍后者的路从创建一个空项目开始到一条活蹦乱跳的贪吃蛇在屏幕上跑起来。我会把完整的源码贴出来但更重要的是我会拆解每一个模块的设计思路和容易踩的坑。2. 开发环境准备与项目创建2.1 Dev-C的安装与配置首先你得有个Dev-C。虽然它古老但依然能在其官网或一些可靠的软件下载站找到。安装过程没什么坑一路“Next”就行。安装完成后第一次启动可能会让你选择语言和主题按喜好来。这里有个关键点确保你的Dev-C使用的是TDM-GCC或MinGW编译器。这是它在Windows上编译C/C代码的核心。检查方法是打开Dev-C点击顶部菜单栏的“工具(Tools)” - “编译选项(Compiler Options)”。在“编译器(Compiler)”标签页下确认“编译器集(Compiler set to use)”是“TDM-GCC …”或“MinGW …”开头的。如果不是你需要手动配置或重新安装带编译器的版本。注意有些绿色版或精简版可能不带编译器导致你写完代码点编译没反应。最稳妥的方法是去下载包含TDM-GCC的完整安装包。接下来我们创建一个新项目。不要直接新建源文件创建项目能更好地管理文件。点击“文件(File)” - “新建(New)” - “项目(Project…)”。在弹出的窗口里选择“Console Application”控制台应用程序在右边选择“C项目”给你的项目起个名字比如“MySnakeGame”。选择一个干净的文件夹来存放你的项目文件然后点击“保存”。Dev-C会自动为你生成一个main.cpp文件里面包含了一个最基本的main函数框架。我们可以在这个基础上开干。2.2 控制台游戏的基础设置贪吃蛇游戏运行在Windows控制台那个黑窗口里。我们需要对这个黑窗口进行一些初始化设置让它更适合我们的游戏。首先我们要包含必要的头文件。在main.cpp的最开头写上#include iostream #include conio.h // 用于_getch()非阻塞输入 #include windows.h // 用于控制台API函数 #include cstdlib // 用于rand()和srand() #include ctime // 用于time() using namespace std;解释一下iostream标准输入输出虽然我们这个游戏主要用Windows API画图但保留它以备不时之需。conio.h这是关键。里面的_kbhit()和_getch()函数能让我们检测键盘输入而不阻塞程序运行不像cin会卡住等待。windows.h提供了大量Windows系统函数我们将用它来控制光标位置、设置颜色、获取控制台大小等。cstdlib和ctime用于随机数生成给食物一个随机位置。接下来在main函数一开始我们就对控制台进行“装修”int main() { // 1. 设置控制台窗口标题 SetConsoleTitle(My Snake Game - By YourName); // 2. 隐藏光标让控制台不显示那个闪烁的光标画面更干净 HANDLE hConsole GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo(hConsole, cursorInfo); cursorInfo.bVisible false; // 隐藏光标 SetConsoleCursorInfo(hConsole, cursorInfo); // 3. 禁用控制台窗口的快速编辑模式和插入模式防止鼠标点击暂停游戏 HANDLE hInput GetStdHandle(STD_INPUT_HANDLE); DWORD prev_mode; GetConsoleMode(hInput, prev_mode); SetConsoleMode(hInput, ENABLE_EXTENDED_FLAGS | (prev_mode ~ENABLE_QUICK_EDIT_MODE)); // 4. 设置随机数种子 srand((unsigned int)time(NULL)); // ... 游戏主逻辑将在这里编写 return 0; }这几步设置非常重要尤其是隐藏光标和禁用快速编辑模式。我见过不少新手写的贪吃蛇玩着玩着不小心点了一下黑窗口游戏就卡住了就是因为控制台进入了“快速编辑”模式。上面的代码能有效避免这个问题。3. 游戏核心逻辑设计与数据结构在动手写代码前我们必须先想清楚游戏里有哪些“东西”以及它们怎么表示。这就是设计数据结构。3.1 地图、蛇与食物的抽象我们用一个二维的字符数组来表示游戏地图或者说画布。比如定义一个宽80高25的区域。const int WIDTH 80; const int HEIGHT 25; char canvas[HEIGHT][WIDTH]; // 画布数组这个canvas数组里的每一个字符就代表了屏幕上的一个点。我们可以约定空格 表示空地。#表示墙壁边界。表示蛇头。*表示蛇的身体。$表示食物。蛇怎么表示蛇是一节一节的会动会变长。最直观的方法是用一个数组来存储每一节身体的坐标。但这里有个问题蛇移动时我们需要在数组头部插入新的蛇头位置并在尾部删除旧的蛇尾位置。用普通数组做插入删除效率很低。因此一个更好的选择是使用双向队列deque它支持在头尾两端高效地插入和删除。不过为了更清晰地展示逻辑我们先使用一个结构体数组和两个变量来手动管理蛇头和蛇尾这能让你更深刻地理解移动过程。我们定义一个Point结构体表示坐标然后用数组存储蛇身。struct Point { int x; int y; }; Point snake[WIDTH * HEIGHT]; // 理论上蛇最长可以占满整个画布 int snakeLength; // 当前蛇的长度 int head, tail; // 指向蛇头和蛇尾在数组中的索引环形数组思想这里用了一个小技巧我们把snake数组想象成一个环。head指向当前蛇头的位置tail指向当前蛇尾的位置。当蛇移动时新的蛇头坐标会覆盖掉tail位置旧的蛇尾坐标如果没吃到食物然后head和tail指针向前移动一格。这样就模拟了队列FIFO的行为避免了大规模的数据搬移。这是实现蛇移动的核心技巧。食物就简单了一个Point变量足矣。Point food;3.2 方向控制与游戏状态蛇有四个移动方向上、下、左、右。我们用整数常量来表示。enum Direction { STOP 0, UP, DOWN, LEFT, RIGHT }; Direction dir;STOP表示游戏初始或暂停状态。dir变量存储蛇当前的行进方向。游戏还需要一些全局状态变量bool isGameOver; int score; int speed; // 控制游戏速度值越大蛇移动越慢Sleep时间越长isGameOver标志游戏是否结束。score是得分吃一个食物加多少分可以自己定。speed用来控制游戏的难度通过调节每次移动后Sleep()的毫秒数来实现。4. 核心函数模块实现有了设计图现在开始砌砖把一个个功能模块实现出来。4.1 初始化函数Init()这个函数负责在游戏开始前把所有东西摆到正确的位置。void Init() { isGameOver false; dir STOP; // 游戏刚开始蛇不动 score 0; speed 100; // 初始速度100毫秒移动一次可调整 // 初始化蛇放在地图中央初始长度为3 snakeLength 3; head 0; tail snakeLength - 1; int startX WIDTH / 2; int startY HEIGHT / 2; for (int i 0; i snakeLength; i) { snake[i].x startX - i; // 蛇头在最右身体向左延伸 snake[i].y startY; } // 生成第一个食物 GenerateFood(); // 初始化画布清空并绘制边界 for (int i 0; i HEIGHT; i) { for (int j 0; j WIDTH; j) { if (i 0 || i HEIGHT - 1 || j 0 || j WIDTH - 1) { canvas[i][j] #; // 边界 } else { canvas[i][j] ; // 内部空地 } } } // 把蛇和食物画到画布上 DrawSnakeAndFood(); }GenerateFood()和DrawSnakeAndFood()是我们接下来要实现的函数。初始化时让蛇水平放置头朝右。4.2 绘制函数Draw()这个函数负责将内存中的canvas画布“打印”到控制台屏幕上。直接逐行逐列输出整个数组效率很低因为每次都要重绘整个屏幕。更高效的做法是只更新发生变化的位置。但对于初学者我们先采用简单清晰的全屏重绘理解了原理后再优化。我们实现一个Draw()函数和一个辅助函数DrawSnakeAndFood()。void DrawSnakeAndFood() { // 先清除画布上旧的蛇和食物将非边界区域重置为空格 for (int i 1; i HEIGHT - 1; i) { for (int j 1; j WIDTH - 1; j) { canvas[i][j] ; } } // 绘制食物 canvas[food.y][food.x] $; // 绘制蛇身从尾到头画确保蛇头最后画万一有重叠头覆盖身体 for (int i tail; i ! head; i (i 1) % (WIDTH * HEIGHT)) { canvas[snake[i].y][snake[i].x] *; } // 绘制蛇头 canvas[snake[head].y][snake[head].x] ; } void Draw() { // 将光标移动到控制台左上角(0,0)实现“清屏”效果 COORD coord {0, 0}; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); // 打印整个画布 for (int i 0; i HEIGHT; i) { for (int j 0; j WIDTH; j) { cout canvas[i][j]; } cout endl; // 换行 } // 打印游戏信息 cout Score: score | Speed: speed (Lower is faster) endl; cout Press x to quit. endl; }这里用SetConsoleCursorPosition将光标移回原点再打印比用system(cls)清屏要流畅且不会闪屏。DrawSnakeAndFood()函数先清空内部区域再根据当前的snake数组和food坐标更新画布。注意绘制蛇身的循环它按照从tail到head的顺序遍历环形数组。4.3 输入处理函数Input()游戏需要实时响应键盘输入来改变蛇的方向。我们用_kbhit()来非阻塞地检测是否有按键按下。void Input() { if (_kbhit()) { // 如果有按键被按下 char key _getch(); // 获取按键不需要回车 switch (key) { case w: case W: if (dir ! DOWN) dir UP; break; case s: case S: if (dir ! UP) dir DOWN; break; case a: case A: if (dir ! RIGHT) dir LEFT; break; case d: case D: if (dir ! LEFT) dir RIGHT; break; case x: case X: isGameOver true; break; // 可以添加空格键暂停功能 // case : isPaused !isPaused; break; } } }这里有一个非常重要的细节在判断方向改变时我们加了限制条件if (dir ! DOWN)。这是为了防止蛇直接掉头。比如蛇正在向右移动你瞬间按下左键如果没有这个限制蛇头会立刻向左导致蛇身“撞”到自己游戏瞬间结束。这个限制保证了蛇只能转弯不能原地180度回头。4.4 游戏逻辑函数Logic()这是游戏最核心的部分包含了移动、吃食物、碰撞检测等所有规则。void Logic() { if (dir STOP) return; // 如果方向是STOP不移动 // 1. 计算新的蛇头位置 Point newHead snake[head]; switch (dir) { case UP: newHead.y--; break; case DOWN: newHead.y; break; case LEFT: newHead.x--; break; case RIGHT: newHead.x; break; } // 2. 碰撞检测 // 2.1 撞墙检测 if (newHead.x 0 || newHead.x WIDTH - 1 || newHead.y 0 || newHead.y HEIGHT - 1) { isGameOver true; return; } // 2.2 撞自身检测遍历蛇身从tail到head-1 for (int i tail; i ! head; i (i 1) % (WIDTH * HEIGHT)) { if (newHead.x snake[i].x newHead.y snake[i].y) { isGameOver true; return; } } // 3. 吃食物检测 if (newHead.x food.x newHead.y food.y) { // 吃到食物蛇长度增加分数增加 score 10; snakeLength; // 生成新的食物 GenerateFood(); // 可以随着分数增加而提高速度 // if (score % 50 0 speed 20) speed - 10; } else { // 没吃到食物蛇尾需要移动即tail指针前移覆盖掉旧的蛇尾 tail (tail 1) % (WIDTH * HEIGHT); } // 4. 移动蛇头将新的蛇头位置存入数组并更新head指针 head (head 1) % (WIDTH * HEIGHT); snake[head] newHead; }逻辑分解计算新蛇头根据当前方向dir算出蛇头下一步的坐标newHead。碰撞检测撞墙判断newHead是否碰到了画布边界#。撞自己遍历当前蛇身注意不包括当前的head因为那是旧蛇头检查newHead是否与任何一节身体重合。发生碰撞游戏结束。吃食物检测如果newHead和食物坐标重合则加分。蛇长snakeLength加1。注意这里我们只增加了长度变量并没有立刻在数组里添加新节点。因为吃食物后蛇尾不动不执行else里的tail移动所以下一次移动时新的蛇头占据了新位置而蛇尾还在原地自然就“变长”了一节。调用GenerateFood()在空地生成新食物。移动执行如果没吃到食物蛇尾指针tail需要向前移动一格环形数组这意味着旧的蛇尾位置被“遗弃”将在下一次绘制时被覆盖为空地。无论是否吃到食物新的蛇头坐标newHead都要存入数组并且蛇头指针head向前移动一格指向这个新位置。这个利用环形数组和头尾指针的移动逻辑是理解贪吃蛇算法的关键。它高效地模拟了队列的“先进先出”。4.5 食物生成函数GenerateFood()这个函数需要在画布内部的空地随机生成一个食物且不能生成在蛇身上。void GenerateFood() { bool onSnake; do { onSnake false; // 在[1, WIDTH-2]和[1, HEIGHT-2]范围内生成随机坐标 food.x rand() % (WIDTH - 2) 1; food.y rand() % (HEIGHT - 2) 1; // 检查是否与蛇身重叠 for (int i tail; i ! head; i (i 1) % (WIDTH * HEIGHT)) { if (food.x snake[i].x food.y snake[i].y) { onSnake true; break; } } // 还要检查是否与蛇头重叠虽然概率极低 if (food.x snake[head].x food.y snake[head].y) { onSnake true; } } while (onSnake); // 如果重叠了就重新生成 }这里用了一个do...while循环确保生成的食物一定在空地上。这是一个简单的“拒绝采样”方法。在蛇很长时这个循环可能会多跑几次但对于这个小游戏来说完全够用。5. 主循环与游戏流程整合所有零件都准备好了现在把它们组装起来形成游戏的主循环。int main() { // ... 之前的环境设置代码设置标题、隐藏光标等 Init(); // 游戏初始化 while (!isGameOver) { Draw(); // 绘制当前帧 Input(); // 处理用户输入 Logic(); // 更新游戏逻辑 Sleep(speed); // 控制游戏速度制造动画帧间隔 } // 游戏结束处理 system(cls); // 清屏也可以用之前的API方法 cout Game Over! endl; cout Your final score is: score endl; cout Press any key to exit...; _getch(); // 等待一个按键 return 0; }主循环while (!isGameOver)是游戏的心脏。每一帧一次循环都按照“绘制 - 输入 - 逻辑更新 - 等待”的顺序执行。Sleep(speed)函数让程序暂停指定的毫秒数这决定了蛇的移动速度。speed值越小循环越快蛇移动得也越快游戏难度越高。6. 完整源码与深度优化建议将上述所有代码模块按顺序整合到main.cpp中你就得到了一个完整的、可编译运行的贪吃蛇游戏。由于篇幅完整代码在此不逐行展开但结构已完全清晰。当你成功运行后可能会觉得有点“简陋”或“卡顿”。这里提供几个深度优化和扩展的方向让你的贪吃蛇更上一层楼6.1 性能优化局部刷新我们之前的Draw()函数每次都重绘整个屏幕80*252000个字符效率低且可能闪烁。优化方法是只重绘发生变化的位置。我们需要记录上一帧画布的状态或者直接计算哪些坐标需要更新。双缓冲在内存中维护两个画布一个“后台画布”用于逻辑计算一帧结束后将其与“前台画布”比较只将不同的字符输出到屏幕。这需要更精细的坐标记录。脏矩形记录蛇头、旧蛇头、蛇尾、食物等关键坐标的变化只更新这几个点。这是控制台小游戏最常用的优化手段。你可以修改DrawSnakeAndFood和Draw函数不再清空整个内部区域而是在移动前将旧的蛇尾位置snake[tail]输出为空格。将新的蛇头位置newHead输出为。将旧的蛇头位置移动前的snake[head]输出为*如果蛇长度大于1。食物被吃后在旧食物位置输出空格在新食物位置输出$。边界只需要绘制一次。这样每一帧只输出几个字符极其流畅。6.2 功能扩展暂停功能在主循环中增加一个bool isPaused变量。在Input()函数里监听空格键切换暂停状态。在Logic()函数开头如果isPaused为真则直接返回。难度分级将speed与score关联。例如每得50分speed减少10毫秒但设置一个下限比如20毫秒。关卡设计不止是空矩形。可以在Init()函数里在canvas上画一些固定的障碍物X然后在Logic()的碰撞检测中增加对障碍物的判断。图形化界面如果你不满足于字符界面可以学习简单的图形库如EasyX针对VC、SDL或SFML。将canvas从字符数组变为像素数组用图形函数绘制方块原理完全一样。存档/读档将当前的snake数组、snakeLength、head、tail、food、score、dir等所有状态变量写入一个文件。下次游戏时从文件读取并恢复。6.3 常见编译与运行问题**conio.h和_kbhit()/_getch()报错**确保你使用的是Windows下的Dev-CTDM-GCC/MinGW。这些函数不是标准C库是Windows/MS-DOS的扩展。如果在其他平台如Linux的GCC编译需要换用ncurses等库。Sleep()函数报错Sleep()函数在windows.h中首字母S大写。注意包含头文件。游戏速度太快或太慢调整主循环中的Sleep(speed)参数。speed是毫秒数。控制台窗口一闪而过这通常是因为程序正常结束。确保你的main函数最后有_getch()或system(pause)等待用户输入或者直接在Dev-C中按F11编译运行而不是CtrlF10它会自动暂停。蛇的移动方向不受控制或反向重点检查Input()函数中的方向限制逻辑if (dir ! OPPOSITE_DIR)以及Logic()函数中计算newHead的switch-case是否正确。从零开始实现一个贪吃蛇最大的收获不是代码本身而是这个过程中对程序状态管理、循环控制、数据结构应用和问题分解能力的锻炼。当你看到自己写的蛇在屏幕上按照你的指令游走时那种成就感是看再多教程都无法替代的。试着去修改它优化它加入你自己的创意这才是编程乐趣的开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价