Minecraft(我的世界)中文论坛

 找回密码
 注册(register)
查看: 4007|回复: 57

[Mod开发教程] [长篇]浅析1.13世界生成

    [复制链接]
姚氏帅哥 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
4149
钻石
性别
保密
注册时间
2013-9-13
查看详细资料
发表于 2019-2-18 21:27:00 | 显示全部楼层 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 姚氏帅哥 于 2019-4-27 09:23 编辑

浅析1.13世界生成

特别说明:本文将会持续修订,并会首发于Github,最后更新:2019-4-27

目录

  • 引子:为什么?
  • 概念一览
  • 区块生成概述
  • 区块生成任务
    • 基础
      • GenLayer的产生
      • 高度图
      • 地表构造器
    • 镂空、流体镂空
    • 装饰
    • 光照
    • 生物生成
    • 收尾
    • 完整区块、处理后
  • 区块生成——最后的最后
  • 生物群系
  • 结构生成
    • buildComponent
    • addComponentParts
    • 题外话
  • 模板
    • 调色盘
    • TemplateStructurePiece
  • 维度与世界类型
    • Dimension#createChunkGeneratorIForgeWorldType#createChunkGenerator
    • IForgeWorldType#getBiomeLayer
  • 应用:自定义
    • 自定义矿物生成
    • 自定义结构
    • 自定义生物群系
  • 附录:1.13Forge地形生成代码表
    • 事件
    • 钩子
  • 后记
  • 鸣谢
  • 参考
  • 相关链接

引子:为什么?

世界生成(World Generation)是我的世界重要的组成部分,没有它,就没有世界,玩家将失去几乎一切游戏的载体。Minecraft在迅速发展,世界生成的代码却在很长的一段时间里没有发生太大的变化,而1.13正是对这一切进行变革的一个版本。

那么为什么我们需要推翻一个使用这么长时间的、看似并没有太大问题的世界生成机制呢?我们为什么要这样变?这样变又有什么好处呢?

要解答这些问题,就要从旧版的世界生成说起,囿于主题、篇幅,也囿于我的能力,实在不想,也不能把1.12以前的地形生成解析一遍,在文末的相关链接我给出了土球的一个简要解析,供读者参考。也许在实际的游戏中,我们很难看出什么区别,但潜移默化间,Mojang已经向前迈出了一大步。

这次Mojang代码的重构采用了全新的设计模式,增加了代码的可扩展性,主要体现在:

  • 将世界生成的功能被集中在了区块生成器和生物群系两部分上面,而不是离散在方方面面,更便于对代码之间的关系进行分析
  • 细化了分类和世界生成的步骤,将不同的功能分离开,实现逻辑分层和模块化,代码更加清晰,复用性更高
    • 比如把定位和生成分离,灵活组合,告别了过去一写特性就花半天写定位代码或复制定位代码的冗余
    • 因为步骤被细化了,更加利于分配任务,所以世界生成可以异步了。
  • 采用大量函数式接口,助力函数式大潮,让传递代码成为一种时尚
  • 采用了全新的设计模式,告别了过去Mojang一贯的代码风格,打破了我们的刻板印象
    • (X-XType)模式,即用X表示对象本身,实现它的功能,而XType描述这类对象性质,提供对象的工厂方法
    • (X-XConfig)模式,即用X实现功能,而用XConfig来传递实现此功能的参数,通过泛型避免不同行为的参数不同和大量无意义的类型转换
    • 如有限状态机,用以表示区块生成的不同阶段和更加灵活地方式区分生物群系,删除了过去的硬编码。
  • 统一化的世界生成接口,让所有生成的装饰都归于一个概念——特性。避免了以往的混乱不堪、互相踩脚的弊端
  • 开放了生物群系的接口,提供了生物群系的Builder,让你体验搭积木式的快感,让你不用Forge也能加特性

下面我们将从世界生成的各个方面,逐一探讨其中的奥秘,揭开新版世界生成神秘复杂的面纱。

概念一览(部分)

