本篇教程的视频

本篇教程的源代码

GitHub地址:

查看源代码

那么本期教程我们来写树,后面一期的话就是树的世界生成

树的话,在游戏里面属于地物(Feature)的一种,是一种多方块结构,这也就意味着,它有自己的构造特征

原版的树构造特征类在TreeFeatures这个类中,后面我们还会接触TreePlacements这个类

前者是我们这期教程需要理解的构造特征类,后面是涉及其世界生成用的放置特征类

TreeFeatures类似,原版其他地物也有各种各样的构造特征类,感兴趣的同学可以自行研究

构造特征类

我们直接来看源代码吧

1
public static final ResourceKey<ConfiguredFeature<?, ?>> OAK = FeatureUtils.createKey("oak");

这里我们瞅瞅橡木的,可以看到,这里注册了一个构造特征的注册键

FeatureUtils.createKey方法是一个公共的方法,我们自己写的时候也可以直接用,但要加上模组的命名空间

我们继续往下看

1
2
3
4
5
6
7
8
private static TreeConfiguration.TreeConfigurationBuilder createStraightBlobTree(Block pLogBlock, Block pLeavesBlock, int pBaseHeight, int pHeightRandA, int pHeightRandB, int pRadius) {
return new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(pLogBlock),
new StraightTrunkPlacer(pBaseHeight, pHeightRandA, pHeightRandB),
BlockStateProvider.simple(pLeavesBlock),
new BlobFoliagePlacer(ConstantInt.of(pRadius), ConstantInt.of(0), 3),
new TwoLayersFeatureSize(1, 0, 1));
}

这里我们看到了一个createStraightBlobTree方法,这个方法返回一个TreeFeatureConfig.Builder对象

这个其实就是树它的构造特征的生成器,不过它是只用于最简单的直树干的,而像樱花树那样的,也有它自己的生成器

我们先来看看builder方法中的返回值

BlockStateProvider.simple需要传入一个方块,上面第一个传入树干方块,下面第二个传入树叶方块

StraightTrunkPlacer是树干生成器,它需要传入树干的高度,以及两个随机高度,两个随机高度将决定树的最终高度

BlobFoliagePlacer是树叶生成器,它的第一个参数是树叶的半径,第二个参数是树叶的偏移量,第三个参数是树叶的生成高度

TwoLayersFeatureSize是配置树的层级结构,它的第一个参数是树底部的高度,一般指树干下方的叶子层数
第二个参数是树顶高度,一般指树干上方叶子层数,第三个是树顶高度的随机增量

1
2
3
4
5
private static TreeConfiguration.TreeConfigurationBuilder createOak() {
return createStraightBlobTree(
Blocks.OAK_LOG, Blocks.OAK_LEAVES, 4, 2, 0, 2)
.ignoreVines();
}

再往下看,我们就能看到一个oak方法,这个是橡树的构造生成方法,调用了上面的createStraightBlobTree方法

1
2
3
public static void bootstrap(BootstapContext<ConfiguredFeature<?, ?>> pContext) {
...
}

接下来我们能看到一个bootstrap方法,这个东西呢,在我们之后的开发过程中,你就将其理解为用于数据生成的方法,后面的数据生成我们也会用到

在这个方法中,我们也可以找到橡木相关的内容

1
FeatureUtils.register(pContext, OAK, Feature.TREE, createOak().build());

不过也就这一句话,因为它的构造方法已经在上面写了

而其他的树,其实也都类似

1
2
3
4
5
6
7
8
FeatureUtils.register(pContext, DARK_OAK, Feature.TREE, 
(new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(Blocks.DARK_OAK_LOG),
new DarkOakTrunkPlacer(6, 2, 1),
BlockStateProvider.simple(Blocks.DARK_OAK_LEAVES),
new DarkOakFoliagePlacer(ConstantInt.of(0), ConstantInt.of(0)),
new ThreeLayersFeatureSize(1, 1, 0, 1, 2, OptionalInt.empty())))
.ignoreVines().build());

比如这个深色橡木,它的生成器写法和橡木其实差不多

它这里还有ignoreVines方法,这个方法的意思是忽略藤蔓,也就是生成树的时候,不会生成藤蔓

树的构造特征就这样差不多了,当然这里我们只看来最基本的橡树的构造特征,光是橡树也有很多变种,比如珍异橡木大型珍异橡木覆藤橡木等等,还有带蜂巢

不过呢,还是得从最基本的开始了解,后面的再慢慢研究

树苗生成器

我们还要来看树苗的生成器,让我们的树从树苗长成一个多方块结构的树

这里我们看看OakTreeGrower这个类,这个橡树树苗的生成类

1
2
3
4
5
6
7
8
9
10
public class OakTreeGrower extends AbstractTreeGrower {

protected ResourceKey<ConfiguredFeature<?, ?>> getConfiguredFeature(RandomSource p_256119_, boolean p_256536_) {
if (p_256119_.nextInt(10) == 0) {
return p_256536_ ? TreeFeatures.FANCY_OAK_BEES_005 : TreeFeatures.FANCY_OAK;
} else {
return p_256536_ ? TreeFeatures.OAK_BEES_005 : TreeFeatures.OAK;
}
}
}

这个类是继承AbstractTreeGrower,重写了getConfiguredFeature方法

当然,上面也说过了,橡树有很多变种,所以它这里就取随机来选择橡树的构造特征

如果我们没那么复杂的话,就直接返回构造特征的注册键就行

注册树

编写构造特征

这里我们创建ModConfiguredFeatures类,用于定义地物的构造特征,当然,你也可以像原版那样按照不同种类分开来

