Mod開發(fā)教程:村莊級巨型結(jié)構(gòu)生成
序言
評估難度等級:大師級。
環(huán)節(jié)非常多,很容易疏漏;一旦疏漏,需要自行排查。
我不認為在我之前有任何國人搞定1.12.2的巨構(gòu)生成。
在新增一種巨構(gòu)之前,我們先要想清楚幾個問題。第一個問題,就是巨構(gòu)和普通結(jié)構(gòu)的區(qū)別。


當你確定了你要做村莊這樣的巨構(gòu)時,你最好做好充足的心理準備,并且已經(jīng)閱讀了我教程里講到的世界生成原理(5個分P都要看)。尤其要了解的,是1.12里的世界生成,分為基本生成和populate這兩步,而巨構(gòu)在此之外還有一步預先生成抽象結(jié)構(gòu)。這一點不理解的話,就無從做起了。此外,每個版本的世界生成流程都不一樣,本教程對于1.12.2以外的沒有參考意義。
哦,還有一點,你的這個巨構(gòu)必須可以序列化。多數(shù)情況下,玩家可能距離你七八個區(qū)塊的時候,就觸發(fā)了巨構(gòu)的【抽象生成】,此時那些巨構(gòu)對應的區(qū)塊多半還沒生成,玩家就離開并下線了。這種情況下,就需要序列化這些結(jié)構(gòu)為數(shù)據(jù),然后存儲。如果你的結(jié)構(gòu)零件過于復雜,那么你得自己咽下序列化的苦果。
當你遇到問題的時候,一個好的參考例子是暮色。他們把所需的類都翻新了一遍。理想境也有。

正文
巨構(gòu)類生成需要一個MapGenBase作為巨構(gòu)生成的享元類,一般每一個維度都對應一個該對象。
多數(shù)情況下,我們是用它的派生類MapGenStructure。
你可以看到這個類有幾個主要的接口等待你覆寫:
getStructureName,無需多言,是指這類巨構(gòu)的名字。
getNearestStructurePos,locate指令用的。一般是返回findNearestStructurePosBySpacing(世界對象,this,參數(shù)pos,32【最大的建筑間距離,單位是區(qū)塊】,8【一般是最小建筑間距離,單位是區(qū)塊】,每類巨構(gòu)對應的隨機種子,false【二重隨機判定,沒有必要】,100【尋找次數(shù)】,參數(shù)findUnexplored)
canSpawnStructureAtCoords,某位置是否能生成巨構(gòu)。我一般是規(guī)定XZ區(qū)塊號余數(shù)為某特定數(shù)時為true,比如XZ除以32余8的時候。哦呵呵,java里負數(shù)要單獨處理同余,記得這件事哦。
getStructureStart,返回一個新的StructureStart對象。激動人心的構(gòu)造開始。這玩意你忘寫的話,會net.minecraft.world.gen.structure.MapGenStructure.recursiveGenerate甩NPE崩游戲。
以上為冒險一號的例子。

好,接下來就是寫StructureStart了。一般來說會作為內(nèi)部類來書寫,并命名為Start。這個東西雖然叫Start,但他實際上對應整個巨構(gòu),每一份巨構(gòu)都有一個Start對象統(tǒng)率之,村莊A和村莊B各有各的Start對象。
首先,你必須有一個公開的無參構(gòu)造函數(shù)public Start(),不然在游戲重新加載時會炸。你自己調(diào)用與否無所謂,MC會用特殊方式強制調(diào)用的。這個構(gòu)造函數(shù)的內(nèi)容可以留空不管,不重要。如果你的這個東西是子類,那么子類也要有這玩意,不信邪的可以試試去,第一次加載游戲生成結(jié)構(gòu),第二次在開游戲,去那看看崩不崩。
然后就是你自己的構(gòu)造函數(shù)了,什么樣的參數(shù)列表都無所謂,反正是你自己在getStructureStart里調(diào)用的。記得調(diào)用super,把xz參數(shù)給傳進去。
注意,你自己的構(gòu)造函數(shù)里必須完成整份巨構(gòu)的抽象生成。這個里面一個blockstate都不能改,但必須立刻完成巨構(gòu)規(guī)劃。是的,你此時往往甚至不知道這個地方地表populate后有多高,但必須立刻決定你所有零件的抽象邊界。這也是巨構(gòu)的Y坐標locate不給的原因之一。
構(gòu)造函數(shù)里必須立刻構(gòu)造所有的StructureComponent對象,規(guī)劃他們的邊界,并且調(diào)用它們的buildComponent。