翻译Class泛型参数
继承
世界类型WorldType
维度Dimension
世界IWorldextends IWorldReaderBase, ISaveDataAccess, IWorldWriter
区块IChunkextends IBlockReader
区块提供器IChunkProviderextends AutoCloseable
区块状态ChunkStatus
区块任务ChunkTask
区块生成器IChunkGenerator<C extends IChunkGenSettings>
区块生成器类型ChunkGeneratorType<C extends IChunkGenSettings, T extends IChunkGenerator<C>>
implements IChunkGeneratorFactory<C, T>
区块生成器设置IChunkGenSettings
高度图Heightmap
生成阶段GenerationStage*
生物群系Biome
生物群系提供器BiomeProviderimplements ITickable
生物群系提供器类型BiomeProviderType<C extends IBiomeProviderSettings, T extends BiomeProvider>
区域IArea
内容IContext
扩展内容IContextExtended<R extends IArea> extends IContext
像素转换器IPixelTransformer
区域转换器IDimTransformer
地层内容LayerContext<R extends IArea> implements IContextExtended<R>
地层生成GenLayer
地表构造器ISurfaceBuilder<C extends ISurfaceBuilderConfig>
地表构造器配置ISurfaceBuilderConfig
复合地表构造器CompositeSurfaceBuilder<C extends ISurfaceBuilderConfig>
implements ISurfaceBuilder<SurfaceBuilderConfig>
镂空器IWorldCarver<C extends IFeatureConfig>
包装镂空器WorldCarverWrapper<C extends IFeatureConfig>
implements IWorldCarver<NoFeatureConfig>
BUG特性IFeature<C extends IFeatureConfig>
特性配置IFeatureConfig
结构Structure<C extends IFeatureConfig>
extends Feature<C>
结构起点StructureStart
结构部分StructurePiece
基础定位器BasePlacement<T extends IPlacementConfig>
定位器配置IPlacementConfig
模板Template
定位设置PlacementSettings
复合特性CompositeFeature<F extends IFeatureConfig, D extends IPlacementConfig>
extends Feature<NoFeatureConfig>

* 这个Generation和旧版本的Generation无联系

区块生成概述

