原文载于知乎专栏《【真正的和平模式】三、结构的生成》,作者为 Viola-Siemens, 原地址:https://zhuanlan.zhihu.com/p/646365902
【真正的和平模式】三、结构的生成 上一篇讲了任务系统的实现,内容毕竟偏,并非所有人都需要它。本篇讲结构如何生成。
开头想说点骚话,实在想不出就算了,直接步入正题。
一、结构的分类 生成结构多种多样,为了方便区分,我根据生成方式大致可以分为三类:普通结构、多级结构和拼图结构。
从原版里举几个例子:
埋藏的宝藏、沉船、沙漠神殿、丛林神庙等属于普通结构 ,特点是它们只有一个piece,而且通常使用nbt来保存结构的模板。
林地府邸、要塞、下界要塞、海底神殿等属于多级结构 ,特点是它们有多个piece,生成分多个层级,而且没有nbt模板,仅通过代码控制生成,不同piece随着结构的生成通过某种状态转移来切换。
掠夺者前哨站、村庄、堡垒遗迹(即猪堡)、远古城市属于拼图结构 ,特点是有一个中心、仍然是多层生成、有多个结构中可能存在的模板、模板中通过拼图方块控制接下来的生成方向与目标池。
需要注意的是,有些结构比较特殊,如雪屋这个结构似乎要介于普通结构与多级结构之间——取决于它是否有地下室,不过我们可以把它归入第二类中。
在代码实现方面,拼图结构无需额外代码控制生成,只需完成数据包即可。而其他两种则需要额外的结构注册与生成,尤其是第二种,需要相对多一些的代码量。
二、结构的注册 1.19.3及之后,结构和结构集本身无需多余的注册,通过数据包即可实现生成。不过StructureType和StructurePieceType依然需要常规注册。
StructureType用于世界生成时唯一标识当前区块包含的结构类型,并链接特定的CODEC完成序列化和反序列化。
以《真正的和平模式》中的五个结构举例(无需包含拼图结构),注册方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 private static final DeferredRegister<StructureType<?>> REGISTER = DeferredRegister.create(Registries.STRUCTURE_TYPE, MODID);public static final RegistryObject<StructureType<CrystalSkullIslandFeature>> CRYSTAL_SKULL_ISLAND = register("crystal_skull_island" , () -> CrystalSkullIslandFeature.CODEC);public static final RegistryObject<StructureType<AbandonedMagicPoolFeature>> ABANDONED_MAGIC_POOL = register("abandoned_magic_pool" , () -> AbandonedMagicPoolFeature.CODEC);public static final RegistryObject<StructureType<PinkCreeperFeature>> PINK_CREEPER = register("pink_creeper" , () -> PinkCreeperFeature.CODEC);public static final RegistryObject<StructureType<ZombieFortFeature>> ZOMBIE_FORT = register("zombie_fort" , () -> ZombieFortFeature.CODEC);public static final RegistryObject<StructureType<SkeletonPalaceFeature>> SKELETON_PALACE = register("skeleton_palace" , () -> SkeletonPalaceFeature.CODEC);private static <T extends Structure> RegistryObject<StructureType<T>> register(String name, StructureType<T> codec) { return REGISTER.register(name, () -> codec); }
而StructurePieceType则用于世界生成中标识结构的每一个部分——比如多级结构中的要塞的每个房间、雪屋的主体、地下室和梯子等等,对于普通结构而言则只需要注册本体的PieceType:
1 2 3 4 5 6 7 8 9 public static final StructurePieceType CRYSTAL_SKULL_ISLAND_TYPE = register("crystal_skull_island" , CrystalSkullIslandPieces.CrystalSkullIslandPiece::new );public static final StructurePieceType ABANDONED_MAGIC_POOL_TYPE = register("abandoned_magic_pool" , AbandonedMagicPoolPieces.AbandonedMagicPoolPiece::new );public static final StructurePieceType PINK_CREEPER_TYPE = register("pink_creeper" , PinkCreeperPieces.PinkCreeperPiece::new );public static final StructurePieceType ZOMBIE_FORT_TYPE = register("zombie_fort" , ZombieFortPieces.ZombieFortPiece::new );public static final StructurePieceType SKELETON_PALACE_TYPE = register("skeleton_palace" , SkeletonPalacePieces.SkeletonPalacePiece::new );private static StructurePieceType register (String name, StructurePieceType type) { return Registry.register(BuiltInRegistries.STRUCTURE_PIECE, new ResourceLocation(MODID, name), type); }
注意,世界生成是允许动态注册的,因此没有客户端服务端的同步检测(因而仅修改世界生成的mod可以仅服务端加载)、甚至有些内容并不需要延迟注册(使用DeferredRegister)。你可以直接在模组的构造时加载这个注册工具类。
以废弃的魔法池为例,生成Piece的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 public class AbandonedMagicPoolPieces { private static final ResourceLocation ABANDONED_MAGIC_POOL = new ResourceLocation(MODID, "abandoned_magic_pool/abandoned_magic_pool" ); public static void addPieces (StructureTemplateManager structureManager, BlockPos pos, Rotation rotation, StructurePieceAccessor pieces) { pieces.addPiece(new AbandonedMagicPoolPiece(structureManager, ABANDONED_MAGIC_POOL, pos, rotation)); } public static class AbandonedMagicPoolPiece extends TemplateStructurePiece { public AbandonedMagicPoolPiece (StructureTemplateManager structureManager, ResourceLocation location, BlockPos pos, Rotation rotation) { super (RPMStructurePieceTypes.ABANDONED_MAGIC_POOL_TYPE, 0 , structureManager, location, location.toString(), makeSettings(rotation), pos.offset(-5 , -1 , -5 )); } public AbandonedMagicPoolPiece (StructurePieceSerializationContext context, CompoundTag tag) { super (RPMStructurePieceTypes.ABANDONED_MAGIC_POOL_TYPE, tag, context.structureTemplateManager(), (location) -> makeSettings(Rotation.valueOf(tag.getString("Rot" )))); } private static StructurePlaceSettings makeSettings (Rotation rotation) { return (new StructurePlaceSettings()) .setRotation(rotation) .setMirror(Mirror.LEFT_RIGHT) .setRotationPivot(new BlockPos(5 , 1 , 5 )) .addProcessor(BlockIgnoreProcessor.STRUCTURE_BLOCK); } @Override protected void addAdditionalSaveData (StructurePieceSerializationContext context, CompoundTag tag) { super .addAdditionalSaveData(context, tag); tag.putString("Rot" , this .placeSettings.getRotation().name()); } @Override protected void handleDataMarker (String function, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox sbb) { } public static final double BROKEN_BRICKS_PERCENTAGE = 0.25D ; public static final double BROKEN_WOOD_PERCENTAGE = 0.15D ; public static final double COBWEB_PERCENTAGE = 0.4D ; @Override public void postProcess (WorldGenLevel level, StructureManager structureManager, ChunkGenerator chunkGenerator, RandomSource random, BoundingBox boundingBox, ChunkPos chunkPos, BlockPos blockPos) { super .postProcess(level, structureManager, chunkGenerator, random, boundingBox, chunkPos, blockPos); BoundingBox curBoundingBox = this .getBoundingBox(); for (int x = 0 ; x < curBoundingBox.getXSpan(); ++x) { for (int z = 0 ; z < curBoundingBox.getZSpan(); ++z) { for (int y = 0 ; y < curBoundingBox.getYSpan(); ++y) { BlockState blockstate = this .getBlock(level, x, y, z, boundingBox); if (isBricks(blockstate)) { if (random.nextDouble() < BROKEN_BRICKS_PERCENTAGE) { if (random.nextDouble() < COBWEB_PERCENTAGE) { this .placeBlock(level, Blocks.COBWEB.defaultBlockState(), x, y, z, boundingBox); } else { this .placeBlock(level, Blocks.AIR.defaultBlockState(), x, y, z, boundingBox); } } } else if (isWood(blockstate)) { if (random.nextDouble() < BROKEN_WOOD_PERCENTAGE) { if (random.nextDouble() < COBWEB_PERCENTAGE) { this .placeBlock(level, Blocks.COBWEB.defaultBlockState(), x, y, z, boundingBox); } else { this .placeBlock(level, Blocks.AIR.defaultBlockState(), x, y, z, boundingBox); } } } } } } } private static boolean isBricks (BlockState blockstate) { return blockstate.is(Blocks.BRICKS) || blockstate.is(Blocks.BRICK_SLAB) || blockstate.is(Blocks.BRICK_STAIRS) || blockstate.is(Blocks.BRICK_WALL); } private static boolean isWood (BlockState blockstate) { return blockstate.is(Blocks.MANGROVE_PLANKS) || blockstate.is(Blocks.MANGROVE_STAIRS) || blockstate.is(Blocks.MANGROVE_SLAB) || blockstate.is(Blocks.MANGROVE_TRAPDOOR) || blockstate.is(Blocks.MANGROVE_LOG); } } }
addPieces方法则是入口方法,将结构的所以部分加入StructurePieceAccessor参与世界生成。注意ABANDONED_MAGIC_POOL表征了结构nbt的路径,它被放在数据包的data\real_peaceful_mode\structures中。
AbandonedMagicPoolPiece继承了TemplateStructurePiece结构部分模板类,若无其它内容,只需重写一个空的handleDataMarker函数即可——如果你使用了数据模式的结构方块,在这里当然需要作额外处理,这部分接下来再讲;如果有其它需要存储的数据,则需要重写addAdditionalSaveData函数;另外生成过程中可能需要额外的方块变化,如随机苔藓化、裂纹、氧化等等,这部分可以用postProcess来实现,其过程中调用this.placeBlock函数改变模板的方块来放置到世界中——本例则实现了砖块和木头的随机破坏和替换为蜘蛛网。
那么入口函数如何调用呢?此处需要一个继承Structure并定义CODEC的类来解决:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class AbandonedMagicPoolFeature extends Structure { public static final Codec<AbandonedMagicPoolFeature> CODEC = simpleCodec(AbandonedMagicPoolFeature::new ); public AbandonedMagicPoolFeature (StructureSettings settings) { super (settings); } @Override protected Optional<GenerationStub> findGenerationPoint (GenerationContext context) { return Optional.of(new GenerationStub(context.chunkPos().getWorldPosition(), builder -> generatePieces(builder, context))); } private static void generatePieces (StructurePiecesBuilder builder, GenerationContext context) { BlockPos centerOfChunk = new BlockPos(context.chunkPos().getMinBlockX() + 2 , 0 , context.chunkPos().getMinBlockZ() + 2 ); BlockPos blockpos = new BlockPos( centerOfChunk.getX(), context.chunkGenerator().getBaseHeight( centerOfChunk.getX(), centerOfChunk.getZ(), Heightmap.Types.WORLD_SURFACE_WG, context.heightAccessor(), context.randomState() ), centerOfChunk.getZ() ); Rotation rotation = Rotation.getRandom(context.random()); AbandonedMagicPoolPieces.addPieces(context.structureTemplateManager(), blockpos, rotation, builder); } @Override public StructureType<?> type() { return RPMStructureTypes.ABANDONED_MAGIC_POOL.get(); } }
其中findGenerationPoint用来找到区块中哪个位置可用于结构生成,如果存在便执行生成函数——generatePieces是生成结构的函数,addPieces入口便是在这里被调用。type则是生成结构的类型,对应着前文的StructureType。
以前还需重写step函数,否则会抛出异常——1.19.3后不再需要多此一举,因为可以直接在数据包的json里写了。
三、普通结构数据包的编写 前文只是实现了结构的注册和生成逻辑的编写,还没能真正将结构生成在世界中。由于1.19.3代码结构的大改,仅需实现数据包即可实现结构的生成。
以废弃的魔法池为例,我们在data\real_peaceful_mode\worldgen\structure目录下创建abandoned_magic_pool.json:
1 2 3 4 5 6 7 { "type" : "real_peaceful_mode:abandoned_magic_pool" , "biomes" : "#real_peaceful_mode:has_structure/abandoned_magic_pool" , "spawn_overrides" : {}, "step" : "surface_structures" , "terrain_adaptation" : "beard_thin" }
其中type 对应着StructureType,需要与注册保持一致;spawn_overrides 重写了结构内部的生物生成,如古城不会生成任何生物、掠夺者前哨站会源源不断生成掠夺者等;step 表示生成的步骤,最常见的则是”underground_structures”和”surface_structures”;terrain_adaptation 是生成后的地形调整,建议地表建筑使用”beard_thin”保证不会悬空,而地下建筑则多为”bury”,古城使用了”beard_box”,悬空建筑(如水晶头骨浮岛)则留空;最重要的,biomes 表示结构生成的群系,最好使用TagKey表示,于是我们在data\real_peaceful_mode\tags\worldgen\biome\has_structure目录下创建abandoned_magic_pool.json:
1 2 3 4 5 6 { "values" : [ "minecraft:swamp" , "minecraft:mangrove_swamp" ] }
实现了结构,接下来实现结构集——即多个变种的相同类型结构的集合,如村庄结构集包括平原、热带草原、沙漠、雪原和针叶林五个变种,废弃的传送门结构集则包括下界、高山、深海、沙漠和普通等变种。
我们在data\real_peaceful_mode\worldgen\structure_set目录下创建abandoned_magic_pools.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "placement" : { "type" : "minecraft:random_spread" , "salt" : 2123071613 , "separation" : 9 , "spacing" : 30 }, "structures" : [ { "structure" : "real_peaceful_mode:abandoned_magic_pool" , "weight" : 1 } ] }
salt 建议随机生成,不要跟任何结构重复;separation 是同一结构集内两个结构间的最小距离(单位:区块),而spacing 则是两个结构间的平均距离(单位:区块),显然这个值要大于separation;type 则是结构的分布,random_spread是随机分布,而concentric_rings则是类似要塞的分布方式——若是random_spread,还有可选的字段spread_type ,可选值包括linear和triangular,即距离分布的类型,triangular使得结构更加平均分散,而linear距离的方差则更大。structures 包括了这个结构集的所有结构,并以权重组织着它们的生成。
按照这个步骤走下来的话,你的结构大概率就生成在世界中了。不过还有一些特殊情况,比如拼图结构的定义,以及processor list的编写。
四、拼图结构的模板池和processor list 首先给原版知识储备较少的朋友们介绍一下什么是拼图结构。在处理结构群(如村庄)或较大面积结构的生成(如古城)时,或者希望在结构生成过程中不同“部分”的连接引入随机性时(如堡垒遗迹),可以考虑使用拼图结构。拼图结构的每个模板结构都应该包含拼图方块,并指定name(拼图名称)、final_state(拼图方块最终转变成什么方块)、joint(拼接类型)、pool(目标池)、target(需要对接的拼图名称)——两个拼图方块之间分别以相同的name与target互相连接,而生成过程中拼图方块则从pool目标模板池中选择新的结构并拼接好后生成。
这部分同样只需要添加数据包。以苦力怕小镇为例,需要在data\real_peaceful_mode\worldgen\template_pool\creeper_town目录下编写模板池,以房屋池为例(houses.json):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 { "elements" : [ { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/small_house1" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 4 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/small_house2" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 4 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/small_house3" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 3 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/middle_house1" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 2 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/middle_house2" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 2 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/middle_house3" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 2 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/large_house1" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 1 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/large_house2" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 1 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/farm1" , "processors" : "real_peaceful_mode:ruin_8_percent" , "projection" : "rigid" }, "weight" : 2 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/farm2" , "processors" : "real_peaceful_mode:ruin_8_percent" , "projection" : "rigid" }, "weight" : 2 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/fountain" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 1 }, { "element" : { "element_type" : "minecraft:legacy_single_pool_element" , "location" : "real_peaceful_mode:mission/creeper_town/houses/well" , "processors" : "real_peaceful_mode:ruin_8_and_crack_40_percent" , "projection" : "rigid" }, "weight" : 1 }, { "element" : { "element_type" : "minecraft:empty_pool_element" }, "weight" : 5 } ], "fallback" : "real_peaceful_mode:creeper_town/terminators" }
elements包含了所有池内的模板结构,每个结构由element和weight组成,分别表示结构细节和权重。
element_type常有以下三种取值:
minecraft:empty_pool_element:即空结构,表示在一定权重下,该位置不生成新的模板。 minecraft:legacy_single_pool_element:单结构,通过location定位在structures文件夹中的模板结构。 minecraft:feature_pool_element:即地物结构,直接生成一个地物,通过feature定位一个地物的注册名。 另外两个很好理解,主要介绍一下单结构的一些属性——
location:结构模板的id processors:processor list的id projection:投影类型,包括rigid(视为整体放置,多用于房屋、农田、水井等不因地形改变而改变的结构部分)和terrain_matching(匹配地形,多用于道路)。 那么processor list又是什么?其实就是一个结构的部分生成结束后再进行的处理,与前文的postProcess函数功能类似。如苦力怕小镇的道路使用了如下处理,使得水和极少部分的沙砾被替换为了凝灰岩砖(street_creeper_town.json):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 { "processors" : [ { "processor_type" : "minecraft:rule" , "rules" : [ { "input_predicate" : { "block" : "minecraft:gravel" , "predicate_type" : "minecraft:block_match" }, "location_predicate" : { "block" : "minecraft:water" , "predicate_type" : "minecraft:block_match" }, "output_state" : { "Name" : "real_peaceful_mode:tuff_bricks" } }, { "input_predicate" : { "block" : "minecraft:gravel" , "predicate_type" : "minecraft:random_block_match" , "probability" : 0.1 }, "location_predicate" : { "predicate_type" : "minecraft:always_true" }, "output_state" : { "Name" : "real_peaceful_mode:tuff_bricks" } }, { "input_predicate" : { "block" : "real_peaceful_mode:tuff_bricks" , "predicate_type" : "minecraft:block_match" }, "location_predicate" : { "block" : "minecraft:water" , "predicate_type" : "minecraft:block_match" }, "output_state" : { "Name" : "real_peaceful_mode:cracked_tuff_bricks" } } ] } ] }
其详细写法,建议参考官方wiki 。
编者注:上述链接目前已随 Minecraft Wiki 迁移而转移至 https://zh.minecraft.wiki/w/%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%96%E7%95%8C%E7%94%9F%E6%88%90/processor
五、怎么生成和编辑结构nbt文件? 前文的所有介绍,默认你知道了所有关于生成nbt文件的知识,这里额外多提一嘴,防止有人不知道这个问题的答案,影响阅读体验。
首先随便建点什么,想生成啥就建啥:
然后拿出结构方块,调整到save模式,然后调整边框和大小使得整个结构完全被白线包裹:
然后输入结构名——这是个id,以命名空间:名称的方式命名。如果需要储存实体,请将右侧的Include Entities调为ON。
最后点击右侧SAVE保存,便可在世界文件夹/generated/<命名空间>/structures目录下找到它。将这个nbt复制到你的模组数据包目录的对应位置中,必要时可打开NBTExplorer,甚至IDEA安装Minecraft Development插件也可直接编辑:
这些nbt由三部分构成——方块、实体、调色板。方块即结构每个位置的方块,包括方块实体等;而实体则在开启Include Entities时被保存;调色板包括所有可能的方块状态,方块中state数值即为调色板的下标。
接下来我可能会讲讲地物和群系的生成,这部分也以数据包为主,涉及代码较少,敬请期待!
笔者从事开发不久,文章如有疏漏,敬请斧正!