简介:基于EasyX图形库与C语言实现的简易版植物大战僵尸游戏源码,附带sln解决方案,可一键编译直接运行。项目完整呈现了从窗口绘制、动画帧播放、碰撞检测到关卡控制的2D游戏基础实现,麻雀虽小五脏俱全,适合C语言初学者学习EasyX开发,也可作为课程设计或毕设选题参考。压缩包共2000个文件,大小147.38MB,其中包含1017个png图片、551个gif动图、178个mp3与166个ogg音频等美术音效素材,加上cpp/h源码、vcxproj/sln工程配置,并内置编译好的exe程序,拿到即可体验完整游戏流程,素材与代码分离、目录结构清晰,便于后续按需替换或调整。目前已有439人学习下载。对于想动手扩展玩法或深入C语言游戏制作的读者,整套源码和素材可直接二次开发,省去从零搭建环境和绘制素材的时间,便于集中研究游戏主循环、碰撞逻辑、动画播放等关键模块。
1. 从 C 语言到大作业:这份 EasyX 植物大战僵尸源码能直接撑起一个课设
如果你正在找 C 语言课程设计题目,多半会碰到网上那一大批只有一个 main.c 的残血项目——代码能看,一编译就死。这份基于 EasyX 和 C 语言开发的简易植物大战僵尸游戏不太一样:压缩包里带着完整的 sln 解决方案,解压后 VS 打开工程直接编译运行,这在作业类开源项目里相当少见。游戏走经典塔防套路:种植物、攒阳光、挡僵尸,功能不算多,但主循环、鼠标事件、碰撞检测、贴图绘制这几条 C 语言大作业常考主线全部覆盖。适合刚学完结构体和指针、想拿完整项目练手的人,也适合计科、大数据、人工智能等专业拿去做课程设计或初期立项演示。
2. 工程文件结构拆解:main.cpp、tools.cpp、vector2.cpp 各管哪摊事
先说一个容易劝退新人的细节:名称叫"C 语言项目",但源码后缀是 .cpp。不用纠结——EasyX 本身就要求 C++ 编译环境,graphics.h 里的 IMAGE、ExMessage 这些类型都带 C++ 特性,项目统一按 C++ 打开即可,语法上绝大多数还是 C 的写法。这个项目拆成了三个源文件,各自职责非常清晰,恰好是学习模块化拆分的好样本。
2.1 三份源码的分工:主循环、工具函数与二维向量
main.cpp 管的是游戏本体:初始化窗口、维护游戏状态、跑主循环、调度各个 update 和 draw 函数。tools.cpp 是通用工具集合,图片加载、文字绘制、随机数、碰撞辅助函数这类跟具体玩法无关的代码都会放这里。vector2.cpp 则是一个极简二维向量模块,封装了位置、位移、距离这些数学运算。
为什么单独抽一个 vector2?因为游戏里几乎所有实体——豌豆子弹、僵尸、植物——都要算位置。如果每个地方都写 x + vx、x - vx 这种裸算术,改一个全局速度就要全文件搜索。抽成向量之后,子弹每帧移动就是一次向量加法:
// vector2.h typedef struct Vector2 { float x, y; } Vector2; Vector2 vec2_add(Vector2 a, Vector2 b); // 向量加法 Vector2 vec2_scale(Vector2 a, float s); // 向量缩放 float vec2_distance(Vector2 a, Vector2 b); // 两点距离对应实现里全是四则运算,比如 vec2_distance 就是 sqrt 开根号算两点欧氏距离。值得注意的一点是这里用 float 而不是 int:帧间位移往往小于 1 像素,用 int 存位置会积累舍入误差,表现为子弹走几步顿一下。
tools.cpp 里的典型函数是这种图片加载封装:
// tools.cpp —— 统一加载图片并返回 IMAGE 指针 IMAGE* load_image(const char* path, int w, int h) { IMAGE* img = new IMAGE(); loadimage(img, path, w, h, true); return img; }封装的原因是 EasyX 的 loadimage 有多个重载,直接裸调容易把参数写错;封装后全局只维护一份加载逻辑,路径写错也只在 tools.cpp 里改一处。main.cpp 里你基本只会看到对这三个模块的调用,游戏逻辑本身没有被读写文件、坐标换算这些杂事淹没。
2.2 sln / vcxproj / Browse.VC.db:哪些要留、哪些可删
解压后文件列表里除了源码,还有几个跟编译相关的东西。很多新手第一次见到会懵,我列个表说明各自职责:
| 文件 | 作用 | 建议 |
|---|---|---|
| PVZ.sln | 解决方案入口,VS 双击它就打开整个工程 | 保留 |
| PVZ.vcxproj | 工程配置,包含编译选项和源文件清单 | 保留 |
| PVZ.vcxproj.filters | 解决方案资源管理器里的虚拟目录分组,不参与编译 | 可删可留 |
| Browse.VC.db | VS 的 IntelliSense 索引缓存,每次打开会自动重建 | 坏了直接删 |
| PVZ.exe | 已编译好的可执行程序,不想装环境可以直接跑 | 保留 |
| gamecapture-*.gif | 开发期录的运行预览图 | 可删 |
这里重点说 Browse.VC.db:它本质是本地缓存,记录了符号索引,方便你鼠标悬停看定义。但它跟 VS 版本强相关,用旧版 VS 打的工程拿到新版 VS 打开,这个文件经常报损坏。报损坏不用慌,删掉让 VS 重新生成即可,跟源码无关。真正参与编译的只有 .sln、.vcxproj 和三个 .cpp,其他都是辅助文件。如果你用 Git 管理代码,建议把这个缓存文件加进 .gitignore,省得每次换机器都冲突。
提示:想最快验证游戏是否正常,先别开工程,直接双击 PVZ.exe。能跑起来再谈改代码,这能帮你排除"其实是环境问题"的不确定因素。
2.3 EasyX 环境匹配:版本、字符集与编译选项
直接编译运行的前提是本机装好了 EasyX,且版本跟你用的 VS 匹配。EasyX 通过安装包往 VS 的公共包含目录里塞 graphics.h,装错版本最常见的报错是找不到头文件。装好后不用在工程设置里额外配 include 路径,它默认装进了 VS 公共目录。
打开 sln 时 VS 可能弹"重定目标"或"SDK 版本无效",这是工程里记录的 Windows SDK 版本和你本机不一致导致的。按提示重定目标到本机已有的 SDK 即可,源码一行都不用改。另一个高频配置项是字符集:工程属性 → 常规 → 字符集,EasyX 的中文版示例多用多字节字符集,如果你在 VS 里手动建工程而不是打开这份 sln,记得把默认的 Unicode 切到多字节,否则涉及字符串的接口要换成宽字节版本。
如果出现链接错误 unresolved external symbol WinMain,说明入口函数类型没对上。EasyX 项目常见两种入口:控制台 main() 或 WinMain()。这份工程用的是 main.cpp,所以子系统应该设为控制台,或者干脆在代码里加一句:
#pragma comment(linker, "/SUBSYSTEM:WINDOWS")这句的意思是告诉链接器按窗口程序处理,配合 EasyX 的 initgraph 使用,这样从控制台启动也不会冒出黑色命令行窗口。实际项目里见过有人两种写法混用,结果一个报 WinMain 一个报 main 冲突,根源都是入口与子系统不匹配。
3. 核心玩法实现:种植判定、子弹轨迹与僵尸波次
先看整体玩法闭环:玩家用阳光购买植物 → 点击棋盘格子种植 → 植物按固定节奏发射子弹 → 子弹飞行并碰撞僵尸 → 僵尸掉血死亡,同时僵尸持续从右侧进场向左推进。这个闭环里最考验 C 语言功底的是种植判定和碰撞检测,我把两条主线的常见实现拆开讲。
3.1 种植判定:鼠标点击到棋盘坐标的换算
棋盘通常是一个 9 列 × 5 行的网格,每格 80×100 像素,棋盘左上角在窗口内的坐标是固定的 (BOARD_X, BOARD_Y)。鼠标点下去后,第一步是把屏幕坐标换算成格子下标:
#define BOARD_X 40 #define BOARD_Y 90 #define CELL_W 80 #define CELL_H 100 #define COLS 9 #define ROWS 5 int col = (msg.x - BOARD_X) / CELL_W; int row = (msg.y - BOARD_Y) / CELL_H; if (col >= 0 && col < COLS && row >= 0 && row < ROWS) { if (plant_grid[row][col] == 0) { plant_grid[row][col] = selected_plant; sun -= plant_defs[selected_plant].sun_cost; } }这里有个最容易写错的地方:坐标减棋盘原点后必须判断范围。很多初版代码直接拿 (msg.x - BOARD_X) / CELL_W 当下标用,点到棋盘左侧外部区域时 col 是负数,数组下标越界,游戏随机闪退,而且不是必现,排查起来非常头疼。
鼠标消息的读取要用非阻塞方式。EasyX 里接收鼠标事件的常见写法是 ExMessage 配合 peekmessage:
ExMessage msg; while (peekmessage(&msg, EM_MOUSE)) { if (msg.message == WM_LBUTTONDOWN) { handle_click(msg.x, msg.y); } }peekmessage 不会阻塞主循环,保证游戏在等待点击的同时僵尸照常移动。如果误用阻塞的 getmessage / wait,会出现"点一下游戏才动一下"的卡死感,这是初学 EasyX 时一个非常典型的翻车点。
3.2 子弹与僵尸碰撞:基于 Vector2 的移动与距离判定
豌豆子弹生成后挂在子弹数组里,每帧做一次位移。配合前面说的 vector2 模块,更新逻辑非常干净:
for (int i = 0; i < bullet_count; i++) { bullets[i].pos = vec2_add(bullets[i].pos, vec2_scale(bullets[i].dir, bullets[i].speed * dt)); if (bullets[i].pos.x > SCREEN_W) { remove_bullet(i); // 出界回收,注意删除后要处理下标 } }dt 是上一帧到当前帧的时间差,按秒算。乘 dt 的目的是让子弹速度不依赖帧率:如果直接写成每帧固定移动 N 像素,帧率高时子弹明显更快,在不同机器上表现不一致。对于这个量级的 2D 游戏,用 Sleep(16) 锁帧配合固定步长也够用,但养成乘 dt 的习惯,二次开发加功能时会舒服得多。
碰撞检测用的是圆心距离判定:
#define HIT_RADIUS 40.0f for (int i = 0; i < bullet_count; i++) { for (int j = 0; j < zombie_count; j++) { if (vec2_distance(bullets[i].pos, zombies[j].pos) < HIT_RADIUS) { zombies[j].hp -= 20; remove_bullet(i); break; } } }距离判定比矩形相交写起来省事,缺点是精度靠半径调。HIT_RADIUS 取 40 是典型的"玄学调参"——它不是一个计算值,而是把两个精灵图叠在一起目测出来的经验值。调太大手感宽容、击杀偏容易,调太小子弹容易穿模,实际调试时可以先用 50 再逐步收紧。
僵尸侧的推进逻辑也很简单:每帧检查所在行前方有没有植物,有就停下啃咬,没有就向左走。这段逻辑放在 update_zombies 里,和子弹更新互不干扰,但要注意啃食是持续行为,不能只触发一次。
3.3 主循环的节奏:帧率、输入响应与绘制顺序
EasyX 的主循环骨架长这样:
initgraph(800, 600); BeginBatchDraw(); while (running) { // 1. 输入 handle_messages(); // 2. 逻辑更新 update_plants(); update_bullets(); update_zombies(); check_win_lose(); // 3. 绘制 cleardevice(); draw_background(); draw_plants(); draw_bullets(); draw_zombies(); draw_ui(); FlushBatchDraw(); Sleep(16); } EndBatchDraw(); closegraph();两个关键点。第一,BeginBatchDraw/EndBatchDraw 是 EasyX 的双缓冲开关,没有它画面会闪到怀疑人生,这是所有 EasyX 游戏必开的东西。第二,绘制顺序决定遮挡关系:背景最先画,植物在僵尸之前,僵尸在最上层,这样能自然做出"僵尸走到植物面前啃咬"的层次感。如果顺序反了,植物会盖住僵尸,视觉上变成僵尸被植物挡住,游戏体验直接崩。
太阳、分数这类 UI 最后画,并且建议用输出文字而不是贴图,调试时改数值不用重新做图。主循环里 Sleep(16) 是把帧率压到 60 附近,既保证流畅又不让 CPU 空转,这是固定步长方案最省事的调法。
4. 避坑清单:EasyX 游戏联调最容易翻车的五个点
这一章算血泪经验总结。我从实际联调里挑了五个出现频率最高的坑,每条按现象、原因、解决三步展开,照着排查能省下大量对着代码发呆的时间。
4.1 编译报错:找不到 graphics.h
现象:用 VS 打开 sln 后按 F5,输出窗口直接报 fatal error C1083: Cannot open include file: 'graphics.h'。
原因:本机没装 EasyX,或者装的版本和当前 VS 不匹配。EasyX 的安装是往 VS 公共目录里释放头文件和库文件,VS 升级后旧安装目录失效,重新装一次即可。这里有个容易误判的点:在工程属性里手动配 include 路径不一定有用,因为 EasyX 是全局安装的,配到工程级别反而可能被覆盖。
解决:关闭 VS,到 EasyX 官网下载对应当前 VS 版本的安装包,按默认路径安装,再重新打开工程编译。验证环境是否通的办法是新建一个空项目,只写一行 #include <graphics.h>,能编译过就说明环境到位。千万别在原工程里反复改配置,环境问题要先在最小项目里排除。
4.2 一打开工程就提示重定目标 / SDK 版本无效
现象:双击 sln 后 VS 弹出"需要升级"或"Windows SDK 版本无效"的对话框,确认后编译报一堆宏未定义的错误。
原因:.vcxproj 工程文件里记录了作者机器上的 PlatformToolset 和 WindowsTargetPlatformVersion。这两项是 VS 和 SDK 的版本号,换机器不一致非常正常,尤其是不同年份装的 VS。
解决:右键工程 → 重定目标项目,选择本机已安装的 SDK 版本,确定后重新编译。重定目标只改工程配置,源码一行不动,不要被弹窗吓到。这个操作属于打开别人工程的基本功,和第二章提到的 Browse.VC.db 报损坏是同一类问题——版本差异导致的本地缓存失效。
4.3 中文全部乱码
现象:游戏界面里绘制的中文标题、植物说明全是问号或乱码,英文和数字正常。
原因:源码文件保存的是 GBK 编码,而 VS 在较高版本里默认按 UTF-8 解析源文件;或者工程字符集设成了 Unicode,代码里用的却是窄字符 API。两条原因可能同时存在,需要逐一排除。
解决:先看工程属性 → 常规 → 字符集,确认用的多字节字符集。再处理源文件编码:文件 → 另存为,编码选"简体中文(GB2312)"或带 BOM 的 UTF-8,两个都试一次,乱码消失即为正确编码。也可以在源码首行加编译指令强制指定字符集,但我一般优先改工程配置,改动面最小。
4.4 贴图黑底、移动出残影
现象:植物、僵尸图片周围一圈黑框,拖动时还会拖出残影。
原因:两个问题叠在一起。黑框是因为 putimage 直接做矩形拷贝,图片透明区域没有处理;残影是因为绘制前没有清屏,上一帧的画面残留。
解决:每帧绘制前先 cleardevice(),再按掩码两遍绘制处理透明:
cleardevice(); putimage(x, y, &mask_img, SRCAND); // 保留掩码图形状 putimage(x, y, &src_img, SRCPAINT); // 叠加彩色图像SRCAND 的作用是把目标区域中掩码图白色形状的部分保留下来,黑色部分清掉,SRCPAINT 再把彩色图叠加进去。前提是掩码图本身是黑底白形、且和彩色图的形状像素级对齐。换素材后掩码图必须重新生成,这是被反复踩过的坑——换图不换掩码,黑边马上回来,属于典型的"后悔药"包。
4.5 运行几秒后闪退,无任何报错
现象:窗口正常出现,操作几下之后突然闪退,VS 输出窗口里没有任何异常信息,断点都来不及打。
原因:最常见的是数组越界访问。比如种植判定没检查棋盘范围、col 或 row 为负数照样去操作 plant_grid;或者子弹删除之后没有跳出循环,继续访问下标前移后的数组。这类问题不触发还好,一旦触发就是随机闪退,属于最磨人的一类。
解决:第一步给所有数组下标加范围判断;第二步在关键函数入口临时加 printf 日志,把坐标、数量、当前帧数打出来,跑一遍看日志死在哪一项。比如:
// 删除子弹后必须跳出循环,否则下标失效 for (int i = 0; i < bullet_count; i++) { if (hit_any_zombie(bullets[i])) { remove_bullet(i); break; // 没有 break 就会越界 } }日志定位是最笨但最有效的办法,比对着代码猜强得多。等定位到问题函数,再把 printf 删掉就行。
5. 二次开发路线:把示例工程改成你自己的课设
拿到源码先别急着改玩法,第一步应该是把植物属性从硬编码分支里解放出来。原工程大概率在 main.cpp 里用一堆 if else 区分植物类型,加一个新植物要在五六个地方各加一段代码,很容易改漏。做到数据与逻辑分离,后面所有改动都会轻松很多。
5.1 数据驱动改造:植物属性抽成结构体数组
常见的做法是定义一份植物配置结构体,所有的数值属性全部收进数组:
typedef struct { const char* name; int sun_cost; int hp; int attack_interval; // 单位:帧 int damage; int bullet_speed; // 像素/秒 } PlantDef; PlantDef plant_defs[] = { {"豌豆", 100, 300, 30, 20, 400}, {"坚果", 50, 800, 0, 0, 0}, {"寒冰", 175, 300, 30, 15, 350}, };改造完成后,新增一个植物就是在数组里加一行,同时补一张贴图和一张掩码图。画面上的卡牌选择、阳光消耗、射击节奏全部从这张表读取,逻辑代码一行不用改。这是 C 语言项目里"数据与逻辑分离"的典型做法,课程设计答辩时几乎必被问到,值得花半小时认真重构一遍。
5.2 存档与关卡进度:文本文件读写
游戏加存档功能,用 stdio 的文本读写足够:
void save_game(int level, int sun, int score) { FILE* fp = fopen("save.dat", "w"); if (!fp) return; fprintf(fp, "%d %d %d\n", level, sun, score); fclose(fp); } bool load_game(int* level, int* sun, int* score) { FILE* fp = fopen("save.dat", "r"); if (!fp) return false; int ret = fscanf(fp, "%d %d %d", level, sun, score); fclose(fp); return ret == 3; }两个细节值得注意。第一,写入前先备份旧存档:把 save.dat 改名为 save.bak,再写新文件,写成功后再删备份,避免写一半程序退出导致存档损坏。第二,读取时不要默认格式一定正确,fscanf 返回值要判断,文件被改成非数字内容时直接返回失败,而不是带着垃圾数据继续跑。工程里文件路径默认相对 exe 所在目录,调试时要留意工作目录设置,VS 默认工作目录是工程目录,不要和 exe 输出目录搞混。
5.3 加新僵尸与难度曲线:波次脚本表
默认玩法是僵尸按固定时间间隔从右边冒出来,做课设加分项的话,可以改成按波次脚本生成。脚本表就是一个只读数组:
typedef struct { float spawn_at; // 游戏开始后第几秒出现 int zombie_type; int hp; float speed; } SpawnScript; SpawnScript level1[] = { { 10.0f, 0, 100, 30.0f }, { 20.0f, 0, 100, 30.0f }, { 35.0f, 1, 250, 20.0f }, // 1 号是铁桶僵尸,血厚走得慢 { 45.0f, 2, 60, 60.0f }, // 2 号是跑步僵尸,脆但快 };主循环里每帧检查当前游戏时间是否到达脚本里的 spawn_at,到了就往僵尸数组里塞一个。难度曲线不需要复杂算法,把生成密度、血量、移速三个参数往上调就行。这个脚本表模式做三关基本不费力,而且答辩时能讲清楚"我用了配置驱动的波次生成",比硬编码循环有说服力得多。
5.4 卡牌选择与种植状态机
种植界面通常要做成"点一下卡牌 → 进入种植模式 → 点棋盘落子"的交互闭环。实现时用一个全局状态机变量即可:
enum GameMode { MODE_NORMAL, MODE_PLANTING }; GameMode mode = MODE_NORMAL; int selected_plant = -1;点击卡牌时设置 mode = MODE_PLANTING 和 selected_plant;点击棋盘时先检查模式,种植成功后再切回 MODE_NORMAL。如果漏了这层状态机,玩家会出现"没点卡牌也能种植物"的穿模级 bug,这也是代码评审时老师最爱问的交互逻辑。
6. 验证三步法:从编译通过到功能稳定
编译通过只代表没有语法错误,玩法逻辑有没有被改坏,得靠验证说话。我给自己定的流程是三步冒烟,每改一次功能都强制走一遍,比对着屏幕瞎点高效得多。
第一步,功能冒烟。开一局,逐项核验下表,任何一项不符合预期都要停下来排查:
| 检查项 | 预期结果 |
|---|---|
| 点击卡牌再点棋盘 | 植物种在最近空位,阳光正确扣除 |
| 植物持续射击 | 豌豆按攻击间隔出膛 |
| 子弹命中僵尸 | 僵尸掉血,血量归零后消失 |
| 僵尸碰到植物 | 停止前进并啃咬,植物掉血 |
| 僵尸走到房子 | 触发失败结算 |
| 手动存档并重开 | 读档后等级、阳光、分数一致 |
第二步,边界操作回归。连续快速点击棋盘外区域、卡牌之间反复切换、窗口最小化再恢复,重点看有没有负下标、越界和绘制残留。这类操作正常玩家不会做,但老师演示时很可能做出来。
第三步,挂机稳定性。游戏挂着 5 分钟不操作,观察两件事:内存占用是否持续上涨、僵尸是否越积越多导致帧率下滑。多数 EasyX 项目的性能问题出在数组只增不减,挂机能提前暴露这类隐患。
从那以后,我每次改完玩法都强制自己跑完这三步才敢说"改完了"。顺序也不能跳——先功能、再边界、最后压测,跳过的每一步都会在答辩或演示现场精准翻车。希望帮到你。
本文还有配套的精品资源,点击获取