区块(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的进步

区块生成任务

接下来详细讲解每一个各个任务的内容

[注意] 再说一遍是对应的任务的内容,其他内容不再此列

基础

在基础状态时,生物群系将会被选定,下面给出生物群系选定所用到的概念

Biome[] -> BiomeProvider -> GenLayer -> Transformer -> Area

生物群系存在于区域内,这个区域由GenLayer生成后交给BiomeProvider管理,之后,生物群系就直接从BiomeProvider获得。而Transformer则是Area的转码器、调整器,属于最底层的生物群系规划

GenLayer的产生

下面给出GenLayer生成详细步骤(如果是相同的内容我却分开写,说明有些参数是不同的,比如种子)

[注意] 下面说的放大化指的是Zoom,而不是一个世界类型

  • 基底:生成海洋->放大化6次
  • 主线1:添加岛屿->放大化(模糊)->添加岛屿->放大化->添加岛屿->添加岛屿->添加岛屿->删除过多的海洋->添加雪->添加岛屿->添加生物群系边界(冷暖)->添加生物群系边界(炎寒)->添加生物群系边界(特殊)->放大化->放大化->添加岛屿->添加蘑菇岛->添加深海->放大化0次(源代码如此)
  • 主线2:主线1->放大化0次(源代码如此)->初始化河流->放大化2次->放大化[河流大小(RiverSize),默认为4]次->生成河流->平滑
  • 主线3:主线1->根据世界类型获取生物群系地层生成->放大化2次并生成山丘->生成稀有生物群系->放大化生物群系[生物群系大小(BiomeSize),默认为4,巨型生物群系为6]次(第一次放大化以后,添加小岛,第二次放大化之后(如果生物群系大小为1,那就是第一次放大化之后),添加海岸->平滑
  • 最后:主线2、3一起混合河流->再与基底混合海洋->得到一个GenLayer记为G

把最后得到的G泰森放大化(VoronoiZoom)得到了另外一个GenLayer记为V

最后我们得到的GenLayer[]就是{G,V,G}

如果是超平坦,就应该用SingleBiomeProvider,自然也就没有后面那么多事了。

高度图

高度图记录每个(x, z)下最高的方块,用于描述地形的起伏升降,根据"最高方块"的标准不同,高度图分为以下几种

中文栏应理解为 "最高的 (...) 的方块",如第一个就是 "最高的 非 (空气 或 透明) 方块",注意的优先级是高于的。

英文中文
LIGHT_BLOCKING空气 或 透明
MOTION_BLOCKING空气 或 允许移动不包含流体
MOTION_BLOCKING_NO_LEAVES空气 或 树叶允许移动不包含流体
OCEAN_FLOOR空气 或 允许移动
OCEAN_FLOOR_WG空气 或 流体
WORLD_SURFACE空气
WORLD_SURFACE_WG空气

[说明]

  • 透明:即不透明度为0的方块,不透明度会影响光的传递
  • 树叶:即拥有minecraft:leaves标签的方块
  • 允许移动:允许实体进入方块并在其中移动,比如树苗、按钮、花盆、地毯
  • 不包含流体:即该方块里面没有流体浸入,比如正常的梯子,就属于 允许移动 且 不包含流体,但是如果梯子里面倒桶水,它就"包含流体"了

其中有_WG后缀的都是为世界生成准备的

这些类型都可以在Heightmap.Type找到,这两种用途对应枚举Heightmap.Usage的值

为了构造地表,我们需要一些高度图,高度图怎么得来,生成随机数?那样生成的地形七上八下支离破碎,麻麻赖赖的一点都不圆润,为了盘它确保地形看上去更加平滑真实,就需要用到柏林噪声算法(PerlinNoise),可以最大程度保证不出现**的高度跳跃。

上面说到的高度图,指的是世界生成专用的高度图,而其他的高度图,则是在每个区块生成的任务执行前进行更新。区块生成的前面四个阶段都不会更新高度图,而剩下的都会更新高度图

地表构造器

有了高度图,我们就可以据此生成地表了。新版本生成地表的工具便是地表构造器

[注意] 地表构造器(ISurfaceBuilder)的Builder跟通常的Builder不同,它本身就是功能性的一个类,这个Build指的是地形的构造,而不是对象的构造

地表构造器就是原来的Biome#genTerrainBlocks,独立出来之后,它变得更加灵活、复用性更强

而地表构造器配置提供至少两个信息,即顶层(top)方块和中层(middle)方块,比如在普通的平原生物群系,顶层(top)方块就是草方块,而中层方块则是泥土方块

基岩的生存代码独立出来,而不是像过去一样包含在Biome#genTerrainBlocks的代码里,这也就意味着你不会因为不谨慎的覆盖而导致基岩的生成被取消了

镂空、流体镂空

所谓镂空就是钻洞注水,采用IWorldCarver

GenerationStage.Carving枚举的AIR表示镂空,LIQUID表示流体镂空

镂空是生物群系相关的,因为镂空器是从生物群系那里取得的。

装饰

装饰可以说是整个地形生成中最为重要的一部分了,你不想看到光秃秃的世界吧,在这个阶段,所有可掉落掉落的方块都会立即掉落(但是镂空阶段却不会,这也就是为什么原版生成的地图有这么多浮沙)

装饰的载体是生物群系,它实际上就是从原来的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需要tick的方块和流体
  • 检验区块里的TileEntity

在收尾完成之后,Chunk#isPopulated将会返回true,这个Populate和旧版也没多大联系,硬要说,就是"Populated"都表示这个区块的生成已经完成

生物群系

新版的生物群系提供了一个Biome.Builder,同时不难发现,大部分Biome子类里面只有包含一个构造器,而在构造器里面几乎调用的都是Biomepublic的方法,需要覆盖的方法比以往少的多了。这也是前面提到模块化的结果。

Category RainTypeTempCategory

令人惊讶的,Mojang竟然加入了这三个描述生物群系属性的枚举,这与Forge的BiomeDictionary又一次不谋而合(抄完OreDictionary又抄BiomeDictionary的Mojang),这有什么好处呢?在以前原版判断生物群系类型要不然是这样

  1. if(biome == Biomes.DESERT) generateXXX(...);
复制代码

也可能会是这样

  1. if(biome instanceof BiomeMesa) populateXXX(...);
复制代码

后者我们也许可以强行继承BiomeMesa(即使我们本来并不想要这么做)来完成,前者可以说真实一点办法都没有

可是如果有了这些枚举,我们只需要简单的在builder里面输入相应的Category即可,大大方便了生物群系的自定义

结构生成

先上概念图

Structure -> StructureStart -> StructurePiece

在生成结构(你知道结构也是特性的一种,对吧?)时,先判断是否为正确的生物群系,接着以中心区块(Feature本应生成于的区块)为正方形的中心,结构尺度(size,单位是区块,这也就是为什么不译作大小)的两倍加一为正方形的边长,在这些区块中寻找一个合适的区块作为整个结构的起点,这个起点本身的大小不能越过正方形的边界。

结构起点储存这个这个结构的所有可能有的部分。而在确认此处要生成结构之后,会把任务交给结构起点,结构生成就正式开始了。

在结构起点的构造器里面,第一个阶段将会被调用。

buildComponent

在这个阶段,从起点开始延伸,结构的各个组成环节产生,但是此时此刻,方块不会被放置,而是形成一个个Component,为了便于讲解,我译作(结构生成)任务,把所有Component成为任务列表

为了更好地说明,我们用村庄做例子

[说明] 村庄是一个易于理解的,常见的例子,但是Mojang的源代码确实太难看。我不原封不动的解析,而是把真正有意义的内容讲出

  • 在最开始的时候,只有一个水井任务被加入任务列表
  • 水井的buildComponet里面加入了朝向四个方面路的任务,我们有四条"开路"
  • 现在我们对每一条路都buildComponet,毫无疑问路会延伸,我们把已经build之后的路从我们的开路列表里面移除,同时刚才有新增了几个开路
  • 路在延伸的时候,有一定的几率添加一个屋子到任务列表,如果是在路边,路还能继续延伸,否则路到这就变成死路了
  • 也有可能会生成岔路,岔路也会被放进开路的列表
  • 如果生成屋子和岔路都生成失败了,那么还有一定几率拐弯
  • 如果上面的尝试都失败了,那么这条路就成了死路

[说明] 上面的原理揭示了为什么把村庄的size设置得非常大(比如正常=0,超平坦=1,我设置为65535)并不会让村庄真的变得非常大,因为村庄的生存实际上很大程度受到路的限制,一旦没路了,村庄就不会生成了

同样的原理对于地狱堡垒也适用,但是地狱堡垒相对来说更加复杂多变。

为了防止发生两个部分撞在一起的悲剧,所有的过程都是被监管的,只要这个部分太大,那么任务就无法被添加,但是这只对同一个结构有效,如果不巧有两个结构,那就无法避免地其中一个会被另外一个替换方块,这可能是我的世界极少数资源与区块加载顺序联系在一起的实例,而造成的影响也几乎是可以忽略不计的,除非他把你的末地门弄坏了2333

addComponentParts

真正破坏末地门是在这里(雾

在这个阶段,所有结构的方块被放置,始终确保方块不超出结构的边界,同时也要保证方向的正确,返回的boolean表示它的生成是否成功,此后,还会对这个结构的大小进行重算

题外话

Mojang为了防止熊孩子暴力反推种子,还为零碎结构(? extends ScatteredStructure<C extends IFeatureConfig> extends Structure<C>)增设了丧心病狂的种子修正器(SeedModifier),不知道反编译、反混淆出来之后这个修正器还顶不顶用呢?

模板

模板是已经被证明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) {
  2.      if ("chest".equals(function)) {
  3.         worldIn.setBlockState(pos, Blocks.CHEST.getDefaultState().with(BlockChest.WATERLOGGED, Boolean.valueOf(worldIn.getFluidState(pos).isTagged(FluidTags.WATER))), 2);
  4.         TileEntity tileentity = worldIn.getTileEntity(pos);
  5.         if (tileentity instanceof TileEntityChest) {
  6.            ((TileEntityChest)tileentity).setLootTable(this.field_204040_h ? LootTableList.CHESTS_UNDERWATER_RUIN_BIG : LootTableList.CHESTS_UNDERWATER_RUIN_SMALL, rand.nextLong());
  7.         }
  8.      } else if ("drowned".equals(function)) {
  9.         EntityDrowned entitydrowned = new EntityDrowned(worldIn.getWorld());
  10.         entitydrowned.enablePersistence();
  11.         entitydrowned.moveToBlockPosAndAngles(pos, 0.0F, 0.0F);
  12.         entitydrowned.onInitialSpawn(worldIn.getDifficultyForLocation(pos), (IEntityLivingData)null, (NBTTagCompound)null);
  13.         worldIn.spawnEntity(entitydrowned);
  14.         if (pos.getY() > worldIn.getSeaLevel()) {
  15.            worldIn.setBlockState(pos, Blocks.AIR.getDefaultState(), 2);
  16.         } else {
  17.            worldIn.setBlockState(pos, Blocks.WATER.getDefaultState(), 2);
  18.         }
  19.      }
  20.   }
复制代码

在这里,根据不同的函数(function),调用了不同的代码,比如生成箱子、生成溺尸。无一例外的,他们往往对附近环境进行的判断和适应,来达到更好的结构生成效果,这是单纯的NBT数据无法做到的。如果函数比较多,switch或许是更好的选择。

维度与世界类型

[提示] 这一部分严格说来很大程度上已经超出了狭义的"世界生成",之所以放在这里是因为它们有与世界生成千丝万缕的联系

Dimension#createChunkGeneratorIForgeWorldType#createChunkGenerator

默认行为:前者返回当前维度的区块生成器,后者调用前者。其中前者被废弃要求调用后者

因为返回了一个区块生成器(而里面实际上就包含了生物群系提供器),所以决定了这个世界的地形生成。

自定义维度或自定义世界类型时应当覆盖。

IForgeWorldType#getBiomeLayer

参见GenLayer的产生栏

注意:仅对主世界有效

根据生物群系调整GenLayer

默认行为:

参数->生成生物群系->放大化2次->生成生物群系边界->返回

应用:自定义

唉唉唉,等一下,你确定这不是Forge干的事情?

是的,新版的矿物生成完全不需要Forge插手即可实现自定义于是Forge就真的懒到没有插手,其他相关内容Forge也少了很多的话语权,大量Forge钩子消失,甚至出现了存在但未被使用的Forge事件(来自旧版本),可见原版对Forge的冲击。下面的内容尽量少的提及Forge知识,但不会回避Forge

自定义矿物生成

我们在mod主类的构造器里这么写

  1. Biomes.PLAINS.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES,
  2.     Biome.createCompositeFeature(Feature.MINABLE,
  3.         new MinableConfig(MinableConfig.IS_ROCK, Blocks.GOLD_BLOCK.getDefaultState(), 9),
  4.         Biome.COUNT_RANGE, new CountRangeConfig(20, 0, 0, 64)));
复制代码

我就完成了矿物生成的注册,上面的代码可以让金块像铁矿一样地在平原的地下生成,我们不妨来解析一下

首先是Biomes.PLAINS,即平原,在实际应用中,我们往往会用ForgeRegistries.BIOMES.forEach(biome -> biome.add...());来向所有生物群系种添加,但是你就得找一个模组Biome已经全部注册完成的地方了。

首先addFeature可以用于添加特性或是花,而addCarver则是添加镂空器,addStructure添加结构

接着我们直接调用了Biome的工厂方法createCompositeFeature创建复合特性,使用了Feature预定义的minable特性及其相应配置,配合Biome里面的预定义的定位器及其相应配置,对应这个方法的四个参数。

另外,也可以在Biome里找到createWorldCarverWrappercreateCompositeFlowerFeature

[提示] 不难看出,BiomeFeature里面都预定义了大量的实用特性、定位器,甚至还有镂空器和一些protectedIBlockState,应该尽可能多的使用它们。

自定义结构

不不不,我不会真的写一个结构给你看,我只会提出几个自定义结构时所需要的注意事项

  • StructurePiece里面提供了很多摆放方块的方法,请务必使用这些方法而不是直接放置,这不仅可以防止你放置到结构的外面,还会根据结构的方向对你的方块转向。
  • 任何结构及其部分都需要在StructureIO进行注册,否则会导致结构无法生成并造成游戏崩溃。原版采用大写下划线的形式(如:Desert_Pyramid),但是我更加建议是modid + ':' + 小写下划线(即ResourceLocation)的形式,这样有助于防止模组之间发生命名冲突,并更加规范、统一。


自定义生物群系

不不不,我不会真的写一个生物群系给你看,我只会提出几个自定义生物群系时所需要的注意事项

  • 你是不是发现许多Biome的构造器都有共同的代码?是,极大的自由也会带来极大的工作量,我把几乎每个生物群系都调用的方法列出来,以便读者复制参考(你倒是可以做个BiomeBase

  1. //废弃矿井
  2. this.addStructure(Feature.MINESHAFT, new MineshaftConfig(0.004D, MineshaftStructure.Type.NORMAL));

  3. //要塞
  4. this.addStructure(Feature.STRONGHOLD, new StrongholdConfig());

  5. //镂空器
  6. this.addCarver(GenerationStage.Carving.AIR, createWorldCarverWrapper(CAVE_WORLD_CARVER, new ProbabilityConfig(0.14285715F)));
  7. this.addCarver(GenerationStage.Carving.AIR, createWorldCarverWrapper(CANYON_WORLD_CARVER, new ProbabilityConfig(0.02F)));

  8. //原版结构的特性注册:无论你前面有没有addStructure这里都调用一下,不会错的
  9. this.addStructureFeatures();

  10. //地牢
  11. this.addFeature(GenerationStage.Decoration.UNDERGROUND_STRUCTURES, createCompositeFeature(Feature.DUNGEONS, IFeatureConfig.NO_FEATURE_CONFIG, DUNGEON_ROOM, new DungeonRoomConfig(8)));

  12. //矿
  13. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.DIRT.getDefaultState(), 33), COUNT_RANGE, new CountRangeConfig(10, 0, 0, 256)));
  14. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.GRAVEL.getDefaultState(), 33), COUNT_RANGE, new CountRangeConfig(8, 0, 0, 256)));
  15. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.GRANITE.getDefaultState(), 33), COUNT_RANGE, new CountRangeConfig(10, 0, 0, 80)));
  16. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.DIORITE.getDefaultState(), 33), COUNT_RANGE, new CountRangeConfig(10, 0, 0, 80)));
  17. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.ANDESITE.getDefaultState(), 33), COUNT_RANGE, new CountRangeConfig(10, 0, 0, 80)));
  18. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.COAL_ORE.getDefaultState(), 17), COUNT_RANGE, new CountRangeConfig(20, 0, 0, 128)));
  19. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.IRON_ORE.getDefaultState(), 9), COUNT_RANGE, new CountRangeConfig(20, 0, 0, 64)));
  20. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.GOLD_ORE.getDefaultState(), 9), COUNT_RANGE, new CountRangeConfig(2, 0, 0, 32)));
  21. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.REDSTONE_ORE.getDefaultState(), 8), COUNT_RANGE, new CountRangeConfig(8, 0, 0, 16)));
  22. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.DIAMOND_ORE.getDefaultState(), 8), COUNT_RANGE, new CountRangeConfig(1, 0, 0, 16)));
  23. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.MINABLE, new MinableConfig(MinableConfig.IS_ROCK, Blocks.LAPIS_ORE.getDefaultState(), 7), DEPTH_AVERAGE, new DepthAverageConfig(1, 16, 16)));
  24. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.SPHERE_REPLACE, new SphereReplaceConfig(Blocks.SAND, 7, 2, Lists.newArrayList(Blocks.DIRT, Blocks.GRASS_BLOCK)), TOP_SOLID, new FrequencyConfig(3)));
  25. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.SPHERE_REPLACE, new SphereReplaceConfig(Blocks.CLAY, 4, 1, Lists.newArrayList(Blocks.DIRT, Blocks.CLAY)), TOP_SOLID, new FrequencyConfig(1)));
  26. this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, createCompositeFeature(Feature.SPHERE_REPLACE, new SphereReplaceConfig(Blocks.GRAVEL, 6, 2, Lists.newArrayList(Blocks.DIRT, Blocks.GRASS_BLOCK)), TOP_SOLID, new FrequencyConfig(1)));

  27. //单格的岩浆和水
  28. this.addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, createCompositeFeature(Feature.LIQUIDS, new LiquidsConfig(Fluids.WATER), HEIGHT_BIASED_RANGE, new CountRangeConfig(50, 8, 8, 256)));
  29. this.addFeature(GenerationStage.Decoration.VEGETAL_DECORATION, createCompositeFeature(Feature.LIQUIDS, new LiquidsConfig(Fluids.LAVA), HEIGHT_VERY_BIASED_RANGE, new CountRangeConfig(20, 8, 16, 256)));

  30. //在温度足够低的位置生成冰雪,就算是沙漠也应该调用一下,不会错的
  31. this.addFeature(GenerationStage.Decoration.TOP_LAYER_MODIFICATION, createCompositeFeature(Feature.ICE_AND_SNOW, IFeatureConfig.NO_FEATURE_CONFIG, PASSTHROUGH, IPlacementConfig.NO_PLACEMENT_CONFIG));

  32. //刷怪列表
  33. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.SHEEP, 12, 4, 4));
  34. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.PIG, 10, 4, 4));
  35. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.CHICKEN, 10, 4, 4));
  36. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.COW, 8, 4, 4));
  37. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.HORSE, 5, 2, 6));
  38. this.addSpawn(EnumCreatureType.CREATURE, new Biome.SpawnListEntry(EntityType.DONKEY, 1, 1, 3));
  39. this.addSpawn(EnumCreatureType.AMBIENT, new Biome.SpawnListEntry(EntityType.BAT, 10, 8, 8));
  40. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.SPIDER, 100, 4, 4));
  41. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.ZOMBIE, 95, 4, 4));
  42. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.ZOMBIE_VILLAGER, 5, 1, 1));
  43. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.SKELETON, 100, 4, 4));
  44. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.CREEPER, 100, 4, 4));
  45. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.SLIME, 100, 4, 4));
  46. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.ENDERMAN, 10, 1, 4));
  47. this.addSpawn(EnumCreatureType.MONSTER, new Biome.SpawnListEntry(EntityType.WITCH, 5, 1, 1));