1
2
3
public class ModConfiguredFeatures {

}

这里我们先来写注册键

1
2
3
4
5
public static final ResourceKey<ConfiguredFeature<?, ?>> ICE_ETHER_TREE_KEY = createKey("ice_ether_tree");

public static ResourceKey<ConfiguredFeature<?, ?>> createKey(String id) {
return ResourceKey.create(Registries.CONFIGURED_FEATURE, ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, id));
}

注册键的注册方法用的是FeatureUtils.createKey,不过我们加上了模组的命名空间

接下来我们创建bootstrap方法,用于注册构造特征,也用于后面的数据生成

1
2
3
public static void bootstrap(BootstapContext<ConfiguredFeature<?, ?>> context) {

}

bootstrap方法需要传入一个BootstapContext<ConfiguredFeature<?, ?>>对象,这个对象是用于注册构造特征的,这个与原版是一样的

接下来就是写树的构造特征

1
2
3
4
5
6
7
8
FeatureUtils.register(context, ICE_ETHER_TREE_KEY, Feature.TREE,
new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(ModBlocks.ICE_ETHER_LOG.get()),
new StraightTrunkPlacer(4, 2, 1),
BlockStateProvider.simple(ModBlocks.ICE_ETHER_LEAVES.get()),
new BlobFoliagePlacer(ConstantInt.of(3), ConstantInt.of(2), 3),
new TwoLayersFeatureSize(1, 0, 2)
).build());

注册键传入我们上面写的注册键,原木和树叶都是我们在之前的教程中注册好的

另外的树干树叶层级构造特征我们就不一一解释了,上面都说过了

其实,树干、树叶以及层级的构造器都是可以自己自定义的,你可以参考参考相关的模组

树苗生成器

接下来我们创建IceEtherTreeGrower类,用于定义树苗的生成器

1
2
3
4
5
6
public class IceEtherTreeGrower extends AbstractTreeGrower {
@Override
protected @Nullable ResourceKey<ConfiguredFeature<?, ?>> getConfiguredFeature(RandomSource pRandom, boolean pHasFlowers) {
return ModConfiguredFeatures.ICE_ETHER_TREE_KEY;
}
}

继承AbstractTreeGrower类,并重写getConfiguredFeature方法,这里我们没有那么多变种,所以就直接返回树的构造特征注册键

注册树苗方块

1
2
public static final RegistryObject<Block> ICE_ETHER_TREE_SAPLING = registerBlocks("ice_ether_tree_sapling",
() -> new SaplingBlock(new IceEtherTreeGrower(), BlockBehaviour.Properties.copy(Blocks.OAK_SAPLING)));

那么树苗它还是一个方块,所以我们还得来注册一下

这里实例化的是SaplingBlock,第一个参数是对应的树苗生成器,传入我们上面写的类即可

加入物品栏

1
pOutput.accept(ModBlocks.ICE_ETHER_TREE_SAPLING.get());

最后别忘了加入物品栏

数据生成

世界生成类

这里我们创建一个ModWorldGenProvider,并继承DatapackBuiltinEntriesProvider

1
2
3
4
5
6
7
8
9
public class ModWorldGenProvider extends DatapackBuiltinEntriesProvider {

public static final RegistrySetBuilder BUILDER = new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, ModConfiguredFeatures::bootstrap);

public ModWorldGenProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries, BUILDER, Set.of(TutorialMod.MOD_ID));
}
}

这个数据生成类与我们之前写的都不一样,它继承的是DatapackBuiltinEntriesProvider,这是Forge提供的一个动态数据生成类,它允许我们在数据生成的时候动态的注册数据

这里我们需要创建一个RegistrySetBuilder类型的BUILDER变量,实例化RegistrySetBuilder,添加ModConfiguredFeatures中的bootstrap方法

ModDataGenerator

接下来我们到模组的数据生成主类中,将我们写的世界生成类注册进去

gatherData方法中添加

1
generator.addProvider(event.includeServer(), new ModWorldGenProvider(output, lookupProvider));

世界生成相关的内容也是属于服务端的数据,所以这里用的也是includeServer

语言文件

1
add(ModBlocks.ICE_ETHER_TREE_SAPLING.get(), "Ice Ether Sapling");

模型文件

树苗的模型文件生成方法与其他的方块有一点区别

1
2
simpleBlock(ModBlocks.ICE_ETHER_TREE_SAPLING.get(), 
models().cross("ice_ether_tree_sapling", modLoc("block/ice_ether_tree_sapling")).renderType("cutout"));

它用的是cross方法

还有物品的模型文件

1
2
3
4
5
6
7
simpleBlockItem(ModBlocks.ICE_ETHER_TREE_SAPLING.get().asItem());

private ItemModelBuilder simpleBlockItem(RegistryObject<Block> item) {
return withExistingParent(item.getId().getPath(),
mcLoc("item/generated")).texture("layer0",
ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID,"block/" + item.getId().getPath()));
}

战利品列表

我们可以来改一下之前树叶的战利品列表

1
2
3
add(ModBlocks.ICE_ETHER_LEAVES.get(), 
createLeavesDrops(ModBlocks.ICE_ETHER_LEAVES.get(), ModBlocks.ICE_ETHER_TREE_SAPLING.get(), NORMAL_LEAVES_SAPLING_CHANCES));
dropSelf(ModBlocks.ICE_ETHER_TREE_SAPLING.get());

顺便再加一下树苗的战利品列表

测试

那么,放好树苗的贴图文件之后,数据生成也要运行一下

数据生成会生成树的构造特征的json文件,在数据文件夹中的worldgen/configured_features文件夹中

最后我们就可以进入游戏进行测试了