本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 搞清矿脉是怎么生成的:先铺岩石,再替换
  • 学会写 OreConfiguration 和 RuleTest
  • 学会给同一个矿石配三套维度规则
  • 学会用高度范围和次数控制分布

介绍

矿脉也是地物,所以它和树一样,走的是「构造特征 + 放置特征 + 群系注入」这条链

不过它比树简单:树要算树干、树叶、层级,矿只要「替换掉哪块石头」和「一个区块挖几次」

按区块生成

原版的矿脉是按区块算的

每个区块单独掷骰子,决定这一次生成多少个矿石、生成在什么高度

所以你在游戏里看到的矿脉,边界经常正好卡在区块边界上

像 GTNH 那种 3×3 区块一起算的,是改过生成逻辑的,我们这期不涉及

查看源代码

OreFeatures

原版的矿物构造特征都在 OreFeatures 里,看煤矿

1
2
3
RuleTest ruletest1 = new TagMatchTest(BlockTags.STONE_ORE_REPLACEABLES);
RuleTest ruletest2 = new TagMatchTest(BlockTags.DEEPSLATE_ORE_REPLACEABLES);
RuleTest ruletest3 = new BlockMatchTest(Blocks.NETHERRACK);

RuleTest 就是「能替换哪些方块」的规则,有两种

TagMatchTest:按标签匹配,比如「所有能被矿石替换的石头」

BlockMatchTest:按方块匹配,比如「只有末地石」

为什么要分这两种?因为主世界的石头有个「深板岩」变体,用标签能一把捞全;末地石只有一种,直接指定方块更省事

接着是组合

1
2
3
4
List<OreConfiguration.TargetBlockState> list5 = List.of(
OreConfiguration.target(ruletest1, Blocks.COAL_ORE.defaultBlockState()),
OreConfiguration.target(ruletest2, Blocks.DEEPSLATE_COAL_ORE.defaultBlockState())
);

target(规则, 方块状态),意思是「碰到符合规则的方块,把它换成这个」

所以主世界的矿要写两条:碰到普通石头换成煤矿,碰到深板岩换成深层煤矿

最后注册

1
FeatureUtils.register(context, ORE_COAL, Feature.ORE, new OreConfiguration(list5, 17));

OreConfiguration 三个参数:替换表、矿脉规模、丢弃概率(可选)

规模 17 是「一个矿脉最多连着几个矿石」

丢弃概率是给「埋藏矿」用的,值越大,矿石越不愿意暴露在空气里,避免悬空的方块

OrePlacements

1
2
3
4
PlacementUtils.register(context, ORE_COAL_UPPER, holder12,
commonOrePlacement(30, HeightRangePlacement.uniform(VerticalAnchor.absolute(136), VerticalAnchor.top())));
PlacementUtils.register(context, ORE_COAL_LOWER, holder13,
commonOrePlacement(20, HeightRangePlacement.triangle(VerticalAnchor.absolute(0), VerticalAnchor.absolute(192))));

commonOrePlacement(次数, 高度范围) 就是「每区块试几次 + 在哪个高度区间」

高度有两种分布

uniform:在区间里均匀随机,上下一样密

triangle:中间多、两头少,像座山

煤矿分成上下两段就是这原因:高处均匀铺一层,低处用梯形堆一团

commonOrePlacement 这个方法在原版里是 private 的,我们得自己抄一份

1
2
3
4
5
6
7
private static List<PlacementModifier> orePlacement(PlacementModifier frequency, PlacementModifier heightRange) {
return List.of(frequency, InSquarePlacement.spread(), heightRange, BiomeFilter.biome());
}

private static List<PlacementModifier> commonOrePlacement(int count, PlacementModifier heightRange) {
return orePlacement(CountPlacement.of(count), heightRange);
}

四条:次数、区块内随机撒点、高度区间、群系过滤

动手写

构造特征

回到 ModConfiguredFeatures,先加三个注册键

1
2
3
public static final ResourceKey<ConfiguredFeature<?, ?>> ICE_ETHER_ORE = createKey("ice_ether_ore");
public static final ResourceKey<ConfiguredFeature<?, ?>> NETHER_ICE_ETHER_ORE = createKey("nether_ice_ether_ore");
public static final ResourceKey<ConfiguredFeature<?, ?>> END_ICE_ETHER_ORE = createKey("end_ice_ether_ore");

然后写替换规则

1
2
3
4
5
6
RuleTest stoneReplace = new TagMatchTest(BlockTags.STONE_ORE_REPLACEABLES);
RuleTest deepslateReplace = new TagMatchTest(BlockTags.DEEPSLATE_ORE_REPLACEABLES);
RuleTest netherReplace = new TagMatchTest(BlockTags.BASE_STONE_NETHER);
RuleTest endReplace = new BlockMatchTest(Blocks.END_STONE);

BlockState ore = ModBlocks.ICE_ETHER_ORE.defaultBlockState();

末地没有「可替换的末地石」这种标签,所以只能用 BlockMatchTest 指定末地石

然后三份替换表

1
2
3
4
5
6
7
List<OreConfiguration.TargetBlockState> overworldTargets = List.of(
OreConfiguration.target(stoneReplace, ore),
OreConfiguration.target(deepslateReplace, ore));
List<OreConfiguration.TargetBlockState> netherTargets = List.of(
OreConfiguration.target(netherReplace, ore));
List<OreConfiguration.TargetBlockState> endTargets = List.of(
OreConfiguration.target(endReplace, ore));

