摘要
世界生成是我的世界的一个重要内容。Minecraft 在发展,世界生成的代码却在很长的一段时间里没有发生太大的变化,而 1.13 正是对这一切进行变革的一个版本。在之后各个版本的世界生成中,1.13 版本的核心价值一直在不断地体现。这就是为什么会有这篇文章:因为这是一次划时代的更新。本文从世界生成的各个方面,逐一探讨其中的奥秘,揭开新版世界生成神秘复杂的面纱。
文章链接
更多链接参见文章相关链接一栏。
引子:为什么?
世界生成(World Generation, or WorldGen for short)是我的世界的一个重要内容。Minecraft 在发展,世界生成的代码却在很长的一段时间里没有发生太大的变化,而 1.13 正是对这一切进行变革的一个版本。那么为什么我们需要推翻一个使用这么长时间的、看似并没有太大问题的世界生成机制呢?我们为什么要这样变?这样变又有什么好处呢?
要解答这些问题,就要从旧版的世界生成说起,在这里,我不能把 1.12 及以前的地形生成解析一遍,在文末的相关链接我给出了土球的一个简要解析,供读者参考。也许在实际的游戏中,我们很难看出什么区别,但潜移默化间,Mojang 已经向前迈出了一大步。
这次 Mojang 代码的重构采用了全新的设计模式,增加了代码的可扩展性,主要体现在:
- 将世界生成的功能被集中在了区块生成器和生物群系两部分上面,而不是离散在方方面面,更便于对代码之间的关系进行分析。
- 细化了分类和世界生成的步骤,将不同的功能分离开,实现逻辑分层和模块化,代码更加清晰,复用性更高。
- 比如把定位和生成分离,灵活组合,告别了过去一写特性就花半天写定位代码或复制定位代码的冗余。
- 因为步骤被细化了,更加利于分配任务,所以世界生成可以异步了。
- 采用大量函数式接口,助力函数式大潮,让传递代码成为一种时尚。
- 采用了全新的设计模式,告别了过去Mojang一贯的代码风格,打破了我们的刻板印象。
- 如
(X-XType)
模式,即用X
表示对象本身,实现它的功能,而XType
描述这类对象性质,提供对象的工厂方法。 - 如
(X-XConfig)
模式,即用X
实现功能,而用XConfig
来传递实现此功能的参数,通过泛型避免不同行为的参数不同和大量无意义的类型转换。 - 如有限状态机,用以表示区块生成的不同阶段和更加灵活地方式区分生物群系,删除了过去的硬编码。
- 如
- 统一化的世界生成接口,让所有生成的装饰都归于一个概念——特性。避免了以往的混乱不堪、互相踩脚的弊端。
- 开放了生物群系的接口,提供了生物群系的
Builder
,让你体验搭积木式的快感,让你不用Forge也能加特性。
在之后各个版本的世界生成中,1.13 版本的核心价值一直在不断地体现。这就是为什么会有这篇文章:因为这是一次划时代的更新。下面我们将从世界生成的各个方面,逐一探讨其中的奥秘,揭开新版世界生成神秘复杂的面纱。
区块生成概述
区块(Chunk)是Minecraft世界里一个大小为 16×256×16 的部分 ——Minecraft 中文维基
一个存档有多个维度,每个维度都有一个世界,世界是由一个个区块组成的,所以生成世界的实质就是生成区块。
在过去,区块生成分为 Generation 和 Population 两个阶段,而在 1.13 以后,不再这样笼统的区分,每个区块都拥有一个状态,每个状态都表示他已经完成的某一个任务,并提供下一个任务,以此实现异步的区块生成。
按顺序,它们分别是:
状态 | 翻译 |
---|---|
empty | 空 |
base | 基础 |
carved | 镂空 |
liquid_carved | 流体镂空 |
decorated | 装饰 |
lighted | 光照 |
mobs_spawned | 生物生成 |
finalized | 收尾 |
fullchunk | 完整区块 |
postprocessed | 处理后 |
前面的阶段属于样板(proto)区块,而最后两个属于存档(level)区块,后者已经可以序列化为
NBT 了。这个两种类型定义在ChunkStatus.Type
枚举中。
[提示] 为了更加直观的观看区块生成时所处的不同状态,可以观看 1.14 世界加载时的动画,其中不同的颜色代表不同的状态,可以说没有 1.13 的变革也不会有 1.14 的进步。
区块生成任务
接下来详细讲解每一个各个任务的内容。
[注意] 再说一遍是对应的任务的内容,其他内容不再此列。
基础
选定生物群系
生物群系选定是区块生成的第一步,所以也是世界生成的第一步。因此重要程度不言而喻。随着版本更迭,这一部分变化也很大。有关这一部分的内容,请参考我的新作。
高度图
高度图记录每个(x, z)
下最高的方块,用于描述地形的起伏升降,根据“最高方块”的标准不同,高度图分为以下几种,注意且的优先级是高于或的。其中有_WG
后缀的都是为世界生成准备的。
枚举名 | 含义:最高的 非 (...) 的方块 |
---|---|
LIGHT_BLOCKING |
空气 或 透明 |
MOTION_BLOCKING |
空气 或 允许移动 且 不包含流体 |
MOTION_BLOCKING_NO_LEAVES |
空气 或 树叶 或 允许移动 且 不包含流体 |
OCEAN_FLOOR |
空气 或 允许移动 |
OCEAN_FLOOR_WG |
空气 或 流体 |
WORLD_SURFACE |
空气 |
WORLD_SURFACE_WG |
空气 |
[说明]
- 透明:即不透明度为0的方块,不透明度会影响光的传递
- 树叶:即拥有
minecraft:leaves
标签(tag)的方块 - 允许移动:允许实体进入方块并在其中移动,比如树苗、按钮、花盆、地毯
- 不包含流体:即该方块里面没有流体浸入,比如正常的梯子,就属于 允许移动 且 不包含流体,但是如果梯子里面倒桶水,它就“包含流体”了
这些类型都可以在Heightmap.Type
找到,这两种用途对应枚举Heightmap.Usage
的值。
为了构造地表,我们需要一些高度图,高度图怎么得来,生成随机数?那样生成的地形七上八下支离破碎,为了确保地形看上去更加平滑真实,就需要用到噪声算法,可以最大程度保证不出现极端的高度跳跃。
上面说到的高度图,指的是世界生成专用的高度图,而其他的高度图,则是在每个区块生成的任务执行前进行更新。区块生成的前面四个阶段都不会更新高度图,而剩下的都会更新高度图。
地表构造器
有了高度图,我们就可以据此生成地表了。新版本生成地表的工具便是地表构造器。
[注意]
地表构造器(ISurfaceBuilder
)的Builder
跟通常的Builder
不同,它本身就是功能性的一个类,这个Build
指的是地形的构造,而不是对象的构造
地表构造器就是原来的Biome#genTerrainBlocks
,独立出来之后,它变得更加灵活、复用性更强。
而地表构造器配置提供至少两个信息,即顶层(top)方块和中层(middle)方块,比如在普通的平原生物群系,顶层(top)方块就是草方块,而中层方块则是泥土方块,此外还会有水底方块的信息(比如默认为砂砾)。
基岩的生成代码独立出来,而不是像过去一样包含在Biome#genTerrainBlocks
的代码里,这也就意味着你不会因为不谨慎的覆盖而导致基岩的生成被“取消”了。
镂空、流体镂空
所谓镂空就是钻洞注水,采用IWorldCarver
。
GenerationStage.Carving
枚举的AIR
表示镂空,LIQUID
表示流体镂空。
镂空是生物群系相关的,因为镂空器是从生物群系那里取得的。
[注意] GenerationStage 中的那个与旧版的 Generation 无关。
装饰
装饰可以说是整个地形生成中最为重要的一部分了,你不想看到光秃秃的世界吧,在这个阶段,所有可掉落掉落的方块都会立即掉落(但是镂空阶段却不会,这也就是为什么原版生成的地图有这么多浮沙)。
装饰的载体是生物群系,它实际上就是从原来的Biome#decorate
独立而成,独立出来之后,它变得更加灵活、复用性更强,不过在过去,有一个叫装饰器(decorator)的东西,被彻底移除了,现在用特性来统一管理所有生物群系特性,分为三类。
类别 | 说明 |
---|---|
特性 | 无 |
花 | 可以通过用骨粉催熟后天生成的特性 特殊之处:有一个专门的列表以便骨粉迭代 |
结构 | 它们的一些数据会被单独保存在世界里面,我们会在下面单独谈到 特殊之处:有一个专门的映射以便保存相关配置 必须要调用两个方法来注册,分别加入特性列表和添加配置的映射 |
GenerationStage.Decoration
枚举里面的值列出了装饰的所有阶段,依次是:
状态 | 翻译 | 例子 |
---|---|---|
raw_generation | 原生生成 | 目前只有末地岛 |
local_modifications | 本地修饰 | 湖、岩浆湖、冰山、苔石堆 |
underground_structures | 地下结构 | 略,特例:地牢(它只是特性而不是结构) |
surface_structures | 地表结构 | 略,特例:冰刺地区的冰刺和冰道 |
underground_ores | 地下矿石 | 略 |
underground_decoration | 地下装饰 | 化石、萤石、下界火、岩浆、蠹虫刷怪蛋 下界蘑菇(主世界蘑菇在下一个阶段生成) |
vegetal_decoration | 植物装饰 | 略,特例: 主世界蘑菇(因为 单格的水和岩浆(常在矿洞里面形成瀑布, 鬼知道Mojang为什么要把它放到这里面) |
top_layer_modification | 顶层修饰 | 在温度足够低的位置生成冰雪 |
生成装饰时,特性往往要借助定位器的协助,定位器可以根据周围的地形调整生成的位置,而复合特性就包含了定位器和特性,在生成时就自带定位了。
[提示] 定位器常常会用到前面说的高度图来确保特性在地表生成。
光照
此时区块的方块已经生成完成,但是光照没有更新,所以根据目前区块中方块的光照,当然还有天空的光照来更新区块的光照。
生物生成
为了让玩家在进入游戏后里面看到生物,这个阶段将会预先生成一些生物。
所以下界和末地在这个阶段什么都不做。因为就算是空的也没关系,过会就有了嘛,反正都是怪物。
收尾
确保更新高度图。
完整区块、处理后
什么都不做,直接由相关代码设置为此状态。
结束了?结束了。
当然了,最后两个阶段并不是真的什么都不做,而是由别人做,下面介绍一下相关代码:
当到完整区块的阶段时,表示一个Chunk
对象已经完成了构造。
而在玩家正式进入世界之前,还需要一些处理。
当一个区块周围的 8 个区块都生成完毕,那么它会:
- 对区块内的一些方块状态进行更新。
- 更新需要 tick 的方块和流体。
- 检验区块里的
TileEntity
。
在收尾完成之后,Chunk#isPopulated
将会返回true
。
[注意] 这个 Populate 和旧版也没多大联系,硬要说,就是都表示这个区块的生成已经完成。
生物群系
有关这一部分的内容,请参考我的新作。
结构生成
Structure -> StructureStart -> StructurePiece
如图,在生成结构(你已经知道结构也是特性的一种,对吧?)时,先判断是否为正确的生物群系,接着以中心区块(Feature本应生成于的区块)为正方形的中心,结构尺度(size,单位是区块,这也就是为什么不译作大小)的两倍加一为正方形的边长,在这些区块中寻找一个合适的区块作为整个结构的起点,这个起点本身的大小不能越过正方形的边界。
结构起点储存这个这个结构的所有可能有的部分。而在确认此处要生成结构之后,会把任务交给结构起点,结构生成就正式开始了。
在结构起点的构造器里面,第一个阶段将会被调用。
buildComponent
在这个阶段,从起点开始延伸,结构的各个组成环节产生,但是此时此刻,方块不会被放置,而是形成一个个 Component,为了便于讲解,我译作(结构生成)任务,把所有 Component 成为任务列表。
为了更好地说明,我们用村庄做例子。
[说明] 村庄是一个易于理解的,常见的例子,但是 Mojang 的源代码确实太难看。我不原封不动的解析,而是把真正有意义的内容讲出。
- 在最开始的时候,只有一个水井任务被加入任务列表。
- 水井的
buildComponet
里面加入了朝向四个方面路的任务,我们有四条“开路”。 - 现在我们对每一条路都
buildComponet
,毫无疑问路会延伸,我们把已经build之后的路从我们的开路列表里面移除,同时刚才有新增了几个开路。 - 路在延伸的时候,有一定的几率添加一个屋子到任务列表,如果是在路边,路还能继续延伸,否则路到这就变成死路了。
- 也有可能会生成岔路,岔路也会被放进开路的列表。
- 如果生成屋子和岔路都生成失败了,那么还有一定几率拐弯。
- 如果上面的尝试都失败了,那么这条路就成了死路。
[说明] 上面的原理揭示了为什么把村庄的size设置得非常大(比如正常=0,超平坦=1,我设置为65535)并不会让村庄真的变得非常大,因为村庄的生存实际上很大程度受到路的限制,一旦没路了,村庄就不会生成了。
同样的原理对于下界堡垒也适用,但是下界堡垒相对来说更加复杂多变。
为了防止发生两个部分撞在一起的悲剧,所有的过程都是被监管的,只要这个部分太大,那么任务就无法被添加,但是这只对同一个结构有效,如果不巧有两个结构,那就无法避免地其中一个会被另外一个替换方块,这可能是我的世界极少数资源与区块加载顺序联系在一起的实例,而造成的影响也几乎是可以忽略不计的,除非他把你的末地门弄坏了2333。
addComponentParts
真正破坏末地门是在这里(雾。
在这个阶段,所有结构的方块被放置,应始终确保方块不超出结构的边界,同时也要保证方向的正确,返回的boolean
表示它的生成是否成功,此后,还会对这个结构的大小进行重算。
模板
模板是已经被证明C++最强大的功能之一,但却常常被人们忽视、误解或误用。
——Nicolai M. Josuttis《C++Templates》
咳咳,串味了,此模板非彼模板。
在生成结构的时候,你是否对不断地调用方法放置方块地硬编码感到厌倦?模板可是说是你的大救星,“但却常常被人们忽视、误解或误用”。早在 1.9,Mojang 就引入了结构方块,随之而来的就是 template,可以说,结构从硬编码转为 template 可以说是大势所趋。那么,模板究竟是怎样存储结构数据,又是怎样呈现在世界上的呢?
模板最重要的三个内容便是方块、实体、大小,其中方块和实体的位置都是相对于这个结构的原点—— xyz 都最小的那一个角表示的。这也就意味着模板易于在任意区域建造。同时,模板同样储存了方块和实体的朝向,这也就意味着模板本身具有方向性——好在模板还有一个特点,那就是易于旋转或者镜像。在模板建造在世界上的同时,当中的方块也会更新,来保证结构的功能性。
调色盘
往往一个模板里面会有很多完全相同的方块,如果这些方块都一一存入NBT,实在是有些浪费空间。因此Mojang引入了调色盘这个概念,说白了就是一个字典——给Blockstate
编号,按照读入的先后,从0开始依次编号,与以前的数字id不同,这只是一个临时编号,仅仅对于这个结构有效,此外,这个id是NBT无关的,即它不表示IBlockstate
所在的TileEntity
所携带的NBT。
[提示] 如果你对原版的区块格式有所了解,你就会知道,实际上区块也是有调色盘的。
TemplateStructurePiece
为了更方便的使用模板生成结构,Mojang 准备了这个类。
其他的不用说,我们把眼光投向TemplateStructurePiece#handleDataMarker
。
嘿,DataMaker
是什么?
不知道大家注意到没有,结构方块的四种模式里面,有一种叫做“数据”,这就是所谓的DataMaker
,它填补了结构中无法携带特殊信息的不足,通过这个,我们能够吧代码和模板联系起来,让模板“调用”代码。
Data
模式的结构方块在存入结构时会被自动替换为结构虚空。
我们不妨以海底废墟为例:
1 | protected void handleDataMarker(String function, BlockPos pos, IWorld worldIn, Random rand, MutableBoundingBox sbb) { |
在这里,根据不同的函数(function
),调用了不同的代码,比如生成箱子、生成溺尸。无一例外的,他们往往对附近环境进行的判断和适应,来达到更好的结构生成效果,这是单纯的
NBT 数据无法做到的。
维度与世界类型
这一部分看似已经超出了狭义的“世界生成”,但其实它们有与世界生成千丝万缕的联系。
这里指简单介绍了几个方法,在新的教程里,有关于这一部分更详细的介绍。
Dimension#createChunkGenerator
和
IForgeWorldType#createChunkGenerator
默认行为:前者返回当前维度的区块生成器,后者调用前者。其中前者被废弃要求调用后者。
因为返回了一个区块生成器(而里面实际上就包含了生物群系提供器),所以决定了这个世界的地形生成。
自定义维度或自定义世界类型时应当覆盖。
IForgeWorldType#getBiomeLayer
略,见新教程。
应用:自定义
唉唉唉,等一下,你确定这不是 Forge 干的事情?
是的,新版的矿物生成完全不需要 Forge
插手即可实现自定义于是Forge就真的懒到没有插手,其他相关内容
Forge 也少了很多的话语权,大量 Forge
钩子消失,甚至出现了存在但未被使用的 Forge
事件(来自旧版本),可见原版对 Forge 的冲击。下面的内容尽量少的提及
Forge 知识,但不会回避 Forge。
自定义矿物生成
我们在mod主类的构造器里这么写:
1 | Biomes.PLAINS.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, |
我就完成了矿物生成的注册,上面的代码可以让金块像铁矿一样地在平原的地下生成,我们不妨来解析一下。
Biomes.PLAINS
,即平原,在实际应用中,我们往往会用ForgeRegistries.BIOMES.forEach(biome -> biome.add...());
来向所有生物群系种添加,但是你就得找一个所有模组Biome
都已经全部注册完成的地方调用了。
addFeature
可以用于添加特性或是花,addCarver
则是添加镂空器,addStructure
添加结构。我们直接调用了Biome
的工厂方法createCompositeFeature
创建复合特性,使用了Feature
预定义的minable特性及其相应配置,配合Biome
里面的预定义的定位器及其相应配置,对应这个方法的四个参数。另外,也可以在Biome
里找到createWorldCarverWrapper
和createCompositeFlowerFeature
。
[提示]
不难看出,Biome
和Feature
里面都预定义了大量的实用特性、定位器,甚至还有镂空器和一些protected
的IBlockState
,应该尽可能多的使用它们。
自定义结构
不不不,我不会真的写一个结构给你看,我只会提出几个自定义结构时所需要的注意事项:
在
StructurePiece
里面提供了很多摆放方块的方法,请务必使用这些方法而不是直接放置,这不仅可以防止你放置到结构的外面,还会根据结构的方向对你的方块转向。任何结构及其部分都需要在
StructureIO
进行注册,否则会导致结构无法生成并造成游戏崩溃。原版采用大写下划线的形式(如:Desert_Pyramid
),但是我更加建议是modid + ':' + 小写下划线
(即ResourceLocation
)的形式,这样有助于防止模组之间发生命名冲突,并更加规范、统一。
自定义生物群系
不不不,我不会真的写一个生物群系给你看,我只会提出几个自定义生物群系时所需要的注意事项:
- 你是不是发现许多
Biome
的构造器都有共同的代码?是的,极大的自由同时也带来了极大的工作量。好在在新的版本里面,Mojang 也意识到也改善了这一点:他们为此设置了一个类,里面装满了许多生物群系共用的特性添加的代码。这毫无疑问可以直接被我们调用,或者作为我们设置组合的参考。 - 出生点生物群系会被优先选中为出生点,若你的生物群系想被选中,可以直接向
BiomeProvider#BIOMES_TO_SPAWN_IN
中添加。 - 更多内容参见我的新教程。
附录
下面介绍 1.13 Forge 提供的事件和钩子。
事件
值得注意的是,原有的MinecraftForge#TERRAIN_GEN_BUS
和MinecraftForge#ORE_GEN_BUS
已经被移除。
~
表示上面的主事件。
值得注意的是,弃用指的不是@Deprecated
而是简单的“存在,但是从未被使用”。
事件 | 描述 |
---|---|
BiomeEvent |
生物群系相关事件 参见前文生物群系栏 |
~.GetVillageBlockID |
获取村庄特色方块 |
~.BiomeColor |
生物群系颜色相关事件 新版已经弃用 参见前文生物群系栏 |
~.GetGrassColor |
获取草的颜色 新版已经弃用 参见前文生物群系栏 |
~.GetFoliageColor |
获取植物的颜色 新版已经弃用 参见前文生物群系栏 |
~.GetWaterColor |
获取水的颜色 新版已经弃用 参见前文生物群系栏 |
ChunkGeneratorEvent |
区块生成器相关的事件 参见前文区块生成部分 |
~.ReplaceBiomeBlocks |
替换生物群系方块 参见前文地表构造器栏 |
~.InitNoiseField |
初始化噪声字段事件 参见前文高度图部分 |
InitNoiseGensEvent |
噪声生成器事件 参见前文高度图栏 |
WorldTypeEvent |
世界类型相关事件 参见前文世界类型栏 |
~.BiomeSize |
生物群系大小时间 参见前文 GenLayer 的产生栏 |
~.InitBiomeGens |
初始化生物群系生成(即GenLayer )新版已经弃用 参见前文 GenLayer 的产生栏 |
钩子
钩子 | 描述 |
---|---|
TerrainGen#getModdedNoiseGenerators |
获取模组修改后的噪声生成器 |
LayerUtil#getModdedBiomeSize |
获取模组修改后的生物群系大小 |
ForgeEventFactory#onReplaceBiomeBlocks |
构造模组修改后的地表 |
后记
本来这个介绍是附在我的《浅析 1.13 Minecraft - MCP | Forge - FML》里面的,不过我后来直接放弃了那个项目,专心投身于这个部分。
1.13 Mojang 做出的努力使我眼前一亮,世界生成可以说是最重要的一部分,Mojang 迈出了第一步,希望这不是最后一步。
我这篇文章写的我很艰辛,大量未反混淆的的代码和 Mojang 奇怪的脑回路使我感到痛苦,但是我还是在最短的时间内肝出这篇文章,不出意外,是世界上第一篇(中文)新版地形生成解析。
鸣谢
- MCP 对 Minecraft 源代码的反混淆,如果不是他们,我也不能写出这篇文章。
- 森林蝙蝠 审读并提出来让我加入的结构的部分。
- 33 让我加入浅析新版本世界生成与旧版本的本质区别和好处分析一栏。
- 素学姐 和 3T 对教程提出的宝贵建议。
- 感谢 chyx 指出我文章中一系列的谬误。
- 土球(ustc_zzzz)制作的 BBCode 转换器,为当初发布到 MCBBS 提供了便利。
参考
按参考内容质量、数量降序排列:
- Minecraft 1.13.2 源代码 - 反编译、反混淆 by MCP 快照 20180921-1.13
- MinecraftForge 1.13.2-25.0.10 源代码
- Minecraft 旧版(1.12.2)源码 - 反编译、反混淆 by MCP
- MinecraftForge 旧版(1.12.2-14.23.5.2768)源代码
- Minecraft 维基百科 - 英文、中文相关页面
- 土球的旧版本地形生成解析