ARTICLE DETAIL

资讯详情

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

C语言+EasyX植物大战僵尸课程设计源码拆解

C语言+EasyX植物大战僵尸课程设计源码拆解 简介一款基于C语言与EasyX图形库开发的简易植物大战僵尸小游戏源自C语言程序设计期末作业代码与说明文档均已打包。适合计算机相关专业在校学生作为课程设计参考也适合C语言学习者通过源码理解图形界面编程、游戏循环、鼠标响应与碰撞检测等常见知识点。压缩包共98个文件约16.73MB内含cpp源文件、Visual Studio工程文件以及大量gif、png、bmp图片素材和mp3背景音频可直接编译运行并在此基础上修改扩展。包内同时附带项目文档说明代码已经过测试并成功运行可用于期末答辩演示或课程设计提交。目前已有118人学习下载是一份实用性较高的C语言课程设计范例。1. 课程设计里的植物大战僵尸EasyX 能把 C 语言学成什么样C 语言课程设计做什么题目最划算植物大战僵尸这个题材技术门槛不高展示效果又强年年都有人交但真正能编译、能跑、能当场演示的版本真不多。这份用 C 语言 EasyX 库写的简易植物大战僵尸就是一份 C 语言程序设计期末作业源码VS 工程、图片资源、文档说明都收在一个包里。它把窗口初始化、鼠标种植、子弹发射、僵尸刷怪、碰撞判定这些游戏小程序的主干逻辑都串成了一整套代码对正在找 c 课程设计或者 C 语言源代码的人来说是一个可以直接打开对照学习的参考样例。代码已经跑通放在期末作业、初期项目演示或者刚学完 C 语言基础的人手里都合适。下面从工程结构到核心玩法再到底层容易翻车的地方逐一拆开讲。2. EasyX 环境与工程骨架先把 VS 工程拆明白再动手改2.1 解压后先认文件这套工程不是单文件项目大一交上来的 C 语言作业大概率是一个 .cpp 写到底所有函数堆在一起。这份不太一样解压以后是一个完整的 Visual Studio 解决方案文件作用MyPvZInC.sln解决方案文件双击直接打开整个工程MyPvZInC.vcxproj项目配置包含编译器参数、字符集、子系统设置MyPvZInC.cpp入口文件负责窗口初始化和主循环common.cpp公共数据与素材加载图片资源在这里统一处理function.cpp游戏功能函数种植、移动、碰撞、刷怪大多在这里img背景、植物、僵尸的图片素材.gitattributes / .gitignore版本管理辅助文件对运行没有影响这种拆分方式在课程设计里算是讲究的入口文件只负责循环框架common 负责资源function 负责玩法细节。拿到源码后从 MyPvZInC.cpp 开始读先看主循环和窗口创建再进 function.cpp 看具体函数阅读成本比单文件低很多。如果你打算在这个基础上加新功能新函数放 function.cpp新图片素材放 img结构不会乱。一个常见的失误是直接在 Visual Studio 里双击 .sln 打开就跑完全不管编译器版本和配置。EasyX 项目对字符集、子系统、平台位数都有要求直接编可能蹦出一堆链接错误。先确认三件事EasyX 已安装项目配置是 x86 也就是 32 位字符集设置要和源码保持一致。源码里如果写的是_outtext(L...)工程属性就应该用 Unicode 字符集源码若是普通char*字符串就改成“使用多字节字符集”。这两样对不上后面控制台和窗口里的中文都是乱码。2.2 EasyX 的窗口创建和图像加载是怎么组织的EasyX 本质是一套 Windows GDI 封装库程序启动后先initgraph创建绘图窗口之后所有的drawimage、fillsolidrectangle都作用在这个窗口上。这套工程里最基础的初始化是这样的#include graphics.h #include conio.h int gameWindowWidth 960; int gameWindowHeight 540; void initWindow() { // 创建 960x540 的绘图窗口保留控制台方便看调试日志 initgraph(gameWindowWidth, gameWindowHeight, EX_SHOWCONSOLE); }第一个参数是窗口宽度第二个是高度。不要一上来就写 1920x1080植物大战僵尸这类横版地图通常按 960x540 或 800x600 设计草坪格子数、卡片栏高度、僵尸出生位置都和窗口尺寸绑定窗口太大会导致素材拉伸变形太小又放不下整排僵尸。第三个参数EX_SHOWCONSOLE表示保留控制台黑窗调试的时候可以往里打印变量给老师演示时改成EX_DEFAULT控制台就不会弹出来。窗口创建完紧接着是加载图片资源。常见的做法是用一个全局IMAGE对象把背景和角色每一帧都读进去#include graphics.h IMAGE imgBackground; IMAGE imgZombie[3]; void loadAllImages() { loadimage(imgBackground, L./img/background.png); loadimage(imgZombie[0], L./img/zombie_walk1.png); loadimage(imgZombie[1], L./img/zombie_walk2.png); loadimage(imgZombie[2], L./img/zombie_eat.png); }这里有两个细节值得注意路径用的是相对路径代表可执行文件运行目录下的 img 文件夹字符串带了L前缀说明工程是 Unicode 字符集。loadimage不是一个不失败的函数它找不到文件时一般不弹错误框只是把图片对象留空画面上一片黑或一片白。我拿到这套工程后第一件事就是检查工作目录Visual Studio 默认的工作目录是工程文件所在目录而img文件夹可能没有被复制到Debug或Release输出目录里。把图片路径写死成./img/...直接双击 exe 时没问题在 VS 里按 F5 却不一定能找到。后面避坑章会有针对性的解决办法。2.3 主循环游戏不是靠延时逻辑撑起来的如果程序只是initgraph后画一张静态图那和图片查看器没有区别。游戏要动起来靠的是“主循环”每帧做三件事收集消息更新逻辑重新绘制。常见实现长这样#include graphics.h void update(long long deltaTime); void drawAll(); bool gameRunning true; int lastClickX 0; int lastClickY 0; void gameLoop() { while (gameRunning) { // 用 peekmessage 而不是 getmessage保证没有消息时画面也持续刷新 ExMessage msg; while (peekmessage(msg, EX_MOUSE)) { if (msg.message WM_LBUTTONDOWN) { lastClickX msg.x; lastClickY msg.y; } } update(16); // 更新子弹位置、僵尸位置、冷却时间 drawAll(); // 按当前数据重新绘制整帧 Sleep(16); // 约 60 FPS } }逻辑说明那 16 毫秒的 Sleep 是人为降频防止 CPU 满载真正的时间尺度不要依赖循环次数而要用一个全局累计时间。每次 update 时读取当前毫秒数减上次时间戳得到 deltaTime再累加到游戏时钟里。Sleep(16)在大量物体同时刷新时会有波动但课程设计这个数量级完全够用。这里一个关键点是peekmessage和getmessage的区别。getmessage在没有消息时会阻塞线程画面停在最后一帧鼠标不动游戏就不刷新peekmessage是非阻塞的即使玩家没有操作逻辑和绘图也照常执行。做过 EasyX 的人都知道鼠标不动画面就卡住十有八九是这里用了阻塞取消息。2.4 绘制函数最好集中在一个 drawAll 里工程里常见的问题是每个功能函数自己画自己种下一棵植物立刻执行一次绘图子弹飞一下也立刻刷新最终相互覆盖。正确做法是集中绘制逻辑层只改数据绘图层根据所有数据重新画一整帧。void drawAll() { BeginBatchDraw(); // 批量绘图防止闪烁 putimage(0, 0, imgBackground); // 先画背景 for (int i 0; i plantCount; i) { drawPlant(plants[i]); } for (int i 0; i zombieCount; i) { drawZombie(zombies[i]); } for (int i 0; i bulletCount; i) { drawBullet(bullets[i]); } drawCardBar(); FlushBatchDraw(); // 一次性将内容刷新到窗口 }BeginBatchDraw和FlushBatchDraw这两个函数是 EasyX 专门为防闪烁准备的。先画进内存再一次性提交到窗口否则每个对象各自刷新窗口会不停闪。数据驱动绘制的另一个好处是调试方便想临时隐藏某类实体只要注释掉对应绘制循环不需要动逻辑代码。工程骨架到这里就清楚了入口文件负责创建窗口和跑循环common 负责资源加载function 实现 update 和 drawAll。下一步看核心玩法植物、子弹、僵尸三者是怎么在代码里互相作用的。3. 植物、子弹、僵尸的数据建模一个结构体数组就够了3.1 用结构体而不是 class保持 C 语言风格如果这是 C 课程设计你大概率会写class Plant、class Zombie。但这份工程是 C 语言作业风格文件名后缀虽然是 .cpp实际写作方式还是面向过程结构体描述实体全局数组保存所有实体函数操作数组。植物、子弹、僵尸三段数据大体是这种形态#define MAX_PLANTS 128 #define MAX_ZOMBIES 64 #define MAX_BULLETS 256 typedef struct { int x, y; // 所在格子的左上角坐标 int type; // 0豌豆射手 1向日葵 2坚果 int hp; // 当前生命值 int attackTimer; // 攻击间隔计时 } Plant; typedef struct { int x, y; // 当前图片左上角坐标 int width, height; // 图片尺寸用于碰撞检测 int hp; // 剩余血量 float speed; // 每毫秒移动像素数 int state; // 0行走 1啃植物 2死亡 } Zombie; typedef struct { int x, y; int active; // 0 表示这个槽位空着 int speed; // 子弹每帧移动像素数 } Bullet; Plant plants[MAX_PLANTS]; Zombie zombies[MAX_ZOMBIES]; Bullet bullets[MAX_BULLETS]; int plantCount, zombieCount, bulletCount;参数说明三个数组都是固定容量。128 个植物槽位草坪六行九列最多 54 格留了很大余量僵尸一屏同时出现十几只就算高压波次64 够用子弹上限 256 是为了防止多个豌豆射手同时输出时数组溢出。用数组而不是链表一个直接好处是遍历代码简单for 循环从头扫到尾删除时把最后一个元素搬到被删位置然后 count--不会产生内存碎片。缺点是从中间删元素需要搬移数据但这类小规模游戏完全可以接受。答辩时老师常问“为什么不用链表”标准回答是实体数量有明确上界数组访问效率高、实现简单链表适合数量动态变化且频繁插入删除的大规模场景。课程设计用数组是合理取舍。3.2 鼠标点击种植坐标换算成格子玩家点击左边卡片栏再点草坪程序要做两件事判断选中的卡片是否在冷却判断点击位置是不是可种植的草坪格子。核心是把窗口像素坐标换算成格子坐标#define LAWN_START_X 120 #define LAWN_START_Y 90 #define GRID_SIZE 80 int selectedCard -1; long long cardCooldown[3] {0}; bool isInLawnArea(int mx, int my) { if (mx LAWN_START_X || mx LAWN_START_X 9 * GRID_SIZE) return false; if (my LAWN_START_Y || my LAWN_START_Y 5 * GRID_SIZE) return false; return true; } void tryPlant(int mx, int my) { if (selectedCard 0) return; if (!isInLawnArea(mx, my)) return; int gridX (mx - LAWN_START_X) / GRID_SIZE; int gridY (my - LAWN_START_Y) / GRID_SIZE; for (int i 0; i plantCount; i) { if (plants[i].x / GRID_SIZE gridX plants[i].y / GRID_SIZE gridY) { return; // 格子被占了不能重复种 } } if (GetTickCount64() cardCooldown[selectedCard]) return; plants[plantCount].x LAWN_START_X gridX * GRID_SIZE; plants[plantCount].y LAWN_START_Y gridY * GRID_SIZE; plants[plantCount].type selectedCard; plants[plantCount].hp 300; plants[plantCount].attackTimer 0; plantCount; cardCooldown[selectedCard] GetTickCount64() 5000; selectedCard -1; }坐标换算用整数除法gridX 取到 0 到 8 之间的列号存入坐标时乘回 GRID_SIZE这样绘制函数直接用植物的 x,y 画图不需要每次再做除法。检查已占用格子这一步容易漏漏掉的话多个植物叠在一起僵尸会同时啃到两颗植物画面和判定逻辑都乱。这里的冷却写成了固定 5000 毫秒实际工程里每种植物应该有自己的冷却时间向日葵快、豌豆射手次之、坚果慢半拍。推荐把植物属性做进一张常量表type、血量、冷却、攻击间隔、子弹伤害都放在一起答辩时改参数方便很多。3.3 子弹移动与碰撞检测先移动再判定碰撞检测是横版游戏最容易被新手写歪的地方。最简单的做法是每帧遍历所有子弹对每颗子弹遍历所有存活僵尸做矩形相交判断。课程设计这个数量级双重 for 循环完全跑得动void updateBullets() { for (int i 0; i bulletCount; i) { bullets[i].x bullets[i].speed; if (bullets[i].x gameWindowWidth) // 飞出屏幕就回收 { bullets[i] bullets[bulletCount - 1]; bulletCount--; i--; continue; } for (int j 0; j zombieCount; j) { if (zombies[j].hp 0 || zombies[j].state 2) continue; int bulletRight bullets[i].x 14; int zombieLeft zombies[j].x; int zombieRight zombies[j].x zombies[j].width; if (bulletRight zombieLeft bullets[i].x zombieRight) { int bulletTop bullets[i].y; int bulletBottom bullets[i].y 14; int zombieTop zombies[j].y; int zombieBottom zombies[j].y zombies[j].height; if (bulletBottom zombieTop bulletTop zombieBottom) { zombies[j].hp - 20; bullets[i] bullets[bulletCount - 1]; bulletCount--; i--; break; } } } } }逻辑说明子弹从左向右移动speed 是每帧增加的像素数。每秒 600 像素的子弹60 FPS 下每帧就是 10 像素。检测顺序是先把子弹的 x 和图片尺寸算成一个命中盒判断和僵尸的 x 区间是否有重叠x 方向重叠后再检查 y 方向。y 方向这里用的是两个区间是否有交叠比单纯判断“子弹中心是否落在僵尸身体内”更稳定否则子弹从僵尸头顶或脚下掠过时容易被误判。删除元素用“尾部覆盖法”把最后一个有数据的元素复制到被删除的位置然后再把计数减一。这个操作比移动后续所有元素快得多代价是实体顺序会变遍历时不保证顺序但对碰撞检测来说顺序无所谓。3.4 删除实体时别用 memset 清空新手拿到源码后喜欢把删除操作写成memset(bullets[i], 0, sizeof(Bullet))看着像“清空这个格子”。问题在于数组中间会出现空洞遍历时要反复判断 active 或 hp 是否为 0计数逻辑也会乱。尾部覆盖法能保证数组前 count 个位置全是有效数据遍历时可以少写很多无效判断。还有一个和图片尺寸相关的坑僵尸图片通常是透明底 PNG视觉上有留白直接拿图片宽高做碰撞盒会让“中弹区域”比画面大一整圈。更合理的做法是给每个实体单独维护 hitWidth 和 hitHeight例如图片 80x80碰撞盒只取中间 50x60再配合绘制函数把命中框可视化。这个后面会单独展开。4. 僵尸刷新与节奏控制难度不是随机出来的4.1 刷怪定时器用游戏时间而不是随机数僵尸不应该是每帧随机生成否则会出现“这一波挤在一起下一波空场半分钟”的断崖体验。常住做法是全局记录游戏累计时间和下一只僵尸的出场时间long long gameTime 0; long long nextZombieTime 10000; // 第一只僵尸 10 秒后出现 int leftZombies 20; // 本关剩余僵尸总数 void updateSpawner(long long delta) { gameTime delta; if (gameTime nextZombieTime leftZombies 0) { spawnOneZombie(); leftZombies--; // 剩余僵尸越少下一只来得越快保底下限 1200 毫秒 int interval 5000 (int)(leftZombies * 200); if (interval 1200) interval 1200; nextZombieTime gameTime interval; } }这个 interval 公式是最简单的难度曲线后期僵尸数量少但间隔会压缩到 1.2 秒制造连续压力。更平滑的做法是把公式改成interval 8000 - gameTime / 50000随时间线性缩短。刷怪逻辑要绑定游戏状态否则胜利界面弹出来之后还有僵尸继续进场非常出戏。我拿到这套源码后会先找主循环里有没有维护 gameTime 这个变量。如果整个工程都没有统一时钟僵尸生成、子弹移动、冷却倒计时各算各的不同帧率下游戏节奏完全不一样那就是代码里最需要优先改的部分。4.2 僵尸属性表不要在生成函数里写散落魔数僵尸不会只有一种样子最少也应该有普通、路障、铁桶三种。参数差异主要在血量、速度和击杀奖励僵尸类型血量速度击杀奖励普通僵尸20012 px/s100路障僵尸45012 px/s150铁桶僵尸7008 px/s200这个表是我习惯的数值起点不是源码必须等于多少。拿到代码后可以直接改常量数组不用去函数里翻赋值语句typedef struct { int hp; float speed; int score; } ZombieTypeInfo; ZombieTypeInfo zombieTypeInfo[3] { {200, 12, 100}, {450, 12, 150}, {700, 8, 200} }; void spawnOneZombie() { for (int i 0; i MAX_ZOMBIES; i) { if (zombies[i].hp 0) continue; int type 0; if (gameTime 60000) type 1; if (gameTime 120000) type 2; zombies[i].x gameWindowWidth - 20; zombies[i].y 90 (rand() % 5) * 80; zombies[i].type type; zombies[i].hp zombieTypeInfo[type].hp; zombies[i].speed zombieTypeInfo[type].speed; zombies[i].state 0; break; } }一个容易忽略的问题随机生成 y 坐标时两只僵尸可能落在同一行然后一前一后叠加在同一个格子上视觉上变成一只实际攻击判定却是两只。简单解决是生成前检查当前行有没有存活僵尸如果有就换到相邻行。4.3 僵尸啃植物和游戏失败判定僵尸走到植物面前要停下来啃而不是直接穿过植物。实现思路是预测下一步位置看能不能够到植物void updateZombie(int i, long long delta) { int targetX zombies[i].x (int)(zombies[i].speed * delta / 1000); for (int j 0; j plantCount; j) { if (targetX plants[j].x GRID_SIZE zombies[i].x plants[j].x) { zombies[i].state 1; // 进入啃食状态 zombies[i].x plants[j].x GRID_SIZE; break; } } if (zombies[i].state 0) { zombies[i].x targetX; } else if (zombies[i].state 1) { // 每隔固定时间扣植物血量并且画面要播放啃食动画 if (zombies[i].lastAttackTime 1000 gameTime) { plants[nearestPlantIndex].hp - 25; zombies[i].lastAttackTime gameTime; } } }失败判定和这个逻辑是绑定的僵尸走到 x 小于某个阀值比如 80 像素就说明它进入房子了游戏切换成失败状态。很多课程设计只写了“僵尸吃光植物”才失败漏了“僵尸进屋也算失败”这条规则导致玩家挂机几分钟后才发现已经输了。游戏整体状态建议用一个枚举管理从菜单、战斗中、胜利、失败到退出一次性把状态流转理清楚enum GameState { STATE_MENU, STATE_PLAYING, STATE_WIN, STATE_LOSE, STATE_QUIT }; GameState currentState STATE_MENU;主循环每帧只执行当前状态下该做的事菜单状态只处理开始按钮战斗状态才更新刷怪和碰撞胜利或失败状态弹出结算界面并等待鼠标点击重新开始。如果不做状态机最常见的表现是游戏失败后僵尸还在移动子弹还在飞行背景音乐还在继续整个程序就“失控”了。5. 避坑与常见问题EasyX 课程设计最容易翻车的六个片断5.1 编译报错找不到 graphics.h现象一打开工程点编译控制台提示fatal error C1083: Cannot open include file: graphics.h: No such file or directory。原因EasyX 没安装或者安装了但版本和当前 VS 不匹配。EasyX 的安装器会扫描本机 VS 版本如果 VS 装在非标准路径或者用的是精简版、绿色版安装器可能没识别到头文件自然没有放进 include 目录。解决重新运行 EasyX 安装包选择当前正在用的 VS 版本再装一次。然后单独建一个空项目写一行#include graphics.h测试能否编译。测试通过说明环境没有问题问题在课程设计工程属性上测试不通过就重装或者手动检查 include 目录是否被修改过。5.2 loadimage 失败背景一片黑但程序不报错现象运行后窗口能打开但只看到黑底或白底植物、僵尸、背景都没有显示程序也没有任何报错对话框。原因loadimage在文件不存在时不会弹出错误提示只会留下一个空 IMAGE 对象。最典型的原因是 Visual Studio 调试运行时的当前工作目录和 exe 所在目录不一致。工程默认工作目录是 vcxproj 所在路径而图片引用写的是./img/...实际运行去 exe 旁边找 img结果没找到。解决不要依赖于相对路径。用GetModuleFileName取当前 exe 的绝对路径截掉文件名再拼接上img文件夹得到完整图片路径TCHAR exePath[MAX_PATH]; GetModuleFileName(NULL, exePath, MAX_PATH); TCHAR dir[MAX_PATH]; wsprintf(dir, L%s\\img\\background.png, exePath); // 这里还需要去掉文件名部分只保留目录再拼接 img这个方法在 VS 里按 F5 和直接双击 exe 运行效果一致是治本的办法。赶时间的话也可以把 img 文件夹手动复制到 Debug 或 Release 输出目录里但每次清理解决方案后都要重新复制比较烦。5.3 中文显示乱码现象outtextxy输出的汉字变成一堆符号或者 Linux 上正常的中文注释在 VS 里变成乱码。原因VS 工程默认使用 Unicode 字符集而源文件可能是无 BOM 的 UTF-8。字符串常量在编译期的编码转换和运行期_outtext的输出解析不一致中文必乱。解决把源文件统一保存成 UTF-8 with BOM工程属性里的字符集和代码里字符串前缀对齐。最保险的方案是代码里的文字类内容全部用英文或拼音路径、卡片名称、游戏标题都用英文中文只出现在注释里。这样即使字符集配置不小心被改动也不会影响游戏画面上的文字显示。5.4 碰撞判定总感觉“打偏了”现象子弹明明离僵尸还有一段距离僵尸却在扣血反过来子弹穿过僵尸图片僵尸毫发无伤。原因碰撞矩形直接用了整张图片的宽高。PNG 素材往往带透明边缘或者美术上角色本身没有充满整个画布导致视觉身体和逻辑碰撞盒不一致。还有种情况是 y 方向判断误写成了“子弹中心点必须落在僵尸矩形内”坐标偏移几像素就会产生明显误判。解决给实体单独维护逻辑碰撞盒不直接用 image 宽高。图片 80x80碰撞盒宽度可以只取 40高度取 60然后写一个绘制调试框的函数把碰撞盒画出来肉眼看偏差再微调。没有可视化之前全靠猜有了可视化之后三分钟就能调准。5.5 关闭窗口后进程还在后台现象游戏窗口点关闭后消失了但任务管理器里还能看到 exeCPU 占用居高不下只有结束调试才消失。原因主循环是while(true)没有退出条件。EasyX 的关闭消息进入消息队列后程序没有处理WM_CLOSE循环继续跑。另一个原因是程序执行了return但没有调用closegraph()绘图窗口的 GDI 资源没有被释放。解决定义bool gameRunning true主循环条件写while(gameRunning)处理消息段检查msg.message WM_CLOSE时设置gameRunning false循环结束前调用closegraph()。这是我见过课程设计里最常见又最尴尬的问题演示完代码结果发现进程杀不掉。5.6 画面闪烁僵尸多的时候速度明显变慢现象窗口里图片频繁闪烁或者僵尸数量一多整个游戏速度明显下降僵尸少时又恢复。原因没有使用批量绘图每一张图片都立即提交到窗口GDI 刷新跟不上。另一个根源是固定Sleep(16)没有真实帧间隔补偿机器卡顿会把帧周期拉长游戏时间却按循环次数累加产生“僵尸越多跑得越慢”的雪崩。解决绘制函数外层套BeginBatchDraw()和FlushBatchDraw()时间基准用GetTickCount64或timeGetTime计算真实 delta不要把循环次数当成时间。6. 把 Demo 调出手感可视化命中框和参数平衡6.1 命中框可视化碰撞微调不靠猜我拿到任何一份带碰撞的游戏源码第一步就是加调试绘制。做法是在绘制函数末尾把所有实体的逻辑碰撞盒画出来void drawDebugHitbox() { setlinecolor(LIGHTRED); for (int i 0; i zombieCount; i) { rectangle(zombies[i].x 15, zombies[i].y 5, zombies[i].x zombies[i].width - 15, zombies[i].y zombies[i].height - 5); } for (int i 0; i bullets.length; i) { rectangle(bullets[i].x, bullets[i].y, bullets[i].x 14, bullets[i].y 14); } }这是给代码加调试开关的好时机用一个全局bool debugDraw通过键盘按键随时开关。演示时关掉调试时打开不用反复注释代码。6.2 把 FPS 和游戏时间打到窗口上手感问题很难靠眼睛判断但可以量化。调试期内我会在窗口左上角显示两个数字当前 FPS 和累计游戏时间。帧率长期低于 50就是绘制或者逻辑有性能问题游戏时间走得比真实时间慢就是 deltaTime 计算有误。实现方法不复杂统计一秒钟内真实经过了多少帧再绘制到图形窗口上。void drawDebugInfo() { char buff[64]; wsprintf(buff, LFPS:%d Time:%lldms, fps, gameTime); settextcolor(WHITE); outtextxy(10, 10, buff); }这段代码对答辩也有好处。老师看到窗口上有 FPS 和运行时间说明你知道性能和时间基准是怎么回事比空口讲解更有说服力。6.3 手感参数调到哪里算合适植物大战僵尸的“手感”本质是三组数值的关系植物攻击间隔、子弹速度、僵尸移动速度。攻击间隔太长玩家觉得植物在偷懒子弹速度太慢玩家要到子弹打中那一刻才觉得输出有了反馈僵尸速度太快防线还没摆好就崩。我常用的起步参数是豌豆射手攻击间隔 1.4 秒、子弹每帧 14 像素、普通僵尸速度每秒 12 像素。调整方向现象参数改法打不死僵尸子弹打在僵尸身上很久才死提高子弹伤害或降低僵尸血量植物攻击太慢僵尸走到面前时植物还没打死它缩短攻击间隔游戏太紧张第一波僵尸还没种下植物就来了推迟首刷时间降到 15 秒游戏太无聊全程不需要移动鼠标就能赢提高僵尸速度或减少植物冷却从这个角度来看课程设计代码能跑只是及格线能把参数调到有基本可玩性才算真正理解了这套源码。从那以后我每次拿到 EasyX 相关的课后作业源码都会先花十分钟做三件事确认字符集配置、加一个全局调试开关、把命中框画出来。这三件事做完后面改功能、调平衡都顺畅很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表