因为这里我们只有一个矿石方块,所以就直接共用了,大家根据自己的实际情况进行替换

注册

1
2
3
FeatureUtils.register(context, ICE_ETHER_ORE, Feature.ORE, new OreConfiguration(overworldTargets, 9));
FeatureUtils.register(context, NETHER_ICE_ETHER_ORE, Feature.ORE, new OreConfiguration(netherTargets, 12));
FeatureUtils.register(context, END_ICE_ETHER_ORE, Feature.ORE, new OreConfiguration(endTargets, 10));

矿脉规模大家也可以根据实际情况进行修改

放置特征

到 ModPlacedFeatures,先把那两个辅助方法抄进来(就是前面贴的那段)

然后是三个注册键

1
2
3
public static final ResourceKey<PlacedFeature> ICE_ETHER_ORE_PLACED = createKey("ice_ether_ore_placed");
public static final ResourceKey<PlacedFeature> NETHER_ICE_ETHER_ORE_PLACED = createKey("nether_ice_ether_ore_placed");
public static final ResourceKey<PlacedFeature> END_ICE_ETHER_ORE_PLACED = createKey("end_ice_ether_ore_placed");

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PlacementUtils.register(context, ICE_ETHER_ORE_PLACED,
configured.getOrThrow(ModConfiguredFeatures.ICE_ETHER_ORE),
commonOrePlacement(10, HeightRangePlacement.uniform(
VerticalAnchor.absolute(-64), VerticalAnchor.absolute(128))));

PlacementUtils.register(context, NETHER_ICE_ETHER_ORE_PLACED,
configured.getOrThrow(ModConfiguredFeatures.NETHER_ICE_ETHER_ORE),
commonOrePlacement(12, HeightRangePlacement.uniform(
VerticalAnchor.absolute(20), VerticalAnchor.absolute(80))));

PlacementUtils.register(context, END_ICE_ETHER_ORE_PLACED,
configured.getOrThrow(ModConfiguredFeatures.END_ICE_ETHER_ORE),
commonOrePlacement(8, HeightRangePlacement.uniform(
VerticalAnchor.absolute(0), VerticalAnchor.absolute(80))));

三个维度的高度不一样,这是原版地形决定的

主世界 -64 到 128:从基岩层到地表附近,全程都能挖

下界 20 到 80:下界顶部是基岩顶,太低太高都没意义

末地 0 到 80:末地岛就那么薄,给宽了也是空

注入三个维度

1
2
3
4
5
6
BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(),
GenerationStep.Decoration.UNDERGROUND_ORES, ModPlacedFeatures.ICE_ETHER_ORE_PLACED);
BiomeModifications.addFeature(BiomeSelectors.foundInTheNether(),
GenerationStep.Decoration.UNDERGROUND_ORES, ModPlacedFeatures.NETHER_ICE_ETHER_ORE_PLACED);
BiomeModifications.addFeature(BiomeSelectors.foundInTheEnd(),
GenerationStep.Decoration.UNDERGROUND_ORES, ModPlacedFeatures.END_ICE_ETHER_ORE_PLACED);

三行,三个维度,用的是同一个生成阶段 UNDERGROUND_ORES

数据生成

这期不用改数据生成的主类

前面做树的时候,我们已经在 buildRegistry 里挂了 CONFIGURED_FEATURE 和 PLACED_FEATURE,也已经在 ModDynamicRegistryProvider 里 addAll 过了,新的矿物会自动跟着生成

跑完会多出六个文件

1
2
3
4
5
6
data/tutorial/worldgen/configured_feature/ice_ether_ore.json
data/tutorial/worldgen/configured_feature/nether_ice_ether_ore.json
data/tutorial/worldgen/configured_feature/end_ice_ether_ore.json
data/tutorial/worldgen/placed_feature/ice_ether_ore_placed.json
data/tutorial/worldgen/placed_feature/nether_ice_ether_ore_placed.json
data/tutorial/worldgen/placed_feature/end_ice_ether_ore_placed.json

测试

跑完数据生成之后,我们就可以启动游戏进行测试了

常见问题(大肥鱼总结版)

  • 挖不到矿:BiomeModifications 那三行没写,或者写成了别的 GenerationStep
  • 主世界有,下界没有:foundInTheNether() 那行漏了,或者下界的 RuleTest 写成了主世界的石头标签
  • 矿长在地表:生成阶段写成了 VEGETAL_DECORATION,应该是 UNDERGROUND_ORES
  • 矿悬空、飘在空中:构造特征的第三个参数(丢弃概率)没给,试试 new OreConfiguration(targets, 9, 0.5F)
  • 下界挖到的矿长得像主世界的:正常,我们三个维度共用同一个方块
  • 末地替换不了:BlockMatchTest(Blocks.END_STONE) 写错了,末地石只有这一种
  • 数据生成没有新文件:Registries.CONFIGURED_FEATURE / PLACED_FEATURE 没挂在 buildRegistry 里
  • 启动报找不到 ice_ether_ore_placed:ModDynamicRegistryProvider 里没 addAll
  • 矿特别多,满地都是:count 给太大了,原版铁矿是每区块 10 次左右,照着调
  • 矿石挖出来不掉东西:战利品表是之前那期做的,这期只改生成,不碰掉落