简介:基于C语言实现的植物大战僵尸小游戏完整项目,包含可直接编译运行的源码与配套设计报告,适合C语言课程设计、期末项目或游戏开发入门参考;项目按功能模块清晰划分,覆盖游戏主循环、地图渲染、植物种植与攻击、僵尸生成与移动、碰撞检测等核心逻辑,代码组织紧凑,能够帮助学习者理解C语言在小型游戏项目中的实际组织方式。压缩包共30个文件,大小约174KB,以7个cpp源文件和7个h头文件为主体,另有10个png游戏素材、设计报告word、地图设计说明文档以及makefile/CMakeLists编译配置,既方便研读源码,也支持跨平台构建运行。目前已有2123人在CSDN学习下载,是同类课程设计中较受关注的一份。透过这份项目,从数据结构设计到文件读取、从键盘交互到简单图形绘制,均能获得直观实践体验;配套设计报告还可按章节对照代码复盘,特别适合期末答辩准备、系统练习C语言综合编程的同学;整体工程虽小,胜在结构完整,适合快速复盘C语言项目开发全流程。
1. 一个 C 语言写的植物大战僵尸小游戏 zip,拿到手别急着双击
很多做课程设计或期末大作业的同学,最后都会收到一个「基于C语言设计的植物大战僵尸小游戏.zip」这样的压缩包。可能是学长传的,也可能是从某资源共享站下的,里面通常躺着一堆 .c、.h 源码和一个编译好的 exe。第一反应是双击 exe 看一眼效果,结果控制台闪一下就没了;第二反应是拖进现成的开发环境,又报错说找不到 graphics.h,或者整个项目根本编不过。这个 zip 的真正价值从来不在 exe,而在那几份 C 源码里:植物怎么种、子弹怎么飞、僵尸怎么死,全部是结构体、数组和主循环就能表达的逻辑,没有引擎也没有复杂框架。这篇笔记按我拿到一个陌生 C 游戏项目包的常规操作顺序来写,从解压看文件、确认图形库依赖、搭编译环境、跑通游戏主循环,到排掉高频编译坑,帮你把一个「不知道能不能跑」的 zip 变成「知道自己能改哪里」的源码项目。适合正在做 C 语言课程设计,或者想用纯 C 练手的小游戏爱好者。
2. 拆包先看依赖:源码清单决定了整套编译策略
很多人拿到压缩包,第一件事是全部解压,然后在开发环境里新建一个工程,把所有 .c 文件拖进去。这个做法一半会翻车,因为 C 项目没有自动依赖解析,图形库、输入库、编译器的匹配关系全藏在 #include 和文件结构里,不看清单直接动手编,后面每一步都可能卡在同一个「找不到头文件」上。我习惯的做法是「先看再动」:用命令行看压缩包内部列表,用 grep 扫 include,最后才决定这个项目到底适合用 Visual Studio 还是 gcc。
这一步做对之后,后面几乎不会出现大的方向性错误。比如看到easyx.h,基本说明它依赖 Windows 图形库;看到conio.h和windows.h,说明至少是 Windows 控制台程序;看到SDL.h,则是另一个跨平台多媒体方案。提前把这个判断做完,后面所有编译命令都不会白写。
2.1 用 unzip 和 find 做静态检查:别让 exe 骗了你
先从压缩包内部结构开始。在 Linux 或 Windows 的 Git Bash 里,unzip 命令可以直接列出 zip 里的文件清单,不需要先解压。这一步会让你快速看到哪些是源码、哪些是垃圾文件、哪些是作者随手丢进去的旧版本:
unzip -l "基于C语言设计的植物大战僵尸小游戏.zip" | awk '{print $1, $4}' | tail -n +2 | head -50命令里awk '{print $1, $4}'负责把 unzip 输出重排成「大小 文件名」两列,tail -n +2跳过表头,head -50只取前 50 行,避免解压后杂物太多时屏幕直接刷爆。
如果列表里同时出现.exe、.obj和.c,说明作者把编译中间产物也打包了,目录会比较乱,但也是好消息:中间产物能帮助我们判断它之前用的是 MinGW 还是 Visual Studio,后面选环境就有依据了。
接下来解压到一个独立目录,并再次确认文件树。解压这一步我建议永远用带-d参数的方式,放到专门目录里,避免和下载目录混在一起:
mkdir -p pvz_c && unzip -q "基于C语言设计的植物大战僵尸小游戏.zip" -d pvz_c cd pvz_c find . -maxdepth 2 -type f | sort | head -40-q让 unzip 不打印解压过程,只保留错误提示;-maxdepth 2只看两层目录,防止 lib 目录里塞了几十个静态库时你还要逐一过目。看到文件树后,重点关注有没有Makefile、*.sln、*.vcxproj。如果这些工程文件存在,编译策略基本就有现成答案了。
2.2 从 include 头文件反推图形库:easyx、SDL 还是纯控制台
文件清单只是第一层,真正决定性的是头文件依赖。这里我习惯用一条 grep 把项目里所有源文件引用的头文件汇总去重,一眼看清依赖面:
grep -rh "#include" --include="*.c" --include="*.h" . | sort -u注意-h是不显示文件名,-r是递归扫描子目录,sort -u做去重。这样的输出非常干净,基本会看到以下几类:stdio.h、stdlib.h、windows.h、conio.h,这是控制台程序;easyx.h或graphics.h,这是图形库;SDL.h,这是跨平台多媒体库。
如果看到graphics.h但项目目录里没有这个文件,则很可能是传统 BGI 图形库的遗留依赖,这类头文件在主流编译器下几乎没法直接编,需要额外处理。如果是easyx.h,情况相对好一些,因为 EasyX 的使用绑定 Windows 加 Visual Studio,配置起来有明确的安装步骤。判断之前别急着下结论,先确认项目有没有自带头文件:
find . -name "graphics.h" -o -name "easyx.h" -o -name "SDL.h"项目自带同名头文件时,它是作者自己写的模块接口,编译时需要通过-I指定目录;项目里没有这些文件但源码引用了它们,就是明确的外来依赖,必须装对应运行库。这一步判断错,后面代码里所有setfillstyle、outtextxy都会被当成普通函数报错,非常误导人。
2.3 确认入口和模块边界:main 到底散在哪里
确认完图形库依赖,还要定位 main 函数的入口位置。C 游戏项目经常把 main 放在一个不起眼的位置,比如start.c、game.c、src/main.c,如果你在工程里打开的是main.c,未必就会看到int main,选错文件后直接会编译报错,报错信息还可能是不知所云的undefined reference to WinMain。用 grep 定位入口很快:
grep -rn "int main" --include="*.c" .如果只找到一处int main,那问题就简单了,把它对应的源文件记下来。如果找到多处,说明作者用了条件编译或者定义了调试入口,这时候再看#ifndef、#ifdef的宏开关。多入口设计在课程设计里很常见,有人在正式游戏模式下加一个测试模式,专门用来刷金币或者测试碰撞,你要是把多个 main 都拖进去,必定重定义。继续用 grep 带上下文看:
grep -rn -B2 -A2 "int main" --include="*.c" .看到#ifdef DEBUG那一层结构后,你会发现需要在编译命令里加-D DEBUG或者去掉宏定义,才能决定到底编哪一份 main。入口定了,源文件列表也定了,下一步的编译命令才有资格写死。
3. 编译落地:图形库版走 Visual Studio,纯控制台版用 gcc
头文件分析完成以后,编译路线基本可以一锤定音。这里面有一条常见规律:使用 EasyX 的项目,最稳妥的编译环境是 Visual Studio;纯控制台或者只依赖标准库、Windows API 的项目,Visual Studio、MinGW 甚至 Linux 下的 gcc 都能编。如果你在课程设计里拿到一个 EasyX 项目却坚持用某轻量编辑器加 gcc,那大概率会卡在链接阶段,因为 EasyX 不是单纯的头文件,它还需要对应的库文件与 VC 环境匹配。
这一章就是奔着「把源码变成可执行文件」去的。我不建议在还不知道项目是图形版还是控制台版时就去下载一堆环境,先把下面两条路线看一遍,对照你的早期分析结果,再选一条执行即可。
3.1 为什么图形库版优先选 Visual Studio:EasyX 对编译器有硬性匹配关系
EasyX 是为 Windows 上 VC 编译器设计的简单绘图库,它提供的initgraph、beginpage、putimage这些 API 需要在链接阶段找到正确的实现。Visual Studio 自带的 MSVC 编译器与 EasyX 的二进制兼容性最好,安装 EasyX 时它也会自动把头文件加到 VC 的默认搜索路径。而如果你用 MinGW 或某些编辑器内置的 gcc,就算头文件找到了,链接也容易因为 ABI 不一致而报错。
反过来,如果项目只是控制台程序,用printf打印格子,用getch捕捉方向键或者数字键,那它根本没有图形库依赖,用 gcc 编起来反而更省事。控制台版植物的绘制常是字符拼接,僵尸移动是清屏重打,这类程序逻辑很直白,并没有多少复杂图形操作。我的建议是:看到easyx.h就不要再折腾命令行交叉编译器了,直接打开 Visual Studio 装好 EasyX,五分钟能见到窗口跑起来,省下来的时间都值得投入在游戏逻辑调试上。
3.2 用 Visual Studio 新建空工程挂接源码:从文件混杂到第一个窗口
拿到一包 .c、.h 但看不到 .sln 文件时,我一般会新建一个空项目,再把所有源码加进去。具体步骤是:打开 Visual Studio,创建空 C++ 控制台应用(C 语言程序在 VS 里也用 C++ 工程承载,修改一处配置即可),然后在解决方案资源管理器里右键源文件文件夹,选择添加现有项,把之前解压出的所有.c、.h全部选中,注意把.obj、.exe这些旧产物排除掉,它们不该进入源码列表。
如果项目引用 EasyX,需要在 VS 的菜单「视图 - 其他窗口 - 属性管理器」里找到项目配置,确认 C/C++ 常规项目里的附加包含目录是否指向 EasyX 的 include 目录。最省时间的检测方法是直接编译,只要不报Cannot open include file: 'easyx.h',就说明库已经装到位。加好所有源文件后,点一下「生成解决方案」,程序大概率直接出结果。命令行模式也能编,不过要在开发人员命令提示符里跑 cl:
cl /std:c11 /EHsc game.c main.c plant.c zombie.c /Fe:pvz.exe/std:c11指定 C11 标准,/EHsc是 C++ 异常处理开关,纯 C 项目其实用不到,但保留它不会出错;/Fe:pvz.exe指定输出文件名。之所以不建议初学者用这条命令,是因为一旦头文件路径和环境变量不对,报错会比 IDE 模糊得多。用 vs 工程的好处是库路径配置有图形界面,失败了也有清晰的错误列表。
3.3 用 gcc 编译纯控制台版:最小命令与哪些参数各有各的作用
如果项目里没有easyx.h,只依赖标准库和windows.h、conio.h,那用 gcc 也能编。这类游戏通常会有多个.c文件,比如main.c、game.c、plant.c,编译时必须一次性把源文件全部列出来,否则链接阶段会出现未定义引用。我最常用的命令是:
gcc -std=c11 -O2 -Wall -Wextra -o pvz.exe main.c game.c plant.c zombie.c -lm-std=c11强制编译器按 C11 标准,避免某些老代码把for循环变量声明当成语法错误;-O2优化以后帧循环里频繁做碰撞判断会快一些;-Wall -Wextra会把隐藏的类型转换、未使用变量警告全打出来,在课程设计阶段这些警告往往是 bug 的信号。-lm链接数学库,如果代码里用了sqrt、fabs这类函数,少了它就会在链接阶段报未定义引用。
这里有个血泪经验:-o pvz.exe后面必须跟输出文件名,不能顺手写成-o game.c,否则目标文件直接覆盖源码,后悔药都找不到。如果你在 Windows 下用的是 MinGW,命令大概率是mingw32-gcc或者x86_64-w64-mingw32-gcc,本质一样。控制台版游戏对图形库要求低,这一个命令通常就能跑起来,特别适合快速验证 zip 里源码的完整性。
3.4 写一个 Makefile 把重复动作收住:同目录再生成 exe 也只需要一条指令
命令行编译虽然简单,但每次改动源码后重新敲一遍长命令还是会烦,尤其是当文件数增加到六七个时。我习惯在项目根目录写一个最小 Makefile,把编译、清理、宏开关都收住:
CC = gcc CFLAGS = -std=c11 -O2 -Wall -Wextra SRCS = main.c game.c plant.c zombie.c OBJS = $(SRCS:.c=.o) TARGET = pvz.exe $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $(OBJS) -lm %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: del $(OBJS) $(TARGET)这个 Makefile 的优点很直接:make一条指令完成编译,make clean清理中间产物。$(SRCS:.c=.o)做的是把源文件名后缀替换成.o,这比手工一个个列出.o文件更不容易漏。-c参数表示只编译不链接,所以预编译生成的是多个目标文件,最后再由第一条规则链接成可执行文件。Windows 下如果用的是mingw32-make,命令要改成mingw32-make,但 Makefile 内容一样。
如果你拿到手的 zip 里已经带了 Makefile,那恭喜,mingw32-make直接编译即可。不过课程设计里常见的情况是 Makefile 写得比较随意,变量名冲突或者换行用了 Tab 键没有对齐,这也算另一个经典踩坑点:Makefile 规则行必须以制表符开头,不能用空格代替,否则报错信息全是疑似缺失分隔符。
4. 把核心机制写进 C 代码:从结构体、数组到碰撞检测
前两章解决了「怎么把这个 zip 编起来」的问题,这一章开始解决「这个游戏是怎么用 C 语言想出来、写出来」的问题。大家在压缩包里看到的代码也许各不相同,但核心框架高度相似:一张二维地图、一组植物对象、一组僵尸对象、一组飞行中的子弹,加上一个大循环去处理输入、更新逻辑和绘制画面。把这套思路理解透了,哪怕你拿到手的源码有自己的命名习惯,也能很快看懂作者在干什么。
4.1 用结构体和二维数组构建格子战场:不让每一种角色单独立牌
植物大战僵尸的基础地图是一张长方形草地,种植行为永远落在格点上,因此用二维数组建模是最自然的方式。常见的 C 语言实现会定义一个Plant结构体和一个全局数组plants[MAX_ROWS][MAX_COLS],而不是允许植物出现在任意连续坐标。这么做能让碰撞检测和渲染都基于行列,逻辑上非常规整。先看一个典型设计:
typedef enum { PLANT_NONE, PLANT_PEASHOOTER, PLANT_SUNFLOWER, PLANT_WALLNUT } PlantType; typedef struct { int row, col; PlantType type; int hp; int cooldown; int shoot_timer; } Plant; typedef struct { int row; double x; // 僵尸在行内的连续坐标 int hp; int speed; // 每帧移动多少 int active; // 0 表示已死亡或已清离 } Zombie; Plant board[MAX_ROWS][MAX_COLS]; Zombie zlist[MAX_ZOMBIES];board是二维数组,row和col是格点坐标;zlist是一维数组,因为僵尸可以多只同时在一条路上走,且它们的位置必须是连续变化的,不能只存格子索引。这个数据结构选型理由很明确:二维数组代码简单、内存连续、访问快,适合课程设计;僵尸用独立数组则是因为每一帧都要对移动中的僵尸做位置更新,二维网格只保存静态植物会更加清晰。
这里也藏着第一个性能要点:不要把僵尸也塞进二维数组。如果瓷片阵里存了僵尸结构,那每帧都要扫描 8×9 个格子来渲染,逻辑会乱成麻。我一般把僵尸统一放到zlist[MAX_ZOMBIES]里,用active字段标记是否存活,遍历时只扫一遍数组。MAX_ZOMBIES通常取 50 或 100,C 语言课程设计阶段用固定数组比动态链表更稳,不会出现malloc和free频繁分配导致的内存碎片。
4.2 主循环三件事:输入、逻辑、绘制与 Sleep 控帧
小游戏的核心永远是主循环。植物大战僵尸里的交互是回合式向即时制的混合:你在种植阶段按键盘选植物,在僵尸不断推进的过程中不断补充火力。实现这个游戏的主循环,一般会用一个while循环包住三个阶段:
while (game.running) { handle_input(&game); // 读取键盘,决定这次种植或铲除 update_plants(&game); // 更新豆射手填充时间与子弹生成 update_zombies(&game); // 僵尸移动、攻击、死亡 render(&game); // 画格子、画植物、画子弹与僵尸 Sleep(50); // 暂停 50ms,把帧率压到约 20FPS }这三个函数各司其职。handle_input里用_kbhit()或getch()获取按键,在键盘按下时修改全局状态;update_plants里判断种植冷却时间,到了就生成一发子弹并塞入子弹数组;update_zombies遍历僵尸数组,把每一只僵尸的x向前推进,同时检查子弹是否命中。渲染阶段最后执行,因为必须先修改完逻辑,再一次性把画面画出来,否则会出现半透明帧或闪烁。
这里要特别强调Sleep(50)的作用。C 语言没有内置delay函数,Windows 下用Sleep(50)把每次循环暂停 50 毫秒,相当于每秒钟跑大约 20 帧。这不是玄学数字,而是平衡操作响应和画面流畅度的常见选择:帧率太高会让 CPU 白白空转,太低则按键反应迟钝。如果代码里用了Sleep(1),容易造成不同电脑上速度差很多,这个问题后面还会遇到。
4.3 用距离碰撞代替 AABB:一行判断子弹是否命中
碰撞检测是植物大战僵尸里最容易糊涂的地方。很多人一提到碰撞就写 AABB 矩形相交,但在这个游戏里,子弹是圆点,僵尸是矩形接近体,真实需求只是判断「这发豌豆是否到达了这行僵尸的胸口」。视角简化后,根本不需要 Stasher 级别算法。同一个row上,子弹与僵尸之间的水平坐标差足够小,就判定击中。
bool pea_hit_zombie(const Pea *p, const Zombie *z) { if (p->row != z->row) return false; return fabs(p->x - z->x) <= 0.3f; }这里的最核心判断是同一行,因为豌豆和僵尸永远在同一行内活动,跨界命中不合法。第二行判断用水平距离小于 0.3 个格子宽度,相当于大约 30 像素,正好对应子弹直径加僵尸半个身位。实际项目里,这个阈值要根据素材大小调节,素材小调 0.2 就行,素材大方块化就调 0.4。记住,碰撞检测不需要完全精确,能对玩家反馈做到位置合理即可。
用这个办法替代 AABB 还有一个好处:计算量小,遍历所有子弹和僵尸时,每帧最多做MAX_PEAS * MAX_ZOMBIES次比较,在 20 帧率下压力完全可以忽略。若把子弹和僵尸都做成完整矩形碰撞盒,代码量会变大,而玩家感受差异很小。
4.4 消灭僵尸后的数据清理:用 active 标记而不是物理删除
每帧至少有几十发子弹飞过,僵尸一旦被子弹消灭,如果直接把它从数组里删除,后面的遍历很容易出现索引错位,或者把未初始化的内存当僵尸继续渲染。常见做法是用active标记实现伪删除:
void kill_zombie(Game *g, int index) { g->zlist[index].hp = 0; g->zlist[index].active = 0; } void remove_inactive_zombies(Game *g) { for (int i = 0; i < MAX_ZOMBIES; i++) { if (!g->zlist[i].active) { g->zlist[i] = (Zombie){0}; // 整块结构体清零 } } }kill_zombie只是把 hp 和 active 置零,让它在逻辑上消失,不直接改变数组长度。remove_inactive_zombies在每一帧逻辑更新后调用,把非活动对象统一清空。这样做有两个直接好处:一是避免在遍历中途修改数组结构,导致漏判或重复判断;二是方便调试时看僵尸数量,空闲单元格一直是 0 值,不会残留上一次的坐标。很多新手在这里直接做内存拷贝前移,造成一个僵尸死亡后,后面所有僵尸同时跳位,画面看起来像是「瞬移」,其实就是索引没处理好。
5. 常见问题与排查:编译、乱码、闪退和帧率不稳的高频坑
这一章写的是我拿到不少 C 游戏 zip 后,反复遇到的项目级高发问题。这些问题不是某一份源码独有的,而是 C 语言写控制台游戏和图形库游戏时代的通病。每一条我都按现象、原因、解决三步走写,照着排查能省很多时间。
5.1 现象:编译器报错找不到 graphics.h 或 easyx.h
编译时先是预处理阶段就中断,错误信息类似fatal error: graphics.h: No such file or directory。原因是项目引用了图形库头文件,但编译器搜索路径里没有包含图形库目录。
原因分成两种情况。第一种是 EasyX 版本,Visual Studio 没安装 EasyX,或者版本不匹配;第二种是老 BGI 头文件,这种历史遗留库几乎无法在当前主流编译器下直接编译,只适合拿来做代码分析,不适合跑通。
解决方法是先确认项目里的引用形式。如果你看到的是#include <graphics.h>,而文件列表里没有同名本地文件,直接换用 VS 并在官网安装对应 EasyX。如果坚持用 gcc,则需要把图形库目录用-I参数加进去。一句话结论:图形库项目优先用 Visual Studio,少折腾命令行交叉编译。
5.2 现象:中文全部变成乱码,按钮文字成了口口口
运行游戏时,界面上的「植物」「僵尸」等中文字符变成一排方块,或者控制台输出完全是乱码。原因是源码文件编码与编译环境期望的编码不一致。老项目常用 GBK/GB2312,而你的开发环境默认用 UTF-8;Windows 控制台代码页也影响显示。
解决分两层。第一层在 Visual Studio 里给编译器加/utf-8参数,告诉编译器源码是 UTF-8 编码;第二层在控制台程序里用system("chcp 65001")切换代码页。如果项目是把文本输出到 EasyX 窗口里,则要检查字体创建代码是否传入了中文字符,EasyX 默认字体可能是英文字体,显示中文会失败。老代码里常见的settextstyle需要指定_T("宋体")之类的字体名,同时注意宽字节还是窄字节。
5.3 现象:双击 exe 闪退,控制台窗口一闪就没了
这是最简单也最误导人的问题。你把编译好的 exe 文件单独挪到桌面双击,控制台窗口闪一下就消失,你甚至没看清报错。原因是程序无阻塞地运行完毕,或者初始化失败直接退出。控制台游戏在没有图形环境时,入口函数执行完窗口就关闭了。
解决方式分两种。如果只是想让 exe 停下来,在 main 末尾加一个等待按键:
printf("按任意键退出...\n"); getchar();如果是程序初始化失败,加getchar()不能让错误暴露。推荐在 main 开头加SetConsoleTitle,并在可能失败的位置判断返回值,或者直接用 IDE 的调试模式启动,看断点和输出窗口。闪退问题最大的坑是很多人下意识认为程序有问题,其实多半只是没有做暂停。弹窗版游戏通常没问题,控制台版一定要预留getchar或Sleep的收尾。
5.4 现象:僵尸移动一顿一顿,不同电脑上速度差很多
换一台电脑后,僵尸移动变快或变慢,画面一跳一跳,甚至整个游戏节奏失控。原因是主循环没有以帧为目标进行时间控制,而是用Sleep(1)或干脆不暂停。不同 CPU 的Sleep调度精度不一致,帧率因此不可控。
解决方法是把Sleep参数写到只按帧周期控制的位置,按恒定帧率推进逻辑。更稳的做法是把逻辑更新和渲染分离,用时间戳计算真实帧间隔,然后在update_zombies里按比例移动僵尸。课程设计阶段可以简化,但至少Sleep(50)要放在一帧渲染完成后。把那台电脑插电、不插电、屏幕刷新率不同导致的误差控制到可接受范围内,这就够了。
6. 进阶技巧:用一把调试开关,把游戏运行过程变成可视化的验证过程
看到一个 C 游戏项目的 final 版本,我想大家最缺的不是新功能,而是确认「这个代码凭什么按我预期工作」的手段。课程设计源码里大家都喜欢直接写业务逻辑,很少留验证的钩子,导致改动一个数值之后不知道是变好了还是变坏了。我一般会在拿到任何 C 游戏项目后,第一时间加一个调试开关,在逻辑更新时打印关键状态。
具体做法是在编译命令里加一个宏,并用条件编译控制调试信息。在项目里找一个全局位置定义:
#ifdef DEBUG_GAME #define DBG_PRINT(fmt, ...) printf("[debug] " fmt "\n", ##__VA_ARGS__) #else #define DBG_PRINT(fmt, ...) ((void)0) #endif这个小工具的作用非常直接:所有需要观察状态的位置,比如僵尸被子弹命中、植物冷却完成、子弹生成,都可以用DBG_PRINT打出当时的坐标和血量。正常编译时它不产生任何输出,对游戏运行毫无影响;调试时用-D DEBUG_GAME编一版,所有关键路径就像开了透视一样摆在眼前。
我会在render函数前面加一行逻辑自检,把每帧僵尸数量、存活子弹数和全局时间戳打印出来:
DBG_PRINT("frame: %d | zombies: %d | peas: %d", g->tick, count_alive_zombies(g), count_active_peas(g)); if (count_alive_zombies(g) == 0) { DBG_PRINT("level clear, restore zombies after 5s"); }这样当你调大僵尸速度或者植物攻击力时,不用靠肉眼盯着闪过的画面判断,而是直接看数值是否符合预期。加了这个钩子以后,很多看似玄学的「有时候有 bug 有时候没有」,都会变成具体哪一帧哪一行出错的精确证据。
我也习惯在update_zombies里打印每一只移动僵尸的row、x和hp,第一次跑通时看到x稳定递增、hp同步下降,比读 500 行代码更有说服力。这个调试开关既是给未来的合作者留的接口,也是给自己的安全带。等调试完成只需要去掉-D DEBUG_GAME重新编一版,就能得到干净的可交付程序。久而久之,我会把这种调试工具当成每个 C 游戏项目里默认的一部分,希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取