Mod开发教程:村庄级巨型结构生成
序言
评估难度等级:大师级。
环节非常多,很容易疏漏;一旦疏漏,需要自行排查。
我不认为在我之前有任何国人搞定1.12.2的巨构生成。
在新增一种巨构之前,我们先要想清楚几个问题。第一个问题,就是巨构和普通结构的区别。


当你确定了你要做村庄这样的巨构时,你最好做好充足的心理准备,并且已经阅读了我教程里讲到的世界生成原理(5个分P都要看)。尤其要了解的,是1.12里的世界生成,分为基本生成和populate这两步,而巨构在此之外还有一步预先生成抽象结构。这一点不理解的话,就无从做起了。此外,每个版本的世界生成流程都不一样,本教程对于1.12.2以外的没有参考意义。
哦,还有一点,你的这个巨构必须可以序列化。多数情况下,玩家可能距离你七八个区块的时候,就触发了巨构的【抽象生成】,此时那些巨构对应的区块多半还没生成,玩家就离开并下线了。这种情况下,就需要序列化这些结构为数据,然后存储。如果你的结构零件过于复杂,那么你得自己咽下序列化的苦果。
当你遇到问题的时候,一个好的参考例子是暮色。他们把所需的类都翻新了一遍。理想境也有。

正文
巨构类生成需要一个MapGenBase作为巨构生成的享元类,一般每一个维度都对应一个该对象。
多数情况下,我们是用它的派生类MapGenStructure。
你可以看到这个类有几个主要的接口等待你覆写:
getStructureName,无需多言,是指这类巨构的名字。
getNearestStructurePos,locate指令用的。一般是返回findNearestStructurePosBySpacing(世界对象,this,参数pos,32【最大的建筑间距离,单位是区块】,8【一般是最小建筑间距离,单位是区块】,每类巨构对应的随机种子,false【二重随机判定,没有必要】,100【寻找次数】,参数findUnexplored)
canSpawnStructureAtCoords,某位置是否能生成巨构。我一般是规定XZ区块号余数为某特定数时为true,比如XZ除以32余8的时候。哦呵呵,java里负数要单独处理同余,记得这件事哦。
getStructureStart,返回一个新的StructureStart对象。激动人心的构造开始。这玩意你忘写的话,会net.minecraft.world.gen.structure.MapGenStructure.recursiveGenerate甩NPE崩游戏。
以上为冒险一号的例子。

好,接下来就是写StructureStart了。一般来说会作为内部类来书写,并命名为Start。这个东西虽然叫Start,但他实际上对应整个巨构,每一份巨构都有一个Start对象统率之,村庄A和村庄B各有各的Start对象。
首先,你必须有一个公开的无参构造函数public Start(),不然在游戏重新加载时会炸。你自己调用与否无所谓,MC会用特殊方式强制调用的。这个构造函数的内容可以留空不管,不重要。如果你的这个东西是子类,那么子类也要有这玩意,不信邪的可以试试去,第一次加载游戏生成结构,第二次在开游戏,去那看看崩不崩。
然后就是你自己的构造函数了,什么样的参数列表都无所谓,反正是你自己在getStructureStart里调用的。记得调用super,把xz参数给传进去。
注意,你自己的构造函数里必须完成整份巨构的抽象生成。这个里面一个blockstate都不能改,但必须立刻完成巨构规划。是的,你此时往往甚至不知道这个地方地表populate后有多高,但必须立刻决定你所有零件的抽象边界。这也是巨构的Y坐标locate不给的原因之一。
构造函数里必须立刻构造所有的StructureComponent对象,规划他们的边界,并且调用它们的buildComponent。

这是什么意思呢?正好我们说完了Start了,我们开始讲StructureComponent。
StructureComponent也一样需要有个无参构造函数,正如前面所说,子类也都得有。可以里面啥都不干,但是必须有。他代表着建筑里的各个零件,对于林地府邸这种,就是各种房间;对于我的迷宫来说,就是迷宫的一格。巨构最后都要分解成这种零件的。
我习惯把零件的包围盒计算写进构造函数,也就是设置this.boundingBox。这个东西的坐标是世界的绝对坐标,你可以通过Start里的参数计算出它。不在这里写的话,就写进buildComponent,也可以。这个包围盒是用来和区块取交集,在生成的时候确保不溢出的,最好还是写一下,不然有可能根本生成不出来。
哦,还有,这些Component要放到StructureStart::components里,他们才能正确地统筹运作,否则直接就会失去关联关系而无效化。这个时机可以是构造函数,也可以是buildComponent,看你喜欢。记得添加,不然你的结构一个方块也不会在世界中显现的。
这玩意有writeStructureToNBT和readStructureFromNBT,写过实体的人一眼就知道这是干嘛的,我就不赘述了。他是序列化和反序列化用的,也正是因为有这玩意,我们的无参构造函数才可以直接摆烂。
buildComponent是抽象初始化,如果你都写进构造函数了,这里啥都不写也行。注意这里不要执行任何的setBlockState。
addComponentParts是实际建造。这里是setBlockState的地方。需要指出的是,这里setBlockState一定要调用他自己的成员setBlockState,而不是直接调用world的同名成员。这使得你可以调用isVecInside,确保不导致加载区块以外的部分而导致CascadeWorldGenearation。
你可能会发现x和z里有一个轴和你的坐标是反的,如果你遇到了并且觉得很难受,那么请覆写setCoordBaseMode,改变它的SOUTH和default情况,这里mojang给你埋了一个天坑。你去看一眼父类的这函数的实现你就知道该怎么改,以及为什么反了。如果你没有遇到,可以无视这段。

这些类都写完了,我们该写他们的调用了。
注意,原版的各个维度,他们的区块生成器已经写死,没法正常调用这些东西。我们需要在自己的新维度写,修改我们的IChunkGenerator的实现,比如理想境里的ChunkGenBase。至于怎么创建新维度,改天再说。这方面的资料很多了,不需要我再写一份。
首先要在开头加入各个MapGenStructure的对象,让他们随着IChunkGenerator被构造出来。
接着,我们在IChunkGenerator::generateChunk里加入这些对象的MapGenStructure::generate调用。generateChunk你很可能会抽象出一些子方法,比如理想境里放在一个buildChunkPrimier里。
再次,我们在IChunkGenerator::populate里加入MapGenStructure::generateStructure的调用。
后面,我们覆写getNearestStructurePos,这里你需要根据传入的name,写一个switch语句,返回各个MapGenStructure::getNearestStructurePos。
IChunkGenerator::recreateStructures这个也要覆写,调用MapGenStructure::generate。
IChunkGenerator::isInsideStructure也要覆写,调用MapGenStructure::isInsideStructure。当然了,也要根据name写个switch语句。switch写烦了的话,你写个字典也行。
以上为冒险一号的ChunkGenBase。populate里大段抄的是原版。

结束了?不不不,我们还没注册呢。在FMLPreInitializationEvent时机下,调用两个函数去注册。一个注册StructureStart,一个注册你的各个零件。每一种巨构的Start、每一种零件都要注册。你可以看看原版在这里写了什么乱七八糟的抽象注册名。这个地方忘掉的话,也会有一个很有特征的报错,你一看就知道是忘了注册了。
举个例子,这是我的写法。
行了,这次是真的结束了。开新世界测试的时候,别忘了开创造和作弊权限哦,不能tp很不方便的。