01 重新认识世界
进入一个存档后,我们通常会把其中的一切统称为 “这个世界”。但当你穿过下界传送门时,一件有趣的事情发生了:你仍然可以使用相同的坐标,那里却有完全不同的方块和实体。
显然,只知道坐标还不够。
1 一组坐标不能确定一个方块
假设我们说 “坐标 (0, 64, 0) 上有一个石头”。这里其实少了一个条件:它位于哪个维度?
主世界的 (0, 64, 0)、下界的 (0, 64, 0) 和末地的 (0, 64, 0) 指向三个不同的地方。它们拥有相同的三个坐标数值,但分别属于三个不同的维度。
因此,要在游戏中确定一个方块,至少需要:
- 它所在的维度;
- 它在该维度中的坐标。
在源码中,BlockPos 就是整数形式的 x、y、z 坐标;World 则表示这些坐标所属的维度。World 中预先定义了主世界、下界和末地的三个维度标记:
这三个常量的类型都是 RegistryKey<World>。它们不是三个额外的世界对象,而是用于说明 “当前 World 属于哪个维度” 的标记。
服务器使用一个以维度标记为索引的表保存所有 ServerWorld。创建存档时,它先创建主世界,再遍历维度注册表,为其他维度分别创建 ServerWorld。所以从代码角度看,一个存档可以包含多个维度;每个 World 对应其中一个维度的方块、区块和实体运行环境。
2 维度规定了什么
不同维度不只是拥有不同的方块。一个 World 还持有自己的维度类型、维度标记、区块管理器、边界、生物群系访问器和随机数生成器等内容。
维度类型进一步规定了这个维度的一些基本规则。例如:
- 最低可用高度;
- 维度的总高度;
- 是否拥有天空光照;
- 是否拥有天花板;
- 坐标缩放比例;
- 是否设置维度固定时间。
在 1.20.1 的原版数据中,主世界的 min_y 为 -64,height 为 384。因此主世界能够放置方块的 Y 坐标范围是 -64 至 319。下界和末地的 min_y 都是 0,height 都是 256。
这里的上界不包含在范围内。源码使用:
并将 y < bottomY 或 y >= topY 判断为超出高度限制。
3 分开保存,但并非全部独立
每个维度拥有独立的区块。LevelStorage.Session#getWorldDirectory 会根据维度标记取得对应的保存目录,服务端创建每个 ServerWorld 时也会为它建立各自的区块和实体存储。
不过,“不同维度” 不意味着所有数据都互相独立。
在原版服务端中,主世界以外的维度使用 UnmodifiableLevelProperties 访问主世界属性。它的 getTime()、getTimeOfDay() 和天气读取都会转交给主世界属性。这就是为什么原版各维度共享游戏时间,而不是每个维度维护一套互不相干的计时器。
3.1 维度固定时间不是另一套计时器
维度类型中的 fixed_time 可以直译为维度固定时间。这个名字很容易让人误以为 “该维度拥有一套停止不动的时间”,但源码并没有用它替换或停止世界属性中的计时器。计划刻、实体和方块实体仍然照常运行。
fixed_time 的直接使用位置是 DimensionType#getSkyAngle。这个方法计算天体角度时,会优先使用 fixed_time;没有设置它时,才使用维度读取到的 timeOfDay。这个角度会进一步用于天空角度和环境黑暗程度等计算。
例如,下界的维度类型包含 fixed_time: 18000,末地包含 fixed_time: 6000。这里的数值会代替 timeOfDay 参与天体角度计算,但不会代替 getTime() 返回的游戏时间。
因此,“视觉时间” 容易把作用范围说得过窄,“维度时间” 又容易让人误以为存在另一套计时器。本文统一称其为维度固定时间,并将它理解为 “维度类型为天体角度计算指定的固定输入值”。
4 游戏如何根据坐标查找方块
虽然我们可以向 World 询问某组坐标对应的方块状态,但 World 并没有把所有坐标直接塞进一张无限大的表。
当调用 World#getBlockState 时,维度会:
- 根据坐标中的 X、Z 数值计算区块坐标;
- 取得对应的
WorldChunk; - 让区块返回该位置的方块状态。
因此,维度是查找方块时的入口,区块才是组织方块数据的基本单位。下一篇将具体介绍区块。
5 客户端维度与服务端维度
World 中有一个 isClient 字段,用于区分当前维度对象属于客户端还是服务端。它的两个重要实现是:
ClientWorld:客户端用于显示和预测的维度;ServerWorld:服务端用于执行规则和保存结果的维度。
两者都能查询方块和实体,但职责不同。服务端负责决定游戏中真正发生了什么;客户端接收同步结果并将它们展示给玩家。
这一区别能够解释一种常见现象:网络不稳定时,玩家有时会短暂看到一个方块已经被破坏,随后它又重新出现。客户端维度曾经认为方块发生了变化,但最终仍要以服务端同步的状态为准。
这里暂时只需要记住:“我看见了什么” 和 “服务端记录了什么” 可能短暂地不一致。
6 小结
- 一个方块由维度和坐标共同确定。
- 源码中的一个
World对应一个维度;一个存档可以包含多个World。 - 维度规定维度类型、可用高度、区块访问等基本环境。
- 不同维度拥有不同区块,但原版多个维度共享部分存档属性,例如游戏时间。
- 维度固定时间
fixed_time为天体角度计算提供固定输入,不会停止游戏时间或其他游戏逻辑。 - 方块数据由区块组织,
World负责根据坐标找到相应区块。 - 客户端和服务端各有自己的维度对象,最终游戏结果以服务端为准。
7 源码分析
7.1 World 的核心结构
World 同时服务客户端和服务端,通过 isClient 字段区分:
这三个常量是 RegistryKey<World> 类型,用来标识维度身份,不是三个世界对象实例。
7.2 MinecraftServer.createWorlds:创建所有维度
服务端启动时,先创建主世界,再遍历维度注册表创建其余维度:
注意:主世界以外的各维度在构造时传入的是 UnmodifiableLevelProperties,而非独立的属性对象。
7.3 UnmodifiableLevelProperties:维度间的属性共享
这里解释了为什么原版各维度共享游戏时间和天气:非主世界维度没有独立的 ServerWorldProperties,它们的所有时间、天气读取都委托给了主世界的属性对象。
7.4 DimensionType.getSkyAngle:维度固定时间的真正作用
fixedTime.orElse(time) —— 如果维度类型定义了 fixed_time,就用它代替传入的 time;否则正常使用维度读取到的游戏时间。注意这个方法只影响天体角度计算,不修改 World#getTime() 返回的游戏时间。
下界 fixed_time: 18000 和末地 fixed_time: 6000 意味着它们的天体角度始终对应午夜和黄昏,但计划刻、实体、方块实体仍按真实游戏时间运行。
7.5 World#getBlockState:根据坐标查找方块
虽然 World 提供了 getBlockState(BlockPos) 接口,但它本身不直接存储方块数据:
维度只是查找的入口,真正的方块数据由 Chunk → ChunkSection → PalettedContainer<BlockState> 层层委托获取。
7.6 World.removeBlock:破坏方块时用流体替代
破坏含水方块后留下的水不是额外补放的,而是 removeBlock 先获取了当前位置的 FluidState,然后将对应的方块状态写入。这与正文所述一致。
7.7 客户端与服务端维度
客户端维度也有自己的 getBlockState 等查询方法,但它展示的是服务端同步过来的快照。当网络不稳定时,客户端的状态可能与服务端暂时不一致 ——BlockUpdateS2CPacket 就是服务端向客户端同步方块变化的数据包。
7.8 参考类列表
net.minecraft.world.Worldnet.minecraft.server.MinecraftServernet.minecraft.server.world.ServerWorldnet.minecraft.client.world.ClientWorldnet.minecraft.client.network.ClientPlayerInteractionManagernet.minecraft.client.network.ClientPlayNetworkHandlernet.minecraft.network.packet.s2c.play.BlockUpdateS2CPacketnet.minecraft.world.level.UnmodifiableLevelPropertiesnet.minecraft.world.dimension.DimensionTypenet.minecraft.world.HeightLimitViewnet.minecraft.world.level.storage.LevelStorage.Session