复制代码

  • 出生点生物群系会被优先选中为出生点,可以直接向BiomeProvider#BIOMES_TO_SPAWN_IN中添加

附录:1.13Forge地形生成代码表

事件

值得注意的是,原有的MinecraftForge#TERRAIN_GEN_BUSMinecraftForge#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 奇怪的脑回路使我感到痛苦,但是我还是在最短的时间内肝出这篇文章,不出意外,是世界上第一篇(中文)新版地形生成解析。

如果你发现本文的任何纰漏,烦请到Github发issue(下附链接),我会尽快修改,感谢您的指教!

鸣谢

  • MCP 对 Minecraft 源代码的反混淆,如果不是他们,我也不能写出这篇文章
  • 森林蝙蝠 审读并提出来让我加入的结构的部分
  • 33 让我加入浅析新版本世界生成与旧版本的本质区别和好处分析一栏
  • 丢人素学姐 和 u.s.knowledge 对教程提出的宝贵建议
  • 感谢 chyx 指出我文章中一系列的谬误
  • 土球(ustc_zzzz)制作了MCBBS Markdown To BBCode Converter造福一方

参考

按参考内容质量、数量降序排列

  • 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维基百科 - 英文、中文相关页面
  • 土球的旧版本地形生成解析

相关链接


评分

