User Tools

Site Tools


zh_cn:tutorial:trees

This is an old revision of the document!


添加树木 [1.17](高级)

阅读本文之前,建议先学习如何创建一个特征地形。
参见features

树木是在你的mod中拓展原版世界生成的一个好方法。
注意本话题较为高级,因此开始之前,最好要有关于修改世界生成的丰富经验。

关于API

有个API可以让你非常方便的添加树木,但是他还在开发中,但在2000年之后,相信我们的天神modmuss50从苍穹之空下凡审查了pull request之后,就可以使用了

PR见此处。.

创建简单的树木

结构

原版的树的结构分为不同的种类,方便你做出复杂并且漂亮的树。
概览如下:

  1. TrunkPlacer:生成树干。
  2. FoliagePlacer:生成树叶。
  3. SaplingGenerator:根据周围环境,从树苗产生树的ConfiguredFeature
  4. TreeDecorator:可以用这个来为树生成额外的元素,如蜂箱、藤蔓(可选)。
  5. BlockStateProvider:根据周围环境返回方块。如果需要让树的一部分是A方块,另一部分是B方块,就可以使用这个函数。

如果想让树木长得不那么像是原版的树木,可以选择创建自定义的实现。但是实际上原版的实现通常足够模组的开发了。

创建ConfiguredFeature

不需要创建新的Feature,因为原版的TreeFeature可以配置。

把这个添加到你的ModInitializer'主体:

public static final ConfiguredFeature<?, ?> TREE_RICH = Feature.TREE
  // 使用builder配置特征地形
  .configure(new TreeFeatureConfig.Builder(
    new SimpleBlockStateProvider(Blocks.NETHERITE_BLOCK.getDefaultState()), // 树干方块提供器
    new StraightTrunkPlacer(8, 3, 0), // 放置竖直树干
    new SimpleBlockStateProvider(Blocks.DIAMOND_BLOCK.getDefaultState()), // 树叶方块提供器
    new SimpleBlockStateProvider(RICH_SAPLING.getDefaultState()), // 树苗提供器,用来决定树木可以生长在什么方块上
    new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3), // 生成水滴状的树叶(半径、相对于树干的偏移、高度)
    new TwoLayersFeatureSize(1, 0, 1) // 不同层的树木的宽度,用于查看树木在不卡到方块中可以有多高
  ).build())
  .spreadHorizontally()
  .applyChance(3); // 每个区块大约33%的概率生成(1/x)

现在只需要像往常那样向游戏注册ConfiguredFeature然后用Fabric的API修改生物群系:

@Override
public void onInitialize() {
  RegistryKey<ConfiguredFeature<?, ?>> treeRich = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, new Identifier("tutorial", "tree_rich"));
 
  Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, treeRich.getValue(), TREE_RICH);
 
  // 应该为树木使用VEGETAL_DECORATION生成步骤
  BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(), GenerationStep.Feature.VEGETAL_DECORATION, treeRich);
}

创建树苗

树苗是生长树木的一类特殊方块,需要SaplingGenerator

创建SaplingGenerator

简单的生成器接收树木的ConfiguredFeature并将其返回,如下所示:

public class RichSaplingGenerator extends SaplingGenerator {
  private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
 
  public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
    this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
  }
 
  @Nullable
  @Override
  protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) {
    return feature;
  }
}

后面会展示高级的SaplingGenerator的例子。

创建SaplingBlock

创建方块本身需要继承SaplingBlock类,而不是直接将其实例化,因为其构造器的访问权限是protected的。

public class RichSaplingBlock extends SaplingBlock {
  public RichSaplingBlock(SaplingGenerator generator, Settings settings) {
    super(generator, settings);
  }
}

注册SaplingBlock

要注册树苗,按照注册方块的以下步骤(参见blocks),但传入带有ConfiguredFeature的生成器的实例。

把这个放在用于你的树苗方块的类中:

public static final RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(TREE_RICH), FabricBlockSettings.copyOf(Blocks.OAK_SAPLING.getDefaultState()));
 
public static void register() {
  Registry.register(Registry.BLOCK, new Identifier("tutorial", "rich_sapling"), RICH_SAPLING);
  Registry.register(Registry.ITEM, new Identifier("tutorial", "rich_sapling"), new BlockItem(RICH_SAPLING, ItemGroup.MISC));
}

创建TrunkPlacer

TrunkPlacer创建由BlockStateProvider提供的树干方块。

原版TrunkPlacers

在创建TrunkPlacer之前,先看看可以从原版复用的原版TrunkPlacer避免做重复的工作:

  • StraightTrunkPlacer
  • ForkingTrunkPlacer
  • GiantTrunkPlacer
  • BendingTrunkPlacer

