世界平滑生成笔记

需要指出的是,世界生成的流程大多数是写在IChunkGenerator里的,但它毕竟只是个接口而已,实际是什么效果完全看怎么实现。此外,它在mod中又可能被完全重写,所以接下来所说的大部分原理,主要适用于原版主世界,以及根据主世界的设计精神延伸出的其他维度。
想生成大型建筑,不搞明白这个过程是免谈的,做出来的很有可能会遇到各种问题,不仅不知道怎么解决,甚至不知道发生了什么。
原版中,ChunkGenerator有五个子类,它们分别是:
ChunkGeneratorDebug,调试用的,一般玩家不会用到。
ChunkGeneratorEnd,用于生成末地。
ChunkGeneratorFlat,用于生成超平坦世界。它实际上只会用于处理主世界,超平坦模式下,地狱还是老样子,不会变平。
ChunkGeneratorHell,用于生成下界(地狱)。
ChunkGeneratorOverworld,用于生成主世界,最复杂、最关键,也是接下来要重点讨论的内容。
世界生成分为两个大阶段,我们称之为底漆阶段和殖民阶段。
这两个名字是我起的,前者主要操作ChunkPrimier,后者大多数在populate中进行。
第一阶段,premier底漆
我们看ChunkGeneratorOverworld::generateChunk,根据它对这个接口的实现,我们可以看到,
区块首次被加载时,确定随机种子,进入底漆阶段;此时它只是个ChunkPrimer,甚至不是一个Chunk。
【底漆其一:山海定高】
这个Primier先会通过setBlocksInChunk进行基本的地貌雕刻,设置石头和海洋,用以塑造一个高度图的雏形。它的塑造结果完全取决于输入的随机种子,x、z的值,还有根据getBiomeProvider()获取的群系参数。这里,不同区块的平滑连接是靠柏林噪声的连续型实现的。
【底漆其二:群系变貌】
到目前为止,不同的群系只是有高度平均值、高度方差的区别。我们接下来执行ChunkGeneratorOverworld::replaceBiomeBlocks。
他会先触发ChunkGeneratorEvent.ReplaceBiomeBlocks这个事件,这个事件的结果为阻止的话,这一步到此为止。
如果没有被阻止,那么根据x和z的每一个位置,它们会根据柏林噪声的高度图结果和群系分布,让群系Biome执行Biome::genTerrainBlocks。这里会防止沙子、砂砾、石头;水会根据温度变成冰。越往下的地方,越容易形成基岩。这一切都是混在一起的。群系的topBlock和fillerBlock在这里生效,换成对应的方块。各种教程教的群系自定义方块基本都是在这一步生效的。
我们知道,多数地形地表是草方块;雪地表层是雪方块;沙漠沙滩地表是沙子。这一步就是在这里调整topBlock实现的。草下面是泥土;恶地下面是染色黏土,这是靠fillerblock。
不幸的是,作为骨架的石头并不能在群系里指定。实际上,它不依赖群系,而依赖于刚才所说的Generator实现,早在第一步“山海定高”就都做完了。
【底漆其三:八尺画饼】
这一步我们喜闻乐见地开始生成各种矩形结构,包括峡谷、洞穴等天然的,和村庄、矿井等看起来比较“人工”的。他们都是各种MapGenBase在工作。
所谓的八尺,是指MapGenBase这个generate函数里,假设这个正在被处理的区块的区块坐标为x和z(1区块坐标=16方块坐标),那么它会从 x-8遍历到x+8, z-8遍历到z+8,对自身和周围总共加起来17x17个区块全都画一次饼。8这个数是MapGenBase里range域的默认值,原版没有哪个子类改过这值,但mod理论上可以通过派生新的类而修改。
所谓的画饼,是指这里实际上有一半只画饼,不实际生成区块里的方块,只是确定周围有什么东西。
对于自然的洞穴来说,这里真的就会直接修改方块状态。如果这里算出区块内某一格该被挖掉,那真的就直接挖了。这里我不是很确定是否会存储,因为我还没有特别仔细地阅读MapGenCave类源码。(可读性很低)。
对于建筑来说,每种建筑一旦在这个八方画饼行为里被发现要生成建筑,就会被存到专门的建筑映射表里,下次画饼的时候直接读取该建筑即可。需要指出的是,在这个八方画饼的过程中,只有结构的原点(逻辑上的生成起始点)会触发这个生成。
一旦画饼发现了一个起始点,它会生成一个巨大的抽象结构,完全可以超出17x17的范围。映射表是存在chunk以外的专门列表里的,并且有专门的内存和硬盘存储方式,所以此处没有问题。
假如画饼的范围内应该有建筑,但是没有摸到起始点,也没有其他画饼记录标明这范围内有建筑,那么建筑就暂时不会被画饼。
画饼的时候只确定建筑的范围,确定下矿井里的每条走道有没有毒蜘蛛刷怪笼、有没有铁轨之类的,并不会立刻去setBlockState。实际生成要等后面populate,第二阶段的“真殖民”。
【底漆定稿】
这些都完成后,代码计算天空光照的分布,然后new 一个Chunk对象,把它正式从ChunkPremier升格为真正的Chunk,之后就可以设置TileEntity和Entity了。

第二阶段,populate/殖民
Chunk里有两个populate,我称之为真假殖民。
假populate:public void populate(IChunkProvider chunkProvider, IChunkGenerator chunkGenrator)
真populate:protected void populate(IChunkGenerator generator)
假殖民只具有形式上的殖民,不直接做任何事,他只是根据附近的加载情况调用真殖民函数。
这里就是所谓的-X、-Z、-XZ、自身都加载好时,就开始真殖民。假殖民的具体内容可以去看实现,总之就是每次假殖民一共涉及九个区块,有四个可能会被真殖民。
真殖民,除了海洋神殿外,对每个区块,实际内容只会执行一次。我们注册的worldgenerator也是在这里执行。
这里我们调用ChunkGenerator里的populate去做实际的工作。
首先触发PopulateChunkEvent.Pre事件,然后开始生成矿井、村庄、末地要塞、散落小玩意(丛林神庙,冰原小屋等)、海洋神殿、林地府邸。注意这里就这么多,没有地狱要塞。
接下来又是一大通杂七杂八的生成,湖泊、熔岩湖、地牢、动物、冰。每个都有自己的forge事件,可以用来分别取消这些事情。
这些事情都做完后,PopulateChunkEvent.Post事件触发。第二阶段完结,区块生成完毕。
那生成矿井时实际在做什么呢?在根据之前算好的那些饼,取其中在本区块的部分,往里填内容。这里会实际一个个地码放方块和刷怪笼、召唤女巫。需要注意的是,这里只生成那些在本区块里的内容。(本区块是指在进行真殖民的区块,而不是假殖民的发起区块)这个过程在ChunkGeneratorOverworld里规定每个区块只做一次(海洋神殿除外),如果世界种子变动(或者建筑写的不合理),导致真已经殖民完毕后,又发现这里还需要补生成什么建筑,那凉凉了,生成不了。建筑将会缺失该区块的部分。大多数情况下,因为每个区块第一阶段就算清楚了半径八个区块范围的内容(也包括区块自身为核心的情况),而且区块的第二阶段是从玩家周围逐步扩散着生成的再者是先第一个阶段再第二个阶段,所以正常游玩基本不会有某个区块进入第二阶段后还不知道自己头上该有建筑的情况。