结构生成

在这节中我们将来学习如何创建一个自定义的结构,并且在世界中自动生成它。我们将以钻石小屋作为例子。

首先既然我们的要自定义的是一个结构,那么也就需要创建一个结构,内容如下:

public class DiamondHouseStructure extends Structure<NoFeatureConfig> {
    public DiamondHouseStructure(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn) {
        super(configFactoryIn);
    }

    @Override
    public boolean canBeGenerated(BiomeManager biomeManagerIn, ChunkGenerator<?> generatorIn, Random randIn, int chunkX, int chunkZ, Biome biomeIn) {
        if (randIn.nextFloat() < 0.03) {
            return true;
        }
        return false;
    }

    @Override
    public IStartFactory getStartFactory() {
        return (structure, chunkPosX, chunkPosZ, bounds, references, seed) -> {
            return new Start(structure, chunkPosX, chunkPosZ, bounds, references, seed);
        };
    }

    @Override
    public String getStructureName() {
        return "neutrino_house";
    }

    @Override
    public int getSize() {
        return 3;
    }

    public static class Start extends StructureStart {

        public Start(Structure<?> structure, int chunkPosX, int chunkPosZ, MutableBoundingBox bounds, int references, long seed) {
            super(structure, chunkPosX, chunkPosZ, bounds, references, seed);
        }

        @Override
        public void init(ChunkGenerator<?> generator, TemplateManager templateManagerIn, int chunkX, int chunkZ, Biome biomeIn) {
            DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
            this.components.add(diamondHouseStructurePiece);
            this.recalculateStructureSize();
        }
    }
}

首先可以看见我们的DiamondHouseStructure继承了Structure<NoFeatureConfig>,这里的NoFeatureConfig表明了我们的结构是不需要配置文件的。

其中canBeGenerated代表了结构会生成的可能性,这里我们设置为3%。getStructureName代表了结构的名字,getSize具体作用不明,大部分原版结构值都为3。

接下来是Start类。

public static class Start extends StructureStart {

  public Start(Structure<?> structure, int chunkPosX, int chunkPosZ, MutableBoundingBox bounds, int references, long seed) {
    super(structure, chunkPosX, chunkPosZ, bounds, references, seed);
  }

  @Override
  public void init(ChunkGenerator<?> generator, TemplateManager templateManagerIn, int chunkX, int chunkZ, Biome biomeIn) {
    DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
    this.components.add(diamondHouseStructurePiece);
    this.recalculateStructureSize();
  }
} 

这个类的init方法就是你添加StructurePiece(结构组件)的地方,所谓的StructurePiece就是一个结构的组成部分,一个村庄可以有不同的房子组成,每一个房子都是村庄这个结构的StructurePiece。当然我们在getStartFactory这个方法里,返回了构造这个类的方法。

接下来我们来看init方法。

init方法里最要的是这两句话:

DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
this.components.add(diamondHouseStructurePiece);

首先我们创建了一个自定义的StructurePiece,然后将它添加到了Structure自带的components中,也就是给我们的结构添加了一个结构组件。

最后的recalculateStructureSize,用于重新计算结构的边界大小,需要填写。

接下来我们来看看DiamondHouseStructurePiece具体的内容。

public class DiamondHouseStructurePiece extends ScatteredStructurePiece {
    private static final DiamondHouseStructurePiece.Selector BUILD_STONE_SELECTOR = new DiamondHouseStructurePiece.Selector();

    protected DiamondHouseStructurePiece(Random random, int x, int z) {
        super(CommonEventHandler.diamondHouseStructurePieceType, random, x, 64, z, 12, 10, 15);
    }

    protected DiamondHouseStructurePiece(TemplateManager templateManager, CompoundNBT nbt) {
        super(CommonEventHandler.diamondHouseStructurePieceType, nbt);
    }

    @Override
    public boolean create(IWorld worldIn, ChunkGenerator<?> chunkGeneratorIn, Random randomIn, MutableBoundingBox mutableBoundingBoxIn, ChunkPos chunkPosIn) {
        this.fillWithRandomizedBlocks(worldIn, mutableBoundingBoxIn, 0, 0, 0, 4, 4, 4, false, randomIn, BUILD_STONE_SELECTOR);
        this.fillWithAir(worldIn, mutableBoundingBoxIn, 1, 1, 1, 3, 3, 3);
        this.setBlockState(worldIn, Blocks.ACACIA_TRAPDOOR.getDefaultState().rotate(Rotation.CLOCKWISE_90), 2, 2, 0, mutableBoundingBoxIn);
        this.fillWithAir(worldIn, mutableBoundingBoxIn, 2, 1, 0, 2, 1, 0);
        return true;
    }