创建TrunkPlacerType

往游戏注册TrunkPlacer需要TrunkPlacerType

可惜Fabric API目前没有用于创建和注册TrunkPlacer的API,所以我们需要使用mixins。

我们准备创建一个调用器(见https://github.com/2xsaiko/mixin-cheatsheet/blob/master/invoker.md)来调用私有静态的TrunkPlacerType.register方法。

以下是我们的mixin,不要忘记加到mixin配置中:

@Mixin(TrunkPlacerType.class)
public interface TrunkPlacerTypeInvoker {
    @Invoker
    static <P extends TrunkPlacer> TrunkPlacerType<P> callRegister(String id, Codec<P> codec) {
        throw new IllegalStateException();
    }
}

创建TrunkPlacer

TrunkPlacer包含:

  • 用于序列化的编码解码器。编码解码器(codec)是其自己的话题,这里我们只需要使用fillTrunkPlacerFields方法来生成。
  • 获取器(getter),返回TrunkPlacerType
  • generate方法,该方法中放置树干并返回TreeNode列表,用于树叶放置器放置树木。

TrunkPlacer将在世界中创建两个对角线形的树干:

public class RichTrunkPlacer extends TrunkPlacer {
    // 使用fillTrunkPlacerFields来创建编码解码器
    public static final Codec<RichTrunkPlacer> CODEC = RecordCodecBuilder.create(instance -> 
        fillTrunkPlacerFields(instance).apply(instance, RichTrunkPlacer::new));
 
    public RichTrunkPlacer(int baseHeight, int firstRandomHeight, int secondRandomHeight) {
        super(baseHeight, firstRandomHeight, secondRandomHeight);
    }
 
    @Override
    protected TrunkPlacerType<?> getType() {
        return Tutorial.RICH_TRUNK_PLACER;
    }
 
    @Override
    public List<FoliagePlacer.TreeNode> generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, int height, BlockPos startPos, TreeFeatureConfig config) {
        // 将树干下的方块设为泥土
        this.setToDirt(world, replacer, random, startPos.down(), config);
 
        // 迭代到树干高度限制,并使用TrunkPlacer中的getAndSetState方法放置两个方块
        for (int i = 0; i < height; i++) {
            this.getAndSetState(world, replacer, random, startPos.up(i), config);
            this.getAndSetState(world, replacer, random, startPos.up(i).east().north(), config);
        }
 
        // 创建两个树木节点——一个用于第一个树干,另一个用于第二个
        // 将树干中最高的方块设为中心坐标给FoliagePlacer使用
        return ImmutableList.of(new FoliagePlacer.TreeNode(startPos.up(height), 0, false),
                                new FoliagePlacer.TreeNode(startPos.east().north().up(height), 0, false));
    }
}

注册并使用TrunkPlacer

使用你的调用器,为你的TrunkPlacer创建并注册TrunkPlacerType的实例。把这个放到你的ModInitializer主体中:

public static final TrunkPlacerType<RichTrunkPlacer> RICH_TRUNK_PLACER = TrunkPlacerTypeInvoker.callRegister("rich_trunk_placer", RichTrunkPlacer.CODEC);

现在将你的StraightTrunkPlacer替换为你刚创建的RichTrunkPlacer就好了:

[...]
new RichTrunkPlacer(8, 3, 0),
[...]

创建FoliagePlacer

FoliagePlacer会从由BlockStateProvider提供的方块创建树叶。

原版FoliagePlacer

创建FoliagePlacer之前,看看可直接使用的原版FoliagePlacer以避免另起炉灶:

  • BlobFoliagePlacer
  • BushFoliagePlacer
  • RandomSpreadFoliagePlacer

创建FoliagePlacerType

往游戏中注册FoliagePlacer需要FoliagePlacerType

TrunkPlacerType类似,Fabric API不提供创建FoliagePlacerType的实用功能。我们的mixin看上去几乎相同,不要忘记添加到你的mixin配置中!

@Mixin(FoliagePlacerType.class)
public interface FoliagePlacerTypeInvoker {
    @Invoker
    static <P extends FoliagePlacer> FoliagePlacerType<P> callRegister(String id, Codec<P> codec) {
        throw new IllegalStateException();
    }
}

创建FoliagePlacer

FoliagePlacerTrunkPlacer更加复杂一些,包括:

  • 用于序列化的编码解码器。在此例中我们展示了如何往编码解码器中添加一个额外的IntProvider。
  • 用于获取FoliagePlacerType的获取器。
  • generate方法,该方法创建树叶。
  • getRandomHeight方法。不管名字是什么,你通常应该返回你的树叶的最大高度。
  • isInvalidForLeaves方法,可以为放置树叶的地方设置限制。

