
简介这是一份面向计算机相关专业学生的毕业设计与项目实训源码基于 Visual C 6.0 与 GDI 图形接口实现了一款以超级玛丽为蓝本的横版过关游戏。代码覆盖角色移动、跳跃、碰撞检测、动画帧切换、关卡地图加载等核心玩法逻辑地图按关卡单独存放于 txt 文件适合正在做 C 课程设计或游戏方向毕设的读者参考。整包共 32 个文件、约 161KB其中 9 个 h 头文件与 6 个 cpp 源文件构成项目主体6 张 bmp 位图提供背景、角色与动画素材另有 dsw/dsp 工程文件、图标及配置文件结构完整可直接在 VC6 环境中打开编译。资源已有 459 人学习下载。除了可运行的工程骨架还包含 gamemap、bitmaptool、texttool 等模块源码便于理解 GDI 绘图、地图解析与文本渲染的实现思路对想快速搭建横版游戏原型或梳理 MFC/GDI 编程流程的同学较有参考价值。1. 毕设选题的现实逻辑VC6 与 GDI 为什么是横版过关游戏的最稳组合横版过关游戏是每年毕业设计和项目实训里的高频选题。很多人一开始想上 DirectX结果被设备枚举、着色器、调试环境劝退最后退回 Visual C 6.0 GDI 反而把项目做完了。这套源码工程就是一个典型样本用 Visual C 6.0 组织工程核心渲染全部基于 GDI 位图操作地图策略在说明文档里写得很直白——“不是地图拼接的是 1 关加载 1 次地图”。也就是说每一关的地图数据以文本文件独立存放关卡开始时一次性读入内存再配合角色、敌人、背景位图完成整屏绘制。对需要交毕设或课设的学生来说这个选型的价值在于代码量可控、图形原理能讲透、运行效果又确实像一个游戏而不是一个教学演示片段。2. 工程结构与总体架构四个模块和一份地图文件的职责划分2.1 打开工程先看什么从 dsw 到资源文件的完整清单拿到这套源码第一件事不是双击mario01.cpp而是先把工程文件在项目里的位置理清楚。整个工程用 VC6 组织mario01.dsw和mario01.dsp是工作区与工程描述文件mario01.cpp、mario01.h是主程序入口和核心逻辑所在mario01.rc与resource.h负责菜单、图标、对话框等资源编号。下面这张表把源码里出现的文件按模块归好了答辩时可以直接照这个顺序讲。模块文件职责工程入口mario01.dswmario01.dspmario01.cppmario01.hmario01.rcresource.h工程组织、程序入口、资源定义地图逻辑gamemap.cppgamemap.hmap/map1.txt地图数据读取、存储与关卡管理工具类bitmaptool.cppbitmaptool.htexttool.cpptexttool.hfilereport.cppfilereport.htool01.hmyclock.h位图辅助、文本输出、日志记录、计时控制美术资源role.bmppre1.bmpani.bmpmap.bmpmapbk.bmpmapsky.bmpSMALL.ICOmario01.ICO角色帧、标题界面、敌人动画、地面/背景/天空贴图比较容易被忽略的是mario01.opt、mario01.ncb、mario01.plg这三个文件。opt是 VC6 记录窗口布局的缓存ncb是代码智能感知数据库plg是上一次编译的日志。它们不影响程序运行但会影响别人打开工程时的体验——如果拿到工程后代码提示全部丢失多半是ncb被清理或拷丢了重新打开工程让 VC6 重建即可。这套文件结构本身就是一个很好的模块划分范本游戏逻辑不直接操作位图资源而是通过工具类间接访问。bitmaptool负责把位图文件载入内存、取出指定帧gamemap只关心地图数字矩阵filereport负责把运行日志写到trace.txt。每一层只做一件事这也是为什么这套工程能在答辩时把“软件工程实践”这条讲清楚。2.2 逻辑分层渲染、规则与数据怎么分开整个游戏可以被拆成三层渲染层、规则层、数据层。渲染层只负责“把某个位图贴到某个坐标”对应 GDI 的BitBlt、StretchBlt等函数规则层负责角色速度、重力、跳跃状态、碰撞结果和死亡判定数据层负责地图矩阵、关卡编号和资源配置。三层之间通过清晰的接口通信规则层向渲染层输出“角色当前坐标与帧号”数据层向规则层输出“某一行某一列的图块编号”。这个分层思路在源码里的体现是gamemap.h暴露的是LoadMap、GetTile、DrawMap这类接口而不是直接暴露m_map二维数组让外部随便改。角色状态也集中在主逻辑文件里管理工具类不持有游戏状态。这样的好处是后期把map1.txt改成map2.txt只需要改关卡编号把role.bmp换成另一张尺寸相同的角色贴图渲染代码甚至不用动。2.3 GDI 在游戏里扮演的四个角色GDI 本身是 Windows 的图形设备接口它提供的不是游戏引擎而是一套“画图”的能力。在这个工程里它一共承担四件事。第一是图形绘制。CreateBitmap负责在内存里创建位图BitBlt负责把位图从一个 DC 复制到另一个 DC这构成了所有场景绘制的基础背景、砖块、角色、金币、敌人本质上都是“把位图按坐标贴上去”。第二是动画处理。角色跑动、敌人移动、跳跃动作的切换靠的是SetTimer驱动OnTimer回调在每次定时器触发时切换当前帧号并重绘。GDI 本身没有“动画”概念动画是对定时器、帧号和位图切片的组合运用。第三是用户交互。GDI 与 Windows 消息机制天然绑定键盘按下会触发OnKeyDown一类消息游戏在消息处理里改变角色速度和状态实现左右移动、跳跃。第四是地图管理。地图数据不放在代码里而是放在map1.txt中程序启动时读取文件、解析数字矩阵。这一步不属于 GDI但它决定了 GDI 绘制的内容是什么。这四点正好对应摘要里提到的几个技术点。讲清楚这四点GDI 在项目里的角色就完整了。2.4 “一关一图”在架构上意味着什么说明文档里的那句话——“不是地图拼接的是 1 关加载 1 次地图”——值得展开讲。所谓“地图拼接”通常指把预先拆好的多个瓦片纹理文件按坐标拼成整张地图画面。而这套工程的做法不同每一关的地图是一个纯文本数字矩阵程序在关卡开始时整图读入内存之后按需要把对应数字转成具体的小图块绘制。从架构角度看这个决策带来了两个直接好处。第一关卡编辑门槛降到最低随便用记事本打开map1.txt把数字改成别的图块编号关卡就变了不需要图像编辑软件。第二关卡切换逻辑简单进入新关卡时释放上一关的矩阵数据读入新文件重新定位角色出生点即可。代价也很明显地图大小受内存和加载效率限制像“大地图卷轴”这类需求就不适合这种方案。但对一个毕业设计来说把“一关一图”做到稳定、可演示、可讲解比强行上大世界卷轴更务实。工程里专门用一个 txt 文档记录这个决策答辩时可以直接引用“这不是拼贴方案而是整图加载方案取舍是我们主动做的。”3. 核心实现角色结构、动画切帧与定时器驱动的循环设计3.1 角色数据结构结构体承载状态而不是散落一堆变量很多初学者会把角色坐标、速度、朝向、帧号各写成一个全局变量逻辑一长就乱。这套工程的做法更规范用一个结构体把角色的核心状态封装起来。示例结构如下struct stPlayer { float x, y; // 角色左上角坐标单位像素 float vx, vy; // 水平、垂直速度单位像素/帧 int state; // 0站立 1跑动 2跳跃 3死亡 int frame; // 当前动画帧号 int direction; // 1朝右 -1朝左 int width, height;// 碰撞盒尺寸 };这里有个容易被忽略的设计点x和y用的是float而绘制时BitBlt只接受整数坐标。速度乘以时间增量后会产生小数位移如果直接用int保存坐标角色在低速移动时会出现“一步一卡”的锯齿感。用浮点累积、绘制时取整是 2D 游戏里很常规的做法。state和frame是两个容易弄混的字段。state决定“现在处于什么行为”frame决定“当前渲染哪一帧画面”。跑动时frame会不断递增并循环跳跃时frame可能固定在第 2 帧。把行为状态与帧号分开后续做动画切换才不会写成一团乱麻。3.2 OnTimer 游戏循环每一次定时器触发都做同一套动作GDI 游戏没有传统意义上的“游戏主循环”工程里普遍的做法是用SetTimer启动一个固定频率的时钟在OnTimer回调里依次完成输入读取、物理更新、碰撞检测、帧号更新和重绘。伪代码如下void CMarioView::OnTimer(UINT_PTR nIDEvent) { UpdateInput(); // 读取键盘状态设置 vx、vy、state UpdatePhysics(); // 施加重力更新坐标位置 CheckCollision(); // 与地图碰撞纠正位置并改变状态 UpdateFrame(); // 按当前 state 推进动画帧号 InvalidateRect(NULL, FALSE); // 触发重绘但不擦除背景 CView::OnTimer(nIDEvent); }这个顺序不是随便排的。UpdateInput在最前面保证同一帧内操作立即生效UpdatePhysics负责让角色受重力影响CheckCollision在位移之后执行目的是把“穿墙”的角色位置纠正回来最后才切换帧号并重绘。顺序一旦颠倒比如先碰撞后位移角色会卡在墙里一帧视觉上表现为抖动。InvalidateRect(NULL, FALSE)里最后一个参数是FALSE意思是“不擦除背景直接让窗口失效”。如果这里传TRUE系统会在重绘前用背景色擦除整个窗口画面立刻开始闪烁。这也是第 5 章要讲的闪屏问题的根源之一。3.3 角色渲染从 role.bmp 里把当前帧抠出来角色贴图通常不是一张一个动作而是把多个动作帧水平排列在同一张role.bmp里程序通过“源矩形”取出需要的帧。绘制函数的核心逻辑如下int frameW 32; // 单帧宽度 int frameH 48; // 单帧高度 int srcX player.frame * frameW; // 当前帧在贴图中的水平起点 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap* pOld memDC.SelectObject(m_bmpRole); dc.BitBlt((int)player.x, (int)player.y, frameW, frameH, memDC, srcX, 0, SRCCOPY); memDC.SelectObject(pOld);这段代码做的事情很直接先在内存 DC 里选入角色位图然后通过srcX计算出当前帧的起点把frameW × frameH的一块矩形区域复制到窗口 DC 的(x, y)位置。srcX的计算是动画的核心——frame每变化一次贴出来的画面就换一个动作。这里有两个细节。第一SelectObject的返回值pOld必须保存绘制结束后要选回去否则内存 DC 销毁时会尝试释放一个仍然被选中的位图造成资源泄漏。第二角色朝左时如果用BitBlt直接贴贴出来的是反的。常见做法是用StretchBlt配合负宽度把位图水平镜像或者绘制前翻转角色贴图。最简单的处理是准备两张方向不同的贴图资源按direction选择。3.4 跳跃与按键键盘输入怎么变成角色动作键盘处理一般放在OnKeyDown和OnKeyUp里。按下方向键时设置vx松开时清零按下跳跃键时如果角色当前不是跳跃状态就给vy一个向上的初速度。跳跃手感是整个项目里最容易被评委“试出来”的地方。很多人把跳跃写成“按住上就升高、松开就下降”那是电梯不是跳跃。正确的做法是只给一次初速度之后由重力逐步把垂直速度拉回负值case VK_SPACE: if (player.state ! STATE_JUMP) { player.vy -JUMP_SPEED; // 向上初速度 player.state STATE_JUMP; } break;而UpdatePhysics里每一帧执行player.vy GRAVITY; // 重力加速度 player.x player.vx; // 水平位移 player.y player.vy; // 垂直位移JUMP_SPEED和GRAVITY是需要反复试的两个常量。JUMP_SPEED太大会一蹦老高太小会跳不上砖块GRAVITY太大会觉得角色“砸”向地面太小会飘。这个参数没有公式能一步算准基本靠实测调参——这也是项目实训里真实会发生的事情。3.5 GDI 的边界能做什么不能做什么GDI 方案能做到“一个完整可玩的横版关卡”但它不是游戏引擎。它没有现成的物理系统、没有场景图管理、没有资源热更新。所有这些都是你自己用普通 C 代码实现的。在实际交付中GDI 方案的优势恰好体现在它的边界因为功能少代码结构反而清晰每一帧画了什么、为什么画都能在源码里找到对应位置。答辩时被问到“为什么不用 DirectX”可以坦诚回答对于一个固定视角、单关卡规模、以图块绘制为主的横版游戏GDI 在可读性和实现难度上更合适DirectX 的很多能力在这个项目里属于用不上的复杂度。4. 地图加载与碰撞检测文本关卡的一次性读入与格子判定4.1 地图文件的格式先用一份数字矩阵定义整个关卡打开map/map1.txt看到的不是二进制图片数据而是有规律的数字矩阵。一个典型的地图文件大概长这样15 20 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 2 2 2 0 0 0 0 0 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1第一行是两个数字行数和列数。之后每一行对应地图的一行每个数字代表一种图块0是空地1是地面2是砖块3是金币4是敌人刷新点。这个约定需要用表格固定下来否则关卡编辑者与代码实现者对不上。设计地图格式时一个必须想清楚的问题是由谁来决定数字与图块的对应关系。工程里通常把这种映射放在gamemap.cpp的绘制函数里地图数据文件只存编号不存文件名。这样一来美术想换砖块贴图只需要替换map.bmp里的对应小图不需要改map1.txt。4.2 加载逻辑用 fscanf 把数字读进二维数组VC6 时代的工程读地图文件用fopenfscanf是最直接的方式。它依赖少、逻辑简单出问题时也好排查。参考实现如下bool CGamemap::LoadMap(const char* szFile) { FILE* fp fopen(szFile, r); if (fp NULL) { LogToFile(LoadMap failed: %s, szFile); return false; } fscanf(fp, %d %d, m_rows, m_cols); // 先读行数、列数 for (int i 0; i m_rows; i) { for (int j 0; j m_cols; j) { fscanf(fp, %d, m_map[i][j]); // 逐格读编号 } } fclose(fp); return true; }读取成功与否必须做判断。地图文件路径配错、文件被占用、格式多了一个空行都可能导致fscanf读到的数据错位。工程里正好有filereport.cpp和trace.txt可以把失败原因记录到日志里。别小看这一步——答辩时现场打不开地图你拿出一份带时间戳的错误日志比口头解释“可能哪里有问题”有说服力得多。读取完成后m_map[i][j]就构成了关卡在内存中的完整快照。后续所有碰撞检测、金币拾取、敌人刷新都基于这个二维数组进行判断。4.3 从数组到画面逐格绘制与背景分层地图数据本身不可见要把它变成画面需要遍历数组把每个非 0 编号对应的图块贴到屏幕上for (int row 0; row m_rows; row) { for (int col 0; col m_cols; col) { int tileID m_map[row][col]; if (tileID 0) continue; // 空地不绘制 DrawTile(dc, col * TILE_W, row * TILE_H, tileID); } }DrawTile从map.bmp中按编号找到对应小图块然后用BitBlt贴到(col * TILE_W, row * TILE_H)坐标。TILE_W和TILE_H是单个图块的像素尺寸通常与map.bmp里切分好的小图尺寸一致。背景的处理与地图分开远景用mapsky.bmp整张贴在最底层中景用mapbk.bmp贴一层最后才绘制地图数组里的实心图块和角色。分层的顺序就是绘制顺序先画的会被后画的覆盖。这个“画家算法”在 2D 游戏里很基础但很多第一次写游戏的人会忽略导致角色被地面盖住或者背景把角色遮掉。4.4 碰撞检测矩形范围采样而不是全图遍历角色与地图的碰撞是横版过关游戏最容易“翻车”的部分。朴素的做法是让角色和地图里的每一个实心块做矩形相交判断地图一大每一帧要比较几百次效率很差。更好的做法是利用地图的格子特性只检测角色矩形覆盖到的格子bool CheckCollision(stPlayer p) { int x0 (int)p.x / TILE_W; int x1 (int)(p.x p.width - 1) / TILE_W; int y0 (int)p.y / TILE_H; int y1 (int)(p.y p.height - 1) / TILE_H; for (int row y0; row y1; row) { for (int col x0; col x1; col) { if (IsSolid(m_map[row][col])) return true; } } return false; }IsSolid返回该编号是否为地面或砖块。角色向右移动后检查到碰撞就把x回退到贴墙的位置向下移动后检查到碰撞就把y定位到地面顶部同时把vy清零并切换回站立状态。这里要特别注意一个细节碰撞检测必须分轴处理。先处理水平位移的碰撞再处理垂直位移的碰撞否则角色斜着撞到墙角时会被一次性推走两段距离视觉上出现“瞬移”。正确的做法是在UpdatePhysics里把x和y分开更新分别检测、分别修正。4.5 一关加载一次地图关卡切换的状态重置“一关一图”模式下的关卡切换不只是换个文件名那么简单。进入下一关时需要做完整的重置释放上一关地图数据、加载新地图、重置角色坐标与速度、清空敌人实例、重置计时器与金币计数。遗漏任何一项都会出现“进入第二关还是第一关的地图”或“角色出生在墙里”的问题。一个稳妥的做法是把关卡切换封装成一个独立函数bool StartLevel(int levelNo) { char szFile[32]; sprintf(szFile, map/map%d.txt, levelNo); m_pMap-LoadMap(szFile); ResetPlayer(); // 重置角色位置与状态 ResetEnemies(); // 重置敌人列表 return true; }levelNo是当前关卡编号通关后levelNo并再次调用StartLevel。这种做法让“增加一关”的成本变得很低写一个map2.txt然后这个函数自动就能加载它。5. 常见问题与避坑闪烁、黑边、手感与句柄泄漏的排查5.1 画面闪到没法玩问题几乎都出在 OnPaint现象角色一移动整个窗口背景闪烁严重像在快速翻页。原因这是 GDI 游戏最常见的翻车点。默认的OnPaint流程是先擦除背景再绘制内容擦除和绘制之间存在时间差人眼看到的就是交替出现的空白与画面。闪烁本质上不是绘制太慢而是“擦除”这件事本身被用户看到了。解决双缓冲。先在内存 DC 里把整帧画面完整画好最后一次性BitBlt到屏幕。示例void CMarioView::OnPaint() { CPaintDC dc(this); CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap memBmp; memBmp.CreateCompatibleBitmap(dc, m_viewW, m_viewH); CBitmap* pOld memDC.SelectObject(memBmp); // 在 memDC 里依次绘制天空、背景、地图、角色 DrawSky(memDC); DrawMap(memDC); DrawPlayer(memDC); dc.BitBlt(0, 0, m_viewW, m_viewH, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); }这段代码把“画到屏幕”变成了“画到内存再整体送显”屏幕内容从用户视角看是瞬间更新的闪烁自然消失。注意InvalidateRect的第二个参数始终保持FALSE不要再让系统主动擦除背景。5.2 角色贴图边缘带黑框GDI 位图的透明色没处理现象角色四周出现明显的黑色矩形边框和背景格格不入看起来像贴了一块黑胶布。原因位图文件本身没有 alpha 通道黑边就是位图里实际存在的黑色像素。BitBlt用SRCCOPY无脑复制把黑框也复制上去了。这不是 GDI 的问题而是“没有做透明处理”。解决约定一种透明色比如黑色绘制时用两趟掩码操作。先画掩码SRCAND把角色形状镂空出来再画贴图SRCPAINT把角色像素填进去// 第一次将透明区域镂空 dc.BitBlt(x, y, w, h, maskDC, srcX, 0, SRCAND); // 第二次填充角色像素 dc.BitBlt(x, y, w, h, roleDC, srcX, 0, SRCPAINT);实现掩码位图需要做一次预处理遍历角色位图的每个像素把透明色以外的区域在掩码位图中标记为黑色。这是 BitBlt 时代的经典做法代码量不大但效果完全取决于“透明色是否全图唯一”。如果美术贴图里角色身体恰好有同色像素透明区域就会“多挖一块”。所以选透明色时要选一张角色完全不会用到的颜色比如纯品红或纯黑。5.3 跳跃手感飘游戏逻辑不应该直接挂在定时器频率上现象角色跳跃时高时低机器卡一下会突然跳得特别高手感完全不可控。原因假如物理更新直接写在OnTimer里而OnTimer的触发频率受系统负载影响那速度、重力、位移就都被系统时钟绑架了。机器卡顿的几毫秒里漏掉了几帧角色位置就像“瞬移”了一段看起来自然是飘的。解决不要让“帧”天然等于“物理计算次数”而是用一个固定的步长来驱动物理。每次定时器触发时用当前时间与上次时间的差除以一个固定步长决定本帧要执行几次物理更新。代码上可以用一个累计器accumulator deltaTime; while (accumulator FIXED_STEP) { UpdatePhysics(); // FIXED_STEP 固定为 16ms accumulator - FIXED_STEP; }这样无论系统卡多久物理计算始终按固定步长推进跳跃高度和速度曲线是可复现的。这个思路在游戏开发里属于基本功在答辩里也是一个很加分的“工程化细节”。5.4 句柄泄漏长时间运行后花屏或闪退现象游戏运行几分钟后开始卡顿任务管理器里句柄数持续上涨最终贴图消失或程序崩溃。原因每次OnPaint都调用CreateCompatibleDC和CreateCompatibleBitmap但绘制完成后没有DeleteDC和DeleteObjectGDI 对象不断累积最终耗尽系统句柄。这类问题最像“玄学”因为崩溃往往不发生在操作时而是发生在操作之后的某个随机时刻。解决养成统一的资源管理习惯。凡是Create出来的 DC 和位图在离开作用域前必须释放凡是SelectObject选进去的旧对象必须恢复。前面代码里反复出现的pOld保存与恢复不是为了写得好玩是必须的。调试时可以写一个辅助函数统计当前 GDI 对象数量如果每次操作后数量不减就说明泄漏了。排查的优先顺序是先看 DC 有没有释放再看位图有没有释放最后看SelectObject是否恢复。90% 的 GDI 泄漏都出在这三个位置。6. 验收与答辩拿什么证明这套游戏真的能交付6.1 验收清单从启动到通关逐项签字答辩前把功能项列成清单每项标记“通过/不通过”比现场临时点鼠标来得从容。下面这份清单直接可用。验收项操作方式预期结果工程编译打开mario01.dswF7 编译无致命错误生成可执行文件启动进入运行程序显示标题界面能进入游戏角色移动按左右方向键角色左右移动朝向正确跳跃动作按空格 / 跳跃键角色起跳受重力影响落回地面地面碰撞角色走到砖块边缘不能穿墙落点停在实心块表面金币拾取角色接触金币块金币消失计数增加关卡切换到达终点释放当前地图加载map2.txt数据资源稳定性持续运行 10 分钟句柄数不持续增长无闪屏6.2 答辩演示顺序从地图文件开始讲演示顺序建议是先打开map1.txt讲解地图格式再演示游戏运行最后回到代码里讲双缓冲和碰撞检测。这个顺序的好处是逻辑链完整评委先看到“数据是什么样”再看到“数据变成了什么画面”最后理解“画面背后的计算过程”。讲地图文件时指着一行数字说“这里改成 2 就多了一块砖”比任何 PPT 都直观。讲双缓冲时把OnPaint里的内存 DC 画出来说明“先画在内存里最后一次性贴屏”。讲碰撞检测时强调分轴处理与格子采样这两点是区分“会写游戏”和“只是把教程跑通”的分水岭。6.3 常见提问与应对思路别让评委替你把方案讲出来评委最可能问四个问题。第一“为什么不用 DirectX”回答思路项目目标是清晰掌握 2D 渲染原理GDI 以最少黑匣子覆盖了位图绘制、透明处理、定时器动画DirectX 在单关卡固定视角场景下的提升有限。第二“地图为什么不拼接”回答思路一关一图数据与表现分离关卡改文本即可实现适合课程设计的规模控制。第三“双缓冲为什么能解决闪烁”回答思路闪烁源于擦除与绘制的分离双缓冲让整帧画面一次性呈现消除了时间差。第四“碰撞检测为什么用格子采样”回答思路地图本质是网格数据角色只需检测覆盖到的 2 到 4 个格子复杂度从 O(n) 降到常数级别。最后说一个我自己的经验。以前我做类似项目时总喜欢把代码写得“看起来高级”结果答辩时被问到一个边缘情况自己都忘了逻辑是怎么走的。从那以后我每次做图形类课设都强制自己先写一份简短的设计说明把地图格式、渲染顺序、碰撞规则写清楚代码按说明逐条落实。这套工程里有说明文档、有日志文件、有清晰的模块划分就是这种习惯的成果——既是对自己负责也是给答辩时留足后路。希望帮到你。本文还有配套的精品资源点击获取