Dev-C++与EGE图形库:零基础实现“躺平发育”游戏原型
1. 项目缘起为什么是Dev-C与“躺平发育”如果你是一位对游戏开发感兴趣但又觉得Unity、Unreal这些引擎门槛太高或者C#、C的现代开发环境配置起来太麻烦的初学者那么你很可能和我几年前的状态一样。那时候我想做点小游戏但光是搭建一个能跑起来的开发环境就劝退了好几次。直到我重新捡起了学生时代用的Dev-C配合上一些“躺平”式的开发思路才发现原来用最“古老”的工具也能快速体验到游戏创作的乐趣。“躺平发育”这个词最近在游戏圈里挺火它描述的是一种避开激烈竞争、专注于自身成长和资源积累的玩法策略。我把这个概念挪用到了游戏开发学习上。对于新手来说一上来就追求酷炫的3D效果、复杂的网络同步无异于在“新手村”就挑战终极Boss挫败感极强。真正的“躺平发育”应该是选择一个极其轻量、几乎零配置的开发环境从最核心、最有趣的部分比如游戏逻辑直接入手快速获得正向反馈在不断的“小胜利”中积累经验和信心。Dev-C 5.11一个十几年前就停止官方更新的IDE恰恰是“躺平发育”的绝佳选择。它体积小安装包几十MB、绿色免安装解压即用、内置了MinGW编译器你不需要单独配置任何环境变量。更重要的是它支持纯Win32 API和古老的图形库如EGE、EasyX这些库的API简单直接没有现代游戏引擎那层层叠叠的抽象让你写的每一行代码都直接与屏幕上的像素、键盘的按键打交道。这种“所见即所得”的掌控感对于理解游戏底层运行机制至关重要。所以这个项目不是要教你用Dev-C开发一个商业级的游戏而是带你用这个“老伙计”快速搭建一个可玩的游戏原型体验从零到一的完整过程。我们会做一个类似“塔防”或“生存建造”概念的简易版“躺平发育”玩家控制一个角色在场景中收集资源、建造防御设施抵御周期性来袭的“怪物”。整个过程我们将聚焦于游戏循环、图形绘制、键盘交互、简单碰撞检测这些核心概念。2. 环境准备与“踩坑”指南让小熊猫Dev-C跑起来虽然Dev-C以简单著称但在今天的Windows系统上直接使用还是会遇到一些“时代差异”导致的小问题。网上搜索“小熊猫dev-c安装”的热度恰恰说明了大家在这第一步就遇到了麻烦。别担心跟着下面的步骤我们可以完美解决。2.1 获取与安装选对版本是关键首先绝对不要从一些莫名其妙的下载站获取Dev-C。里面捆绑的垃圾软件会让你苦不堪言。推荐以下两个可靠来源Embarcadero Dev-C这是原Dev-C项目停止后由Embarcadero公司维护更新的分支。它修复了原版在Windows 10/11上的很多兼容性问题是当前的首选。你可以去Embarcadero的官网或SourceForge页面下载。Orwell Dev-C这是在原版停止更新后另一个比较活跃的维护版本也修复了大量Bug。我个人的选择是Embarcadero Dev-C。下载下来通常是一个exe安装包安装过程无脑下一步即可。安装路径建议不要有中文和空格比如D:\Dev-Cpp这样可以避免一些潜在的、古老编译器可能遇到的路径解析问题。安装完成后打开Dev-C你可能会看到一个略显复古的界面。先别急着写代码我们需要进行一项关键配置设置编译器参数以支持C11标准。因为即使我们写的是C风格代码一些现代C的便利特性比如nullptr在C11下也是可用的而且这能避免一些奇怪的编译警告。操作步骤点击菜单栏的Tools-Compiler Options。在弹出的窗口中在Settings标签页下点击Code Generation。在Language standard (-std)下拉框中选择ISO C11。勾选底部的Add the following commands when calling the compiler并在下面的文本框中输入-static-libgcc -static-libstdc。这两个参数的意思是静态链接C运行库这样编译出来的exe文件可以独立在没有安装相应运行库的电脑上运行非常适合分享你的“游戏作品”。点击OK保存。注意很多教程会推荐使用-stdc11但在Dev-C的图形化界面里直接选择ISO C11即可效果是一样的。手动添加-static参数是制作独立可执行程序的必备步骤否则你的游戏在别人的电脑上可能会提示缺少libgcc_s_seh-1.dll之类的错误。2.2 图形库的选择与配置EGE vs. EasyXDev-C本身只支持控制台程序。我们要做图形游戏必须借助第三方图形库。主流的两个选择是EGE (Easy Graphics Engine)和EasyX。它们都是为C/C初学者设计的图形库API极其相似都深受早期Turbo C图形库的影响。EGE开源免费更新相对活跃一些功能也更丰富一点支持透明通道、图像旋转等。EasyX闭源但免费用于学习文档和社区资源可能更丰富一些在部分细节实现上略有不同。对于我们的“躺平发育”游戏两者都能完美胜任。我选择EGE来演示因为它开源且我个人更熟悉。下面是如何在Dev-C中配置EGE下载EGE搜索“EGE图形库”进入其GitHub仓库或官方页面下载开发包。通常是一个压缩包里面包含include头文件夹和lib库文件夹。放置文件将下载的压缩包解压。找到include文件夹下的所有.h文件以及lib文件夹下对应你编译器位数Dev-C自带的MinGW通常是32位的.a文件。将它们复制到Dev-C的安装目录下。头文件.h复制到Dev-Cpp安装目录\MinGW64\x86_64-w64-mingw32\include具体路径可能略有不同但原则是找到include文件夹。库文件.a复制到Dev-Cpp安装目录\MinGW64\x86_64-w64-mingw32\lib。更“躺平”的做法很多EGE的发布包会直接提供一个ege文件夹你只需要把这个文件夹整个丢到Dev-C安装目录的include文件夹下即可。库文件libgraphics.a同样放到lib文件夹。项目链接设置新建一个项目File-New-Project选择Empty Project给项目起名比如LyingFlatDev。然后你需要告诉编译器链接EGE的库。在项目管理器中右键你的项目名选择Project Options。切换到Parameters标签页。在Linker框里点击Add Library or Object按钮。导航到你刚才放置库文件的目录lib选择libgraphics.aEGE库文件和libgdi32.aWindows GDI库通常也需要链接。添加进去。同时在Linker框下的文本输入区域确保有-lgraphics -lgdi32 -limm32 -lmsimg32 -lole32 -loleaut32 -lwinmm -luuid这一串参数。这是链接EGE所需的所有Windows系统库。你可以直接复制粘贴进去。完成以上步骤环境就配置好了。你可以创建一个新的源文件File-New-Source File写一个简单的测试程序来验证。#include graphics.h // EGE的头文件 int main() { initgraph(640, 480); // 初始化一个640x480的图形窗口 setcolor(EGERGB(255, 0, 0)); // 设置绘图颜色为红色 circle(320, 240, 100); // 在窗口中心画一个半径为100的圆 getch(); // 等待按键 closegraph(); // 关闭图形窗口 return 0; }按F11编译运行。如果弹出一个画着红圈的窗口恭喜你你的“游戏开发环境”已经就绪这一步的顺利通过意味着你已经解决了90%初学者在“dev-c输代码”前遇到的障碍。3. 游戏框架搭建理解核心循环与基本结构在开始堆砌具体的“躺平发育”游戏逻辑之前我们必须先建立起一个稳固的游戏程序框架。这个框架是所有图形化游戏的心脏理解了它你就掌握了游戏是如何“动”起来的。3.1 消息循环与游戏主循环Windows图形程序是基于消息驱动的。但EGE/EasyX这类库为我们封装了底层消息泵提供了一个更简单的、基于“死循环”的游戏主循环模型。这是游戏开发中最核心的概念之一。一个典型的EGE游戏主循环结构如下#include graphics.h #include conio.h // 用于_kbhit和_getch int main() { initgraph(800, 600); // 初始化窗口 // 游戏初始化加载图片、设置初始变量等 // ... // 游戏主循环 while (true) { // 1. 处理输入键盘、鼠标 if (_kbhit()) { // 检查是否有按键 char key _getch(); // 根据按键更新游戏状态例如移动角色 // ... } // 2. 更新游戏逻辑 // 例如更新怪物位置、检查碰撞、计算资源增长 // ... // 3. 渲染图形 cleardevice(); // 清空屏幕相当于每一帧都重画 // 绘制背景、角色、怪物、UI等 // ... // 4. 控制帧率非常重要 delay_fps(60); // 尝试将帧率稳定在60帧/秒 } closegraph(); return 0; }让我们拆解这个循环处理输入在循环开始处我们检查用户的输入。_kbhit()和_getch()是控制台函数但在EGE窗口下同样有效用于获取键盘输入。这是实现角色移动、建造等交互的基础。更新逻辑这是游戏的“大脑”。所有游戏对象的状态都在这里根据输入和规则进行计算。比如根据按键移动玩家坐标让怪物沿着路径点移动判断子弹是否击中目标。渲染图形这是游戏的“画笔”。根据更新后的游戏状态将一切视觉元素画到屏幕上。切记要先cleardevice()清屏否则上一帧的画面会残留造成拖影。控制帧率delay_fps(60)是EGE提供的非常方便的函数它会自动计算并延迟使循环大约每秒运行60次。稳定的帧率意味着游戏速度不会受电脑性能影响而忽快忽慢。没有它你的循环会以CPU所能达到的最快速度运行游戏会快得无法控制且CPU占用率100%。3.2 为“躺平发育”设计数据结构有了循环框架我们需要数据来描述游戏世界。对于我们的简易游戏可以设计如下结构// 定义一些常量 const int SCREEN_WIDTH 800; const int SCREEN_HEIGHT 600; const int GRID_SIZE 40; // 假设我们用一个网格来管理地图每格40像素 // 玩家结构体 struct Player { int x, y; // 像素坐标 int gridX, gridY; // 网格坐标方便计算 int health; int gold; // 游戏内资源比如“金币” // 可以添加更多属性如攻击力、背包等 }; // 建筑类型枚举 enum BuildingType { NONE, WALL, TOWER, RESOURCE_GENERATOR }; // 建筑结构体 struct Building { BuildingType type; int gridX, gridY; int level; int buildProgress; // 建造进度用于表现建造过程 // 塔会有攻击力、射程等属性 }; // 怪物结构体 struct Monster { int x, y; int health; int speed; int pathIndex; // 当前所在路径点的索引 // 路径点数组假设有一条预设的进攻路径 };实操心得在项目初期不要过度设计数据结构。先用最简单的变量比如全局变量playerX, playerY把核心玩法跑通。当发现代码变得混乱时再逐步抽象成结构体或类。对于“躺平发育”这种小型项目使用全局变量和结构体数组是完全可以接受的这符合“躺平”快速迭代的精神。3.3 实现基础绘制与输入现在让我们把框架、数据和绘制结合起来实现一个可移动的“玩家”方块。#include graphics.h #include conio.h Player player; bool isRunning true; void initGame() { player.x SCREEN_WIDTH / 2; player.y SCREEN_HEIGHT / 2; player.health 100; player.gold 50; } void updateGame() { // 输入处理可以放在这里也可以放在主循环里。为了模块化我们放在这。 // 但注意EGE没有提供完美的实时键盘状态检测我们通常在主循环里用_kbhit处理。 // 这里先留空逻辑更新稍后补充。 } void renderGame() { cleardevice(); // 关键清屏 // 1. 绘制背景例如一个简单的网格 setcolor(EGERGB(100, 100, 100)); // 灰色网格线 for (int i 0; i SCREEN_WIDTH / GRID_SIZE; i) { line(i * GRID_SIZE, 0, i * GRID_SIZE, SCREEN_HEIGHT); } for (int j 0; j SCREEN_HEIGHT / GRID_SIZE; j) { line(0, j * GRID_SIZE, SCREEN_WIDTH, j * GRID_SIZE); } // 2. 绘制玩家一个填充方块 setfillcolor(EGERGB(0, 255, 0)); // 绿色填充 setcolor(EGERGB(0, 200, 0)); // 深绿色边框 fillrectangle(player.x - 15, player.y - 15, player.x 15, player.y 15); // 3. 绘制UI金币和血量 setcolor(WHITE); char info[64]; sprintf(info, Gold: %d HP: %d, player.gold, player.health); outtextxy(10, 10, info); } int main() { initgraph(SCREEN_WIDTH, SCREEN_HEIGHT); initGame(); while (isRunning) { // 处理输入 - 使用方向键移动玩家 if (_kbhit()) { char key _getch(); // _getch()对于方向键会返回两个值第一个是0或224 if (key 0 || key 224) { key _getch(); // 获取第二个值这才是方向键的扫描码 switch (key) { case 72: player.y - GRID_SIZE; break; // 上 case 80: player.y GRID_SIZE; break; // 下 case 75: player.x - GRID_SIZE; break; // 左 case 77: player.x GRID_SIZE; break; // 右 } // 简单边界检查 if (player.x 15) player.x 15; if (player.x SCREEN_WIDTH - 15) player.x SCREEN_WIDTH - 15; if (player.y 15) player.y 15; if (player.y SCREEN_HEIGHT - 15) player.y SCREEN_HEIGHT - 15; } else if (key 27) { // ESC键退出 isRunning false; } } updateGame(); // 目前是空的后续补充游戏逻辑 renderGame(); // 绘制 delay_fps(60); } closegraph(); return 0; }编译运行这个程序你应该能看到一个带网格的背景一个可以用方向键控制的绿色方块以及左上角显示的金币和血量。至此一个最基础的游戏引擎框架和可视化交互就完成了。这已经是一个“可玩”的雏形虽然它现在还什么都没做但所有扩展都将基于这个稳固的循环。4. 核心玩法实现建造、资源与怪物现在我们有了一个可以移动的“主角”。接下来我们要为这个世界注入灵魂资源系统、建造系统和敌人系统。这才是“躺平发育”游戏性的核心。4.1 资源生成与收集逻辑资源是驱动游戏进程的血液。我们设计一种最简单的形式地图上随机生成资源点比如金币玩家靠近即可自动收集。首先我们需要一个资源点的结构体和数组来管理它们struct ResourceNode { int x, y; bool isActive; int value; // 这个资源点提供的金币数 }; const int MAX_RESOURCES 20; ResourceNode resources[MAX_RESOURCES]; void initResources() { for (int i 0; i MAX_RESOURCES; i) { resources[i].isActive false; // 初始全部无效 } } void spawnResource() { // 随机在地图上生成一个资源点 for (int i 0; i MAX_RESOURCES; i) { if (!resources[i].isActive) { resources[i].x rand() % (SCREEN_WIDTH - 30) 15; resources[i].y rand() % (SCREEN_HEIGHT - 30) 15; resources[i].isActive true; resources[i].value 10 rand() % 20; // 10-29金币 break; // 一次只生成一个 } } } void updateResources() { // 检查玩家与所有资源点的碰撞收集 for (int i 0; i MAX_RESOURCES; i) { if (resources[i].isActive) { // 简单矩形碰撞检测 int dx player.x - resources[i].x; int dy player.y - resources[i].y; int distanceSquared dx * dx dy * dy; // 假设玩家和资源点都是半径为15的圆 if (distanceSquared (15 15) * (15 15)) { player.gold resources[i].value; resources[i].isActive false; // 收集后消失 } } } // 每隔一段时间生成一个新资源点简单计时器实现 static int spawnTimer 0; spawnTimer; if (spawnTimer 180) { // 假设60帧每秒即3秒生成一个 spawnResource(); spawnTimer 0; } } void renderResources() { setfillcolor(EGERGB(255, 255, 0)); // 黄色金币 for (int i 0; i MAX_RESOURCES; i) { if (resources[i].isActive) { // 画一个简单的圆形代表金币 fillellipse(resources[i].x, resources[i].y, 10, 10); } } }在initGame中调用initResources()和spawnResource()初始生成几个。在updateGame函数中调用updateResources()在renderGame函数中调用renderResources()在绘制玩家之后UI之前。现在你的绿色方块靠近黄色圆点时金币数就会增加4.2 建造系统从选择到放置建造是“躺平发育”的核心策略。我们需要实现一个建造模式按下某个键比如‘B’进入建造模式选择建筑类型墙、塔然后在网格上预览并放置。Building buildings[100]; // 假设地图上最多100个建筑 int buildingCount 0; bool isBuildingMode false; BuildingType selectedBuildingType WALL; int previewGridX -1, previewGridY -1; // 预览建筑的网格坐标 void handleBuildingInput() { if (_kbhit()) { char key _getch(); if (key b || key B) { isBuildingMode !isBuildingMode; // 切换建造模式 if (!isBuildingMode) { // 退出建造模式时清除预览 previewGridX previewGridY -1; } } if (isBuildingMode) { // 在建造模式下用数字键选择建筑类型 if (key 1) selectedBuildingType WALL; else if (key 2) selectedBuildingType TOWER; // 鼠标交互EGE获取鼠标状态 mouse_msg msg {0}; while (mousemsg()) { msg getmouse(); } if (msg.is_move() || msg.is_down()) { // 将鼠标坐标转换为网格坐标 previewGridX msg.x / GRID_SIZE; previewGridY msg.y / GRID_SIZE; // 鼠标左键按下放置建筑 if (msg.is_left() msg.is_down()) { // 检查该网格是否已被占用 bool canPlace true; for (int i 0; i buildingCount; i) { if (buildings[i].gridX previewGridX buildings[i].gridY previewGridY) { canPlace false; break; } } // 检查金币是否足够假设墙50金塔150金 int cost (selectedBuildingType WALL) ? 50 : 150; if (canPlace player.gold cost) { buildings[buildingCount].type selectedBuildingType; buildings[buildingCount].gridX previewGridX; buildings[buildingCount].gridY previewGridY; buildings[buildingCount].level 1; buildings[buildingCount].buildProgress 0; buildingCount; player.gold - cost; } } } } } } void updateBuildings() { for (int i 0; i buildingCount; i) { // 更新建筑建造进度 if (buildings[i].buildProgress 100) { buildings[i].buildProgress 2; // 每帧增加2% if (buildings[i].buildProgress 100) buildings[i].buildProgress 100; } // 可以在这里添加塔的攻击逻辑如果建筑是塔且已建成 if (buildings[i].type TOWER buildings[i].buildProgress 100) { // ... 遍历怪物数组寻找目标发射子弹等 } } } void renderBuildingsAndPreview() { // 绘制已放置的建筑 for (int i 0; i buildingCount; i) { int pixelX buildings[i].gridX * GRID_SIZE GRID_SIZE / 2; int pixelY buildings[i].gridY * GRID_SIZE GRID_SIZE / 2; color_t buildingColor; switch (buildings[i].type) { case WALL: buildingColor EGERGB(139, 69, 19); break; // 棕色 case TOWER: buildingColor EGERGB(70, 130, 180); break; // 钢蓝色 default: buildingColor WHITE; } setfillcolor(buildingColor); // 如果还在建造用半透明色或根据进度绘制 if (buildings[i].buildProgress 100) { setfillcolor(EGERGB(100, 100, 100)); // 灰色表示在建 // 可以画一个进度条 int progressWidth (GRID_SIZE - 4) * buildings[i].buildProgress / 100; bar(pixelX - GRID_SIZE/2 2, pixelY - GRID_SIZE/2 2, pixelX - GRID_SIZE/2 2 progressWidth, pixelY GRID_SIZE/2 - 2); } fillrect(pixelX - GRID_SIZE/2 2, pixelY - GRID_SIZE/2 2, pixelX GRID_SIZE/2 - 2, pixelY GRID_SIZE/2 - 2); } // 绘制建造模式下的预览 if (isBuildingMode previewGridX 0 previewGridY 0) { int previewPixelX previewGridX * GRID_SIZE GRID_SIZE / 2; int previewPixelY previewGridY * GRID_SIZE GRID_SIZE / 2; setlinewidth(2); color_t previewColor (selectedBuildingType WALL) ? EGERGB(160, 82, 45) : EGERGB(100, 149, 237); setcolor(previewColor); // 画一个半透明的矩形作为预览 setfillcolor(EGERGB(previewColor.red, previewColor.green, previewColor.blue, 128)); // ARGB颜色128是半透明 fillrect(previewPixelX - GRID_SIZE/2 2, previewPixelY - GRID_SIZE/2 2, previewPixelX GRID_SIZE/2 - 2, previewPixelY GRID_SIZE/2 - 2); setlinewidth(1); } }将handleBuildingInput整合到主循环的输入处理部分放在_kbhit判断里注意处理按键冲突。在updateGame中调用updateBuildings在renderGame中调用renderBuildingsAndPreview在绘制资源点之后UI之前。现在你按下B键就可以用鼠标预览并放置墙和塔了4.3 怪物系统路径点与简单AI没有敌人的“发育”是孤独的。我们来创建一些沿着固定路径进攻的简单怪物。struct Monster { float x, y; // 使用浮点数让移动更平滑 int health; float speed; int pathIndex; bool isActive; }; const int MAX_MONSTERS 50; Monster monsters[MAX_MONSTERS]; // 假设一条简单的进攻路径一系列屏幕坐标点 POINT monsterPath[] { {50, 50}, {400, 50}, {400, 400}, {700, 400}, {700, 200} }; const int PATH_POINT_COUNT sizeof(monsterPath) / sizeof(monsterPath[0]); void spawnMonster() { for (int i 0; i MAX_MONSTERS; i) { if (!monsters[i].isActive) { monsters[i].x (float)monsterPath[0].x; monsters[i].y (float)monsterPath[0].y; monsters[i].health 30; monsters[i].speed 0.5f (rand() % 10) / 20.0f; // 0.5 ~ 1.0 的速度 monsters[i].pathIndex 1; // 目标点是路径上的下一个点 monsters[i].isActive true; break; } } } void updateMonsters() { for (int i 0; i MAX_MONSTERS; i) { if (!monsters[i].isActive) continue; // 获取当前目标路径点 int targetIdx monsters[i].pathIndex; float targetX (float)monsterPath[targetIdx].x; float targetY (float)monsterPath[targetIdx].y; // 计算朝向目标的向量 float dx targetX - monsters[i].x; float dy targetY - monsters[i].y; float distance sqrt(dx * dx dy * dy); // 如果已经非常接近目标点则切换到下一个点 if (distance monsters[i].speed) { monsters[i].pathIndex; // 如果走完了所有路径点怪物到达终点对玩家造成伤害 if (monsters[i].pathIndex PATH_POINT_COUNT) { player.health - 5; // 扣血 monsters[i].isActive false; // 怪物消失 continue; } // 更新目标点 targetIdx monsters[i].pathIndex; targetX (float)monsterPath[targetIdx].x; targetY (float)monsterPath[targetIdx].y; dx targetX - monsters[i].x; dy targetY - monsters[i].y; distance sqrt(dx * dx dy * dy); } // 向目标点移动 if (distance 0) { monsters[i].x (dx / distance) * monsters[i].speed; monsters[i].y (dy / distance) * monsters[i].speed; } // 简单碰撞检测怪物与墙这里仅作示例实际需要更精细的网格碰撞 for (int j 0; j buildingCount; j) { if (buildings[j].type WALL buildings[j].buildProgress 100) { int buildingCenterX buildings[j].gridX * GRID_SIZE GRID_SIZE / 2; int buildingCenterY buildings[j].gridY * GRID_SIZE GRID_SIZE / 2; float mdx monsters[i].x - buildingCenterX; float mdy monsters[i].y - buildingCenterY; if (mdx * mdx mdy * mdy (GRID_SIZE/2 10) * (GRID_SIZE/2 10)) { // 简单处理让怪物稍微弹开或停止这里简化仅停止 // 更复杂的可以寻路绕开 monsters[i].x - (dx / distance) * monsters[i].speed; monsters[i].y - (dy / distance) * monsters[i].speed; } } } } // 怪物生成计时器 static int monsterSpawnTimer 0; monsterSpawnTimer; if (monsterSpawnTimer 300) { // 5秒生成一个 spawnMonster(); monsterSpawnTimer 0; } } void renderMonsters() { setfillcolor(EGERGB(255, 0, 0)); // 红色怪物 for (int i 0; i MAX_MONSTERS; i) { if (monsters[i].isActive) { fillellipse((int)monsters[i].x, (int)monsters[i].y, 12, 12); // 可以画一个简单的血条 setfillcolor(GREEN); int healthBarWidth 20 * monsters[i].health / 30; bar((int)monsters[i].x - 10, (int)monsters[i].y - 20, (int)monsters[i].x - 10 healthBarWidth, (int)monsters[i].y - 15); setfillcolor(EGERGB(255, 0, 0)); } } }同样在initGame中初始化怪物数组在updateGame中调用updateMonsters在renderGame中调用renderMonsters顺序建议在建筑之后UI之前。现在红色的怪物会沿着预设的黄色路径点你可以在renderGame里画出来向终点移动撞击终点会扣减玩家血量它们还会尝试绕过你建造的墙虽然现在的碰撞处理很简陋。5. 打磨与优化让游戏更像样现在我们的游戏已经有了核心玩法循环移动、收集、建造、防御。但它还很粗糙。下面进行一些关键的打磨让体验更完整。5.1 游戏状态管理与胜负判定一个完整的游戏需要有开始、运行、结束的状态。我们添加一个简单的游戏状态机。enum GameState { MENU, PLAYING, GAME_OVER }; GameState currentState MENU; void renderMenu() { cleardevice(); setcolor(YELLOW); setfont(48, 0, 黑体); outtextxy(SCREEN_WIDTH/2 - textwidth(躺平发育)/2, 100, 躺平发育); setfont(24, 0, 宋体); outtextxy(SCREEN_WIDTH/2 - textwidth(按 Enter 键开始游戏)/2, 300, 按 Enter 键开始游戏); outtextxy(SCREEN_WIDTH/2 - textwidth(按 ESC 键退出)/2, 350, 按 ESC 键退出); } void updateGameLogic() { // 原来的updateGame改名为这个 if (currentState ! PLAYING) return; updateResources(); updateBuildings(); updateMonsters(); // 胜负判定 if (player.health 0) { currentState GAME_OVER; } // 可以添加胜利条件例如坚守一定波数 } void renderGameScene() { // 原来的renderGame改名为这个 cleardevice(); // 绘制网格背景、资源、建筑、怪物、玩家、UI... // ... (将所有之前的render函数调用整合到这里) renderResources(); renderBuildingsAndPreview(); renderMonsters(); // 绘制玩家 setfillcolor(EGERGB(0, 255, 0)); fillrectangle(player.x - 15, player.y - 15, player.x 15, player.y 15); // 绘制UI setcolor(WHITE); char info[128]; sprintf(info, Gold: %d HP: %d State: %s, player.gold, player.health, isBuildingMode ? BUILDING (1:Wall 2:Tower) : NORMAL (B:Toggle Build)); outtextxy(10, 10, info); } void renderGameOver() { cleardevice(); setcolor(RED); setfont(48, 0, 黑体); outtextxy(SCREEN_WIDTH/2 - textwidth(游戏结束)/2, 200, 游戏结束); setfont(24, 0, 宋体); char scoreMsg[64]; sprintf(scoreMsg, 最终金币: %d, player.gold); outtextxy(SCREEN_WIDTH/2 - textwidth(scoreMsg)/2, 300, scoreMsg); outtextxy(SCREEN_WIDTH/2 - textwidth(按 R 键重新开始)/2, 350, 按 R 键重新开始); } // 主循环需要重构成这样 while (isRunning) { // 输入处理需要根据状态分发 if (_kbhit()) { char key _getch(); if (currentState MENU) { if (key 13) { // Enter initGame(); // 重置游戏数据 currentState PLAYING; } else if (key 27) { isRunning false; } } else if (currentState PLAYING) { // ... 原有的游戏内输入处理移动、建造等 if (key 27) currentState MENU; // 游戏中按ESC回菜单 } else if (currentState GAME_OVER) { if (key r || key R) { initGame(); currentState PLAYING; } else if (key 27) { currentState MENU; } } } // 更新与渲染 if (currentState PLAYING) { updateGameLogic(); } // 根据状态渲染不同场景 switch (currentState) { case MENU: renderMenu(); break; case PLAYING: renderGameScene(); break; case GAME_OVER: renderGameOver(); break; } delay_fps(60); }5.2 性能优化与代码组织随着游戏对象增多你会发现帧率下降。优化是必须的。避免在循环中频繁创建/销毁对象我们使用的固定大小的数组resources[MAX_RESOURCES]就是一种对象池的雏形这比频繁new/delete或malloc/free高效得多。减少不必要的绘制目前我们每一帧都清屏并重画所有元素cleardevice。对于这种像素不多的小游戏完全够用。但如果元素非常多可以考虑脏矩形渲染只更新变化的部分。不过在Dev-C和EGE环境下cleardevice的开销通常可以接受。代码模块化将不同功能的代码分到不同的.cpp和.h文件中。例如创建game.h声明全局变量和函数player.cpp、building.cpp、monster.cpp、resource.cpp分别实现各自的对象逻辑main.cpp只负责主循环和状态调度。这会让代码更易读、易维护。在Dev-C中你只需要在项目中添加这些源文件即可。5.3 音效与图像进阶EGE支持播放wav格式音效和加载bmp,jpg,png等格式图片。音效使用mciSendString函数需要#include windows.h和#pragma comment(lib, winmm.lib)可以播放背景音乐和音效。图像使用PIMAGE类型和newimage(),getimage(),putimage()等函数可以加载并显示图片让你的游戏画面更精美。例如替换掉用fillrectangle画的玩家改用一张精灵图PIMAGE imgPlayer newimage(); getimage(imgPlayer, player.bmp); // 假设有一张60x60的玩家图片 // 在渲染玩家时 putimage(player.x - 30, player.y - 30, imgPlayer);踩坑提示图片路径问题。确保图片文件放在你的项目.dev文件所在的目录或者使用绝对路径。加载失败时getimage不会报错但会显示一片黑色。另外复杂的图像操作如图像旋转、缩放在EGE中性能开销较大在小游戏中需谨慎使用。至此一个功能相对完整的“躺平发育”游戏原型就完成了。从零开始我们只用了一个轻量级的Dev-C和EGE图形库就实现了游戏的核心循环、图形渲染、键盘鼠标输入、对象管理、碰撞检测、状态机等关键概念。这个过程本身就是一种高效的“躺平发育”——避开复杂引擎的沉重直击游戏创作乐趣的核心。你可以在此基础上继续扩展给塔添加攻击子弹、设计不同的怪物波次、加入更多建筑类型和科技树、甚至实现一个简单的存档系统。最重要的是你拥有了一个完全由自己代码驱动的、可任意修改的游戏世界这份成就感是使用现成引擎拖拽组件难以比拟的。