☰
Python + Pyglet 从零实现 Minecraft 风格 3D 方块世界
2026/9/25 4:46:32 网站建设 项目流程

简介:这是一份以Minecraft为灵感、用Python与Pyglet实现的简易3D方块世界演示程序,面向刚接触编程的青少年与Python初学者,也适合希望用游戏化案例开展教学的教师。项目目标是把Minecraft的趣味性与Python的易学性结合,让学习者在搭建世界、放置方块的过程中理解图形渲染与面向对象编程的基本思路。压缩包共4个文件,包含1个Python主程序、1个PNG贴图、1份开源许可证和1份Markdown说明文档,整体仅16KB,轻量易读,代码注释较为充分,便于快速修改参数并查看效果。目前已有510人学习下载。读者可从中获得一个可直接运行的迷你世界原型,参考其世界初始化、方块坐标设置与纹理加载方式,并借鉴作者提出的库化/API化设计思路,尝试将项目封装为可导入配置的模块,是入门图形编程与游戏逻辑的实用练手素材。

1. 用 Python 和 Pyglet 从零搭一个 Minecraft 风格方块世界

很多人第一次想自己写个 3D 游戏,脑子里冒出来的就是 Minecraft 那种方块世界:能走、能看、能放方块。但真打开引擎一看,Unity 要装几个 G,Unreal 编译半小时,还没写一行逻辑就先被工具链劝退。其实如果你只想搞懂「方块世界到底怎么渲染出来的」,Python 加 Pyglet 是一条极短的路:Pyglet 直接封装 OpenGL,不需要游戏编辑器,一个pip install pyglet就能开始,几百行代码就能跑出一个能自由飞行、能看见地形、能放置和破坏方块的窗口程序。这篇笔记面向两类人:一是刚学完 Python 基础语法、想找个能看见成果的项目练手的入门者;二是写过业务代码、想理解 3D 渲染管线但不想被引擎黑盒挡住的老手。下面按「先跑起来、再拆原理、最后调优」的顺序,把这条路径完整走一遍。

2. 环境搭建与最小可运行窗口:先把 Pyglet 跑通再谈方块

2.1 为什么选 Pyglet 而不是 Pygame 或直接上引擎

Pygame 做 2D 很顺手,但它对 OpenGL 的封装偏薄,写 3D 要自己处理大量矩阵和着色器绑定,新手容易卡在「黑屏但不知道哪错了」。直接上 Unity 或 Godot 又太重,你花在学编辑器上的时间远超学渲染原理的时间。Pyglet 的定位刚好卡在中间:它自带窗口、事件循环、OpenGL 上下文管理,同时把glBegin/glEnd这类底层调用暴露给你,你能清楚看到每个顶点是怎么变成屏幕像素的。

选型上还有几个实际理由。第一,Pyglet 是纯 Python 包,没有 C 扩展编译步骤,Windows、macOS、Linux 上pip install pyglet基本一次成功,不会出现「python 下载安装教程」里那种装完发现缺 VC++ 运行库的翻车。第二,它的 API 稳定,1.5 到 2.0 的迁移成本低,网上大量老教程还能用。第三,Pyglet 内置了pyglet.graphics模块,批量提交顶点数据很方便,这对后面渲染成千上万个方块面至关重要。

需要提醒的是,Pyglet 2.0 之后默认要求 OpenGL 3.3 以上,老机器如果显卡驱动太旧会直接报No standard config is available。遇到这种情况,要么升级驱动,要么退回 Pyglet 1.5.x。我一般会在项目开头就固定版本,避免不同机器上行为不一致。

2.2 用 venv 隔离环境并锁定 Pyglet 版本

不管你是用 VSCode 还是 PyCharm,第一步都建议建虚拟环境。很多「vscode 配置 python 环境」的教程会教你直接全局装包,结果项目一多就互相污染。下面这套命令在三大平台通用:

# 创建虚拟环境,目录名用 .venv 是社区惯例 python -m venv .venv # 激活环境:Windows 用下面这行 .venv\Scripts\activate # macOS / Linux 用下面这行 source .venv/bin/activate # 安装并锁定 Pyglet 版本,1.5.27 对老显卡更友好 pip install "pyglet==1.5.27" # 导出依赖清单,方便换机器复现 pip freeze > requirements.txt