這是什么意思呢?正好我們說完了Start了,我們開始講StructureComponent。
StructureComponent也一樣需要有個無參構(gòu)造函數(shù),正如前面所說,子類也都得有??梢岳锩嫔抖疾桓桑潜仨氂?。他代表著建筑里的各個零件,對于林地府邸這種,就是各種房間;對于我的迷宮來說,就是迷宮的一格。巨構(gòu)最后都要分解成這種零件的。
我習慣把零件的包圍盒計算寫進構(gòu)造函數(shù),也就是設置this.boundingBox。這個東西的坐標是世界的絕對坐標,你可以通過Start里的參數(shù)計算出它。不在這里寫的話,就寫進buildComponent,也可以。這個包圍盒是用來和區(qū)塊取交集,在生成的時候確保不溢出的,最好還是寫一下,不然有可能根本生成不出來。
哦,還有,這些Component要放到StructureStart::components里,他們才能正確地統(tǒng)籌運作,否則直接就會失去關(guān)聯(lián)關(guān)系而無效化。這個時機可以是構(gòu)造函數(shù),也可以是buildComponent,看你喜歡。記得添加,不然你的結(jié)構(gòu)一個方塊也不會在世界中顯現(xiàn)的。
這玩意有writeStructureToNBT和readStructureFromNBT,寫過實體的人一眼就知道這是干嘛的,我就不贅述了。他是序列化和反序列化用的,也正是因為有這玩意,我們的無參構(gòu)造函數(shù)才可以直接擺爛。
buildComponent是抽象初始化,如果你都寫進構(gòu)造函數(shù)了,這里啥都不寫也行。注意這里不要執(zhí)行任何的setBlockState。
addComponentParts是實際建造。這里是setBlockState的地方。需要指出的是,這里setBlockState一定要調(diào)用他自己的成員setBlockState,而不是直接調(diào)用world的同名成員。這使得你可以調(diào)用isVecInside,確保不導致加載區(qū)塊以外的部分而導致CascadeWorldGenearation。
你可能會發(fā)現(xiàn)x和z里有一個軸和你的坐標是反的,如果你遇到了并且覺得很難受,那么請覆寫setCoordBaseMode,改變它的SOUTH和default情況,這里mojang給你埋了一個天坑。你去看一眼父類的這函數(shù)的實現(xiàn)你就知道該怎么改,以及為什么反了。如果你沒有遇到,可以無視這段。

這些類都寫完了,我們該寫他們的調(diào)用了。
注意,原版的各個維度,他們的區(qū)塊生成器已經(jīng)寫死,沒法正常調(diào)用這些東西。我們需要在自己的新維度寫,修改我們的IChunkGenerator的實現(xiàn),比如理想境里的ChunkGenBase。至于怎么創(chuàng)建新維度,改天再說。這方面的資料很多了,不需要我再寫一份。
首先要在開頭加入各個MapGenStructure的對象,讓他們隨著IChunkGenerator被構(gòu)造出來。
接著,我們在IChunkGenerator::generateChunk里加入這些對象的MapGenStructure::generate調(diào)用。generateChunk你很可能會抽象出一些子方法,比如理想境里放在一個buildChunkPrimier里。
再次,我們在IChunkGenerator::populate里加入MapGenStructure::generateStructure的調(diào)用。
后面,我們覆寫getNearestStructurePos,這里你需要根據(jù)傳入的name,寫一個switch語句,返回各個MapGenStructure::getNearestStructurePos。
IChunkGenerator::recreateStructures這個也要覆寫,調(diào)用MapGenStructure::generate。
IChunkGenerator::isInsideStructure也要覆寫,調(diào)用MapGenStructure::isInsideStructure。當然了,也要根據(jù)name寫個switch語句。switch寫煩了的話,你寫個字典也行。
以上為冒險一號的ChunkGenBase。populate里大段抄的是原版。

結(jié)束了?不不不,我們還沒注冊呢。在FMLPreInitializationEvent時機下,調(diào)用兩個函數(shù)去注冊。一個注冊StructureStart,一個注冊你的各個零件。每一種巨構(gòu)的Start、每一種零件都要注冊。你可以看看原版在這里寫了什么亂七八糟的抽象注冊名。這個地方忘掉的話,也會有一個很有特征的報錯,你一看就知道是忘了注冊了。
舉個例子,這是我的寫法。
行了,這次是真的結(jié)束了。開新世界測試的時候,別忘了開創(chuàng)造和作弊權(quán)限哦,不能tp很不方便的。