我们的FoliagePlacer会往各个方向(东南西北)创建4行的树叶方块:

public class RichFoliagePlacer extends FoliagePlacer {
    // 对于foliageHeight我们使用由IntProvider.createValidatingCodec生成的编码解码器
    // 方法参数,我们传入IntProvider的最小值和最大值
    // 往你的TrunkPlacer/FoliagePlacer/TreeDecorator等添加多个域,可调用多次.and。
    //
    // 对于创建我们自己的编码解码器类型的例子,可以参考IntProvider.createValidatingCodec方法的源代码。
    public static final Codec<RichFoliagePlacer> CODEC = RecordCodecBuilder.create(instance ->
        fillFoliagePlacerFields(instance)
        .and(IntProvider.createValidatingCodec(1, 512).fieldOf("foliage_height").forGetter(RichFoliagePlacer::getFoliageHeight)
        .apply(instance, RichFoliagePlacer::new));
 
    private final IntProvider foliageHeight;
 
    public RichFoliagePlacer(IntProvider radius, IntProvider offset, IntProvider foliageHeight) {
        super(radius, offset);
 
        this.foliageHeight = foliageHeight;
    }
 
    public IntProvider getFoliageHeight() {
        return this.foliageHeight;
    }
 
    @Override
    protected FoliagePlacerType<?> getType() {
        return Tutorial.RICH_FOLIAGE_PLACER;
    }
 
    @Override
    protected void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, TreeFeatureConfig config, int trunkHeight, TreeNode treeNode, int foliageHeight, int radius, int offset) {
        BlockPos.Mutable center = treeNode.getCenter().mutableCopy();
 
        for (
            // 从X开始:中心 - 半径
            Vec3i vec = center.subtract(new Vec3i(radius, 0, 0));
            // 在X结束:中心 + 半径
            vec.compareTo(center.add(new Vec3i(radius, 0, 0))) == 0;
            // 每次移动1
            vec.add(1, 0, 0)) {
            this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
        }
 
        for (Vec3i vec = center.subtract(new Vec3i(0, radius, 0)); vec.compareTo(center.add(new Vec3i(0, radius, 0))) == 0; vec.add(0, 1, 0)) {
            this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
        }
    }
 
    @Override
    public int getRandomHeight(Random random, int trunkHeight, TreeFeatureConfig config) {
        // 使用IntProvider挑选随机高度
        return foliageHeight.get(random);
    }
 
    @Override
    protected boolean isInvalidForLeaves(Random random, int dx, int y, int dz, int radius, boolean giantTrunk) {
        // 我们的FoliagePlacer不为树叶设置限制
        return false;
    }
}

注册并使用你的FoliagePlacer

该过程几乎相同,只需要使用你的调用器创建并注册FoliagePlacerType

public static final FoliagePlacerType<RichFoliagePlacer> RICH_FOLIAGE_PLACER = FoliagePlacerTypeInvoker.callRegister("rich_foliage_placer", RichFoliagePlacer.CODEC);

并将旧的FoliagePlacer替换成你的新的:

[...]
new RichFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), ConstantIntProvider.create(3)),
[...]

创建一个 TreeDecorator

TreeDecorator 允许你添加额外的元素到你的树之中在执行你的 TrunkPlacerFoliagePlacer (比如苹果,蜂巢等) 之后。如果你有游戏后台开发经验的话,TreeDecorator 本质上是用于树木的一个后处理器,用于修饰树木的额外信息。

Vanilla TreeDecorators

Almost none vanilla TreeDecorators are reusable, except for LeavesVineTreeDecorator and TrunkVineTreeDecorator.

For anything non-trivial, you have to create your own TreeDecorators.

Creating a TreeDecoratorType

A TreeDecoratorType is required to register your TreeDecorator.

Fabric API doesn't provide utilities for creating TreeDecoratorTypes, so we have to use mixins again.

Our mixin will look almost exactly the same, don't forget to add it to your mixin config:

@Mixin(TreeDecoratorType.class)
public interface TreeDecoratorTypeInvoker {
    @Invoker
    static <P extends TreeDecorator> TreeDecoratorType<P> callRegister(String id, Codec<P> codec) {
        throw new IllegalStateException();
    }
}

Creating the TreeDecorator

A TreeDecorator has an extremely simple structure:

  • A codec for serialization, but it's empty by default because the constructor has no arguments. You can always expand it if you want
  • A getter for your TreeDecoratorType
  • The generate method to decorate the tree