逻辑说明:venv把解释器和第三方包隔离在项目目录内,删掉.venv就等于卸载干净。锁定1.5.27是因为这个版本对 OpenGL 2.1 仍兼容,覆盖的机器更广;如果你确定显卡较新,也可以装最新的 2.0.x,但要注意 API 差异。requirements.txt是给未来的自己或协作者留的后悔药,换机器时pip install -r requirements.txt就能还原。

参数上唯一要留意的是 Python 版本。Pyglet 1.5.x 支持 Python 3.6 到 3.11,3.12 之后部分内部调用有变动,可能报AttributeError。如果你本地是 3.12,建议用 3.11 建环境,或者直接上 Pyglet 2.0。

2.3 二十行代码验证窗口和 OpenGL 上下文

装完先别急着写方块,用最小窗口确认渲染管线是通的。下面这段代码只做三件事:开窗、设背景色、画一个三角形。

import pyglet from pyglet.gl import * # 创建 800x600 的窗口,标题随意 window = pyglet.window.Window(800, 600, caption="Minecraft-like Test") @window.event def on_draw(): # 每帧先清屏,颜色是 RGBA,这里用天空蓝 glClearColor(0.5, 0.7, 1.0, 1.0) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # 用立即模式画一个三角形,验证 OpenGL 可用 glBegin(GL_TRIANGLES) glColor3f(1.0, 0.0, 0.0) # 顶点1 红色 glVertex3f(-0.5, -0.5, 0.0) glColor3f(0.0, 1.0, 0.0) # 顶点2 绿色 glVertex3f(0.5, -0.5, 0.0) glColor3f(0.0, 0.0, 1.0) # 顶点3 蓝色 glVertex3f(0.0, 0.5, 0.0) glEnd() pyglet.app.run()

逻辑说明:on_draw是 Pyglet 每帧自动调用的回调,所有绘制都写在这里。glClear清掉颜色和深度缓冲,深度缓冲是后面 3D 遮挡关系的基础,现在先带上。glBegin/glEnd是 OpenGL 立即模式,虽然性能差,但用来验证环境最直观——如果窗口是蓝底加一个彩色三角形,说明驱动、上下文、Pyglet 三者都正常。

参数说明:Window(800, 600)是像素尺寸,可以按屏幕缩放;glClearColor四个分量范围都是 0 到 1,不是 0 到 255,这是新手常踩的坑。如果运行后窗口一闪而过,通常是没调pyglet.app.run(),或者事件循环被阻塞了。

3. 方块世界的渲染原理:从单个立方体到批量顶点

3.1 一个方块为什么是 12 个三角形而不是 6 个面

Minecraft 里的方块看着是六个面,但每个面要拆成两个三角形才能被 GPU 光栅化,所以一个完整立方体是 12 个三角形、36 个顶点。如果你每个方块都单独提交这 36 个顶点,一个 16×16×16 的区块就是 4096 个方块、将近 15 万个顶点,Python 循环提交会直接卡成幻灯片。

这里的关键认知是:GPU 不怕顶点多,怕的是提交次数多。每次glBegin/glEnd或每次 draw call 都有固定开销,Python 层调用越频繁越慢。所以正确做法是把整个区块的顶点数据合并成一个大数组,一次性提交。Pyglet 的pyglet.graphics.Batch和VertexList就是干这个的,它内部用ctypes把数据打包成 C 数组,绕过了 Python 循环的瓶颈。

另一个优化点是面剔除。两个相邻方块之间的面是看不见的,没必要渲染。判断规则很简单:如果某个方向的邻居方块是实心的,这个面就不加入顶点列表。一个被完全包围的方块贡献 0 个面,地表方块通常只贡献 1 到 3 个面。这一条能把顶点数砍掉一半以上,是方块世界性能的分水岭。

3.2 用 Batch 批量构建区块顶点

下面这段代码演示如何把一个区块的方块面收集起来,用 Batch 一次性渲染。为了聚焦原理,先不做纹理,只用颜色区分面朝向。