    static class Selector extends StructurePiece.BlockSelector {
        private Selector() {
        }

        public void selectBlocks(Random rand, int x, int y, int z, boolean wall) {
            this.blockstate = Blocks.DIAMOND_BLOCK.getDefaultState();
        }
    }
}

可以看到DiamondHouseStructurePiece继承了ScatteredStructurePiece类,这个类是StructurePiece子类,StructurePiece还有其他的子类,大家可以按需选用,比如其中的TemplateStructurePiece就可以让你从指定的NBT文件中加载模型。

我们回到我们的类,可以看到在构造方法里有一个CommonEventHandler.diamondHouseStructurePieceType,这个是我们之后需要注册的内容。

这里面最为重要的就是create,这里也是你「画」建筑的地方。StructurePiece提供了一系列的方法,来填充和绘制方块,大家自己看这些函数的签名就能知道其作用了。

这里还有一个内部类是Selector 它继承了StructurePiece.BlockSelector,它的让你可以随机的设置方块的种类的方块的状态,比如原版的丛林神庙,它的墙面的方块种类就是不唯一的,你可以通过这个类的selectBlocks实现同样的效果。

到此,我们的结构算是创建好了,接下来注册它

首先我们注册Structure

public class FeatureRegistry {
    public static final DeferredRegister<Feature<?>> FEATURES = new DeferredRegister<>(ForgeRegistries.FEATURES, "neutrino");
    public static RegistryObject<Structure<NoFeatureConfig>> obsidianBlock = FEATURES.register("house", () -> {
        return new DiamondHouseStructure(Dynamic -> {
            return NoFeatureConfig.deserialize(Dynamic);
        });
    });
}

这里和物品、方块的注册没什么太大的区别,值得注意的是,因为Structure其实只是一种特殊的Feature,所以我们填的是DeferredRegister<Feature<?>>

然后又因为我们的DiamondHouseStructure是没有配置文件的,所以传入了一个NoFeatureConfig.deserialize

同样的,别忘了在你的Mod主类中将FEATURES注册到Mod总线中。

接下来我们看StructurePiece的注册。

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class CommonEventHandler {
    public static IStructurePieceType diamondHouseStructurePieceType;
    @SubscribeEvent
    public static void onCommonSetup(FMLCommonSetupEvent event) {
        diamondHouseStructurePieceType = Registry.register(Registry.STRUCTURE_PIECE, "house", (templateManager, nbt) -> {
            return new DiamondHouseStructurePiece(templateManager, nbt);
        });
        for (Biome biome : ForgeRegistries.BIOMES) {
            biome.addStructure(FeatureRegistry.obsidianBlock.get().withConfiguration(IFeatureConfig.NO_FEATURE_CONFIG));
            biome.addFeature(GenerationStage.Decoration.SURFACE_STRUCTURES, FeatureRegistry.obsidianBlock.get().withConfiguration(IFeatureConfig.NO_FEATURE_CONFIG).withPlacement(Placement.NOPE.configure(IPlacementConfig.NO_PLACEMENT_CONFIG)));
        }
    }
}

这里也很简单,首先我们在外边创建一个一个变量,注意类型是IStructurePieceType,然后在FMLCommonSetupEvent事件中调用Registry.register方法注册,因为我们要注册的是StructurePiece,所以第一个参数填入的是Registry.STRUCTURE_PIECE

然后就和矿物生成的步骤类似添加结构。但是这里请注意,我们需要调用两个方法首先我们得调用addStructure添加结构,然后调用addFeature让这个结构可以生成,因为我们的StructureNoFeatureConfig的,所以withConfigurationwithPlacement都填入相对应的NoFeatureConfig就行。

到此,结构已经注册和添加完毕,打开游戏,新建一个存档看看,你应该就能发现世界中自然生成的钻石小屋了。

image-20200512174944649

源代码

编程小课堂

当你不知道应该如何使用一个类时,可以去Github上搜索看看如何使用,一般情况下都能找到别人使用的例子。