本篇教程的视频

本篇教程的源代码

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
9
10
11
private static TreeConfiguration.TreeConfigurationBuilder createStraightBlobTree(
Block logBlock, Block leavesBlock, int baseHeight, int heightRandA, int heightRandB, int radius
) {
return new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(logBlock),
new StraightTrunkPlacer(baseHeight, heightRandA, heightRandB),
BlockStateProvider.simple(leavesBlock),
new BlobFoliagePlacer(ConstantInt.of(radius), ConstantInt.of(0), 3),
new TwoLayersFeatureSize(1, 0, 1)
);
}

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

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

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

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

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

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

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

1
2
3
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(BootstrapContext<ConfiguredFeature<?, ?>> pContext) {
...
}

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FeatureUtils.register(
context,
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方法,这个方法的意思是忽略藤蔓,也就是生成树的时候,不会生成藤蔓

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

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

树苗生成器

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class TreeGrower {
...
public static final TreeGrower OAK = new TreeGrower(
"oak",
0.1F,
Optional.empty(),
Optional.empty(),
Optional.of(TreeFeatures.OAK),
Optional.of(TreeFeatures.FANCY_OAK),
Optional.of(TreeFeatures.OAK_BEES_005),
Optional.of(TreeFeatures.FANCY_OAK_BEES_005)
);
}

我们可以看到橡树有很多变种,所以它这里就取随机来选择橡树的构造特征

注册树

编写构造特征

这里我们创建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());

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

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

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

树苗生成器

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

1
2
3
4
public class ModTreeGrower {
public static final TreeGrower BLOODWOOD = new TreeGrower(TutorialMod.MOD_ID + ":ice_ether_tree",
Optional.empty(), Optional.of(ModConfiguredFeatures.ICE_ETHER_TREE_KEY), Optional.empty());
}

注册树苗方块

1
2
public static final DeferredBlock<SaplingBlock> ICE_ETHER_TREE_SAPLING = registerBlock("ice_ether_tree_sapling",
() -> new SaplingBlock(ModTreeGrower.ICE_ETHER_TREE, Block.Properties.ofFullCopy(Blocks.OAK_SAPLING)));

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

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

加入物品栏

1
output.accept(ModBlocks.ICE_ETHER_TREE_SAPLING);

最后别忘了加入物品栏

数据生成

世界生成类

这里我们创建一个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(packOutput, 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
blockItem(ModBlocks.ICE_ETHER_TREE_SAPLING.get());

private <T extends Block> ItemModelBuilder blockItem(DeferredBlock<T> 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文件夹中

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