import pyglet from pyglet.gl import * from pyglet.graphics import Batch # 定义立方体六个面的顶点偏移,每个面两个三角形 # 顺序:底面、顶面、前、后、左、右 FACES = [ [(0,0,0),(1,0,0),(1,0,1),(0,0,0),(1,0,1),(0,0,1)], # 底 [(0,1,0),(0,1,1),(1,1,1),(0,1,0),(1,1,1),(1,1,0)], # 顶 [(0,0,1),(1,0,1),(1,1,1),(0,0,1),(1,1,1),(0,1,1)], # 前 [(0,0,0),(0,1,0),(1,1,0),(0,0,0),(1,1,0),(1,0,0)], # 后 [(0,0,0),(0,0,1),(0,1,1),(0,0,0),(0,1,1),(0,1,0)], # 左 [(1,0,0),(1,1,0),(1,1,1),(1,0,0),(1,1,1),(1,0,1)], # 右 ] # 每个面的颜色,方便肉眼区分朝向 FACE_COLORS = [ (0.4,0.4,0.4), (0.6,0.6,0.6), (0.8,0.2,0.2), (0.2,0.8,0.2), (0.2,0.2,0.8), (0.8,0.8,0.2), ] def build_chunk(batch, blocks, ox, oy, oz): """把 blocks 集合里的方块面加入 batch,ox/oy/oz 是区块原点偏移""" for (x, y, z) in blocks: for i, face in enumerate(FACES): # 面剔除:检查该方向邻居是否存在 nx, ny, nz = x, y, z if i == 0: ny -= 1 elif i == 1: ny += 1 elif i == 2: nz += 1 elif i == 3: nz -= 1 elif i == 4: nx -= 1 elif i == 5: nx += 1 if (nx, ny, nz) in blocks: continue # 邻居是实心,这个面不渲染 # 收集这个面的 6 个顶点坐标和颜色 verts = [] colors = [] for (vx, vy, vz) in face: verts.extend([ox + x + vx, oy + y + vy, oz + z + vz]) colors.extend(FACE_COLORS[i]) # 加入 batch,模式是三角形 batch.add(6, GL_TRIANGLES, None, ('v3f', verts), ('c3f', colors))

逻辑说明:FACES用局部坐标描述每个面的两个三角形,顶点顺序遵循逆时针,保证法线朝外。build_chunk遍历方块集合,对每个面先做邻居检查,被遮挡就跳过。batch.add的第二个参数是图元类型,('v3f', verts)表示顶点位置是 3 个 float,('c3f', colors)表示颜色也是 3 个 float。所有面加完后,在on_draw里调batch.draw()一次提交。

参数说明:batch.add的第一个参数是顶点数,一个面 6 个顶点所以填 6。None位置是索引数组,这里不用索引所以传 None。如果你的方块数量很大,建议改用IndexedVertexList配合索引缓冲,能进一步省显存。面剔除的判断顺序要和FACES的索引严格对应,写错一个方向就会出现「某些面莫名消失」的玄学问题。

3.3 用投影矩阵把 3D 坐标映射到屏幕

上面画的顶点还是 2D 平面坐标,要变成能走进去的 3D 世界,必须设置投影矩阵和视图矩阵。Pyglet 提供了window.projection和window.view两个矩阵,分别控制「怎么投影」和「摄像机在哪」。

from pyglet.gl import glu @window.event def on_resize(width, height): # 设置视口,保证画面不被拉伸 glViewport(0, 0, width, height) # 投影矩阵:45 度视野,近裁剪面 0.1,远裁剪面 100 glMatrixMode(GL_PROJECTION) glLoadIdentity() gluPerspective(45.0, width / float(height), 0.1, 100.0) glMatrixMode(GL_MODELVIEW) def set_camera(px, py, pz, yaw, pitch): """根据玩家位置和朝向设置视图矩阵""" glLoadIdentity() # 先旋转再平移,顺序不能反 glRotatef(pitch, 1, 0, 0) glRotatef(yaw, 0, 1, 0) glTranslatef(-px, -py, -pz)

逻辑说明:gluPerspective的四个参数分别是视野角度、宽高比、近裁剪面、远裁剪面。视野越大画面越广但边缘畸变越明显,45 到 70 是常见范围。set_camera里旋转和平移的顺序很关键:摄像机变换等价于把整个世界反向变换,所以要先转再移,写反了会出现「转头时场景绕着某个点乱飞」的翻车。

参数说明:近裁剪面不能设 0,否则深度缓冲精度会崩;远裁剪面越大,远处物体越容易闪烁(z-fighting),100 到 300 对小型方块世界够用。yaw是水平旋转角度,pitch是俯仰角,通常把pitch限制在 -89 到 89 度之间,避免万向节死锁导致视角翻转。

