ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言+EasyX打造简易植物大战僵尸:课程设计全攻略

C语言+EasyX打造简易植物大战僵尸:课程设计全攻略 简介一份用C语言与EasyX图形库实现的简易版《植物大战僵尸》课程设计资源定位为C语言程序设计期末作业适合计算机相关专业学生借鉴参考可用于课程设计、期末考核或入门游戏编程练习。压缩包共含98个文件总大小约16.73MB。代码部分包含3个C源文件以及Visual Studio解决方案文件.sln、.vcxproj、.filters游戏素材则包括76个GIF动画、7张JPG、3张PNG、2张BMP图片、1个MP3背景音乐与1个PSD源文件覆盖角色动作、场景贴图及音效。整体目录结构清晰附带源码与文档说明便于直接打开工程运行、替换素材或二次修改。项目已通过功能测试并成功运行作者表示答辩评审平均分达96分。目前已有118人学习浏览值得作为C语言图形编程、游戏循环、碰撞检测、键盘交互等知识点的实操范例也可在此基础上继续扩展玩法或用于其他课设、毕设初期演示。1. 用 C 语言和 EasyX 库做简易植物大战僵尸期末作业里最值得投入的方向用 C 语言和 EasyX 库做一版简易植物大战僵尸是不少 C 语言程序设计期末作业里性价比很高的选择。它没有大型游戏引擎的黑匣子又能用“向日葵产阳光、豌豆射手开火、僵尸走到房子就结束”这条主线把指针、结构体、数组、函数拆分这些考点全部串进一套能现场演示的代码里。这篇笔记写给要交源代码和文档说明、又不想把项目做成纯玩具的同学从环境搭建到游戏对象建模从鼠标交互到答辩前要盯住的坑都按一人两天能完成的程度来取舍。方案不追求完整复刻原版而是让你能在评审老师面前把每个模块讲清楚。2. 从空窗口到能跑的游戏循环EasyX 环境与最小骨架2.1 为什么课程作业普遍选 C 语言 EasyXC 语言标准库没有图形能力控制台程序只能让“植物大战僵尸”停留在文字描述里。EasyX 恰好是面向 C/C 初学者的图形库安装后只需要#include graphics.h就能在 Windows 窗口里画点、线、矩形、圆形和图片API 比 Win32 原生绘图直观得多不用理解CreateWindow和消息处理那一整套东西。对比同样常见的 SDL 或 UnitySDL 要下载开发包、配置链接库期末周很容易卡在环境上Unity 则引进了 C#、场景、预制体已经把学习重心从 C 语言挪走了。对课程设计来说EasyX 最大的价值是让 C 语言的知识点“看得见”——结构体对应植物数组对应战场上的一排对象指针用于修改对象属性文件读取还能做存档。不过 EasyX 的边界也很明显它只面向 Windows 平台和 Visual C。如果平时用 MinGW 或 Linux 写 C 语言第一件事是换到 VS 环境而不是和 EasyX 较劲。另一个容易被忽略的点是老师评审时重点不是画面有多好看而是源码里有没有用上结构体、枚举、函数拆分、数组边界控制这些知识点。所以后面所有设计都围绕“代码结构清晰”来做而不是堆贴图。2.2 环境准备VS 与 EasyX 的版本必须对得上安装 EasyX 的常见流程是先装好 Visual Studio 2019 或 2022再下载对应安装包双击后会扫描本机已安装的 VS 版本把graphics.h和静态库复制到 VC 的 include 与 lib 目录。装完以后做一个最小验证很重要我一般会新建一个“空项目”只写一行#include graphics.h然后编译一个initgraph(800, 600)的空窗口确认通路没问题再开始写游戏逻辑。这里最容易翻车的是版本错配VS 2022 装了安装时却勾选了 VS 2019编译就报“无法打开包括文件 graphics.h”或链接阶段报一堆无法解析的外部符号。另一个常见问题是平台位数课程项目建议把“解决方案配置”选成 x86兼容性比 x64 更稳尤其是老项目代码里可能隐含 int 和指针混用的情况。项目属性里字符集建议用“使用多字节字符集”后面用outtextxy输出中文时少一层字符转换问题当然用 Unicode 加_T宏也行但新手容易在宽字符串上反复碰壁。2.3 最小可运行窗口先跑通再往里面塞逻辑环境装好以后第一个程序不需要有任何游戏内容只需要验证三件事窗口能创建、绘制接口能调用、程序不会闪退。// 最小 EasyX 窗口先验证图形环境再开始写游戏 #include graphics.h #include conio.h #define WIN_W 800 #define WIN_H 600 int main() { // 创建 800x600 绘图窗口坐标原点在窗口左上角 initgraph(WIN_W, WIN_H); // 设置背景为浅绿色并清屏 setbkcolor(RGB(144, 238, 144)); cleardevice(); // 随手画一个深棕色矩形验证绘制接口 setfillcolor(RGB(139, 90, 43)); solidrectangle(100, 100, 200, 160); // _getch() 等待一个按键防止窗口一闪而过 _getch(); // 关闭图形窗口释放图形库资源 closegraph(); return 0; }initgraph负责创建图形窗口并进入图形模式cleardevice用当前背景色清空整个窗口。solidrectangle是最直接的验证如果这里能画出一个方块说明编译、链接、运行的通路是好的。窗口宽高建议定义成宏WIN_W和WIN_H后面所有坐标计算都基于这两个宏避免调整窗口大小时出现一堆改不完的魔法数字。_getch来自conio.h名字前加下划线是为了符合 VS 的 ANSI 兼容要求写了getch也能编译但会得到 C4996 警告课程作业还是干净一点好。接下来把单次绘制升级成游戏循环。游戏和普通程序最大的差别是普通程序一次执行就结束游戏需要一个持续刷新、持续接收输入的循环直到用户退出或游戏结束。// 简易主循环按 ESC 退出Sleep 控制帧率 #include graphics.h #include conio.h #define WIN_W 800 #define WIN_H 600 int main() { initgraph(WIN_W, WIN_H); setbkcolor(RGB(144, 238, 144)); cleardevice(); bool running true; while (running) { // 非阻塞检查键盘有按键才取 if (_kbhit()) { int key _getch(); if (key 27) running false; // ESC 键退出 } // 这里之后会放 update() 和 draw() 两个函数 // 绘图前清屏避免上帧内容残留 cleardevice(); setfillcolor(GREEN); solidcircle(400, 300, 50); // 简单画一个圆作为循环验证 Sleep(16); // 等待约 16 毫秒让刷新率接近 60FPS } closegraph(); return 0; }这段骨架已经是一个可不断扩展的主循环。_kbhit不会阻塞程序适合放在游戏循环里检查按键_getch读取键值27 是 ESC 的 ASCII 码。Sleep(16)是人类可感知的帧率控制初步方案但真实项目里不能只靠它因为不同电脑的执行速度不同固定Sleep会放大性能差异。后面的核心章节会换成基于时间的更新方式也就是用“上一帧到这一帧的毫秒差”去计算僵尸的移动距离。先记住这一点帧率问题放到第三章一起处理。3. 用结构体和数组把游戏对象组织起来核心数据结构3.1 枚举和结构体植物、僵尸、子弹怎么建模植物大战僵尸的对象种类不多但对初学者来说最忌讳的是用十几个变量分别表示sunflowerX、sunflowerY、zombie1Hp、zombie2Hp。这样写在 5 个对象以内还能凑合僵尸数量一上来就失控。更合理的方式是用枚举表示类型用结构体表示一个对象再用定长数组表示对象集合。// 对象定义可放在 game.h 中统一管理 #define MAX_PLANTS 64 #define MAX_ZOMBIES 40 #define MAX_BULLETS 128 // 植物类型 typedef enum { PLANT_SUNFLOWER 0, // 向日葵产出阳光 PLANT_PEASHOOTER, // 豌豆射手 PLANT_WALLNUT // 坚果墙血量高 } PlantType; // 僵尸类型 typedef enum { ZOMBIE_NORMAL 0, // 普通僵尸 ZOMBIE_CONE // 路障僵尸血量更厚 } ZombieType; // 植物对象 typedef struct { PlantType type; float x, y; // 格子左上角坐标 int hp; // 当前生命 int maxHp; // 最大生命画血条用 int cooldown; // 攻击计时单位帧 int alive; // 1 表示存活0 表示空位 } Plant; // 僵尸对象 typedef struct { ZombieType type; float x, y; // 左上角坐标 float speed; // 像素/秒 int hp; int alive; } Zombie; // 子弹对象 typedef struct { float x, y; float vx; // 水平速度正方向向左飞 int alive; } Bullet;对象定义完成后需要一个全局状态把整个游戏串起来。用结构体当作全局游戏状态比一堆全局变量规范得多也方便后面做存档和重置关卡。// 全局游戏状态 typedef struct { int sun; // 当前阳光数 int tick; // 全局帧计数从 0 累加 int level; // 当前关卡 int gameOver; // 1 表示游戏结束 int plantCount; // 当前场上植物数量 int zombieCount; int bulletCount; Plant plants[MAX_PLANTS]; Zombie zombies[MAX_ZOMBIES]; Bullet bullets[MAX_BULLETS]; } GameState; GameState g_state; // 全局唯一游戏状态枚举的好处是类型名替代魔法数字写PLANT_SUNFLOWER比写0可读性强得多。结构体里的alive字段承担“对象是否在场上”的职责平时遍历只处理alive 1的对象。这样删除一个僵尸不需要在数组中间做数据搬移只需要把alive置 0新对象创建时优先找一个alive 0的空闲槽位。课程项目的对象数量最多几十个用 O(n) 线性查找完全够用不要为了“效率”去写链表反而增加答辩风险。3.2 游戏主循环基于时间驱动而不是基于运气驱动很多初版代码会把移动逻辑写成“每循环一次僵尸 x 坐标减 1”。这样在性能高的机器上同样一秒循环 1000 次僵尸快得像瞬移在性能差的机器上又慢吞吞。解决办法是引入 delta time每帧开始时记录当前时间与上一帧时间做差得到dt秒数然后所有的移动、冷却、生产都用像素/秒乘以dt来计算。#include graphics.h #include conio.h #include windows.h void update(GameState* g, float dt) { /* 稍后填充 */ } void draw(GameState* g) { /* 稍后填充 */ } int main() { initgraph(WIN_W, WIN_H); GameState g_state {0}; g_state.sun 50; g_state.level 1; DWORD last GetTickCount(); // 记录上一帧时间 bool running true; while (running) { DWORD now GetTickCount(); // 当前毫秒数 float dt (now - last) / 1000.0f; if (dt 0.05f) dt 0.05f; // 防止窗口拖动后 dt 过大造成瞬移 last now; // 键盘处理 if (_kbhit()) { int key _getch(); if (key 27) running false; // ESC 退出 } // 鼠标处理 while (MouseHit()) { MOUSEMSG msg GetMouseMsg(); if (msg.uMsg WM_LBUTTONDOWN) { // 这里调用处理点击的函数传入坐标 } } // 游戏逻辑更新 update(g_state, dt); // 批量绘制结束时统一刷新到屏幕 BeginBatchDraw(); draw(g_state); EndBatchDraw(); } closegraph(); return 0; }GetTickCount是 Windows 提供的毫秒计时函数dt的语义是“距离上一帧过去了几秒”。把速度单位设为像素/秒后僵尸移动就是x - speed * dt无论在哪台机器上同一秒内移动距离一致。dt加上限是很有用的防御当用户拖动窗口或调试时卡顿程序恢复后的第一帧dt可能高达几百毫秒如果不限制僵尸会瞬间横穿屏幕限制到 0.05 秒后异常大跳变被挡住游戏不会变得不可解释。BeginBatchDraw和EndBatchDraw是 EasyX 提供的内存缓冲机制所有绘制先写入后台画布结束时一次性呈现。没有这组调用画面会出现明显闪烁尤其是场景里对象多的时候。这个机制应该从主循环一开始就使用而不是等到贴图变多了再改。3.3 碰撞检测和对象回收安全用数组的基本功子弹打到僵尸、僵尸啃植物本质都是碰撞检测。课程项目不需要物理引擎矩形相交检测就够了每个对象都看作一个“带尺寸的方块”两个方块不相交当且仅当某一方在另一方的左、右、上、下之外。// 矩形碰撞检测ax,ay 为对象 A 左上角坐标aw,ah 为宽高 int rectHit(float ax, float ay, float aw, float ah, float bx, float by, float bw, float bh) { // 不相交的四种情况 if (ax aw bx) return 0; if (bx bw ax) return 0; if (ay ah by) return 0; if (by bh ay) return 0; return 1; // 否则判定为相交 }这个函数的返回逻辑很直白一方向右边界仍在另一方左边界左边说明还没碰上。把四种“不相交”排除后剩下的就是相交。写碰撞检测时常见的错误是误用和边界刚好重叠时应该算碰撞所以不相交判断必须使用而不是。子弹和僵尸的碰撞更新建议单独写一个函数。遍历所有子弹如果子弹出界就标记alive 0如果没有出界再遍历所有僵尸命中后扣血并标记子弹失效。// 更新子弹并检测命中僵尸 void updateBullets(GameState* g, float dt) { for (int i 0; i MAX_BULLETS; i) { Bullet* b g-bullets[i]; if (!b-alive) continue; // 子弹向左飞行速度是像素/秒 b-x b-vx * dt; // 子弹离开窗口左侧直接回收 if (b-x -20) { b-alive 0; continue; } // 与所有存活僵尸做矩形相交检测 for (int j 0; j MAX_ZOMBIES; j) { Zombie* z g-zombies[j]; if (!z-alive) continue; // 子弹 14x14僵尸按 70x80 粗略计算 if (rectHit(b-x, b-y, 14, 14, z-x, z-y, 70, 80)) { z-hp - 20; // 单发子弹伤害 b-alive 0; // 子弹命中后消失 if (z-hp 0) { z-alive 0; // 僵尸死亡 g-sun 20; // 简单回收奖励 } break; } } } }这里有两个重要的工程习惯第一遍历时只跳过alive 0的对象而不是立刻把数组往前搬第二子弹命中后立刻break避免同一发子弹穿透多个僵尸。数组长度固定时MAX_ZOMBIES就是上限任何时候新增僵尸都要先找空闲槽找不到就说明已经满了直接放弃生成而不是继续增加数组下标越界写入。很多答辩翻车现场都是因为这里少了一个边界判断游戏运行几分钟后内存被写坏表现成“突然闪退”或“画面花掉”。4. 把游戏画出来EasyX 绘图、鼠标交互和 UI 绘制4.1 先画形状后贴图没有美术素材也能交作业很多同学一上来就到处找“向日葵去背透明 png”结果卡在图片背景上浪费半天。EasyX 里加载图片确实简单但透明通道处理、路径问题、尺寸缩放对新手并不友好。如果是课程设计我建议第一版先全部用 EasyX 的几何画图函数来实现画面。颜色、位置、大小都在代码里答辩时还能现场改参数展示比贴素材更有说服力。// 绘制向日葵用椭圆、圆、矩形组合 void drawSunflower(float x, float y) { // 花盆深棕色矩形 setfillcolor(RGB(139, 90, 43)); fillrectangle((int)x 12, (int)y 40, (int)x 58, (int)y 80); // 花瓣8 个黄色圆围成一圈 setfillcolor(YELLOW); for (int i 0; i 8; i) { double angle i * 3.14159265 / 4; int px (int)(x 35 18 * cos(angle)); int py (int)(y 25 18 * sin(angle)); solidcircle(px, py, 10); } // 花芯棕色实心圆 setfillcolor(RGB(160, 82, 45)); solidcircle((int)(x 35), (int)(y 25), 12); // 叶片绿色小椭圆 setfillcolor(RGB(34, 139, 34)); fillellipse((int)x 5, (int)y 50, (int)x 45, (int)y 70); }用fillrectangle、solidcircle、fillellipse组合图形只需要把每个植物的特征用三四种颜色表现出来。位置坐标统一走“格子左上角 偏移量”之后放入网格时只需要传入该格子的左上角坐标。这样做的另一个好处是如果后续想换成图片只需要替换这个绘制函数内部的内容而不用改动任何游戏逻辑。如果你想体验贴图常见代码是IMAGE img; loadimage(img, Lres/sunflower.png, 80, 80); putimage((int)x, (int)y, img);需要注意路径字符串前面的L表示宽字符图片应放在运行目录下的res文件夹里至于“透明通道能不能完美保留”不同 EasyX 版本表现不一致简直像个玄学这也是我劝你先用几何图形的原因。4.2 绘制阳光数、冷却条和血条信息要先让玩家看见游戏界面除了草地图还要让玩家知道当前阳光数、卡片是否冷却、僵尸还剩多少血。这些信息在 EasyX 里都是普通绘制调用关键是字符格式和坐标布局。// 绘制左上角 UI阳光数、冷却条 void drawUI(GameState* g) { // 设置字体28 号使用 _T 宏兼容宽窄字符 settextstyle(28, 0, _T(微软雅黑)); settextcolor(BLACK); TCHAR buf[32]; _stprintf_s(buf, 32, _T(阳光: %d), g-sun); outtextxy(10, 10, buf); // 豌豆射手冷却条 setfillcolor(LIGHTGRAY); solidrectangle(90, 18, 190, 28); setfillcolor(GREEN); int percent 100; // 这里应按冷却进度填写 int fillW percent * 100 / 100; if (fillW 0) { solidrectangle(90, 18, 90 fillW, 28); } }outtextxy的字符集问题很典型在 Unicode 字符集下char*字符串会乱码所以统一写成_T(...)宏配合TCHAR类型VS 会自动选择宽字符或窄字符版本。_stprintf_s是安全格式化函数替代容易越界的sprintf函数名前的下划线同样是 VS 风格。课程设计里用这个函数能少解释很多安全问题。僵尸血条可以直接挂在僵尸头上方。血条宽度可以用固定值按当前血量 / 最大血量缩放。// 在僵尸头部绘制血条 void drawHpBar(float x, float y, int hp, int maxHp) { int width 60; if (maxHp 0) return; int fillW width * hp / maxHp; if (fillW 0) fillW 0; if (fillW width) fillW width; // 黑色背景 setfillcolor(BLACK); solidrectangle((int)x - width / 2, (int)y - 8, (int)x width / 2, (int)y - 2); // 红色前景表示剩余血量 setfillcolor(RGB(220, 50, 50)); solidrectangle((int)x - width / 2, (int)y - 8, (int)x - width / 2 fillW, (int)y - 2); }注意整型乘除法会先执行乘法再除法width * hp / maxHp比width / maxHp * hp精度高后者会因为整除直接变成 0。这个细节在血量很低时非常明显也是新手最容易写出的“血条瞬间变空”的 bug。4.3 鼠标选卡与放置植物屏幕坐标换算成网格坐标鼠标点击时拿到的是窗口像素坐标比如(350, 200)而游戏逻辑需要的是第几行第几列。换算公式很简单行号 (y - 网格顶端) / 格子高度列号 (x - 网格左端) / 格子宽度。难的是先判断点击的是顶部卡片还是草地以及放置前要检查阳光数和格子占用情况。// 处理鼠标左键按下 void handleMouseDown(GameState* g, int mx, int my) { // 顶部卡片区域三张卡片横向排列 for (int i 0; i 3; i) { int cardX 10 i * 80; if (mx cardX mx cardX 70 my 10 my 70) { selectedCard i; // 选中卡片等待放置 return; } } // 点击草地区域 if (selectedCard 0 mx GRID_LEFT my GRID_TOP) { int row (my - GRID_TOP) / CELL_H; int col (mx - GRID_LEFT) / CELL_W; // 越界保护 if (row 0 || row GRID_ROWS) return; if (col 0 || col GRID_COLS) return; int cellX GRID_LEFT col * CELL_H; int cellY GRID_TOP row * CELL_H; // 判断是否可放置 if (canPlantAt(g, row, col)) { placePlant(g, selectedCard, cellX, cellY); selectedCard -1; // 放置后取消选中 } } }selectedCard是一个普通全局变量表示当前选中的卡片类型没有选中时为 -1。这样的交互方式比“点一次卡片就立刻种一棵”更接近原版也让学生练习了状态管理。handleMouseDown里使用了两种保护一种是对整数除法结果做边界限制另一种是放置前再次检查阳光数、冷却、格子占用。很多崩溃现场都是因为少了这些检查比如阳光不足时仍然创建植物导致后续逻辑读到了不正确的状态。5. 踩坑与避坑EasyX 期末项目的 5 个高频翻车现场5.1 编译报错“无法打开包括文件 graphics.h”版本错配现象新建 VS 项目后#include graphics.h直接报 C1083或者在链接阶段报一堆“无法解析的外部符号 _initgraph”。原因EasyX 安装时没有选择当前使用的 VS 版本或者换电脑后复制代码但新电脑没装 EasyX还有一种情况是编译平台选成 x64而安装的库只覆盖了 x86。解决重新下载安装包运行安装程序并选中正在使用的 VS 版本然后把解决方案平台切换为 x86。检查项目属性里的“VC 目录”确认 include 路径存在。不要试图在 MinGW 或 Dev-C 里用 EasyX那不是它支持的环境。5.2 窗口一闪而过缺少等待或主循环没接住现象双击运行程序弹出一个白窗口瞬间消失甚至还没看清背景色就退出了。原因很多初版代码在initgraph后直接执行了closegraph中间没有任何阻塞或循环。如果在控制台项目里跑也可能因为main函数返回而直接结束进程。解决最小程序在结束前用_getch()等待按键完整游戏则必须使用while (running)主循环。注意_getch()不能放入closegraph()之后窗口都关了等按键已经没有意义。正确的顺序是先在图形模式下等用户确认再关闭图形资源。5.3 帧率不稳定僵尸有时候瞬移有时候原地踏步现象同一份代码在老师电脑上僵尸快得像开了倍速在自己电脑上又很慢窗口拖动后恢复僵尸会突然出现在不该出现的位置。原因代码里用了固定Sleep(16)和每帧x--这种固定步长更新。循环跑得快移动就快循环被阻塞一下移动就停滞两者没有统一成时间单位。解决用GetTickCount计算dt移动量统一写成speed * dt。dt的数值就是“上一条命令执行到这一条之间经过的秒数”。同时给dt设置上限比如 0.05 秒防止卡顿后恢复造成的跳跃。这是课程答辩里很值得讲的一个优化点。5.4 点击放置植物突然闪退数组越界写坏了内存现象游戏能正常开但一旦连续放置多个植物或生成多个僵尸程序就会崩溃而且崩溃时机飘忽不定。原因最常见的是addPlant或生成僵尸时没有检查数组长度直接写g_state.plants[g_state.plantCount] ...。当plantCount超过MAX_PLANTS时数组越界写到了相邻内存后面读取该区域的对象时全部变成脏数据。解决所有对象创建都走“查找空闲槽”的方式找到alive 0的槽才写入找不到就返回 -1调用方决定放弃生成或弹提示。删除对象不搬移数组只标记alive 0避免在遍历过程中修改数组结构。这个做法虽然简单却能挡住绝大多数崩溃。5.5 图片加载失败工作目录和资源路径搞不清现象图片文件明明放在项目文件夹里程序运行时却是一块黑色或直接显示不了图片而编译不报错。原因EasyX 的loadimage使用相对路径时是相对于当前工作目录解析的。在 VS 里直接按 F5 运行工作目录通常是项目目录但如果去Debug文件夹里双击 exe工作目录就变成了Debug所在目录。两者的文件位置完全不同。解决最直接的方案是在项目目录下建一个res文件夹代码里写_T(res/xxx.png)并在 README 里说明“请把 res 文件夹放在运行目录”。更稳妥的做法是程序启动时把当前目录切成 exe 所在目录这样无论从哪启动都能找到资源。我常用的代码是通过GetModuleFileName拿到 exe 路径再截取目录并SetCurrentDirectory切过去这样后面所有相对路径都以 exe 为基准。// 切换当前目录到 exe 所在目录 #include windows.h #include shlwapi.h #pragma comment(lib, shlwapi.lib) void chdirToExe() { TCHAR path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH); PathRemoveFileSpec(path); SetCurrentDirectory(path); }这个函数在main开头调用一次之后所有loadimage和后续的文件读写都相对 exe 目录工作不会因为启动方式不同而翻车。注意PathRemoveFileSpec需要链接shlwapi.lib代码中的#pragma comment就是让链接器自动带上这个库。6. 让它更像一个能答辩的课程设计状态控制、存档和源代码管理做到这里项目已经有了“能玩”的雏形。但期末作业能拿高分重点往往在最后收尾这一步把代码整理成别人能看懂、能运行、能验证的样子。我一般会在第一时间把主循环改成状态机至少包含运行、暂停、结束三态。实现方式是在游戏状态里加一个enum GamePhase { PHASE_RUNNING, PHASE_PAUSED, PHASE_OVER }然后在update和draw之前分别判断当前状态。按 P 键在运行与暂停之间切换僵尸血量归零则进入结束状态界面上显示“按 R 重新开始”。状态机的价值不仅是功能更是答辩时的展示亮点代码结构不再是“一条 while 打天下”而是有明确的阶段划分。存档功能不用做得很复杂能实现“退出后再次打开阳光数不变”就已经超过课程要求。做法是把需要保存的字段按二进制写入文件常见代码是这样void saveGame(GameState* g, const TCHAR* path) { FILE* fp _tfopen(path, _T(wb)); if (fp NULL) return; fwrite(g-sun, sizeof(int), 1, fp); fwrite(g-level, sizeof(int), 1, fp); fwrite(g-plants, sizeof(Plant), MAX_PLANTS, fp); fwrite(g-zombies, sizeof(Zombie), MAX_ZOMBIES, fp); fwrite(g-bullets, sizeof(Bullet), MAX_BULLETS, fp); fclose(fp); }这里用_tfopen是为了适配宽字符路径_T(wb)表示以二进制写模式打开。读取时用同样的顺序fread回来。直接保存整个结构体数组在固定平台上没问题因为这些结构体内部没有指针不会保存无效地址。如果后续要跨平台就要改成逐字段读写或使用文本格式但课程项目做到二进制存档已经足够了。最后是源代码管理和文档说明。哪怕只有一个人写我也建议在项目最开始就建一个 git 仓库每完成一个功能就 commit 一次。这样改烂了代码还能退回来相当于吃后悔药。文档不必要长篇大论只需要写清楚开发环境、操作方式、代码模块划分、已知缺陷。答辩时主动说“目前已知的问题是坚果血量还没有做成动画表现”远比被老师问住要好。演示顺序也很重要先讲结构体再讲主循环然后是碰撞检测最后才展示画面。你用 C 语言和 EasyX 完成的不只是一个图形程序而是一个能讲清楚数据结构和组织方案的小型系统。希望这篇笔记能让你少走点弯路交出一份自己心里有底的作业。本文还有配套的精品资源点击获取
返回列表