参与人数 30人气 +73 金粒 +723 贡献 +6 收起 理由
china521 + 3 + 3 + 1 MCBBS有你更精彩~
ff98sha + 2 + 30 MCBBS有你更精彩~
天狼星black + 2 神乎其技,不服不行!
1582952890 + 3 + 50 MCBBS有你更精彩~
橘子冰 + 2 + 30 神乎其技,不服不行!
zmz2333 + 1 MCBBS有你更精彩~
世界边境 + 4 + 20 MCBBS有你更精彩~
chyx + 3 评分上线真低
llopllop + 2 + 40 神乎其技,不服不行!
ok3151238 + 1 + 10 神乎其技,不服不行!
猜猜谁是谁 + 3 + 20 MCBBS有你更精彩~
ROF + 4 + 50 + 1 sausage so niubi!
wutong10086 + 1 + 10 MCBBS有你更精彩~
SPGoding + 4 神乎其技,不服不行!
ItIsEnderman + 1 MCBBS有你更精彩~
bo_bo2001 + 2 + 50 神乎其技,不服不行!
zclzcl + 2 + 40 神乎其技,不服不行!
Lss233 + 2 + 40 MCBBS有你更精彩~
一样的心愿 + 2 MCBBS有你更精彩~
45gfg9 + 40 神乎其技,不服不行!