4. 交互与地形生成:让世界能走、能挖、能放

4.1 键盘鼠标事件绑定与第一人称移动

一个能玩的方块世界,核心交互就三样:WASD 移动、鼠标转视角、左右键挖放。Pyglet 的事件系统用装饰器绑定,写起来很直白。

keys = set() @window.event def on_key_press(symbol, modifiers): keys.add(symbol) if symbol == pyglet.window.key.ESCAPE: window.close() @window.event def on_key_release(symbol, modifiers): keys.discard(symbol) @window.event def on_mouse_motion(x, y, dx, dy): global yaw, pitch yaw += dx * 0.2 pitch -= dy * 0.2 # 限制俯仰角,防止翻转 pitch = max(-89, min(89, pitch)) def update(dt): """每帧根据按键更新玩家位置""" speed = 5.0 * dt # 每秒 5 个单位 # 根据 yaw 计算前后左右方向 import math rad = math.radians(yaw) fx, fz = -math.sin(rad), -math.cos(rad) # 前方向 rx, rz = math.cos(rad), -math.sin(rad) # 右方向 if pyglet.window.key.W in keys: player[0] += fx * speed; player[2] += fz * speed if pyglet.window.key.S in keys: player[0] -= fx * speed; player[2] -= fz * speed if pyglet.window.key.A in keys: player[0] -= rx * speed; player[2] -= rz * speed if pyglet.window.key.D in keys: player[0] += rx * speed; player[2] += rz * speed pyglet.clock.schedule_interval(update, 1/60.0)

逻辑说明:keys集合记录当前按下的键,on_key_press/release负责增删。鼠标移动的dx/dy是相对位移,乘一个灵敏度系数累加到yaw/pitch。update用dt做时间步长,保证不同帧率下移动速度一致,这是所有实时程序的基本功。方向向量用三角函数从yaw推导,前方向是(-sin, -cos),右方向是(cos, -sin),这个推导基于 OpenGL 默认相机朝 -Z 看。

参数说明:灵敏度0.2可以按手感调,太大容易晕,太小转不动。移动速度5.0单位每秒对小型世界合适,想飞快点可以加 Shift 加速。schedule_interval的1/60是目标帧率,Pyglet 会尽量按这个间隔调用,但实际帧率受渲染负载影响。

4.2 用射线检测实现方块拾取

挖方块和放方块的前提是知道「玩家看着哪个方块」。标准做法是从摄像机位置沿视线方向发射一条射线,和方块网格求交。方块世界有个便利:所有方块都是轴对齐的单位立方体,可以用 DDA 算法逐格步进,比通用射线求交快得多。

def raycast(origin, direction, max_dist=8.0): """DDA 步进,返回第一个命中的方块坐标和命中面法线""" import math x, y, z = [math.floor(v) for v in origin] dx, dy, dz = direction step_x = 1 if dx > 0 else -1 step_y = 1 if dy > 0 else -1 step_z = 1 if dz > 0 else -1 # 到下一个格边界的距离 t_max_x = ((x + (1 if dx > 0 else 0)) - origin[0]) / dx if dx != 0 else float('inf') t_max_y = ((y + (1 if dy > 0 else 0)) - origin[1]) / dy if dy != 0 else float('inf') t_max_z = ((z + (1 if dz > 0 else 0)) - origin[2]) / dz if dz != 0 else float('inf') t_delta_x = abs(1 / dx) if dx != 0 else float('inf') t_delta_y = abs(1 / dy) if dy != 0 else float('inf') t_delta_z = abs(1 / dz) if dz != 0 else float('inf') normal = (0, 0, 0) t = 0.0 while t < max_dist: if (x, y, z) in blocks: return (x, y, z), normal # 沿最小的 t_max 方向步进 if t_max_x < t_max_y and t_max_x < t_max_z: x += step_x; t = t_max_x; t_max_x += t_delta_x normal = (-step_x, 0, 0) elif t_max_y < t_max_z: y += step_y; t = t_max_y; t_max_y += t_delta_y normal = (0, -step_y, 0) else: z += step_z; t = t_max_z; t_max_z += t_delta_z normal = (0, 0, -step_z) return None, None

逻辑说明:DDA 的核心是维护三个方向的「到下一个格边界的参数距离」t_max,每次挑最小的那个方向走一格,同时更新该方向的t_max。这样射线经过的格子顺序和真实相交顺序一致,不会漏格。命中时返回方块坐标和进入面的法线,法线用来决定新方块放在哪一侧。

