ARTICLE DETAIL

资讯详情

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

C++ EasyX推箱子实战:从二维数组到BFS自动求解

C++ EasyX推箱子实战:从二维数组到BFS自动求解 简介这是一份基于C和EasyX图形库开发的推箱子小游戏项目文档面向C/C初学者与游戏编程入门者主要解决从零开发2D小游戏时涉及的图形绘制、地图建模和按键交互问题。压缩包内共1个docx文档大小约799KB内容包含完整源代码、地图二维数组定义、碰撞检测逻辑、音效播放设置等关键部分便于在课程设计或自学时直接对照阅读。目前已有1150人学习下载。文档以EasyX的graphics.h为核心展示了用数值区分墙体、地板、箱子、目的地与玩家的地图设计思路并通过putimage完成游戏场景渲染同时逐步分析地图初始化与角色移动等函数的职责划分、玩家移动时对前后两格状态的判断、推箱子的合法条件以及胜利检测等扩展玩法读者既可逐段理解实现原理也能在此基础上改造地图与关卡达到巩固C语法和逻辑思维的目的。1. 基于 C 和 EasyX 的推箱子为什么说这是算法入门最该做的小项目很多人在学完 C 语法、看过几篇 STL 教程之后会陷入一个典型的困境语法都认识但拿到一个需求不知道怎么下手。推箱子这个小游戏恰好卡在这个节点上——它不需要网络、不需要数据库、不需要复杂的工程架构但要跑通一个完整版本你必须把二维数组、键控逻辑、碰撞检测、胜负判定全部串起来。而 EasyX 图形库把绘图和键盘输入封装得足够薄让你能把注意力放在游戏逻辑本身而不是赶在 OpenGL 或 Win32 消息循环里挣扎。这大概也是为什么“基于 C 和 EasyX 写推箱子”会成为这么多学校的大作业选题——它不是玩具是把 C 功底和基础数据结构打成实战能力的磨刀石。你的地图本质上是一张二维矩阵人的每一步移动都是对矩阵状态的合法改写而 BFS 最少步数求解器又会让这款小游戏变成你验证图论算法的活体实验台。如果你能独立完成这个项目并把细节踩平BFS、DFS、状态搜索这些算法就不再是卷子上的分数线而是你亲手调试过的代码路径。2. 推箱子到底在做什么把游戏规则拆成三个可编码的模块2.1 地图建模用二维数组定义“墙、地、箱子、目标、人”推箱子的核心数据结构就是一张由字符或枚举填充的二维矩阵。常见做法是定义一个vectorvectorchar或vectorstring用不同的字符代表五种元素#代表墙、 代表空地、$代表箱子、.代表目标点、代表玩家。这里有一个非常关键的设计细节——目标点在被箱子占着的时候它在地上留下的标记不能被覆盖掉否则你无法判断胜利条件。所以很多实现里会引入*表示“箱子上有目标点”表示“人站在目标点上”这种状态提升是推箱子实现中第一个值得你反复体会的抽象技巧。我在自己的实现里用的是枚举类型enum Cell { WALL, FLOOR, BOX, TARGET, PLAYER, BOX_ON_TARGET, PLAYER_ON_TARGET }。为什么不用字符因为字符在地图解析时很好用但进入游戏逻辑后枚举让 switch 和 if 的判断变得更安全不容易因为手滑打错字符而翻车。地图文件则直接用纯文本每行字符串对应矩阵一行加载时逐字符映射到枚举顺便记录玩家的初始坐标。为了调试方便我建议在内存里维护两张地图——一张是原始关卡图用于重置一张是当前游戏状态图用于渲染和逻辑这样按 R 键重开一局就只是做一次深拷贝不需要重新读文件。2.2 移动逻辑坐标变换与三步合法性校验玩家的每一次移动本质上是对“目的坐标”和“目的坐标的下一个坐标”做检查。以向上移动为例player.x - 1是玩家的下一步坐标nextplayer.x - 2是再往前一格的坐标next2。合法性判断分三种情况next是墙直接拒绝next是空地或目标点玩家直接走过去next是箱子或箱上目标点则必须继续看next2只有next2是空地或目标点不是墙、不是箱子时箱子才能被推动。这段逻辑看起来简单但我见过太多新手在这里写出又臭又长的 if 嵌套。我的做法是把方向统一抽象成偏移量数组dx[4] {-1, 1, 0, 0}和dy[4] {0, 0, -1, 1}然后写一个movePlayer(dx, dy)函数对上下左右四个方向一视同仁。这样你就不用复制四遍几乎相同的代码了。判断箱子能不能动我封装了一个canPush(boxX, boxY, dx, dy)返回布尔值让主逻辑保持一行if (canPush) doPush(...)的清爽结构。注意箱子推动后原位置如果本来就是目标点需要恢复成 TARGET 而不是 FLOOR这个细节漏掉的话你在后面做胜利判定时会发现箱子始终“少一个”。2.3 胜负判定与渲染刷新把状态变化及时画到屏幕上胜利条件非常明确所有箱子都落在目标点上。你不需要每帧遍历整个地图去数箱子数量只需要在每次箱子移动后判断被移动的箱子是否停在了目标点上同时维护一个全局计数boxesRemaining箱子一旦到位就减一一旦被推离目标点就加一。当boxesRemaining 0时触发胜利画面。这种增量判定比全量扫描更高效也更符合 EasyX 这种即时刷新模型的需求。渲染部分EasyX 提供的loadimage和putimage足够用了。我一般会提前加载好五种图片墙、地、箱子、目标、人并在每一帧循环里用cleardevice()清屏然后按矩阵重新绘制。这里有个性能认知要纠正一下推箱子地图通常 10x10 左右即使每帧全量重绘也绝对跑得满 60FPS完全没必要做局部脏矩形刷新。真正值得注意的反而是BeginBatchDraw和EndBatchDraw的配对使用——不加批量绘制的话箱子移动的时候画面会有肉眼可见的闪烁这个问题会在后面避坑章节单独讨论。3. 搭建 C 和 EasyX 开发环境VS Code 与 Visual Studio 的取舍和配置细节3.1 EasyX 只支持 Visual Studio 系但你可以用 VS Code 写代码再交给 MSVC 编译先泼一盆冷水EasyX 是一个基于 Windows GDI 的图形库它的头文件和库文件是定向提供给 Microsoft Visual C 工具链的官方安装包本身就只检测 Visual Studio 或 Visual Studio Build Tools 的环境。这意味着网上常见的“VS Code 配置 C/C 环境跑 EasyX”不是直接选 g 编译器就能解决的因为 MinGW 的头文件路径里没有graphics.h而 EasyX 在 2021 年之后的版本对 MinGW 的支持也非常僵硬。所以常见做法是在 VS Code 里写代码然后用 Visual Studio 的 MSVC 编译器来构建或者干脆直接装一个 Visual Studio Community 用 IDE 跑。这里我推荐一条相对平滑的路径先单独安装 Visual Studio Build Tools勾选“使用 C 的桌面开发”工作负载。这样你不需要打开庞大的 VS IDE 就能拿到 MSVC 编译器。然后在 VS Code 里安装 C/C 扩展配置tasks.json时把command指向cl.exe的完整路径。但要注意MSVC 不是像 g 那样一条命令就能用的它依赖一系列环境变量INCLUDE、LIB、PATH常见做法是打开“x64 Native Tools Command Prompt”运行code命令启动 VS Code这样集成终端自动继承了 MSVC 环境再在tasks.json里调cl /std:c17 /EHsc /I EasyX目录 xxx.cpp /link user32.lib gdi32.lib。3.2 如果你不想折腾 VS Code直接 Visual Studio EasyX 安装包是零配置路线如果你是非要用 VS Code 不可但又不想在 tasks.json 里折腾 cl.exe 的环境变量还有一个更省事的变通方案用 VS Code 写代码和调试但构建和运行交给一个.bat脚本。脚本里先call C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat -archx64然后执行cl编译命令。这样做的核心原因是 MSVC 的 cl.exe 对当前目录和头文件搜索路径极度敏感直接用 VS Code 的默认终端调 cl 很容易报“无法打开包括文件: graphics.h: No such file or directory”。这不是你代码的问题是编译环境没被激活的经典翻车点。EasyX 本身的安装则非常无脑——从官网下载安装包并运行它会自动识别系统里已有的 Visual Studio 版本并写入头文件和库文件的引用路径。安装完后你不需要手动把graphics.h拷贝到任何地方也不需要手动配置附加依赖项因为 EasyX 安装程序已经帮你在 IDE 的全局属性里挂好了链接配置。这里唯一要记住的是EasyX 提供的头文件有两种引用方式#include graphics.h是传统 2D 绘图接口#include easyx.h是新版接口。如果你看到了#include graphics.h的教程那多半是旧项目不影响使用但建议优先用尖括号版。3.3 执行流程与目录组织可维护的单文件到多文件演进很多教程会让你把全部代码塞进一个main.cpp——这对教学可以但我要给你的建议是至少拆成三个文件再动手map.h和map.cpp负责地图加载、关卡解析和碰撞检测game.cpp负责玩家移动逻辑和胜负判定main.cpp只放 EasyX 窗口初始化和主循环。这样做的好处很直接——你的推箱子游戏做到中期一定会想加“下一关”“重新开始”“步数统计”如果全部堆在一个文件里函数之间互相耦合改一个功能要牵连全局。拆文件之后map.cpp只暴露loadLevel(int id)、isValidMove(int x, int y)这类接口game.cpp只关心逻辑状态。文件拆分的另一个好处是支持增量编译。地图文件我建议单独建一个levels.txt用分隔不同关卡加载时用ifstream读取并按分隔符切分。地图的坐标系在文件里是从第 0 行开始的但在屏幕上渲染时我会统一加一个TILE_SIZE常量EasyX 下推荐 64 像素做偏移这样地图数据和渲染坐标互相独立后面你要调整窗口大小或者缩放地图就只需要改一个常量。4. 核心代码实现地图加载、移动控制和 BFS 自动求解器4.1 地图加载模块从文本文件到内存矩阵下面是我常用的一组加载代码它把levels.txt中按分隔的关卡切分出来并把字符映射到枚举矩阵。// map.cpp #include map.h #include fstream #include sstream bool GameMap::loadFromFile(const std::string path, int levelId) { std::ifstream file(path); if (!file.is_open()) return false; std::string content((std::istreambuf_iteratorchar(file)), std::istreambuf_iteratorchar()); std::stringstream ss(content); std::string segment; int currentId 0; while (std::getline(ss, segment, )) { // 去掉可能出现的换行符残留 if (segment.size() 3) continue; if (currentId levelId) { parseSegment(segment); return true; } currentId; } return false; } void GameMap::parseSegment(const std::string segment) { std::stringstream lineStream(segment); std::string line; grid.clear(); playerPos {0, 0}; while (std::getline(lineStream, line)) { if (line.empty() || line[0] \r) continue; std::vectorCell row; for (char ch : line) { switch (ch) { case #: row.push_back(WALL); break; case : row.push_back(FLOOR); break; case $: row.push_back(BOX); break; case .: row.push_back(TARGET); break; case : row.push_back(PLAYER); playerPos {static_castint(row.size() - 1), static_castint(grid.size())}; break; case *: row.push_back(BOX_ON_TARGET); break; case : row.push_back(PLAYER_ON_TARGET); playerPos {static_castint(row.size() - 1), static_castint(grid.size())}; break; default: row.push_back(WALL); } } if (!row.empty()) grid.push_back(row); } initState grid; }核心逻辑说明loadFromFile用getline(ss, segment, )按字符切分所以你在关卡的结束标记处写会被拆成三截而我用segment.size() 3跳过空段。parseSegment中读取行时每一行末尾的\r在 Windows 文本模式下会被自动去除但如果是 Git 换行符设置导致文件里混入了\r那一行字符串会明显异常所以代码里主动对\r做了判断。initState这个成员用于重开关卡时恢复初始矩阵比重新读文件快了不止一个量级。参数调整建议grid的类型是std::vectorstd::vectorCell如果你的关卡很大比如 20x20每次深拷贝initState会有一些开销但推箱子的典型关卡在 10x10 到 15x15 之间完全不用担心。如果你要支持多个关卡来回切换你需要维护一个std::vectorGridState的数组而不是单个initState。4.2 玩家移动与箱子推动增量状态切换// game.cpp bool GameLogic::tryMove(int dx, int dy) { int px map.playerPos.x dx; int py map.playerPos.y dy; Cell nextCell map.grid[py][px]; if (nextCell WALL) return false; // 如果前方是箱子再检查箱子前方 if (nextCell BOX || nextCell BOX_ON_TARGET) { int bx px dx; int by py dy; Cell beyondCell map.grid[by][bx]; if (beyondCell WALL || beyondCell BOX || beyondCell BOX_ON_TARGET) { return false; } // 移动箱子先恢复箱子的原位置 map.grid[py][px] (nextCell BOX_ON_TARGET) ? TARGET : FLOOR; // 箱子新位置如果是目标点标记为 BOX_ON_TARGET map.grid[by][bx] (beyondCell TARGET) ? BOX_ON_TARGET : BOX; // 更新胜利计数 if (nextCell BOX_ON_TARGET) boxesPlaced--; if (beyondCell TARGET) boxesPlaced; } // 移动玩家原位置如果是目标点恢复 TARGET Cell currentPlayerCell map.grid[map.playerPos.y][map.playerPos.x]; map.grid[map.playerPos.y][map.playerPos.x] (currentPlayerCell PLAYER_ON_TARGET) ? TARGET : FLOOR; // 玩家新位置如果踩到目标点标记 PLAYER_ON_TARGET map.grid[py][px] (nextCell TARGET) ? PLAYER_ON_TARGET : PLAYER; map.playerPos {px, py}; steps; return true; }这段代码的关键在“先恢复后设置”的顺序。移动箱子时你必须在覆盖(py, px)之前把这个位置原本的BOX_ON_TARGET还原成TARGET否则箱子虽然走了但那个目标点被永远错误地标记成了地板最后胜利判定会少一个目标点。其次boxesPlaced的增减逻辑是“先减后加”如果箱子原本就在目标点上先把它移出目标点计数减一再判断新位置是否是目标点是则加一。还有一个我现在觉得非常值得强调的细节这里判断箱子前方用的是“不能是墙、不能是箱子”但不要忘了BOX_ON_TARGET本质上也还是箱子你如果把 “箱子已在目标点” 当成普通目标点去踩箱子会被穿透。这大概是推箱子新手最容易遇到的“箱子穿墙”类玄学 bug根源就在于没有把BOX_ON_TARGET当箱子处理。4.3 BFS 自动求解器验证你的推箱子关卡是否真的有解当你做完基本游戏后一个常见的需求是验证自制关卡有没有解。这里我直接上 BFS 状态搜索——把“玩家位置 所有箱子位置”作为状态节点搜索方向就是四个移动。注意 BFS 的具体实现是快速验证可行性的捷径但不是最优解因为它默认每个节点的权重相同只能保证找“最少步数”而不是“最短路径损耗”。// solver.cpp #include queue #include set #include tuple struct State { int playerX, playerY; std::vectorstd::pairint,int boxPositions; int steps; }; bool operator(const State a, const State b) { if (a.playerX ! b.playerX) return a.playerX b.playerX; if (a.playerY ! b.playerY) return a.playerY b.playerY; return a.boxPositions b.boxPositions; } int bfsShortest(GameMap map) { std::queueState q; std::setState visited; State start {map.playerPos.x, map.playerPos.y, /*收集箱子坐标*/ {}, 0}; q.push(start); visited.insert(start); int dx[4] {-1, 1, 0, 0}; int dy[4] {0, 0, -1, 1}; while (!q.empty()) { State cur q.front(); q.pop(); // 胜利判定每个箱子都在目标点 if (allBoxesOnTarget(cur.boxPositions, map)) return cur.steps; for (int i 0; i 4; i) { int nx cur.playerX dx[i]; int ny cur.playerY dy[i]; // 复用 GameLogic 的碰撞判定如果合法就生成新状态 // 新状态要注意如果人推了箱子boxPositions 中对应箱子的坐标要更新 } } return -1; // 无解 }参数说明这里State的operator重载是给std::set用的因为std::set默认用比较器。boxPositions必须排序后才比较否则同一组箱子坐标在不同顺序下会被误认为不同状态BFS 就无法去重。实际项目里我一般会把箱子坐标压成一个 64 位整数编码(x 16) | y然后按整体比较性能更好但可读性差教学代码里保持operator更容易理解。搜索空间的上限是玩家位置不超过地图格子数每个箱子位置不超过地图格子数理论上最坏情况是爆炸性组合。但对于标准推箱子关卡BFS 能在几秒内出结果。如果你发现求解器一直跑不出来多半是状态去重不完整BOX 和 BOX_ON_TARGET 没有归一化或者 BFS 遍历了循环移动路径。5. 推箱子落地的 6 个常见坑数组越界、非法移动和渲染顺序5.1 越界崩溃地图边缘的访问越界不报错但行为诡异现象玩家把箱子推到地图最边缘程序偶尔闪退或者画面出现雪花状噪点而不是稳定崩溃。原因tryMove里检查nextCell时直接索引了grid[py][px]如果py或px超出了grid的行数或列数这是未定义行为。在 Debug 模式下可能会触发断言弹窗在 Release 模式下可能读到相邻内存数据导致箱子“飞到”墙里或者消失。解决在所有索引前加上边界守卫。我习惯写一个宏或内联函数bool isInside(int x, int y) { return y 0 y grid.size() x 0 x grid[0].size(); }。然后在tryMove最前面判断不在边界内直接return false。不要觉得这是多余——推箱子玩家最喜欢做的事就是故意把箱子往墙角怼你的代码必须对这种操作零容忍。5.2 箱子推穿墙把“箱子在目标点上”当成普通地板现象箱子明明已经在目标点上*人走到箱子旁边推它箱子“穿过”墙继续移动。原因在判断箱子前方beyondCell时只写了if (beyondCell WALL || beyondCell BOX) return false;漏掉了BOX_ON_TARGET。由于BOX_ON_TARGET在地图逻辑上是“目标点 箱子”的复合体你不能拿它当作普通地板。解决把BOX_ON_TARGET加入不可通过列表。这类问题最典型的特征是“只在目标点上的箱子能穿墙”如果你在用 EasyX 调试时发现这种玄学你要立刻意识到是复合状态没处理干净。更稳健的做法是给Cell加一个辅助函数bool isBox(Cell c) { return c BOX || c BOX_ON_TARGET; }让所有箱子判定统一走这个函数避免到处手写两个条件。5.3 画面闪烁每一帧单次绘制造成的撕裂感现象箱子每移动一格画面就像眨了一下眼尤其连续快速移动时闪烁严重。原因EasyX 默认是单缓冲绘制每次putimage直接写显存画面刷新过程中就会闪。窗口越大地图图块越多闪烁越明显。解决在窗口初始化后调用BeginBatchDraw()每帧所有绘制结束后调用EndBatchDraw()或者FlushBatchDraw()。BeginBatchDraw让所有绘图操作先写到内存中的缓冲画布上最后一次交付给显存整个过程对用户不可见。我自己的经验是批绘制对 EasyX 性能提升是肉眼级的——箱子移动从“眨眼”变成“顺滑”代码只加两行没有副作用。5.4 屏幕坐标和地图坐标混淆用错坐标系导致人物飞到天花板现象鼠标点击或键盘移动后玩家移动的位置和按键方向完全对不上上变成了斜移。原因grid的行索引是y列索引是x但在 EasyX 绘制时putimage(x * TILE_SIZE, y * TILE_SIZE, ...)的x是列、y是行。如果你在移动逻辑里把行当成了x列当成了y画出来的人物轨迹就是转置的。解决在代码注释里强制标注grid[y][x]先 y 后 x保持一致。函数参数命名也要统一比如movePlayer(int dx, int dy)不要用movePlayer(int x, int y)却传入了行列。这种 bug 一般在第一次显示人物动起来之后 5 分钟内就会发现但排查成本不低——你会在逻辑里反复打日志找半天最后发现只是变量名误导了自己。最好的预防是绘制函数和逻辑函数共用同一个坐标解释约定。5.5 重开一局恢复不干净重置后箱子和人叠在同一格现象按 R 键重开当前关卡结果画面上出现一个人踩在箱子上或者地图上多了一个箱子少了一个人。原因重置时直接执行grid initState但玩家位置playerPos没有同步重置。如果在initState被赋值之后你又修改了成员变量的初始化顺序或者parseSegment里对玩家坐标赋值发生在initState拷贝之前就会导致玩家的初始位置记录错误。解决重置函数里不仅要恢复grid还要强制恢复playerPos startPlayerPos而且要单独记录startPlayerPos而不是从initState里重新解析。我见过的更隐蔽变体是initState是在parseSegment末尾赋值的但parseSegment里playerPos是最后才赋值的于是initState里的玩家坐标还是(0,0)。解决方法是把startPlayerPos放在加载流程的最前面初始化或者直接在parseSegment的分支里同步更新startPlayerPos。5.6 关卡文件解析时漏掉空白行导致地图变形现象不同的人用不同编辑器编辑levels.txt结果有的人关卡加载出来地图错位墙行数比预期少一行。原因Windows 的文本文件换行是\r\n而某些代码编辑器尤其 Linux 风格配置的 VS Code会以\n结尾读入后每行末尾带着一个\r。std::getline默认只去掉\n保留\r导致你比较行首字符时没问题但做字符串长度判断时出错。如果关卡的某一行末尾恰好是空格这个空格会被替换成意想不到的位置。解决读取每一行时用if (!line.empty() line.back() \r) line.pop_back();统一清理。更进一步我建议你建立关卡文件的统一约定所有行必须等宽也就是每一行字符串的size()必须一致加载时做一次校验不一致直接报错。这比在游戏里出现奇怪碰撞再排查要节省大量时间。6. 从“能玩”到“好用”步数统计、操作回退和关卡校验的三个进阶技巧6.1 操作历史栈实现撤销操作的正确姿势最简单可靠的撤销方案是维护一个std::vectorGridState历史栈每次移动前把当前grid和playerPos压入栈撤销时从栈顶弹出一个GridState并恢复整个状态。栈的深度最多 100 左右就足够因为推箱子的核心乐趣在于规划步数而不是无限悔棋限制栈深还能避免内存占用失控。关键点是撤销操作不要走tryMove的反向逻辑实验因为反向逻辑不仅要处理玩家的移动还得正确处理箱子从目标点被推走后的状态恢复稍有遗漏就会产生“箱子数量守恒被破坏”的修补麻烦。直接恢复快照是最省心的代价只是一次深拷贝而推箱子地图撑死几百个格子深拷贝开销在微秒级别完全可以忽略。如果你对内存洁癖特别严重可以只在箱子被推动时才压栈玩家单纯走路的步骤不压栈——这样历史记录更精简撤销也更像“回到上一步决策点”。6.2 加装 BFS 求解器做关卡合法性验证让自制关卡不交白卷在做关卡编辑器时用第 4.3 节的 BFS 求解器对每一关做预检判断是否有解。这个操作极其重要因为你想出一个看似精妙的陷阱地图结果玩家把所有箱子推到死局之后发现死活无解那你这个关卡设计就是失败的。BFS 的返回值如果小于 0直接把关卡标记成“无解”并拒绝加载。另一个价值点是BFS 返回的最少步数可以作为玩家成绩的参考基线。比如你在界面上显示“作者最少 38 步过关”那玩家会不断尝试刷新这个数字。不过要提醒的是BFS 的最少步数是把“每一步移动”算一次如果你的游戏逻辑把“推箱子的一步”也算一步那玩家的实际步数会和 BFS 的参考值有偏差因为玩家推着箱子走两步时BFS 里会记录成两次移动但你的步数统计可能只算了一步。为了避免这种偏差请在游戏内统一一次物理移动算一步不管有没有推箱子这样才和 BFS 结果可比。6.3 死亡局判定用状态压缩检测“所有箱子都无法再移动”推箱子真正劝退玩家的瞬间是玩到中盘发现所有箱子都被推到墙边死角还要手动重开。你可以在玩家每走一步后调用一个简单检测对每个箱子检查它的四个邻居、以及它邻居的邻居如果所有方向都无法再推动前方是墙/箱子/边界并且该箱子不在目标点上那这一局已经彻底无解直接弹提示“死局是否重开”。这个启发式判定不是完备的有一些非死锁但无法完成的局面它不能识别但已经足够拦截掉绝大多数“物理死局”。要做得更完备就得完整跑一遍 BFS 状态搜索重量级但可以在后台线程做不影响玩家操作。我个人的建议是死局判定做成对话框确认不要强制重开因为有的高手就喜欢在看似无解的局面里寻找翻身路径你一刀切反而打击探索热情。这个细节虽然短但非常影响游戏口碑——玩家对一个推箱子版本的评价很大程度上取决于它在“卡住”时的友好度。最后一个我自己的习惯推箱子不是写完移动逻辑就算完的产品级代码。如果你要把它放进作品集请一定记得封装游戏状态接口把逻辑和渲染分离——这样后续换 Qt、换成 SFML 做跨平台版核心的移动、BFS、存档系统不用重写。我当时做完 C/EasyX 版本后把逻辑部分抽出来换成 Qt 界面只花了一个下午而同一批同学把逻辑和绘制揉在一个main.cpp里的重写时等于从零开始。这个教训希望帮到你。本文还有配套的精品资源点击获取
返回列表