查看全部评分

langyo 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5731
钻石
性别
保密
注册时间
2015-4-17
查看详细资料
发表于 2019-2-18 21:43:36 | 显示全部楼层
总算看到你换头像了,香肠
回复

使用道具 举报

351768593 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
898
钻石
性别
保密
注册时间
2013-8-23
查看详细资料
发表于 2019-2-18 21:45:30 | 显示全部楼层
给大佬递茶!
回复

使用道具 举报

princess01 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
641
钻石
性别
保密
注册时间
2018-11-24
查看详细资料
发表于 2019-2-18 21:56:39 | 显示全部楼层
支持!递茶(表情)
回复

使用道具 举报

zmz2333 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
162
钻石
性别
保密
注册时间
2017-9-13
查看详细资料
发表于 2019-2-18 23:48:14 来自手机 | 显示全部楼层
给dalao递茶!
回复

使用道具 举报

u.s.knowledge 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5698
钻石
性别
保密
注册时间
2013-3-8
查看详细资料
发表于 2019-2-19 02:51:02 | 显示全部楼层
本帖最后由 u.s.knowledge 于 2019-2-19 03:01 编辑
是的,新版的矿物生成完全不需要Forge插手即可实现自定义于是Forge就真的懒到没有插手,其他相关内容Forge也少了很多的话语权,大量Forge钩子消失,甚至出现了存在但未被使用的Forge事件(来自旧版本),可见原版对Forge的冲击。

