树苗生长系统#
从一棵小小的树苗到遮天蔽日的巨型丛林树,Minecraft 的树苗生长系统由两阶段状态机、随机 tick、骨粉催熟、2x2 巨型树检测、蜂巢概率、红树悬挂苗等多重机制组成。本文从
SaplingBlock到SaplingGenerator逐层剖析。
一、继承体系#
Block
└── PlantBlock 植物基类(检查下方是否是泥土/耕地,不能放置就掉落)
│
├── SaplingBlock 普通树苗(实现 Fertilizable 接口,可骨粉催熟)
│ └── PropaguleBlock 红树胎生苗(还实现 Waterloggable,支持悬挂生长)
│
└── AzaleaBlock 杜鹃花丛(独立实现 Fertilizable,没有 STAGE 两阶段)Fertilizable 接口是骨粉催熟的基础:
isFertilizable()— 还能生长吗(如树苗永远 true,小麦到最大就 false)canGrow()— 这次骨粉成功的概率(树苗 45%)grow()— 真正执行生长getFertilizableType()— 默认GROWER(自身变大),苔藓等是NEIGHBOR_SPREADER
二、普通树苗:两阶段状态机#
SaplingBlock 有一个 STAGE 属性(IntProperty,值为 0 或 1),这构成了一个简单的状态机:
STAGE=0 ──(触发)──→ STAGE=1 ──(触发)──→ 长成树(移除树苗,调用 Generator)
小树苗 准备阶段 真正生成世界结构触发来源 1:随机 tick#
@Override
protected void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
if (world.getLightLevel(pos.up()) >= 9 // 头顶光照 ≥ 9
&& random.nextInt(7) == 0) { // 1/7 概率(平均 ~476 秒一次)
this.generate(world, pos, state, random);
}
}触发来源 2:骨粉#
// Fertilizable 接口实现
public boolean isFertilizable(WorldView w, BlockPos p, BlockState s) { return true; }
public boolean canGrow(World w, Random r, BlockPos p, BlockState s) {
return w.random.nextFloat() < 0.45; // 45% 成功概率
}
public void grow(ServerWorld w, Random r, BlockPos p, BlockState s) {
this.generate(w, p, s, r);
}generate():两阶段切换核心#
public void generate(ServerWorld world, BlockPos pos, BlockState state, Random random) {
if (state.get(STAGE) == 0) {
// 阶段 0 → 阶段 1:只是改 BlockState,不生成树
// 这一步让"一次骨粉直接长出一棵树"变成了概率问题
world.setBlockState(pos, state.cycle(STAGE), SKIP_REDRAW_AND_BLOCK_ENTITY_REPLACED_CALLBACK);
} else {
// 阶段 1 → 树:调用 SaplingGenerator.generate 生成世界结构
this.generator.generate(world,
world.getChunkManager().getChunkGenerator(),
pos, state, random);
}
}为什么两阶段? 如果没有 STAGE=0→1 的过渡,每次骨粉直接长树,那"树的大小(普通 vs 大型橡树)“无法与触发次数解耦。两阶段设计让骨粉的使用次数和树类型概率互相独立。
三、SaplingGenerator:树结构生成#
每种树对应一个 SaplingGenerator 实例。它持有:
regularVariant:普通树配置rareRegularVariant:稀有变种树(有rareChance概率选中)megaVariant:2x2 巨型树rareMegaVariant:稀有巨型树beesVariant/rareBeesVariant:附近有花时可能生成带蜂巢的版本
以橡树为例:
SaplingGenerator OAK = new SaplingGenerator(
"oak",
0.1F, // 10% 概率选中稀有变种
Optional.empty(), // 无巨型树(橡树没有 2x2 巨型)
Optional.empty(), // 无稀有巨型
Optional.of(TreeConfiguredFeatures.OAK), // 普通橡树
Optional.of(TreeConfiguredFeatures.FANCY_OAK), // 稀有:大型(弯)橡树
Optional.of(TreeConfiguredFeatures.OAK_BEES_005), // 蜜蜂橡树(5%概率有蜂巢)
Optional.of(TreeConfiguredFeatures.FANCY_OAK_BEES_005)
);特征选择逻辑#
// 普通小树特征(用于 1x1 树苗)
private RegistryKey<ConfiguredFeature<?,?>> getSmallTreeFeature(Random r, boolean flowers) {
if (r.nextFloat() < rareChance) {
// 命中稀有概率
if (flowers && rareBeesVariant.isPresent()) return rareBeesVariant;
if (rareRegularVariant.isPresent()) return rareRegularVariant;
}
// 普通概率:有花 + 有蜜蜂变种 → 蜜蜂橡树;否则普通
return flowers && beesVariant.isPresent() ? beesVariant : regularVariant;
}
// 附近有花吗?搜索 pos.down().north(2).west(2) 到 pos.up().south(2).east(2)
private boolean areFlowersNearby(WorldAccess w, BlockPos p) {
for (BlockPos n : BlockPos.iterate(p.down().north(2).west(2), p.up().south(2).east(2)))
if (w.getBlockState(n).isIn(BlockTags.FLOWERS)) return true;
return false;
}树生成主流程:generate()#
// 第一步:尝试 2x2 巨型树
if (megaVariant != null) {
// 检查 4 种"左上角偏移"对齐方式(0,0)(0,-1)(-1,0)(-1,-1)
for (int i = 0; i >= -1; i--) {
for (int j = 0; j >= -1; j--) {
if (canGenerateLargeTree(state, world, pos, i, j)) {
// 2x2 树苗确认无误,开始生成
// 1. 先把 4 个树苗全部设为 AIR
setBlock(add(i,0,j), AIR, ...)
setBlock(add(i+1,0,j), AIR, ...)
setBlock(add(i,0,j+1), AIR, ...)
setBlock(add(i+1,0,j+1), AIR, ...)
// 2. 调用 ConfiguredFeature.generate(左上角)
if (configuredFeature.generate(world, chunkGen, rand, pos.add(i,0,j)))
return true; // 成功
// 3. 失败回滚:恢复 4 棵树苗
setBlock(..., state) × 4
return false;
}
}
}
}
// 第二步:尝试 1x1 普通树
feature = getSmallTreeFeature(rand, areFlowersNearby(world, pos));
if (feature == null) return false;
// 同样先把自己去掉,腾出位置
BlockState fluidState = world.getFluidState(pos).getBlockState();
world.setBlockState(pos, fluidState, SKIP_REDRAW_AND_BLOCK_ENTITY_REPLACED_CALLBACK);
if (configuredFeature.generate(world, chunkGen, rand, pos)) {
// 生成成功后如果位置还没被树覆盖,通知邻居
if (world.getBlockState(pos) == fluidState)
world.updateListeners(pos, state, fluidState, NOTIFY_LISTENERS);
return true;
} else {
// 失败:恢复树苗
world.setBlockState(pos, state, SKIP_REDRAW_AND_BLOCK_ENTITY_REPLACED_CALLBACK);
return false;
}2x2 巨型树的对齐检测#
为什么有 4 种偏移?因为当玩家种下 2x2 树苗时,触发点可能是 4 个位置中的任意一个,所以要尝试全部对齐:
j=-1 → 列向左对齐
i=-1 → 行向上对齐
位置 = (pos.x + i, pos.z + j) 作为 2x2 的左上角canGenerateLargeTree(state, world, pos, i, j) 检查 (x+i,y,z+j), (x+i+1,y,z+j), (x+i,y,z+j+1), (x+i+1,y,z+j+1) 这四个位置的方块是否都与 state 同种。
四、各类树苗的 Generator 特性#
| 树苗 | Generator | 稀有概率 | 2x2 巨型 | 蜜蜂变种 |
|---|---|---|---|---|
| 橡树 | OAK | 10% → Fancy 大型橡树 | ❌ | ✅ 5%蜂巢 |
| 云杉 | SPRUCE | 50% → Mega Pine(巨型松树) | ✅ Mega Spruce / Mega Pine | ❌ |
| 白桦 | BIRCH | — | ❌ | ✅ |
| 丛林 | JUNGLE | — | ✅ Mega Jungle Tree | ❌ |
| 金合欢 | ACACIA | — | ❌ | ❌ |
| 深色橡 | DARK_OAK | — | ✅(必须 2x2 才能长) | ❌ |
| 红树 | MANGROVE | 85% → Tall Mangrove | ❌ | ❌ |
| 樱花 | CHERRY | — | ❌ | ✅ |
| 杜鹃 | AZALEA | — | ❌ | ❌ |
| 苍白橡 | PALE_OAK | — | ✅(必须 2x2) | ❌ |
为什么深色橡树 1x1 不长? 因为
DARK_OAK的regularVariant = Optional.empty(),没有小树配置,getSmallTreeFeature会返回 null,所以只能 2x2 生成巨型。
五、红树胎生苗:PropaguleBlock#
红树苗是最复杂的树苗,在 SaplingBlock 基础上又增加了 3 个属性:
AGE(0–4):悬挂时的长度阶段HANGING:是否处于悬挂状态(挂在红树叶下)WATERLOGGED:可含水(红树能在水下长出)
独特机制:悬挂生长#
红树胎生苗最初出现在红树林树叶下,是悬挂的:
▓▓▓▓ 红树叶
▒ HANGING=true
▒ AGE 从 0 开始
▒ 随机tick让 AGE 逐步增长
▒
▼ AGE=4 (完全长大) 后,若树叶消失 → 胎生苗掉下来
落在地上 → HANGING=false,此时开始普通的两阶段生长@Override
protected void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
if (!isHanging(state)) {
// 着地状态:1/7 概率长成树(同普通树苗)
if (random.nextInt(7) == 0) generate(world, pos, state, random);
} else {
// 悬挂状态:每次都长一岁,到 AGE=4 就停
if (!isFullyGrown(state)) {
world.setBlockState(pos, state.cycle(AGE), NOTIFY_LISTENERS);
}
}
}AGE 对应的碰撞箱高度(MIN_Y_BY_AGE):
AGE=0 → MIN_Y=13 (只有顶部 3 像素的小芽)
AGE=1 → MIN_Y=10
AGE=2 → MIN_Y=7
AGE=3 → MIN_Y=3
AGE=4 → MIN_Y=0 (完整长度)放置位置的不同#
// PropaguleBlock.canPlaceAt
protected boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) {
if (isHanging(state)) {
// 悬挂:上方必须是红树叶
return world.getBlockState(pos.up()).isOf(Blocks.MANGROVE_LEAVES);
} else {
return super.canPlaceAt(state, world, pos); // 继承 SaplingBlock.canPlaceAt
}
}
// 父类 PlantBlock.canPlaceAt 检查 canPlantOnTop:
// PropaguleBlock 重写为:泥土/耕地 或 粘土
protected boolean canPlantOnTop(BlockState floor, BlockView w, BlockPos p) {
return super.canPlantOnTop(floor, w, p) // 泥土类 / 耕地
|| floor.isOf(Blocks.CLAY); // 红树特有:粘土
}骨粉催熟:悬挂时增长长度,着地时长成树#
public void grow(ServerWorld world, Random r, BlockPos pos, BlockState state) {
if (isHanging(state) && !isFullyGrown(state)) {
world.setBlockState(pos, state.cycle(AGE), NOTIFY_LISTENERS);
} else {
super.grow(world, r, pos, state);
}
}六、杜鹃花丛:AzaleaBlock#
它不继承 SaplingBlock,但实现 Fertilizable,也能骨粉催熟。区别:
- 没有 STAGE 属性,一次骨粉就直接调用
SaplingGenerator.AZALEA.generate() - 上方有水(流体)时禁止生长:
public boolean isFertilizable(WorldView w, BlockPos p, BlockState s) { return w.getFluidState(p.up()).isEmpty(); // 上方不能有水 } - 也能种在粘土上(和红树苗一致)
- 外观是灌木而不是树苗
七、为什么树苗总是"随机消失”#
很多玩家有过"明明种了树苗,加载回来后没了"的经历。原因是 PlantBlock 的 canPlaceAt 检查:
// PlantBlock.getStateForNeighborUpdate
protected BlockState getStateForNeighborUpdate(...) {
return !state.canPlaceAt(world, pos) ? Blocks.AIR : super....;
}任何导致 canPlaceAt 返回 false 的变化(泥土被破坏、悬挂的树叶消失)都会让树苗直接变成空气。如果你在未加载的区块里把泥土挖掉,再回来加载区块时树苗就"消失"了。
八、性能与机制知识速记#
| 问题 | 答案 |
|---|---|
| 平均自然长树要多久? | 1/7 × ~68秒(区块随机tick周期)× 2 阶段 ≈ 2-3 分钟 |
| 1 堆骨粉平均能出几棵树? | 每堆骨粉 × 45% 成功率,阶段 0 → 1 → 树要成功两次,期望 2/0.45 ≈ 4.4 堆骨粉/棵 |
| 为什么蜜蜂树偶尔出现? | 附近有花的位置种植,areFlowersNearby 返回 true,选 bees 配置 |
| 深色橡树 1x1 为什么不长? | regularVariant 为空,只能 2x2 |
| 红树胎生苗在水里能长? | 能,它实现 Waterloggable,也能种在粘土上 |
| 为什么粘液块农场里树苗长不出? | randomTick 只在加载的区块里发生,农场通常是未加载的 |