参数说明:max_dist是交互距离,Minecraft 生存模式大约 4.5 格,创造模式 5 格,这里给 8 格方便调试。direction必须是单位向量,否则t_max的计算会失真。除零用float('inf')兜底,避免方向分量为 0 时崩溃。

4.3 用值噪声生成起伏地形

手工摆方块只能做小场景,想要一片能跑的地形得靠噪声。Perlin 噪声或 Simplex 噪声是标准选择,Python 里可以用noise库,也可以自己写一个简化的值噪声。下面用值噪声加双线性插值生成高度图。

import random, math def value_noise_2d(x, y, seed=42): """简化值噪声:格点随机值 + 双线性插值""" random.seed(seed) def grid_val(gx, gy): random.seed(seed + gx * 73856093 ^ gy * 19349663) return random.random() x0, y0 = math.floor(x), math.floor(y) fx, fy = x - x0, y - y0 # 平滑插值曲线,避免格点处出现折角 sx = fx * fx * (3 - 2 * fx) sy = fy * fy * (3 - 2 * fy) v00 = grid_val(x0, y0) v10 = grid_val(x0 + 1, y0) v01 = grid_val(x0, y0 + 1) v11 = grid_val(x0 + 1, y0 + 1) top = v00 * (1 - sx) + v10 * sx bot = v01 * (1 - sx) + v11 * sx return top * (1 - sy) + bot * sy def generate_terrain(size=32, scale=0.1, height=8): """生成 size x size 的地形,返回方块集合""" result = set() for x in range(size): for z in range(size): # 多个八度叠加,地形更自然 n = value_noise_2d(x * scale, z * scale) n += value_noise_2d(x * scale * 2, z * scale * 2) * 0.5 n /= 1.5 h = int(n * height) for y in range(h + 1): result.add((x, y, z)) return result

逻辑说明:值噪声在每个整数格点生成随机值,格点之间用平滑曲线插值,得到连续起伏。单个八度太规则,叠加两个频率不同的八度(第二个频率翻倍、幅度减半)能模拟出更自然的山丘。generate_terrain把高度以下的方块全部填实,形成实心地形。

参数说明:scale控制地形起伏频率,越小山越平缓,0.05 到 0.2 之间比较自然。height是最大高度,配合size决定世界规模。seed固定后地形可复现,换 seed 就是新地图。注意这个实现每次调用grid_val都重设随机种子,性能一般,正式项目建议预生成一张噪声表缓存起来。

5. 性能与踩坑排查:方块世界的血泪经验

5.1 帧率从 60 掉到 5 的常见原因

现象:方块数量加到几千个后,画面明显卡顿,鼠标移动有延迟。 原因:最常见的是每个方块单独调batch.add且没有做面剔除,顶点数爆炸;其次是每帧重新构建 Batch,把构建开销算进了渲染循环。 解决:面剔除必须做,这是第一优先级。Batch 只在方块集合变化时重建,比如挖掉或放下方块后标记dirty,下一帧再重建,不要每帧都重建。如果还卡,把区块按 16×16 分块,每块一个 Batch,视锥外的区块直接跳过绘制。

5.2 方块面闪烁或深度错乱

现象:相邻方块的接触面出现闪烁,或者远处方块穿透近处方块显示。 原因:深度缓冲没开,或者开了但glDepthFunc设置不对;也可能是近远裁剪面比例过大导致深度精度不足。 解决:在on_draw开头确保glEnable(GL_DEPTH_TEST),并且每帧glClear带上GL_DEPTH_BUFFER_BIT。近裁剪面不要小于 0.1,远裁剪面不要超过 500。如果还有闪烁,检查顶点顺序是否一致,逆时针和顺时针混用会让背面剔除失效。

5.3 鼠标转视角时画面抖动

现象:移动鼠标时视角一顿一顿,或者转头后场景整体偏移。 原因:on_mouse_motion的dx/dy在某些系统上会给出异常大的跳变值,尤其是鼠标移到窗口边缘时;另一个原因是旋转和平移顺序写反。 解决:对dx/dy做限幅,比如dx = max(-50, min(50, dx)),过滤掉跳变。矩阵顺序必须是先glRotatef再glTranslatef,这个顺序错了就会出现「转头时世界绕着玩家转」的诡异效果。另外建议把鼠标设为独占模式,避免指针跑出窗口。