喂,Forge 现在还没正式发布呢,你看到的是测试版本的 Forge,那些事件到底是去是留还不清楚。现在下“原版对 Forge 的冲击巨大”的结论还为时尚早。
虽然我也不否认这冲击会很大就是了
你可以选择和原版一致使用大写下划线(如:Desert_Pyramid)

不推荐,参考 ResourceLocation(MCP 名)中对小写下划线(snake_case,`[a-z0-9_-]*`)的要求。

其他的地方不认为有什么问题,除了一点——我们看到的并不是真正的 Minecraft 源码,只是反编译+MCP 映射名的结果。你应该也注意到有一堆根本没必要的 cast 了吧…… (Object)null 算什么……

以及你不出个英文版的?

评分

参与人数 2人气 +2 金粒 +10 收起 理由
姚氏帅哥 + 2 感谢提醒,已经修订
a6809936 + 10 need English!【x

查看全部评分

回复

使用道具 举报

姚氏帅哥 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
4149
钻石
性别
保密
注册时间
2013-9-13
查看详细资料
 楼主| 发表于 2019-2-19 10:37:21 | 显示全部楼层
本帖最后由 姚氏帅哥 于 2019-2-19 10:50 编辑
u.s.knowledge 发表于 2019-2-19 02:51
喂,Forge 现在还没正式发布呢,你看到的是测试版本的 Forge,那些事件到底是去是留还不清楚。现在下“原 ...

