Python我的世界小游戏源码解析:从体素生成到碰撞检测
发布时间:2026/9/16 12:30:14来源:尧图网络
简介这是一份用Python实现的《我的世界》风格沙盒游戏源代码面向Python初学者及对游戏开发感兴趣的读者可用于理解3D方块世界的简易建模、视角控制与玩家交互。资源共4个文件包含核心py程序、纹理png图片、说明md文档及license授权文件压缩包大小仅15KB结构紧凑、定位清晰。目前已有13347人学习浏览是入门级游戏源码中较受欢迎的参考案例。代码实现了前进后退、左右转向、跳跃、鼠标环顾、Tab切换飞行模式等移动操作并支持按1/2/3键切换砖块、草、沙子等建筑材料左键删除建筑、右键创建建筑块能直观体验从零搭建方块场景的过程。整体逻辑简洁、模块划分清楚适合作为课程设计或入门练手项目便于在此基础上扩展更多功能如新增材质、音效或存档机制。1. 这个 Python 我的世界小游戏源代码到底是什么解压之后你会发现这个「Python 我的世界小游戏源代码」压缩包里没有几十个文件只有main.py、texture.png、LICENSE、README.md。它是用纯 Python 写出来的体素沙盒小游戏操作逻辑和 Minecraft 几乎同构W/S/A/D 控制前后左右鼠标环顾四周空格跳起Tab 切换飞行模式数字键 1/2/3 分别选择砖块、草和沙子鼠标左键删除方块、右键放置方块。对想看懂体素引擎源码、做课程设计或者用 Python 小游戏练手的开发者来说这是一份非常合适的参考实现。它篇幅不大但把纹理加载、碰撞检测、射线拾取、事件循环这几块都串了起来。2. 体素世界的生成与纹理加载先看懂 main.py 的骨架2.1 从 import 到窗口初始化项目跑在哪个 API 上拿到这份 Python 源代码第一件事是看main.py顶部的 import。类似的项目十有八九是建立在 Pyglet 上的偶有用 Ursina 或 Pygame 重写的但基础逻辑都差不多。用文本编辑器打开后你会看到这样的结构import pyglet from pyglet.window import key, mouse from pyglet.gl import *Pyglet 负责了窗口创建、OpenGL 渲染循环和键盘鼠标事件分发。如果直接运行python main.py报ModuleNotFoundError: No module named pyglet说明当前 Python 环境里没有装这个第三方库常见的做法是pip install pyglet很多 Python 初学者卡在python 安装或者环境配置阶段实际上这类小游戏只需要一个 3.7 的解释器再用 pip 装一次 Pyglet 就能跑。不要一上来就试图去装完整版 Minecraft 那种大型引擎这个资源的价值就在于依赖少、单文件可读。窗口初始化的时序通常是这样的WINDOW_WIDTH 800 WINDOW_HEIGHT 600 window pyglet.window.Window( widthWINDOW_WIDTH, heightWINDOW_HEIGHT, captionPython Minecraft, resizableTrue, ) window.set_exclusive_mouse(True) game Game() window.push_handlers(game) pyglet.app.run()这段代码里有几个值得解释的细节。set_exclusive_mouse(True)会把鼠标锁在窗口内并且隐藏光标之后通过鼠标相对位移控制视角这个是「我的世界小游戏」里环顾四周能顺畅工作的前提。push_handlers(game)把Game实例里的on_key_press、on_mouse_press、on_mouse_motion等方法注册成事件回调Pyglet 在每次事件循环中会直接分发到这些方法上。最后pyglet.app.run()是一个阻塞调用等于整个游戏的生命周期都在这个事件循环里。参数上需要留意的一点是resizableTrue允许拉伸窗口但拉伸后 OpenGL 的投影比例需要跟着变化。源码里一般会监听on_resize再重新设置glViewport和透视矩阵如果窗口拉出异常画面优先检查这个回调。width和height可以同时改小到 640x480这对集成显卡的机器很友好也能明显提高帧率。2.2 texture.png 的纹理图集与 UV 坐标这个压缩包里的texture.png不是一张普通图片它是多个 16x16 像素方块纹理拼合在一起的图集。常见做法是用一张小 PNG 把「砖、草、沙子」放进同一张图里避免加载多个纹理对象也方便把纹理坐标交给 VBO 一次画完。加载纹理的代码通常是from pyglet import image from pyglet.sprite import TextureGroup from pyglet.gl import GL_NEAREST texture image.load(texture.png) texture_group TextureGroup(texture.get_texture())TextureGroup的作用是把使用同一张纹理的所有方块打包到一起这样游戏运行时可以减少 OpenGL 的纹理绑定切换。GL_NEAREST属于纹理采样参数拉到最近点而不是线性插值像素风格才不会被模糊掉。换成GL_LINEAR后方块边缘会出现一圈很廉价的过渡带视觉上立刻不对。如果运行后贴图是黑色或花屏不要急着改渲染逻辑先检查两点。第一texture.png是否和main.py在同一目录第二PNG 是否带 alpha 通道有些截图软件导出的 PNG 会自动加透明通道而 OpenGL 端如果没开启混合就会出现黑底。用下面的命令可以快速确认图像信息python -c from PIL import Image; im Image.open(texture.png); print(im.size, im.mode)如果没装 Pillow直接在线打开图片看尺寸也行。一般这种源码里的texture.png会是 48x16 或 16x16 这类规整尺寸因为每个小纹理都按 16 像素切分UV 坐标直接按格子计算。2.3 世界方块表与初始地形生成体素世界的核心数据结构在主源码里通常叫world它是一个把坐标映射到方块类型的字典self.world {} GRASS 1 BRICK 2 SAND 3 for x in range(-20, 20): for z in range(-20, 20): y 0 self.add_block((x, y - 1, z), BRICK) # 底层是砖 self.add_block((x, y, z), GRASS) # 表面是草为什么用字典而不是三维数组因为玩家只会在有限范围内建造和拆除字典天然适合稀疏世界。三维数组在固定 40x40 的地图里没问题但如果把世界扩大到几百乘几百数组会立刻吃掉大量内存。用(x, y, z)作为 key查询复杂度是 O(1)在射线拾取和碰撞检测里非常划算。add_block内部一般只做两件事把坐标写进world字典然后根据相邻方块状态决定要不要把该坐标的顶点数据提交给渲染 Batch。也就是说纹理在texture.png里定义但真正画不画、画几个面取决于周围环境。值得留意的是不同源码里方块 ID 的约定不一致。这份 README 明确写了 1 对应砖、2 对应草、3 对应沙子所以看代码时如果发现self.block 2默认选中草方块不要误以为是 bug。3. 碰撞检测与按键映射移动、跳跃和飞行怎么落地3.1 AABB 碰撞检测原理把玩家抽象成一个长方体叫 AABBAxis-Aligned Bounding Box这是体素游戏里最经典的碰撞方案。玩家的包围盒不需要很精确常规参数是水平方向 0.6 格宽、垂直方向 1.8 格高脚底坐标作为位置基准。碰撞每帧都要做三次检测分别对应 x、y、z 三个轴。这样处理的好处是滑墙走的时候不会因为一侧被挡住而完全锁死移动跳起来碰到天花板也不会把水平速度吃掉。常见实现是这样def collide(self, position, axis): # 遍历玩家包围盒覆盖到的所有方块 for dx in [-0.3, 0.3]: for dz in [-0.3, 0.3]: for dy in [0.0, 1.8]: x floor(position[0] dx) y floor(position[1] dy) z floor(position[2] dz) if (x, y, z) in model.world: collide_axis(axis, position, x, y, z)这段代码的思路是把要移动的那一轴拆出来单独算先按速度移动到新位置然后检查包围盒覆盖范围内的所有体素如果碰上了就把该轴的位置推回到碰撞边界。这样玩家在斜面或低矮洞穴里的表现才可控也避免了直接从方块的角上穿过去。选择 0.3 和 1.8 这两个参数是有讲究的。宽度小于 0.5 会让玩家看起来太瘦撞击判定也偏松大于 1 的话一格通道就过不去了。高度必须是小于 2 的正数否则两格高的空间也站不进去。直接改这些常量不会直接影响渲染但会影响穿越洞穴的手感。3.2 on_key_press 与按键状态WASD、Tab 和数字键按键映射这段逻辑是这份资料里最容易改成自定义风格的部分。常见的键位结构是一个 4 长度的strafe数组和几个布尔状态def on_key_press(self, symbol, modifiers): if symbol key.W: self.strafe[0] - 1 elif symbol key.S: self.strafe[0] 1 elif symbol key.A: self.strafe[1] - 1 elif symbol key.D: self.strafe[1] 1 elif symbol key.SPACE: self.flying not self.flying if tab_active else self.set_jump() elif symbol key.TAB: self.flying not self.flying elif key._1 symbol key._3: self.block symbol - key._1 1strafe数组存的是移动方向累加值而不是直接把速度设置为固定值。这样按 W 的同时按 A两个方向会同时在update阶段叠加最终的移动向量会自然归一化不会出现只响应最后一个按键的问题。按键映射建议在 README 里同步记录。比如这份资料的操作说明里写的是 1/2/3 分别对应砖、草、沙子那么self.block symbol - key._1 1刚好把数字键映射到 1、2、3。如果你自己扩展到 9 个方块只需要把条件判断改成key._1 symbol key._9再补充相应的纹理坐标即可。Tab 切换飞行的语义和空格不同空格是在按下瞬间触发一次跳跃动作Tab 是按下瞬间切换飞行状态。所以代码里self.flying not self.flying一定要写在on_key_press不能写在update中持续判断否则每一帧都会翻转状态导致玩家在飞与不飞之间疯狂抖动。3.3 左键删除、右键放置射线拾取算法鼠标左键拆方块、右键放方块核心是射线拾取。实现上最常用的是 DDA 网格遍历算法也就是从相机位置出发沿着视线方向逐格走过体素空间直到找到第一个非空方块。一个可落地的流程是这样def ray_cast(self, position, direction, max_distance): # 初始化 tMAX 和 tDELTA # 循环最多 32 步逐格走向目标 block self.find_non_empty(position, direction) if block: return block, self.previous_block return None, Noneprevious_block是射线在进入目标方块之前经过的最后一个空格子。这个设计是为了解决一个很常见的误操作当玩家贴着一面墙右键时放置方块必须出现在墙的这一侧而不是穿模到墙的另一侧。左键删除取block右键放置取previous_block两者只有一步之差。射线拾取算法的最大坑是浮点数精度。当玩家位置正好落在 x 或 y 的整数值上时tmax初始计算很容易出现除零异常通常的规避方法是给方向向量的每个分量加一个极小的 epsilon。如果没做这个保护连续快速右键时会偶尔出现方块放在原地不动的情况看起来像按键失灵。4. 鼠标视角与渲染性能让小游戏跑满 60 帧4.1 yaw/pitch 与鼠标相对位移鼠标环顾四周的实现不复杂但容易出错。核心维护两个变量水平角yaw和俯仰角pitch每次鼠标移动时累加相对位移。SENSITIVITY 0.01 def on_mouse_motion(self, x, y, dx, dy): self.yaw dx * SENSITIVITY self.pitch -dy * SENSITIVITY self.pitch max(-90, min(90, self.pitch))dx/dy是鼠标相对上一帧的位移不是绝对坐标。所以启用set_exclusive_mouse(True)之后即使鼠标一直向右甩dx也不会因为到达屏幕边缘而停止增长。pitch必须限制在正负 90 度否则玩家头会翻过去渲染视野会变得很怪。SENSITIVITY是个人手感的调节入口数值调到 0.005 就是慢速瞄准调到 0.03 基本就是甩鼠标转视角了。在 Pyglet 里鼠标拖拽和鼠标移动是两个事件区别在于是否按住鼠标键。这类体素游戏一般同时监听两个事件否则按住右键旋转视角时画面会出现顿挫感。4.2 批处理 batch 和隐藏面剔除体素游戏最容易卡的原因不是方块总数多而是送入 GPU 的面数太多。假设每个方块画 6 个面40x40 的平面就是 1600 个方块、近万个四边形还不算建造出来的柱子。所以这份源码里会看到「只渲染暴露面」的逻辑faces [ (0, 1, 0), # 上 (0, -1, 0), # 下 (1, 0, 0), # 右 (-1, 0, 0), # 左 (0, 0, 1), # 前 (0, 0, -1), # 后 ] for face in faces: if (x face[0], y face[1], z face[2]) not in world: self.add_face_to_batch(x, y, z, face)逻辑很简单一个方块只有在其相邻位置不存在方块时才画这个方向的表面。地下被完全包围的方块就永远不会创建顶点数据这样方块数量越多剔除收益反而越明显。这也是为什么虽然 world 字典可以无限扩大但实际渲染顶点数维持在可接受范围内的主要原因。Pyglet 的Batch会把所有个体积的顶点数据合并成一次 draw call如果你把batch.draw()放在on_draw里可以看到所有方块只调用一次 OpenGL 绘制。千万不要在每帧里逐个画方块那是毁灭性的性能浪费。4.3 性能排查与参数调优如果运行后帧率只有十几帧先不要怀疑电脑太差按这个顺序排查检查点现象处理方式窗口尺寸全屏分辨率太高把Window改为 800x600方块数量初始地形范围过大缩小range(-80, 80)这类循环纹理绑定贴图总是闪烁检查TextureGroup是否统一事件循环多开了一个pyglet.app.run()保证主进程只有一个循环另一个隐蔽问题是垂直同步。Pyglet 默认开启垂直同步刷新率 60Hz 的显示器就锁 60 帧如果显卡强跑满 300 帧反而会提示功耗异常。想关闭垂直同步需要在创建窗口时传入window pyglet.window.Window(... , vsyncFalse)关闭后帧率会立刻跳动但画面可能出现撕裂一般建议保持默认。5. 进阶改造从源码里长出自己的小世界5.1 把平坦地图改成带地形的山丘初始世界是一个纯平面改造的第一步通常是生成起伏地形。最简单的做法是在循环里用随机高度生成一列方块import random height_map {} for x in range(-20, 20): for z in range(-20, 20): h random.randint(0, 4) height_map[(x, z)] h for y in range(h 1): if y h: self.add_block((x, y, z), SAND) else: self.add_block((x, y, z), BRICK)height_map可以看作二维高度图每个(x, z)对应一列 y 轴高度。add_block仍然复用原来的函数所以碰撞和渲染逻辑完全不用动改的只是世界初始化部分。把random.randint换成noise生成柏林噪声就能得到更自然的山丘。5.2 三个高频坑与规避第一个坑是 Pyglet 版本差异。较新的 Pyglet 2.x 里GL_QUADS被移除直接运行会报AttributeError: module pyglet.gl has no attribute GL_QUADS。规避办法是改用GL_TRIANGLES提交 6 个顶点或者单独使用旧的 1.5.x 环境。第二个坑是中文输入法。Windows 下切成中文输入法时按住 W 会触发输入法重音键画面里角色走走停停。运行时切到英文输入法或者在入口处加一个on_text事件把输入消费掉都可以解决。第三个坑是鼠标点击与放置位置的偏差。太多人改完地形之后发现右键放置的方块总是落在脚边一步原因是射线投射用的方向向量没有和相机矩阵同步。移动视角之后重新计算方向而不只是更新 yaw/pitch就不会出现这个问题。想提速的话还可以在main.py末尾加一行print(FPS:, pyglet.clock.get_fps())每帧打印当前 FPS。如果数值始终在 55 以上剩下的优化都可以暂时不考虑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网