简介这是一份面向C初学者与游戏开发爱好者的《我的世界》简易版项目用C实现简化版沙盒玩法涵盖方块生成、玩家移动等基础机制既能作为编程练手案例也可借此了解游戏循环、渲染与交互的粗略实现。压缩包共4个文件包括1个cpp源代码、1个可执行程序exe及2个运行必需的标准库DLL文件大小仅771KB源代码承担核心逻辑DLL负责提供标准库运行支持整体结构轻量适合直接解压对照学习。项目已有3807人学习下载热度可观。借助这份代码读者可以完整走一遍用Dev-C编写、编译和发布C小程序的基本流程理解程序从源码到可执行文件再到依赖环境的关系同时体会方块放置、玩家控制等游戏机制的简单实现是一份能同时训练编程、调试与软件工程思维的入门资源。1. C 写“我的世界简易版”源码和程序都已测你先搞清楚这三件事再动手某个做图形学课程设计的大四学生拿到一份“我的世界简易版C编写源代码程序已测”的参考工程后第一反应是打开源码读然后卡住了。卡住的不是代码看不懂而是他发现自己根本不知道要看什么方块是怎么被画出来的鼠标点一下是怎么知道点到哪个方块的改完代码之后怎么验证自己没改坏这三件事恰好就是这个标题里真正值钱的东西。这个所谓简易版本质是一个最朴素的体素引擎三维数组存地图、OpenGL 画方块、射线拾取做交互再配一个能保存玩家修改的存档系统。它不是什么大工程但把这三条链路跑通你才算是真的“会写”而不是“会抄”。这篇笔记就把这三条链路的实现顺序、关键参数和踩坑点完整拆开让你照着能复现读完能自己改。2. 简易版 MC 的骨架先把体素存储、渲染选型和坐标系统定死2.1 为什么方块世界用三维数组而不是一堆网格模型很多人第一次拿到“我的世界简易版”源码会下意识去找模型文件方块应该有 .obj 吧纹理应该有贴图吧结果翻遍工程目录只看到几张 PNG 和一堆 .cpp。这是因为体素游戏和普通 3D 游戏的数据组织方式完全不同。普通游戏里一个桌子是一份三角网格改位置就移动整个网格而方块世界里一个桌子是 3×3×1 个方块格子的组合玩家随时可能拆掉其中一格你不可能为每一种形状预生成网格。所以简易版 MC 的地图存储方案几乎都是同一个套路用三维数组按格点存方块 ID。数组下标就是方块坐标数组值就是方块类型0 表示空气。这样做的好处是随机访问 O(1)拆块放块只需要改一个数组元素代价是内存连续且固定。一个 128×128×64 的地图用 uint8_t 存储才 1MB 出头完全不需要搞什么稀疏结构。enum BlockType : uint8_t { AIR 0, GRASS 1, DIRT 2, STONE 3, WOOD 4, LEAF 5 }; struct Chunk { static constexpr int SIZE 16; static constexpr int HEIGHT 64; uint8_t blocks[SIZE * SIZE * HEIGHT]; int index(int x, int y, int z) const { return (y * SIZE z) * SIZE x; } uint8_t getBlock(int x, int y, int z) const { if (x 0 || x SIZE || y 0 || y HEIGHT || z 0 || z SIZE) return AIR; // 越界按空气处理省掉一堆分支 return blocks[index(x, y, z)]; } void setBlock(int x, int y, int z, uint8_t type) { if (x 0 || x SIZE || y 0 || y HEIGHT || z 0 || z SIZE) return; blocks[index(x, y, z)] type; } };这段代码的逻辑核心是 index 函数的坐标映射公式。它把三维索引压成一维y 作为最高维这样内存里同一高度层的方块是连续的后续做高度遍历和批量提交顶点时缓存友好。getBlock 里越界返回 AIR 是个很省事的策略你去查询相邻区块的方块时不再需要判断边界读出来的空气自然就不生成面。参数说明SIZE 取 16 是沿用 Minecraft 的区块尺寸主要理由是这个大小在顶点重建时耗时可控而且 16×16 的横截面配 64 层高度单个区块内存约 16×16×6416384 字节正好落在常见 CPU 缓存行友好区间。HEIGHT 取 64 是给地形生成留的合理高度范围真要做高山可以调到 128但内存会翻倍到 2MB简易版没必要。2.2 渲染选型OpenGL 批量绘制、CPU 软渲染与伪 3D 的取舍数据层定了接下来是画面怎么来。市面上“我的世界简易版”常见的做法有三种不同做法决定了代码量和运行性能完全不是一个量级。第一是纯 CPU 软渲染用一个离屏缓冲区手动算每个像素的颜色再交给图形 API 显示。它优点是不依赖 OpenGL 那一套复杂状态机适合刚学 C 还没碰过图形学的同学缺点是每帧要 CPU 遍历几十万个三角形分辨率和视口稍大就卡成幻灯片。第二是伪 3D 投影把方块画成 2D 菱形或者矩形配合 y 偏移模拟高度这种方案做背包界面或 2D 俯视角小游戏可以但做不了第一人称漫游鼠标不能旋转视角交互也受限。第三是用 OpenGL 提交三角形批量绘制也是我推荐、绝大多数简易版采用的方案。三者的关键参数对比如下方案视角自由度60 FPS 稳定上限修改方块后刷新学习曲线CPU 软渲染固定或简单旋转约 2000~5000 三角形整帧重绘低伪 3D 投影固定俯视角约 2000 个方块排序重绘最低OpenGL 3.3完整六自由度数百万三角形只重建对应区块中高OpenGL 方案的核心思路是每一帧把可视方块的六个面剔除不可见面后打包成三角形缓冲区一次 draw call 全部画出来。简易版不追求极致性能可以每帧全量重建所有可见面的顶点进阶再改成“只有方块变化时重建所在区块”。所以渲染层第一步不是写绘制函数而是设计顶点结构体位置 3 个 float、法线 1 个 byte、光照强度 1 个 byte总共 20 字节。struct Vertex { float x, y, z; // 位置 uint8_t nx, ny, nz; // 法线方向用于面着色 uint8_t ao; // 环境光遮蔽系数 0~3 };这个结构体直接决定后面所有绘制代码怎么写。法线方向是每个面固定的朝上就是 0, 1, 0朝左就是 -1, 0, 0。简易版没有纹理可以用法线方向乘一个固定亮度值做“伪光照”让顶面亮、侧面灰、底面暗立体感一下就出来了。ao 参数留到后面讲性能优化时再展开。2.3 坐标系统方块格点、世界坐标与鼠标射线的换算必须一次对齐只写渲染不管坐标方块会出现在你完全想不到的地方。第一人称游戏里有两套坐标要来回切换世界坐标是以米/格为单位的连续浮点坐标玩家位置在这里方块坐标是格点坐标是整数指向“哪一格”。从世界坐标算方块坐标很简单floor(x)、floor(y)、floor(z)但 C 的 int 强转会向 0 取整负数坐标会算错必须自己写 floor 函数。鼠标点击拾取方块是反过来从屏幕像素出发根据相机矩阵生成一条射线再判断射线穿过了哪个方块。这一步很多初学者做不对因为屏幕坐标是窗口的像素位置而射线计算需要的相机方向是由 FOV、宽高比、俯仰偏航角共同决定的。glm::vec3 screenToRayDirection(float mouseX, float mouseY, int winWidth, int winHeight, glm::mat4 viewMatrix, glm::mat4 projMatrix) { float ndcX 2.0f * mouseX / winWidth - 1.0f; float ndcY 1.0f - 2.0f * mouseY / winHeight; // 屏幕 y 向下NDC y 向上 glm::vec4 rayClip(ndcX, ndcY, -1.0f, 1.0f); glm::vec4 rayEye glm::inverse(projMatrix) * rayClip; rayEye.z -1.0f; rayEye.w 0.0f; glm::vec4 rayWorld glm::inverse(viewMatrix) * rayEye; glm::vec3 dir glm::normalize(glm::vec3(rayWorld)); return dir; }这段函数是拾取系统的地基。逻辑上它做的事情是先算鼠标在 NDC 坐标系里的位置然后逆投影矩阵拿到相机空间射线再逆视图矩阵拿到世界空间射线。关键点是 rayEye.w 0.0f这表示它是一个方向而不是一个点逆变换时不会带上相机平移分量最后 normalize 保证方向向量的模长为 1这样后面做射线步进时每步长度才是可控的。参数说明近裁剪面距离 near 一般在 0.05~0.1远裁剪面 far 至少 500否则地形边缘会被裁掉FOV 选 70° 左右最接近人眼常规视角太小会有望远镜感太大会出现明显的透视畸变。mouseX/mouseY 用 GLFW 回调传入的屏幕像素坐标winHeight 换算那一步的负号很容易漏漏了之后鼠标上下移动时射线方向会反过来。3. 把方块真正摆进三维世界从空窗口到可跑地形的完整链路3.1 建窗口与主循环GLFW 初始化、回调注册与帧率控制不管你的地形生成写得多么漂亮没有一个稳定的窗口和主循环程序打开就闪退。简易版用 GLFW 是约定俗成的选择原因很实在跨平台、内置输入回调、不需要额外管理窗口消息循环。初始化顺序有讲究先 glfwInit再设置版本 hint然后创建窗口最后才调用 gladLoadGL 加载 OpenGL 函数指针。顺序反了会出现“glGenVertexArrays 返回 0”这类诡异问题。#include glad/glad.h #include GLFW/glfw3.h int main() { if (!glfwInit()) { return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window glfwCreateWindow(1280, 720, MC Simple, nullptr, nullptr); if (!window) { glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSwapInterval(1); // 垂直同步限制帧率到屏幕刷新率防止 GPU 空转发热 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { return -1; } glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); while (!glfwWindowShouldClose(window)) { glfwPollEvents(); // 每帧都执行的逻辑输入处理、方块网格重建、绘制 glClearColor(0.6f, 0.8f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // drawScene(); glfwSwapBuffers(window); } glfwTerminate(); return 0; }这段代码是后面所有 demo 的骨架逻辑上分三段窗口创建、OpenGL 环境配置、主循环。窗口创建失败后务必调用 glfwTerminate 回收资源否则二次运行时系统会报“context already exists”。glfwSwapInterval(1) 这个参数容易被忽略但实际作用很大不开垂直同步显卡会全力跑几千帧笔记本直接起飞且帧率波动会带来画面撕裂感。着色器的编译、顶点缓冲的创建没有写在这个主函数里但它们都应该放在主循环之前完成。主循环里每帧只需要做三件事处理事件、重建网格、清除缓冲区后绘制。这个顺序不能乱特别是清除调用放在重建之后的话一帧的画面会在下一帧开始时被清掉表现为“闪屏”。3.2 生成第一片地形用哈希值噪声替代柏林噪声20 行搞定起伏地形生成是新人最容易过度设计的地方。很多参考代码直接引入完整柏林噪声实现上百行但这只是背景身体贴合界面强调标注再续一下。简易版只需要做成“有点起伏、看起来自然”就够了。用哈希值噪声就能达到效果先定义一个整数哈希函数把二维坐标映射到 0~255再用双线性插值把块状感抹掉最后叠加几个尺度就得到平滑的高度图。uint8_t hash2D(int x, int z) { uint32_t h (uint32_t)x * 374761393u (uint32_t)z * 668265263u; h (h ^ (h 13)) * 1274126177u; return (uint8_t)((h ^ (h 16)) 0xFF); } float valueNoise(float x, float z) { int xi (int)floor(x), zi (int)floor(z); float xf x - xi, zf z - zi; uint8_t c00 hash2D(xi, zi); uint8_t c10 hash2D(xi 1, zi); uint8_t c01 hash2D(xi, zi 1); uint8_t c11 hash2D(xi 1, zi 1); float u xf * xf * (3.0f - 2.0f * xf); // 平滑插值 float v zf * zf * (3.0f - 2.0f * zf); return (c00 * (1 - u) c10 * u) * (1 - v) (c01 * (1 - u) c11 * u) * v; } void generateTerrain(Chunk chunk, int baseX, int baseZ) { for (int x 0; x Chunk::SIZE; x) { for (int z 0; z Chunk::SIZE; z) { float wx baseX x; float wz baseZ z; float height 32.0f valueNoise(wx * 0.05f, wz * 0.05f) * 20.0f valueNoise(wx * 0.15f, wz * 0.15f) * 5.0f; int top (int)height; for (int y 0; y top; y) { uint8_t type (y top - 1) ? GRASS : (y top - 4 ? DIRT : STONE); chunk.setBlock(x, y, z, type); } } } }生成逻辑的核心是两层噪声叠加。第一层频率 0.05、幅度 20控制大地形起伏第二层频率 0.15、幅度 5增加细节。参数说明频率 0.05 意味着约 20 格一个完整周期频率越大地形越破碎幅度 20 加在基础高度 32 上最终高度范围约 12~52正好落在 Chunk 的 HEIGHT64 范围内。如果调高幅度到 40需要同步调大 HEIGHT否则生成到顶部的草方块会被 setBlock 静默丢弃。这层代码里 y 方向的循环值得注意它从 0 一直写到 top也就是每个柱子内部的所有方块都填满。这样地形是实心的玩家不会掉出地表。种子固定时每次运行生成结果一致方便对比修改效果想随机地图就把 hash2D 里两个魔法数换成随机种子即可。3.3 提交方块网格只给面向外部的面生成三角形地图数据填完了OpenGL 还画不出东西因为方块还不存在于 GPU 缓冲区里。这一步的关键是把方块转成顶点。惊天的坑在于如果你简单地对每个方块生成 6 个面一个 40×40 的地图就要画差不多 50 万个三角形帧率直接个位数。必须做面剔除——只生成“贴着空气的面”。做法很直白检查方块上下左右前后六个邻居邻居是空气的那个方向才生成面。这样山体内部的方块因为六个邻居都是实心方块压根不会出现在顶点缓冲里只有表面方块有面暴露出来。bool shouldFace(int bx, int by, int bz, int dx, int dy, int dz) { return chunk.getBlock(bx dx, by dy, bz dz) AIR; } void addFace(std::vectorVertex verts, int bx, int by, int bz, int dx, int dy, int dz, uint8_t blockType) { if (!shouldFace(bx, by, bz, dx, dy, dz)) return; // 根据法线方向 (dx, dy, dz) 和方块原点计算四个顶点的坐标 // 右侧面举例x1 平面上的四个顶点 if (dx 1) { verts.push_back({bx 1, by 0, bz 0, 1, 0, 0, 1}); verts.push_back({bx 1, by 1, bz 0, 1, 0, 0, 1}); verts.push_back({bx 1, by 1, bz 1, 1, 0, 0, 1}); verts.push_back({bx 1, by 0, bz 1, 1, 0, 0, 1}); } // 其余五个面的顶点同理只是法线和平面位置不同 }逻辑说明shouldFace 查询邻居方块类型是空气才返回 true这是整个渲染优化的第一道闸门。addFace 中 verts 存放的每个顶点都带法线方向法线用于后面的面着色。具体三角面生成时需要把顶点按逆时针顺序排列保证 GL_CULL_FACE 开启后不会把朝向自己的面剔掉如果发现方块“内壁可见”或“方块隐身”优先检查顶点绕序。参数说明这里的 ao 值暂时全部填 1表示无遮蔽。真实环境中方块边缘的阴影效果就是靠它做的相邻实心方块越多ao 值越大面越暗带来类似环境光遮蔽的立体感这部分留到第 5 章展开。顶点颜色也可以用 blockType 查表替代比如草是绿色、石头是灰色视觉上比纯法线着色更接近 Minecraft。3.4 鼠标放块与拆块射线步进拾取的具体参数拆块和放块用的是同一套拾取机制。射线方向算出来后从玩家眼睛位置开始逐步前进每一小步检查当前位置落在哪个方块格子里如果该格子不是空气就命中。步长选太大容易穿过薄墙选太小则性能浪费0.05 格是比较可靠的折中。最大距离一般限制在 8~10 格参考 Minecraft 的默认交互距离太远会破坏战斗和建造的手感。bool raycast(glm::vec3 origin, glm::vec3 direction, float maxDist, glm::ivec3 hitBlock, glm::ivec3 normal) { float step 0.05f; float dist 0.0f; glm::ivec3 prevBlock; bool hasPrev false; for (dist 0.0f; dist maxDist; dist step) { glm::vec3 p origin direction * dist; glm::ivec3 b((int)floor(p.x), (int)floor(p.y), (int)floor(p.z)); if (chunk.getBlock(b.x, b.y, b.z) ! AIR) { if (!hasPrev) return false; // 相机自己就在方块内部 hitBlock b; normal b - prevBlock; // 命中面和上一个空格子的差值就是法线 return true; } prevBlock b; hasPrev true; } return false; }逻辑说明这个循环每一步都把连续坐标转成格点坐标 b查询地图数组。记录 prevBlock 很关键——命中方块时prevBlock 是射线在命中前最后一个空气格子两者坐标相减差值必然是某个分量上的 ±1这就是被命中的那个面的法线方向。放新方块时把 hitBlock normal 作为新方块坐标拆块时直接用 hitBlock。参数说明step 从 0.1 改成 0.01 会多 10 倍循环次数但避免在方块边缘因为浮点误差漏检maxDist 建议在 6~8 之间这个值一般作为常量放在输入处理模块里你可以在开发时分别试试 5 和 10感受距离对手感的影响。方向向量必须在上一步函数中 normalize否则 step 的实际移动距离不是 0.05命中位置会整体偏移这在第 4 章会重点讲。4. 简易版 MC 的避坑记录黑屏、花屏、穿透与存档全没了4.1 黑屏顶点提交了却只看到背景色现象程序编译运行成功窗口和天空色都正常但地形方块完全看不见。很多开发者的第一反应是模型矩阵错了或坐标计算错了实际上最容易出现的问题在初始化阶段VBO 提交到显卡后忘了绑定 VAO或者是着色器编译失败但没检查日志。原因OpenGL 3.3 核心模式下VAO 是必须的。如果你把顶点数据直接绑到 GL_ARRAY_BUFFER 然后马上调用 glDrawArrays在兼容模式下可能侥幸显示在核心模式下没有任何输出。着色器编译失败则更隐蔽因为 glCreateProgram 会返回有效 ID可是 glUseProgram 之后绘制命令全部在无效管线中执行。解决把 VAO 和着色器的错误检查做成强制性的。着色器编译后必须用 glGetShaderiv 检查状态编译失败就输出 info log 再断言退出VAO 绑定顺序固定为“绑定 VAO → 绑定 VBO → glVertexAttribPointer → 启用属性”用一次 glBindVertexArray(0) 收尾后续绘制前再绑定对应 VAO。提示如果你用的是 glad 配合 GLFW记得把 gladLoadGLLoader 放在 glfwMakeContextCurrent 之后调用。加载顺序反了会出现所有 gl 函数指针为空症状同样是黑屏而且通常伴随崩溃。4.2 方块像纸片一样半透明透过山体看到后面现象地形能显示但方块是“透视”的。从某个角度能看到山体背面的方块整个地图像玻璃纸叠在一起旋转视角时还伴随诡异的闪烁。原因八成是 glEnable(GL_DEPTH_TEST) 没开。OpenGL 默认是深度测试关闭的所有三角形的绘制顺序由提交顺序决定后画的永远覆盖先画的。你生成地形的循环顺序是从远到近还是从近到远完全不可控所以画出来就是层层叠叠的混乱画面。解决在 OpenGL 初始化阶段加入 glEnable(GL_DEPTH_TEST)同时注意每次清除缓冲区时带上深度位glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)。如果这两样都做了仍然有遮挡关系错乱查一下 glDepthFunc 是不是被改成了 GL_ALWAYS这个函数在调试某些效果时容易忘记改回来。4.3 拆块总差一格鼠标点到的位置偏到了前面或后面现象鼠标点击想拆掉面前的石头结果拆掉的是旁边一格或者石头前面一格。尤其在玩家面朝斜上方时误差变得更加明显。原因这是射线方向向量没有归一化导致的。很多人在做逆矩阵变换时忘了最后一步 glm::normalize。没有归一化的方向向量射线步进时每一步的 0.05 不再是“0.05 格”而是往某个方向偏移后被放大最终提前或推后命中相邻格。另一个常见叠加因素是 near 平面的大小影响逆投影矩阵转换时 rayEye.z 忘了强制设为 -1.0会导致射线带上了位置偏移。解决在 screenToRayDirection 函数末尾打印 glm::length(dir)如果结果不是 1.0f 就说明没做归一化一步步往回查。修好后把射线的起点坐标准确放在相机位置而不是放在相机前方若干格很多参考代码图省事把起点偏移了 0.5 格看起来差别不大但在斜坡地形上会出现“隔空拆块”的幻觉。4.4 退出程序崩溃、存档文件损坏现象程序运行一切正常一到关闭窗口时偶尔报段错误Segmentation fault存档保存下来倒是能读但加载时随机出现部分地形不完整或方块类型错乱。原因资源释放顺序问题更常见一般在 glfwTerminate 之后又调用了 glDeleteShader、glDeleteVertexArrays此时 GPU 上下文已销毁这些调用会崩溃存档文件则多半是写入时直接覆盖原文件程序在 write 中途异常退出就留下了残缺的新文件。细看时能发现文件大小比正常小一半甚至更小。解决释放顺序严格倒过来——先释放 GPU 资源再 glfwDestroyWindow最后 glfwTerminate。存档写入改成“临时文件 原子替换”的常见策略先把数据写到 save.tmp全部写完后用 std::filesystem::rename 覆盖 save.dat这样任何时刻磁盘上的正式存档都是完整的。void saveGame(const std::string path, const std::vectorEditRecord edits) { std::string tmp path .tmp; std::ofstream out(tmp, std::ios::binary); for (const auto e : edits) { out.write(reinterpret_castconst char*(e), sizeof(e)); } out.close(); std::filesystem::rename(tmp, path); // 覆盖原文件 }逻辑说明所有修改过的方块以 EditRecord 结构体顺序追加到临时文件结构体里只需要三个字段格点坐标压缩进一个 int32方块类型一个 uint8。rename 在 Windows 和 Linux 上的语义有差异但无论如何都比直接写了就往原文件塞安全。4.5 远处地形边缘闪烁、锯齿晃动现象场景中远处的方块边缘像水波纹一样抖动走近就消失转头时尤其严重部分方块面出现黑色细缝。原因浮点精度不够。相机每次移动 0.001 格距离 300 格外的顶点的世界坐标约 300.001float 只能精确到大约第 7 位有效数字300.001 会产生约 0.01 格的误差这个误差叠加在深度测试上就形成了闪烁。另一个因素是 OpenGL 默认的深度缓冲位宽和 near/far 比例不匹配。解决把相机位置和方块位置的运算采用“局部坐标”方案——渲染时把世界坐标减去相机所在区块的原点让顶点坐标保持在 0~64 范围内再通过模型矩阵平移回去。深度缓冲方面far 从 1000 缩到 400near 从 0.1 调到 0.3深度精度在不同的 near/far 比值下会得到明显改善如果仍然闪烁可以增加 glClearDepth 或者后续改用反向 Z 缓冲。5. 让这个简易版真正“可玩”面剔除、下落重力与一个能扩展的存档设计先做面剔除的收尾工作。地形生成时只生成朝外的面这个逻辑还不够彻底草方块顶面朝上、石头底面朝下这些面在玩家永远看不到的角度也在绘制。进一步的做法是生成网格时直接丢弃朝下的底面地平面以下没人看和垂直方向看不见的侧壁再配合 GL_CULL_FACE 剔除背面。做这一步时我记得特别清楚帧率从 14 帧直接跳到 70 帧而且只是删了不到二十行生成面代码。你验证效果时不用开统计工具转动视角时的画面流畅度差异自己就能感觉到。然后是重力。方块世界里的重力逻辑天然简单每个方块在更新时检查下方是否为空气是空气就往下移动一格直到落地。做玩家时更直接——玩家不是连续移动的实体而是一格一格的碰撞盒每帧先尝试向下移动 0.01 格碰到底部方块就停止下落按空格再触发跳跃。这个设计方案比连续碰撞检测简单得多而且玩家不会被卡进方块里。存档设计做 diff 方案比全量存档实用得多每次保存只记录玩家改动过的方块坐标和类型存档文件毫无冗余。启动时先按固定种子生成原始地形再遍历所有修改记录覆盖方块类型这样世界总能还原。用第三方的调用方式封装新增 EditRecord 结构体把鼠标摆放的方块更新进去保存时序列化。我后来把这个简易版交给另一个朋友玩他问了句“岩浆和水能流动吗”直接把我问住了。这提醒我一个事实动态效果才是真正开发的开始。我的习惯是每完成一小节就编译运行一次每次都只用真实操作验证而不是看一眼编译通过就继续写下一个特性。如果你想把它当课程设计或简历项目建议你在此基础上补一个方块类型表、把哈希地形换成可配置种子再加一个最简单的光照传播这三件事做完这个简易版就不再只是“能跑”而是一个能讲出设计思路的作品了。希望帮到你。本文还有配套的精品资源点击获取