感谢您的关注与提醒。
第一个:我是等到Forge在官网出现下载后才开始写的本文。"自定义"那一部分几乎都没用Forge的包,因为原版给出了原本是Forge给出的功能,Forge提供钩子的前提是那个地方被原版堵死了,然而现在大量接口被敞开,Forge的某些自然失去了地位。个人认为Forge不会有重大的改动了,稍后会贴上我整理的Forge相关的资料佐证。我稍后会附上Minecraft、MCP、Forge的版本,以便读者参考

第二个:
你可以选择和原版一致使用大写下划线(如:Desert_Pyramid),但是我更加建议是modid + ':' + 小写下划线

请不要断章取义,我紧跟在后面给出了更好的形式。不过稍后我可以修改一下措辞,让后面的正确形式更为突出
关于源代码称呼:
社区习惯把MCP反编译、反混淆的代码叫做MC源码,而1.13.2的Forge当中,Minecraft和MCP已经合并为一个模组,更加昭示了MC与MCP源代码效果上的一致性。毕竟咱哪去找真的源代码呢

关于英文版:
鄙人不才,英文能力捉襟见肘,肝出这篇文已经实属不易,翻译成英文恐怕无望。如果阁下愿意帮忙,不胜荣幸。


回复

使用道具 举报

u.s.knowledge 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5698
钻石
性别
保密
注册时间
2013-3-8
查看详细资料
发表于 2019-2-19 14:16:00 | 显示全部楼层
姚氏帅哥 发表于 2019-2-19 10:37
感谢您的关注与提醒。
第一个:我是等到Forge在官网出现下载后才开始写的本文。"自定义"那一部分几乎都没 ...
请不要断章取义,我紧跟在后面给出了更好的形式。


是我的锅。大概是回复的时候饿昏了……

至于英文翻译,我一开始的想法是这样的,既然已经有比较详细的资料了,不妨直接走出国门让更多的 Modder 能从中受益。我看看我最近有没有时间吧……
回复

使用道具 举报

NGK3 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5742
钻石
性别
保密
注册时间
2016-9-11
查看详细资料
发表于 2019-2-20 09:26:32 | 显示全部楼层
本帖最后由 NGK3 于 2019-2-20 09:29 编辑

香肠 nb!
(原来你不睡觉是都忙着做这个!!)
回复

使用道具 举报

姚氏帅哥 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
4149
钻石
性别
保密
注册时间
2013-9-13
查看详细资料
 楼主| 发表于 2019-2-21 12:41:55 | 显示全部楼层
本帖最后由 姚氏帅哥 于 2019-2-21 12:46 编辑
u.s.knowledge 发表于 2019-2-19 14:16
是我的锅。大概是回复的时候饿昏了……

至于英文翻译,我一开始的想法是这样的,既然已经有比较详细的 ...

人是铁饭是钢

感谢大佬的支持,我将尽全力提供相关的帮助

Github上的版本几乎保证是最新的、完整的(因为论坛又屏蔽词),我可能还会有小修改,但是基本上已经不会有大计划了。
回复

使用道具 举报

InitAuther97 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
230
钻石
性别
保密
注册时间
2019-3-2
查看详细资料
发表于 2019-3-3 17:12:35 | 显示全部楼层
竟然精华了,特来庆祝
builder套builder不好吧,这在内存比较低的电脑上或许会有点问题?内存这么低还咋玩mc


回复

使用道具 举报

langyo 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5731
钻石
性别
保密
注册时间
2015-4-17
查看详细资料
发表于 2019-3-3 17:30:45 来自手机 | 显示全部楼层
惊了,居然精了!

加油
回复

使用道具 举报

XiaoJun2001 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
679
钻石
性别
保密
注册时间
2016-2-26
查看详细资料
发表于 2019-3-3 17:41:09 | 显示全部楼层
膜拜大佬
回复

使用道具 举报

fanfanbendan 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
901
钻石
性别
保密
注册时间
2017-12-24
查看详细资料
发表于 2019-3-4 12:56:15 | 显示全部楼层
支持qwq
回复

使用道具 举报

zhou888 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
1165
钻石
性别
保密
注册时间
2015-5-10
查看详细资料
发表于 2019-3-6 00:50:43 | 显示全部楼层
很有幫助的文章,正好最近在寫關於隨機地形生成的解決方案,受教了。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 京公网安备 11010502037624号 | 手机版

GMT+8, 2019-8-21 08:19 , Processed in 0.054035 second(s), Total 25, Slave 22 queries , Gzip On, MemCached On.

"Minecraft"以及"我的世界"为Mojang Synergies AB的商标 本站与Mojang以及微软公司没有从属关系

© 2010-2019 我的世界中文论坛 版权所有 本站原创图文内容版权属于原创作者,未经许可不得转载

快速回复 返回顶部 返回列表