简介:这是一份面向C++初学者与游戏开发入门者的贪吃蛇增强版源码工程,基于VC6.0与MFC实现,适合用来练习面向对象设计、链表数据结构与Windows图形界面编程。资源包共58个文件,约8.98MB,包含5个cpp源文件、6个h头文件、10个obj与10个sbr编译中间文件、2个exe可执行程序,以及bmp位图、rc资源脚本、dsp/dsw工程文件和pdb调试符号等,完整保留了从源码到可运行程序的构建链路。项目采用双类设计,蛇体以单链表组织节点,涵盖键盘事件响应、定时器控速、界面绘制与游戏状态管理等核心模块,并附有ReadMe说明。已有178人学习下载,读者可借此理解链表增删遍历、MFC消息机制与VC6.0工程调试流程,是复习C++与Windows游戏开发基础的可运行案例。
1. 贪吃蛇增强版:一个 VC6.0 时代的 C++ 练手项目,现在跑起来依然有收获
如果你手里有一份 VC6.0 的贪吃蛇源码,别急着把它当成“老古董”扔进回收站。这个项目虽然界面朴素,但它把 C++ 面向对象、Windows 消息循环、GDI 绘图、定时器控制、链表/数组管理这些核心知识点全串在了一起。我见过不少新手用 VS2022 写贪吃蛇,结果卡在字符集和 MFC 配置上;反而用 VC6.0 打开这份增强版源码,编译运行一气呵成。它适合谁?适合刚学完 C++ 语法、想找一个能跑起来的完整项目练手的人,也适合想回头看看“没有 STL 和智能指针的年代,代码是怎么组织”的老手。这份资源就是一个完整的 VC6.0 工程,双击 dsw 文件就能打开,编译后直接生成 exe,不需要额外装库。
2. 工程结构与核心模块:从 dsw 到 exe 的完整链路
2.1 目录里有什么:文件清单与职责划分
拿到源码包后,先别急着点“编译”。用资源管理器看一眼根目录,常见的文件布局是这样的:
| 文件/文件夹 | 作用 |
|---|---|
| Snake.dsw | VC6.0 工作区文件,双击它启动 IDE |
| Snake.dsp | 项目文件,记录编译选项、链接库、源文件列表 |
| Snake.cpp | 主程序入口,包含 WinMain 和窗口过程 |
| Snake.h | 全局结构体、常量、函数声明 |
| Game.cpp / Game.h | 游戏逻辑核心:蛇身移动、食物生成、碰撞检测 |
| Render.cpp / Render.h | GDI 绘图封装:画蛇、画食物、画分数 |
| Resource.h / Snake.rc | 菜单、图标、对话框资源 |
| Debug/ | 编译输出目录,生成 Snake.exe |
这个划分不算精细,但胜在清晰。Game 模块不碰任何 HWND 和 HDC,只负责数据;Render 模块只负责把数据画到屏幕上;Snake.cpp 里的窗口过程负责接收键盘消息和定时器消息,然后调用 Game 和 Render。这种“逻辑与界面分离”的思路,在 VC6.0 时代算是比较讲究的写法。
2.2 主循环怎么跑:消息循环 + 定时器驱动
Windows 程序没有“while(1) 游戏循环”那种写法,而是靠消息循环和定时器配合。核心代码在 Snake.cpp 里:
// Snake.cpp 主消息循环片段 int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MSG msg; // 注册窗口类、创建窗口、显示窗口 if (!InitInstance(hInstance, nCmdShow)) return FALSE; // 设置定时器:每 200 毫秒触发一次 WM_TIMER SetTimer(g_hWnd, TIMER_ID, 200, NULL); // 标准消息循环 while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; }逻辑说明:SetTimer的第三个参数 200 是毫秒数,决定了蛇的移动速度。数值越小蛇跑得越快,但低于 80 毫秒时人眼几乎反应不过来。参数TIMER_ID是一个自定义常量,在 Snake.h 里定义,窗口过程里用wParam == TIMER_ID来判断是不是游戏定时器。GetMessage返回 0 时退出循环,整个程序结束。
窗口过程里处理WM_TIMER和WM_KEYDOWN:
// 窗口过程片段 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_TIMER: if (wParam == TIMER_ID) { Game_Update(); // 更新蛇的位置、判断吃食物、碰撞 Render_All(hWnd); // 重绘整个画面 } break; case WM_KEYDOWN: switch (wParam) { case VK_UP: Game_SetDirection(DIR_UP); break; case VK_DOWN: Game_SetDirection(DIR_DOWN); break; case VK_LEFT: Game_SetDirection(DIR_LEFT); break; case VK_RIGHT: Game_SetDirection(DIR_RIGHT); break; } break; case WM_DESTROY: KillTimer(hWnd, TIMER_ID); PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }这里有个细节:Game_SetDirection不能直接把方向改成反方向,否则蛇会瞬间撞到自己。常见做法是在函数内部判断“新方向与当前方向是否相反”,相反就忽略这次按键。这个判断在 Game.cpp 里实现。
2.3 蛇身数据结构:数组还是链表
VC6.0 对 STL 的支持不算好,很多源码直接用固定长度数组存蛇身坐标:
// Snake.h 中的数据结构 #define MAX_SNAKE_LEN 100 typedef struct { int x; int y; } Point; typedef struct { Point body[MAX_SNAKE_LEN]; // body[0] 是蛇头 int length; // 当前蛇身节数 int direction; // 当前移动方向 int speed; // 速度等级 } Snake; typedef struct { Point pos; // 食物坐标 int type; // 食物类型:普通/加速/减速 } Food;用数组的好处是内存连续、访问快,缺点是长度固定。增强版里MAX_SNAKE_LEN设成 100,对普通玩家够用。如果你要改成链表,需要把body换成Point*动态分配,并在增长时realloc。我一般会保留数组结构,因为贪吃蛇长度超过 100 的情况极少,而且数组代码更简单,不容易出内存泄漏。
2.4 碰撞检测与食物生成
碰撞检测分两种:撞墙和撞自己。撞墙判断蛇头坐标是否超出客户区边界;撞自己判断蛇头坐标是否与body[1]到body[length-1]中任意一节重合。
// Game.cpp 碰撞检测片段 int Game_CheckCollision(Snake* snake, int maxX, int maxY) { Point head = snake->body[0]; // 撞墙检测 if (head.x < 0 || head.x >= maxX || head.y < 0 || head.y >= maxY) return COLLISION_WALL; // 撞自己检测:从第 1 节开始比较 for (int i = 1; i < snake->length; i++) { if (head.x == snake->body[i].x && head.y == snake->body[i].y) return COLLISION_SELF; } return COLLISION_NONE; }食物生成要避开蛇身。常见做法是随机生成坐标后,遍历蛇身检查是否重合,重合就重新生成。如果蛇太长导致随机多次都撞上,可以加一个最大重试次数,超过就顺序扫描空白格子。
// 食物生成:随机 + 去重 void Game_SpawnFood(Food* food, Snake* snake, int maxX, int maxY) { int tries = 0; do { food->pos.x = rand() % maxX; food->pos.y = rand() % maxY; tries++; } while (Game_IsOnSnake(snake, food->pos) && tries < 1000); // 随机食物类型:10% 概率出特殊食物 food->type = (rand() % 10 == 0) ? FOOD_SPEED : FOOD_NORMAL; }rand()需要先调用srand((unsigned)time(NULL))初始化种子,否则每次运行食物序列都一样。这个坑我在很多学生作业里都见过。
3. 编译与运行:VC6.0 下的完整操作步骤
3.1 打开工程与编译设置
双击Snake.dsw,VC6.0 会自动加载工作区。左侧文件视图里能看到 Source Files、Header Files、Resource Files 三个分组。按 F7 或点菜单 Build → Build Snake.exe 开始编译。如果报错“cannot open file ‘winmm.lib’”,说明链接器没配好。VC6.0 默认会链接常用库,但有些精简版安装可能缺文件。
编译前检查两个地方:
- Project → Settings → C/C++ → Code Generation:Use run-time library 选 Multithreaded DLL 或 Debug Multithreaded DLL,取决于你是 Release 还是 Debug。
- Project → Settings → Link:Object/library modules 里应该有
kernel32.lib user32.lib gdi32.lib winmm.lib。如果缺winmm.lib,手动加上,它是timeGetTime等函数需要的。
3.2 常见编译错误与解决
错误 1:fatal error C1083: Cannot open include file: 'stdafx.h'
原因:工程用了预编译头,但你的环境里没有生成stdafx.h或者路径不对。解决:在 Project → Settings → C/C++ → Precompiled Headers 里选 Not using precompiled headers,然后重新编译。
错误 2:error LNK2001: unresolved external symbol __imp__timeGetTime@0
原因:链接器找不到winmm.lib。解决:在 Project → Settings → Link → Object/library modules 末尾加上winmm.lib。
错误 3:error C2065: 'rand' : undeclared identifier
原因:忘了#include <stdlib.h>。VC6.0 对头文件要求严格,rand和srand都在stdlib.h里。在 Snake.h 或 Game.cpp 顶部补上即可。
3.3 运行效果与操作说明
编译成功后,Debug 目录下会生成Snake.exe。双击运行,出现一个固定大小的窗口,蛇是绿色方块,食物是红色方块。方向键控制移动,空格键暂停,ESC 退出。吃到普通食物蛇身加一节,吃到加速食物蛇移动速度临时提升,吃到减速食物速度下降。分数显示在窗口标题栏或客户区左上角。
如果窗口一闪而过,检查WinMain里是否在创建窗口后忘了ShowWindow和UpdateWindow。这两个调用缺一不可。
4. 避坑与常见问题:VC6.0 贪吃蛇的 5 个血泪教训
4.1 现象:蛇移动时画面闪烁严重
原因:每次WM_TIMER都直接往窗口 DC 上画,没有双缓冲。GDI 逐块绘制时,屏幕刷新跟不上,人眼就看到闪烁。
解决:在内存里创建一个兼容 DC 和位图,所有绘制先画到内存 DC,最后一次性BitBlt到窗口 DC。代码片段:
// 双缓冲绘制 HDC hdc = GetDC(hWnd); HDC memDC = CreateCompatibleDC(hdc); HBITMAP memBmp = CreateCompatibleBitmap(hdc, width, height); SelectObject(memDC, memBmp); // 所有绘制操作针对 memDC Render_All(memDC); // 一次性拷贝到窗口 BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY); // 清理 DeleteObject(memBmp); DeleteDC(memDC); ReleaseDC(hWnd, hdc);4.2 现象:按方向键后蛇直接反向撞死
原因:Game_SetDirection没有做反向判断。玩家在蛇向右移动时按左键,蛇头立刻向左,撞到第二节身体。
解决:在设置方向前检查新方向与当前方向是否相反。相反就忽略这次按键。
void Game_SetDirection(int newDir) { // 禁止 180 度掉头 if ((g_snake.direction == DIR_UP && newDir == DIR_DOWN) || (g_snake.direction == DIR_DOWN && newDir == DIR_UP) || (g_snake.direction == DIR_LEFT && newDir == DIR_RIGHT) || (g_snake.direction == DIR_RIGHT && newDir == DIR_LEFT)) { return; } g_snake.direction = newDir; }4.3 现象:食物生成在蛇身上,吃不到
原因:随机生成食物坐标后没有检查是否与蛇身重合。蛇越长,重合概率越高。
解决:生成后遍历蛇身检查,重合就重新生成。加最大重试次数防止死循环。
4.4 现象:编译通过但运行时报“内存不能为 read”
原因:数组越界。常见于蛇身增长时length超过MAX_SNAKE_LEN,或者访问body[length]而不是body[length-1]。
解决:在增长蛇身之前判断if (snake->length < MAX_SNAKE_LEN),否则不加长只加分。所有遍历蛇身的循环用i < snake->length,不要用i <= snake->length。
4.5 现象:VC6.0 打开 dsw 提示“无法加载项目”
原因:dsw 文件里的路径是绝对路径,换电脑后路径变了。或者 dsp 文件里的源文件列表路径不对。
解决:用记事本打开 dsw 和 dsp,把里面的绝对路径改成相对路径。或者直接在 VC6.0 里新建一个空工程,把 cpp 和 h 文件手动添加进去。
5. 二次开发:从改速度到加障碍物,几个能落地的改造方向
5.1 改速度:让游戏有难度曲线
原版速度是固定的 200 毫秒。你可以改成随分数递减:每吃 5 个食物,定时器间隔减 10 毫秒,最低到 80 毫秒。核心代码在WM_TIMER处理里加一个判断:
// 动态调速 if (g_score % 5 == 0 && g_speed > 80) { g_speed -= 10; KillTimer(hWnd, TIMER_ID); SetTimer(hWnd, TIMER_ID, g_speed, NULL); }注意KillTimer和SetTimer要成对出现,否则会创建多个定时器,蛇会越跑越快。
5.2 加障碍物:在游戏区域里放静态方块
障碍物可以用一个Point数组存坐标,在Game_CheckCollision里增加一段检测:蛇头坐标与任意障碍物重合就判负。绘制时用灰色方块画出来。障碍物坐标可以硬编码,也可以从文件读取。我一般会写一个level.txt,每行两个整数表示障碍物坐标,游戏启动时加载。
5.3 加音效:用PlaySound播放 wav
VC6.0 里播放 wav 最简单的方式是PlaySound函数,需要#include <mmsystem.h>和链接winmm.lib。
// 吃食物时播放音效 PlaySound("eat.wav", NULL, SND_FILENAME | SND_ASYNC);SND_ASYNC表示异步播放,不阻塞游戏逻辑。wav 文件放在 exe 同目录下。如果没有 wav 文件,可以用SND_ALIAS播放系统声音,但效果一般。
5.4 改造验证方法
每次改完代码,按 F7 编译,然后运行 exe。验证步骤:
- 蛇能正常移动,方向键响应正确。
- 吃食物后蛇身增长,分数增加。
- 撞墙或撞自己后游戏结束,弹出提示。
- 速度变化符合预期,没有突然跳变。
- 双缓冲生效,画面不闪烁。
如果改的是数据结构(比如数组换链表),重点看内存有没有泄漏。VC6.0 没有内置的内存检测工具,可以在 Debug 模式下用_CrtDumpMemoryLeaks(),但需要包含crtdbg.h。
5.5 一个我常犯的错误
早期我改蛇身增长逻辑时,直接snake->length++然后给body[length-1]赋值,结果忘了判断length是否超过MAX_SNAKE_LEN。蛇吃到第 100 个食物时数组越界,程序直接崩溃。从那以后我每次改数组相关的逻辑,都强制在增长前加一行边界检查,并且用assert(snake->length < MAX_SNAKE_LEN)在 Debug 模式下提前暴露问题。这个习惯帮我省了很多调试时间。希望这份源码和改造思路能帮到你,跑起来之后不妨先改速度,再试着加障碍物,一步步来。
本文还有配套的精品资源,点击获取