ARTICLE DETAIL

资讯详情

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

用C语言和EasyX实现植物大战僵尸课设:从结构体到碰撞检测全解析

用C语言和EasyX实现植物大战僵尸课设:从结构体到碰撞检测全解析 简介这是一份基于EasyX与C语言开发的植物大战僵尸简易游戏源码适合计算机相关专业的学生用作课程设计、大作业或初期项目演示也适合对游戏开发感兴趣的初学者学习入门。压缩包共收录2000个文件容量约147.37MB主要包含png与gif图片动画素材、mp3与ogg音频文件以及cpp、h源文件和VS工程配置素材与代码分离清晰便于替换和扩展。项目代码完整、功能经过验证运行稳定可靠可直接编译体验经典游戏的种植、射击与僵尸出场等核心玩法。同时源码结构简洁注释到位具备较好的二次开发空间已有257人学习浏览对于希望快速完成课设或提升C语言实践能力的读者很有参考价值。1. 为什么用 C 语言写《植物大战僵尸》反而是课设里最能打的方案大学课程设计一旦规定只能用 C 语言、不允许接 Unity 或 Python很多人的第一反应是做个图书管理系统、贪吃蛇或通讯录。但如果你把这个题目换成“基于 EasyX 和 C 语言的植物大战僵尸简易游戏源码可作课设”答辩现场的观感会完全不同玩法完整、交互直观、逻辑复杂度适中而且能同时把“结构体数组”“碰撞检测”“状态机”“定时器”这些 C 语言核心考点串成一条线。EasyX 图形库负责把控制台窗口升级成可交互的游戏界面背后的业务逻辑仍然是纯 C没有 C 的 class也没有 STL 容器完全贴合教学大纲要求。这篇文章我按自己实际做课设的思路把从框架拆分、坐标映射、碰撞检测到最终打磨成源码包的完整路径讲一遍。2. 动手之前先拆局把游戏拆成 C 语言看得懂的结构体与状态机2.1 EasyX 到底解决了 C 语言游戏的哪块短板先跑通一个可关闭的窗口C 语言自带的标准库只能做控制台交互stdout 里全是黑底白字。想让游戏有地图、有精灵、有鼠标点击就得选一个合适的图形方案。常见的选项有 RayLib、SDL、EasyX。SDL 和 RayLib 功能强但需要额外配置链接库、处理跨平台编译课设这种场景很容易在环境配置上消耗大量时间。EasyX 走的是 Windows GDI 路线安装后直接包一层graphics.h头文件和 Visual Studio 无缝集成对新手的友好程度远高于前两者。先写一个最小窗口程序确认环境没问题#include graphics.h #include conio.h int main() { initgraph(800, 600); // 创建 800x600 绘图窗口 setbkcolor(RGB(46, 46, 46)); // 深灰色背景方便后面盖图 cleardevice(); // 用当前背景色清空画布 outtextxy(200, 280, EasyX window is ready); getch(); // 等待按键防止窗口一闪而过 closegraph(); // 关掉绘图窗口 return 0; }initgraph有两个参数第一个是窗口宽度第二个是高度单位是像素。cleardevice负责在每一帧开始时把画布恢复成背景色否则上一帧的图形会残留形成拖影。getch在这里的作用是挂起进程让窗口保持住。如果编译时提示找不到graphics.h基本可以断定是 EasyX 没有正确安装到当前 VS 版本目录下需要在项目属性里把“平台”切换成 x86 或 x64再在 EasyX 官网下载对应安装包补齐。2.2 用结构体把植物、僵尸、子弹建模成“对象”EasyX 只是画图和收鼠标消息的工具游戏的数据核心仍然要用 C 语言的结构体来承载。很多初学者会倾向于用两个二维数组直接记录格子状态但二维数组只能表达“有植物”或“没植物”表达不了植物的血量、冷却时间、攻击频率这些动态属性。所以更合适的做法是定义三个结构体分别对应植物、僵尸、子弹。#define MAX_PLANTS 45 // 5 行 x 9 列 #define MAX_ZOMBIES 30 // 同一屏最多 30 只僵尸 #define MAX_BULLETS 100 typedef struct { int active; // 1该槽位正被使用0空槽 int row, col; // 格子坐标row 0~4col 0~8 int type; // 0向日葵1豌豆射手2坚果墙 int hp; // 当前生命值 int cd; // 攻击或产阳光的冷却倒计时 } Plant; typedef struct { int active; int x, y; // 像素坐标用于移动和碰撞 int row; // 所在行决定和哪一排植物交互 int hp; int speed; // 每帧移动像素数 int state; // 0行走1啃植物2死亡 } Zombie; typedef struct { int active; int x, y; int row; // 子弹所在行 } Bullet; Plant plants[MAX_PLANTS]; Zombie zombies[MAX_ZOMBIES]; Bullet bullets[MAX_BULLETS];active是整个游戏对象管理的关键。C 语言里数组长度固定不能在运行期随意增删所以采用“标记法”新增对象时遍历数组找到第一个active 0的槽位写入数据并置 1对象死亡时把active置 0 即可后面的元素无需搬动。相比使用链表的方案这种写法省的不仅是编码量更重要的是避免了频繁 malloc/free 造成的内存碎片对课设这种长时间运行的演示程序更稳。如果你在答辩时被问到“数组越界怎么办”可以说“所有遍历都会先检查 active 标志保证不会访问到未初始化的内存”。2.3 主循环与帧率控制Sleep 不是玄学是同步基线游戏的本质是一个无限循环收集输入、更新状态、绘制画面、等待一小段时间然后进入下一帧。这里的Sleep(16)不是可有可无的延迟而是把帧率限制在约 60 FPS 的同步手段。少了它循环会以 CPU 允许的最快速度空转部分机器会直接跑到几千帧导致逻辑更新过快子弹一帧飞出屏幕。int main() { initgraph(800, 600); initGame(); // 初始化植物、僵尸数组和全局变量 BeginBatchDraw(); // 开启双缓冲批量绘制 while (1) { handleInput(); // 处理鼠标点击 updateGame(); // 更新所有对象的逻辑 drawGame(); // 绘制植物、僵尸、子弹到后台缓冲 FlushBatchDraw();// 把后台缓冲一次性刷新到窗口 Sleep(16); } EndBatchDraw(); closegraph(); return 0; }把逻辑更新和绘制分开写是保持代码清爽的关键。如果把碰撞检测、阳光生成、僵尸移动全部塞进一个while循环里后面调试的时候会非常痛苦。这个结构也适合在答辩时讲handleInput对应交互层updateGame对应业务逻辑层drawGame对应表现层层次清晰老师想追问哪个函数都能快速定位。3. 把玩法落成代码种植、阳光、碰撞与对象回收3.1 种植判定从鼠标像素坐标反向映射到 5x9 网格窗口宽度为 800高度为 600地图区域是 9 列 x 5 行。每格宽 80、高 100则地图总宽度为 720总高度为 500。为了让地图居中左边界GRID_LEFT (800 - 720) / 2 40上边界GRID_TOP (600 - 500) / 2 50。鼠标点击的坐标是相对于窗口左上角的像素坐标要转化为“第几行第几列”只需要做一步减法再整除格子尺寸。#define GRID_LEFT 40 #define GRID_TOP 50 #define CELL_WIDTH 80 #define CELL_HEIGHT 100 #define ROWS 5 #define COLS 9 int getGridRow(int mouseY) { if (mouseY GRID_TOP || mouseY GRID_TOP ROWS * CELL_HEIGHT) return -1; // 点击在地图外通常是顶部阳光栏或底部按钮 return (mouseY - GRID_TOP) / CELL_HEIGHT; } int getGridCol(int mouseX) { if (mouseX GRID_LEFT || mouseX GRID_LEFT COLS * CELL_WIDTH) return -1; return (mouseX - GRID_LEFT) / CELL_WIDTH; }返回值 -1 表示“无效格子”调用侧需要先判断是否为 -1 再执行种植逻辑。这里最容易踩的坑是忘记减去GRID_LEFT和GRID_TOP直接用mouseX / CELL_WIDTH结果就是整个地图往右下方偏移了 40 和 50 个像素鼠标点选的格子和实际落点错位。窗口分辨率改变时GRID_LEFT和GRID_TOP也要跟着重算所以最好写成宏定义而不是散落在代码里的魔法数字。3.2 阳光的产生与收集点击即拾取不追求像素级命中阳光是游戏里的资源货币。我一般设计两种来源一种是向日葵每 10 秒在自身附近产生一个阳光另一种是场景中每隔 8 到 12 秒随机掉落一个阳光。两者都靠一个全局计时器驱动每帧递减减到 0 时重置并生成新阳光。typedef struct { int active; int x, y; int timer; // 阳光存留时间倒计时为 0 时消失 } Sunshine; Sunshine suns[20]; int globalSunTimer; void updateSunshine() { if (globalSunTimer 0) { globalSunTimer rand() % 5 8; // 8~12 秒周期性生成 for (int i 0; i 20; i) { if (!suns[i].active) { suns[i].active 1; suns[i].x GRID_LEFT (rand() % (COLS * CELL_WIDTH)); suns[i].y GRID_TOP (rand() % (ROWS * CELL_HEIGHT)); suns[i].timer 120; // 约 2 秒后消失 break; } } } globalSunTimer--; }鼠标点击收集阳光时不需要精确判断点击是否落在阳光图片的像素范围内。因为用户是快速连点的如果判定太严格很容易出现“明明点到阳光却没收走”的迟滞感。常见的做法是计算鼠标位置和阳光坐标的曼哈顿距离小于 40 像素就算拾取成功。距离判断只涉及加减法和绝对值性能开销几乎为零体验却比矩形碰撞更顺滑。3.3 僵尸移动与子弹命中的碰撞检测逻辑AABB 矩形相交EasyX 的绘图函数putimage和solidrectangle在绘图时只会给出精灵的位置和宽高。如果要判断子弹是否打中僵尸不需要做像素级的透明通道碰撞检测那是图像算法课的内容了在 C 语言课设里做只会徒增 bug。用 AABB轴对齐包围盒相交就足够了。int isCollide(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { if (ax bx bw) return 0; if (bx ax aw) return 0; if (ay by bh) return 0; if (by ay ah) return 0; return 1; } void updateBullets() { for (int i 0; i MAX_BULLETS; i) { if (!bullets[i].active) continue; bullets[i].x 6; // 子弹速度每帧向右移动 6 像素 if (bullets[i].x 800) { bullets[i].active 0; continue; } for (int j 0; j MAX_ZOMBIES; j) { if (!zombies[j].active) continue; if (zombies[j].row ! bullets[i].row) continue; if (isCollide(bullets[i].x, bullets[i].y, 14, 14, zombies[j].x, zombies[j].y, 60, 90)) { zombies[j].hp - 20; bullets[i].active 0; break; } } } }两个对象在发生碰撞前先判断row是否一致可以提前过滤掉大量无关比较。僵尸的碰撞盒我按宽 60、高 90 设置比实际图片略小一圈这样做对玩家更友好因为子弹贴近僵尸侧身时不会被“空气墙”挡住。碰到边界要主动把子弹置为 inactive否则子弹飞出屏幕后仍然占用一个数组槽位长期运行会积累越来越多的无效对象。4. 渲染与贴图EasyX 批量绘制和双缓冲的取舍4.1 没有美术素材时如何让 UI 不显得粗糙几何色块加文本标注很多同学下载植物大战僵尸素材包图片动辄几百张但导入工程后才发现路径混乱、格式不兼容、加载失败。其实课设演示阶段不需要完整的高清素材用 EasyX 内置的几何绘图函数加少量贴图完全可以支撑起观感。void drawPlant(int index) { Plant *p plants[index]; int x GRID_LEFT p-col * CELL_WIDTH; int y GRID_TOP p-row * CELL_HEIGHT; if (p-type 0) { setfillcolor(RGB(255, 200, 0)); // 向日葵黄色块 solidcircle(x 40, y 40, 30); outtextxy(x 15, y 45, SUN); // 绘制血量条 } else if (p-type 1) { setfillcolor(RGB(0, 200, 0)); // 豌豆射手绿色块 solidrectangle(x 10, y 20, x 60, y 60); outtextxy(x 20, y 40, SHOOT); } }这里没有使用任何外部图片资源而是在每个格子中心绘制圆形或矩形再叠加outtextxy写出“SUN”“SHOOT”这类辅助文字。纯几何图形的好处是完全不依赖素材包无论换哪台电脑都能稳定编译运行。如果确实想贴 PNG 图片用loadimage加载后配合putimage绘制注意把图片路径用相对路径写或者干脆放到工程目录下的res文件夹里避免答辩换机器后因绝对路径失效而黑屏。4.2 双缓冲图形绘制流程为什么画面会闪怎么压下去如果不做任何缓冲直接在一帧内调用大量绘图函数窗口会出现明显闪烁或撕裂原因是绘制过程中绘图内容被分多次刷新到屏幕用户的肉眼能捕捉到“半成品”画面。EasyX 提供的解决方案是批处理模式。BeginBatchDraw(); // 进入批处理模式所有绘图操作先写入内存缓冲 while (1) { cleardevice(); drawGrid(); // 画草坪格 drawPlants(); // 画植物 drawZombies(); // 画僵尸 drawBullets(); // 画子弹 drawSunshine(); // 画阳光 drawUI(); // 画阳光数量、冷却条 FlushBatchDraw(); // 把完整的一帧一次性刷新到窗口 Sleep(16); } EndBatchDraw();cleardevice必须在批处理模式下使用否则每一帧开始时的清屏操作也会造成闪烁。绘制顺序是从底层到顶层先是草坪背景然后是植物、僵尸、子弹最后是阳光数量等 UI 层。如果反过来先画 UI 再画草坪整个界面会被背景盖掉一层。FlushBatchDraw 的调用时机也很关键它应该在所有绘制完成后再执行避免在 update 过程中被用户看到中间状态。5. 课设验收避坑指南编译、内存与演示翻车的 5 个瞬间5.1 现象#include graphics.h直接报错找不到文件原因EasyX 安装包没有和当前 Visual Studio 版本匹配或者安装时选了 32 位库而工程编译的是 x64 目标头文件路径自然匹配不上。解决从 EasyX 官网重新下载最新安装包关闭 Visual Studio 后运行它会自动检测已安装的 VS 版本并写入头文件和库文件路径。如果多个版本共存先确认当前工程使用的 IDE 版本和位数。这个坑最容易在答辩前一天爆发临时换机器更容易触发所以安装完第一时间编译最小窗口程序确认跑通再继续写业务代码。5.2 现象豌豆射手每隔一帧发射一颗子弹弹幕密得像机关枪原因主循环每帧都会调用updateGame而创建子弹的代码没有限制频率等于每 16 毫秒就生成一颗。视觉效果就是火力全开游戏难度瞬间崩塌。解决给植物增加冷却计数器。每次触发发射时把cd初始化为 30主循环中每帧将cd减 1只有cd 0时才允许重新生成子弹。这里的 30 对应约 30 帧也就是 0.5 秒一发。void updatePlants() { for (int i 0; i MAX_PLANTS; i) { if (!plants[i].active) continue; if (plants[i].cd 0) { plants[i].cd--; continue; } // 朝向该行最近的一只僵尸发射 if (hasZombieInRow(plants[i].row)) { createBullet(plants[i].row, GRID_LEFT plants[i].col * CELL_WIDTH 40, GRID_TOP plants[i].row * CELL_HEIGHT 30); plants[i].cd 30; } } }5.3 现象僵尸死亡后后面的僵尸全部离奇消失或者数组顺序错乱原因遍历数组删除僵尸时直接把后面的元素整体前移导致循环索引 i 跳过了原本相邻的元素部分僵尸被漏检或者重复处理。解决采用标记删除只把active置为 0不移动内存。后续新增僵尸时用第一个空的槽位覆盖写入。这样遍历逻辑始终稳定不会因为删除操作破坏数组的连续性。5.4 现象窗口内出现半截图形、残影画面被切割成两部分原因没有调用BeginBatchDraw和FlushBatchDraw绘图操作直接作用于窗口显存每一次putimage或solidrectangle都被独立刷新到屏幕上。解决在进入主循环前调用BeginBatchDraw循环末尾调用FlushBatchDraw。这两行代码是所有图形游戏的地基。如果已经用了批处理仍然闪烁检查是否漏了cleardevice因为上一帧的图形会残留在下一帧的背景上。5.5 现象Windows 11 高分屏下画面变模糊鼠标怎么点都对不上原因系统默认开启了显示缩放125% 或 150%EasyX 窗口被系统拉伸放大逻辑像素和物理像素不一致鼠标的屏幕坐标和绘图坐标出现映射偏差。解决在程序入口处调用SetProcessDPIAware()或者在 Visual Studio 的项目属性里把“清单文件”中的 DPI 感知设置为 true。另一个后备方案是把窗口固定为 800x600并在答辩前把系统缩放临时调回 100%。课设现场如果用教室的投影仪分辨率普遍是 1024x768 或更高这个问题往往等到演示时才暴露提前处理最稳妥。6. 课设答辩的进阶收尾把三个不显眼的细节做成加分项答辩时老师看的不只是游戏跑起来没跑起来更多是问“这里的对象怎么管理”“碰撞怎么判断”“如果场景再大你会怎么优化”。所以你至少要在最终源码里埋三个能展开讲的细节。第一个是种植预览光标。鼠标移动到草坪上方时在对应的格子上画一个半透明绿色矩形移动到已有植物的格子上时画为红色。这个效果只需要在handleInput里再调一次getGridRow和getGridCol然后在drawGame后绘制一个描边矩形即可。移植到手写代码上成本极低但答辩现场看起来非常像“完整产品”。第二个是音效。用mciSendString播放 wav 文件只需要一行指令#include windows.h #pragma comment(lib, winmm.lib) void playPlantSound() { mciSendString(play res/plant.wav, NULL, 0, NULL); }没有音效素材就用Beep(880, 50)临时顶替调用 Windows 内置蜂鸣器也能让演示不那么干涩。第三个是本地存档。把最高分写进score.txt启动时读取FILE *fp fopen(score.txt, r); if (fp) { fscanf(fp, %d, highestScore); fclose(fp); }答辩时可以很自然地说一句“这是利用标准文件 I/O 保存的持久化记录”数据结构的分数立刻拉满。我在自己版本的课设里保留了植物类型切换、僵尸波次生成和简易存档功能演示时不慌提问也不怕深挖。希望帮到你。本文还有配套的精品资源点击获取
返回列表