User Tools

Site Tools


zh_cn:tutorial:trees

This is an old revision of the document!


添加树木 [1.17](高级)

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

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

创建简单的树木

结构

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

  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 然后用 FabricAPI 修改生物群系:

@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

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

我们准备创建一个调用器(invoker)(见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)是其自己的话题(topic),这里我们只需要使用 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 类似,FabricAPI 不提供创建 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 的获取器(getter)。
  • generate 方法,该方法创建树叶。
  • getRandomHeight 方法。不管名字是什么,你通常应该返回你的树叶的最大高度。
  • isInvalidForLeaves 方法,可以为放置树叶的地方设置限制。

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

public class RichFoliagePlacer extends FoliagePlacer {
    // 对于foliageHeight我们使用由 IntProvider.createValidatingCodec 生成的编码解码器
    // 方法参数,我们传入 IntProvider 的最小值和最大值
    // 为了向你的 TrunkPlacer/FoliagePlacer/TreeDecorator 等添加多个域(fields),可调用多次.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

该过程几乎相同,只需要使用你的调用器(invoker)创建并注册 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 本质上是用于树木的一个后处理器(post-processer),用于修饰树木的额外信息。

原版的 TreeDecorators

原版的 TreeDecorator 几乎是没办法复用的,除了 LeavesVineTreeDecoratorTrunkVineTreeDecorator

虽然这是一件非常繁琐的事情,但是你还是需要创建你自己的 TreeDecorator

创建一个 TreeDecoratorType

一个 TreeDecoratorType 是需要注册到你的 TreeDecorator 之中的。

FabricAPI 没有提供任何工具用于创建 TreeDecoratorType, 所以我们需要再次使用 mixin 了。

我们的 mixin 大概会看起来非常像是以下内容,同时不要忘记把他们添加到你自己的 mixin 配置文件当中:

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

创建 TreeDecorator

TreeDecorator 有一个特别简单的结构:

  • 一个可用于序列化的编码解码器。但默认情况下为空,因为构造函数是没有参数的。如果需要,你可以随时扩展(expand)它。
  • 你的 TreeDecoratorType 的获取器(getter)。
  • 为修饰树而存在的 generate 方法。

我们的 TreeDecorator 将在树干周围以 25% 的几率在树干的一侧产生金块(简直不要太爽太炫酷对不对):

public class RichTreeDecorator extends TreeDecorator {
    public static final RichTreeDecorator INSTANCE = new RichTreeDecorator();
    // 我们的构造函数没有任何参数,所以我们创建一个单元编解码器,让他返回一个单例对象。
    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) {
        // 遍历方块位置
        for (BlockPos logPosition : logPositions) {
            // 选择一个从 0(含)到 4(不含)的值,如果是 0,则继续
            // 这是一个让树生成金块从而让我们走向富裕的机会,太爽了。
            if (random.nextInt(4) == 0) {
                // 选择一个从 0 到 4 的随机值,并使用它确定将放置金块到树的一侧
                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");
                };
 
                // 通过结果边偏移树木位置
                BlockPos targetPosition = logPosition.offset(side, 1);
 
                // 使用 BiConsumer replacer 放置金块!
                // 这是在 TrunkPlacers、FoliagePlacers 和 TreeDecorators 中放置方块的标准方法。
                replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState());
            }
        }
    }
}

注册和使用你的 TreeDecorator

首先,使用调用器(invoker)创建你的 TreeDecoratorType

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

然后,在创建你的 TreeFeatureConfig.Builderbuild 方法之间调用这个。

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

创建一个高级的 SaplingGenerator

所以,还记得我告诉过你 SaplingGenerator 实际上可以包含更复杂的逻辑吗? 这是一个例子 - 我们这次来创建几棵原版的树木而不是实际的树:

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);
 
        // 每棵树都有 10% 的几率
        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;
        }
 
        // 如果这些都没有发生(随机值在 70 到 100 之间),则创建实际的树
        return feature;
    }
}

其实这没啥练手的,但是他给你展示了 SaplingGenerator 可以有更复杂的逻辑。

给你的树整点额外逻辑!

使用额外的 TreeFeatureConfig.Builder 方法,你可以给你的树添加更多的设定:

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.1627209344.txt.gz · Last modified: 2021/07/25 10:35 by breakice