Our TreeDecorator will spawn gold blocks around the trunk of our tree with a 25% chance on a random side of the trunk:

public class RichTreeDecorator extends TreeDecorator {
    public static final RichTreeDecorator INSTANCE = new RichTreeDecorator();
    // Our constructor doesn't have any arguments, so we create a unit codec that returns the singleton instance
    public static final Codec<RichTreeDecorator> CODEC = Codec.unit(() -> INSTANCE);
 
    @Override
    protected TreeDecoratorType<?> getType() {
        return Tutorial.RICH_TREE_DECORATOR;
    }
 
    @Override
    public void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, List<BlockPos> logPositions, List<BlockPos> leavesPositions) {
        // Iterate through block positions
        for (BlockPos logPosition : logPositions) {
            // Pick a value from 0 (inclusive) to 4 (exclusive) and if it's 0, continue
            // This is the chance for spawning the gold block
            if (random.nextInt(4) == 0) {
                // Pick a random value from 0 to 4 and determine the side where the gold block will be placed using it
                int sideRaw = random.nextInt(4);
                Direction side = switch (sideRaw) {
                    case 0 -> Direction.NORTH;
                    case 1 -> Direction.SOUTH;
                    case 2 -> Direction.EAST;
                    case 3 -> Direction.WEST;
                    default -> throw new ArithmeticException("The picked side value doesn't fit in the 0 to 4 bounds");
                };
 
                // Offset the log position by the resulting side
                BlockPos targetPosition = logPosition.offset(side, 1);
 
                // Place the gold block using the replacer BiConsumer
                // This is the standard way of placing blocks in TrunkPlacers, FoliagePlacers and TreeDecorators
                replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState());
            }
        }
    }
}

Registering and using your TreeDecorator

First, create your TreeDecoratorType using the invoker:

public static final TreeDecoratorType<RichTreeDecorator> RICH_TREE_DECORATOR = TreeDecoratorTypeInvoker.callRegister("rich_tree_decorator", RichTreeDecorator.CODEC);

Then, between the creation of your TreeFeatureConfig.Builder and the build method call, put this:

[...]
.decorators(Collections.singletonList(RichTreeDecorator.INSTANCE))
[...]

Creating an advanced SaplingGenerator

So, remember how I told you that SaplingGenerators can actually contain more complex logic? Here's an example of that - we create several vanilla trees instead of the actual trees depending on the chance:

public class RichSaplingGenerator extends SaplingGenerator {
    private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
 
    public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
        this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
    }
 
    @Nullable
    @Override
    protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) {
        int chance = random.nextInt(100);
 
        // Each tree has a 10% chance
        if (chance < 10) {
            return ConfiguredFeatures.OAK;
        } else if (chance < 20) {
            return ConfiguredFeatures.BIRCH;
        } else if (chance < 60) {
            return ConfiguredFeatures.SPRUCE;
        } else if (chance < 40) {
            return ConfiguredFeatures.MEGA_SPRUCE;
        } else if (chance < 50) {
            return ConfiguredFeatures.PINE;
        } else if (chance < 60) {
            return ConfiguredFeatures.MEGA_PINE;
        } else if (chance < 70) {
            return ConfiguredFeatures.MEGA_JUNGLE_TREE;
        }
 
        // If none of that happened (the random value was between 70 and 100), create the actual tree
        return feature;
    }
}

This isn't a very practical, but it shows what you can achieve using SaplingGenerators.

Extra settings for your tree

Using the extra TreeFeatureConfig.Builder methods, you can add more settings to your tree:

dirtProvider

Sets the BlockStateProvider for the block of dirt generated under the tree.

Example:

[...]
.dirtProvider(new SimpleBlockStateProvider(Blocks.IRON_BLOCK.getDefaultState()))
[...]

decorators

Used to add TreeDecorators to your tree. This was briefly showcased in the TreeDecorator section of this tutorial. If you want, you can add multiple TreeDecorators to the same tree using a convenience method like Arrays.asList.

Example:

[...]
.decorators(Arrays.asList(
    FirstTreeDecorator.INSTANCE,
    SecondTreeDecorator.INSTANCE,
    ThirdTreeDecorator.INSTANCE
))
[...]

ignoreVines

Makes the tree generation ignore vines stuck in the way.

Example:

[...]
.ignoreVines()
[...]

forceDirt

Forces the TreeFeature to generate the dirt underneath the tree.

Example:

[...]
.forceDirt()
[...]

Creating a BlockStateProvider

Coming soon.

zh_cn/tutorial/trees.1626282040.txt.gz · Last modified: 2021/07/14 17:00 by breakice