5.4 地形生成后内存暴涨

现象:生成 64×64 的地形后内存占用几个 G,程序越来越慢。 原因:用 Python 的set存几万个元组,每个元组和哈希表项都有开销;如果还缓存了每个方块的顶点列表,内存翻倍。 解决:方块坐标用整数编码成单个整数存进set,比如x * 1000000 + y * 1000 + z,比元组省一半以上内存。顶点数据不要按方块缓存,按区块构建完就交给 GPU,Python 侧只保留方块集合。如果世界再大,考虑用numpy数组或array模块存体素数据。

5.5 Pyglet 窗口在 macOS 上无法获得焦点

现象:macOS 上运行后窗口出现但键盘鼠标无响应,或者窗口标题栏灰色。 原因:macOS 对窗口焦点和事件循环有额外要求,Pyglet 1.5.x 在某些系统版本上需要手动激活。 解决:在pyglet.app.run()之前调window.activate(),如果还不行,升级到 Pyglet 2.0.x,它对 macOS 的支持更完善。另外确认 Python 是从终端启动而不是从 IDE 的嵌入式终端启动,后者有时不传递焦点事件。

6. 进阶技巧:用显示列表和纹理让方块世界更像样

走到这里,你已经有一个能跑、能挖、能放的第一人称方块世界了。但纯色方块看着还是像工程 demo,离「Minecraft 风格」差一层纹理。这一章讲两个具体技巧:用显示列表降低重复绘制开销,以及用纹理图集给方块贴图。

先说显示列表。如果你的地形是静态的,比如一整片不常改动的地面,可以把它的顶点提交包进一个glNewList/glEndList,之后每帧只调glCallList。显示列表把顶点数据存在 GPU 侧,省掉了每帧从 Python 传数据的开销。实测在几千个面的规模下,显示列表能把 CPU 占用降一半。代价是地形一变就得重建列表,所以适合「静态区块 + 动态区块」混合的方案:地面用显示列表,玩家附近可交互的区块用 Batch。

# 为静态区块创建显示列表 list_id = glGenLists(1) glNewList(list_id, GL_COMPILE) # 这里放该区块所有面的 glBegin/glEnd 绘制 glEndList() # 每帧绘制时直接调用 glCallList(list_id)

逻辑说明:GL_COMPILE表示只编译不执行,顶点数据被记录到列表里。之后glCallList触发 GPU 回放,不再经过 Python。注意显示列表一旦创建就不能修改,方块变化时要glDeleteLists再重建。

再说纹理。Minecraft 的方块纹理是一张图集,每个方块面取其中一小块。用 Pyglet 加载纹理后,把('v3f', verts)换成('v3f', verts), ('t2f', tex_coords),其中tex_coords是该面对应的 UV 坐标。UV 坐标范围是 0 到 1,图集里第i行第j列的小块对应u从j/cols到(j+1)/cols,v同理。这里有个容易翻车的点:OpenGL 的v轴原点在左下,而图片坐标原点在左上,UV 要上下翻转,否则纹理会倒过来。

# 加载纹理并设置最近邻过滤,保持像素风 texture = pyglet.image.load('atlas.png').get_texture() glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) # 每个面的 UV,假设图集是 4x4,取第 0 行第 0 列 u0, v0 = 0.0, 0.75 u1, v1 = 0.25, 1.0 tex_coords = [u0,v0, u1,v0, u1,v1, u0,v0, u1,v1, u0,v1]

参数说明:GL_NEAREST是最近邻采样,方块边缘锐利,符合像素风;用GL_LINEAR会糊成一片。图集尺寸建议是 2 的幂,比如 256×256 或 512×512,老显卡对非 2 幂纹理支持不好。UV 的v翻转如果懒得算,可以在加载图片后调texture.anchor_y或者直接对图片做垂直翻转。

最后说一个验证方法:写个简单的帧率显示,把pyglet.clock.get_fps()画在窗口标题上,改一处代码就看一眼帧率变化。我自己的习惯是每加一个优化点就记录一次帧率,这样能清楚知道哪个改动真正有效,而不是凭感觉。方块世界这个方向,从几十行到几千行都有得玩,关键是先把最小闭环跑通,再一层层加纹理、加光照、加存档。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询