本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 搞清一个群系由哪四部分组成
  • 认识 26.1 的一个改动:天空色、雾色搬进了环境属性
  • 用数据生成写一个完整的群系
  • 用 TerraBlender 把群系塞进主世界,并改掉它的地表

介绍

群系决定了一片地方长什么样

温度、降水、地面刷什么怪、长什么树、天是什么颜色,都是群系说了算

从数据上看,群系就是一份 json,和 configured_feature 一样是数据包注册表

四部分

一个群系可以拆成四块

气候:温度、降水量、有没有降水。这几个数字会影响下雪、结冰、草的颜色

特效:水色、草色、叶色,加上天空色、雾色

生成设置:这个群系里塞哪些地物,洞穴、矿石、树、花都在这里挂着

刷怪设置:刷哪些怪、权重多少、一次刷几只

这期做到哪

群系本身我们能完整做出来,但「让它自然分布在主世界」是另一件事

因为主世界的群系分布是由一张巨大的参数表定的(温度、湿度、侵蚀度这些维度上的参数点),Fabric 没有官方 API 能往那张表里加东西

所以这期分两段:先把群系定义做扎实,再用 TerraBlender 把它接进主世界,顺便把地表换成我们自己的方块

查看源代码

原版平原

原版的群系都在 OverworldBiomes 这个类里,看平原

1
2
3
4
5
6
return baseBiome(0.8F, 0.4F)
.temperature(0.8F)
.specialEffects(new BiomeSpecialEffects.Builder()
.waterColor(4159204)
.build())
.build();

baseBiome(temperature, downfall) 是原版抽出来的小工具,把公共部分(生成设置、刷怪)都塞好了

我们自己的群系没这个便利,得一个个填

26.1 的一个改动

在 1.21 及更早,天空色、雾色是写在 BiomeSpecialEffects 里的

1
2
3
4
5
// 1.21 的写法,26.1 已经不能这么写
new BiomeSpecialEffects.Builder()
.skyColor(0x78a7ff)
.fogColor(0xc0d8ff)
.waterFogColor(0xbf1b26)

26.1 把这三个搬进了「环境属性」(EnvironmentAttributeMap)

1
2
3
EnvironmentAttributeMap darkForestAttributes = EnvironmentAttributeMap.builder()
.set(EnvironmentAttributes.BACKGROUND_MUSIC, new BackgroundMusic(SoundEvents.MUSIC_BIOME_FOREST))
.build();

现在 BiomeSpecialEffects.Builder 只剩四个方法

waterColor、grassColorOverride、foliageColorOverride、dryFoliageColorOverride

颜色类的东西被拆成了两半:水和植物还在 specialEffects,天空和雾去了 attributes

为什么这么改?因为 26.1 搞了一套通用的「环境属性」系统,光照、音乐、迷雾、水雾全都走这套键值,群系只是其中一个使用方

对我们来说就是记住:天空色、雾色、水雾色写在 attributes 里,水色、草色、叶色写在 effects 里

动手写

注册键

新建 ModBiomes

1
2
public static final ResourceKey<Biome> ICE_ETHER_FOREST = ResourceKey.create(Registries.BIOME,
Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, "ice_ether_forest"));

刷怪设置

1
2
3
4
MobSpawnSettings.Builder spawns = new MobSpawnSettings.Builder();
BiomeDefaultFeatures.farmAnimals(spawns);
BiomeDefaultFeatures.commonSpawns(spawns);
spawns.addSpawn(MobCategory.CREATURE, 8, new MobSpawnSettings.SpawnerData(EntityType.RABBIT, 2, 4));

farmAnimals 加的是牛羊猪鸡,commonSpawns 加的是僵尸骷髅这些常见怪

这两行直接把原版平原的配置搬过来了

再补一个自己的:兔子,权重 8,一次 2 到 4 只

SpawnerData 的后两个参数是 minCount 和 maxCount,顺序别写反

生成设置

1
2
3
BiomeGenerationSettings.Builder generation = new BiomeGenerationSettings.Builder(
context.lookup(Registries.PLACED_FEATURE),
context.lookup(Registries.CONFIGURED_CARVER));

第一个参数是「放置特征」的表,第二个是「雕刻器」的表

这两个都得从 context 里拿,因为群系里存的是引用,不是实体

然后往里面塞东西

1
2
3
4
BiomeDefaultFeatures.addDefaultCarversAndLakes(generation);
BiomeDefaultFeatures.addDefaultOres(generation);
BiomeDefaultFeatures.addDefaultSoftDisks(generation);
BiomeDefaultFeatures.addDefaultExtraVegetation(generation, true);

addDefaultCarversAndLakes:洞穴和湖泊

addDefaultOres:所有原版矿石,当然这里实际上指的是石头、深板岩、花岗岩等这种岩石,为后面的真正我们指的煤矿、铁矿等矿物生成做铺垫

addDefaultSoftDisks:沙子、砂砾、黏土那些圆盘(参见 Wiki 圆盘相关内容)

addDefaultExtraVegetation:甘蔗、南瓜这类额外植被

最后加我们自己的

1
2
generation.addFeature(GenerationStep.Decoration.VEGETAL_DECORATION, ModPlacedFeatures.ICE_ETHER_TREE_PLACED);
generation.addFeature(GenerationStep.Decoration.VEGETAL_DECORATION, ModPlacedFeatures.SIMPLE_FLOWER_PLACED);

注意这里要传的是放置特征,不是构造特征

传错了群系能生成,但那两样东西一棵都不会长

特效

1
2
3
4
5
EnvironmentAttributeMap attributes = EnvironmentAttributeMap.builder()
.set(EnvironmentAttributes.SKY_COLOR, 0x8FB4D9)
.set(EnvironmentAttributes.FOG_COLOR, 0xC8DDF0)
.set(EnvironmentAttributes.WATER_FOG_COLOR, 0x2F4F4F)
.build();

天空、雾、水下雾,三个冷色调

1
2
3
4
5
.specialEffects(new BiomeSpecialEffects.Builder()
.waterColor(0x2F4F4F)
.grassColorOverride(0x9FC6D8)
.foliageColorOverride(0xA8CFDF)
.build())

水色、草色、叶色

最终注册

1
2
3
4
5
6
7
8
9
return new Biome.BiomeBuilder()
.hasPrecipitation(true)
.temperature(0.1F)
.downfall(0.7F)
.putAttributes(attributes)
.specialEffects(...)
.mobSpawnSettings(spawns.build())
.generationSettings(generation.build())
.build();

温度给 0.1F, 降水量 0.7F

hasPrecipitation(true) 是有降水

然后 context.register(ICE_ETHER_FOREST, ...) 注册

数据生成

和前面几期一样,两步

buildRegistry

1
registryBuilder.add(Registries.BIOME, ModBiomes::bootstrap);

ModDynamicRegistryProvider

1
entries.addAll(registries.lookupOrThrow(Registries.BIOME));

用 TerraBlender 进主世界

Fabric 官方没有提供「往主世界塞群系」的 API

主世界的群系分布是由 multi_noise_biome_source_parameter_list/overworld 那份数据定的,里面是一大堆「温度 / 湿度 / 侵蚀 / 怪异度」的参数点,每个点指向一个群系

所以社区的标准做法是用第三方库,也就是TerraBlender,NeoForge那期用的也是它

依赖

build.gradle 里加仓库和依赖

1
2
3
4
5
6
7
repositories {
maven { url = 'https://maven.minecraftforge.net/' }
}

dependencies {
implementation 'com.github.glitchfiend:TerraBlender-fabric:26.1-26.1.0.2'
}

版本号这块得留神

TerraBlender 的版本号是 <对应的 MC 版本>-<它自己的版本> 两段拼起来(具体版本可以到模组站上去找已发布的版本)

入口点

TerraBlender 得等它自己初始化完之后,再来调我们的代码

Fabric 这边的做法是加一个自定义入口点,在 fabric.mod.json 里

1
2
3
4
5
6
{
"entrypoints": {
"main": ["com.besson.tutorial.TutorialMod"],
"terrablender": ["com.besson.tutorial.world.ModTerraBlenderApi"]
}
}

少了这一行,代码编译得好好的,运行时就是不执行,一点提示都没有

区域

新建 ModOverworldRegion,继承 TerraBlender 的 Region

1
2
3
4
5
6
7
8
9
10
11
12
public class ModOverworldRegion extends Region {
public ModOverworldRegion(Identifier name, int weight) {
super(name, RegionType.OVERWORLD, weight);
}

@Override
public void addBiomes(Registry<Biome> registry,
Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>> mapper) {
this.addModifiedVanillaOverworldBiomes(mapper, builder ->
builder.replaceBiome(Biomes.SNOWY_TAIGA, ModBiomes.ICE_ETHER_FOREST));
}
}

RegionType.OVERWORLD 限定它只管主世界

addModifiedVanillaOverworldBiomes 的意思是「拿原版那张参数表来改」

replaceBiome(原来的群系, 我们的群系) 就是把雪原针叶林换成冰以太森林

也就是说,以后你走到原本该是雪原针叶林的地方,看到的是我们的群系

如果你想「新增」而不是替换,就用父类的 addBiome(...),自己给六个气候参数和一个偏移值,相当于在原版那张表上多插一个点

表面规则

群系换掉了,但地面还是原版那套(草方块 / 石头)

想让地面长得不一样,得写表面规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ModSurfaceRules {
private static final SurfaceRules.RuleSource GRASS_BLOCK = SurfaceRules.state(Blocks.GRASS_BLOCK.defaultBlockState());
private static final SurfaceRules.RuleSource DIRT = SurfaceRules.state(Blocks.DIRT.defaultBlockState());
private static final SurfaceRules.RuleSource DIAMOND_BLOCK = SurfaceRules.state(Blocks.DIAMOND_BLOCK.defaultBlockState());

private static final SurfaceRules.ConditionSource ABOVE_WATER = SurfaceRules.waterBlockCheck(-1, 0);
private static final SurfaceRules.ConditionSource DIRT_LAYER = SurfaceRules.stoneDepthCheck(0, true, 3, CaveSurface.FLOOR);

public static SurfaceRules.RuleSource makeRules() {
SurfaceRules.RuleSource grassOnTop = SurfaceRules.ifTrue(
SurfaceRules.abovePreliminarySurface(),
SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR,
SurfaceRules.ifTrue(ABOVE_WATER, GRASS_BLOCK)));

SurfaceRules.RuleSource dirtBelowTop = SurfaceRules.ifTrue(
SurfaceRules.abovePreliminarySurface(),
SurfaceRules.ifTrue(DIRT_LAYER,
SurfaceRules.ifTrue(ABOVE_WATER, DIRT)));

SurfaceRules.RuleSource iceEtherDeep = SurfaceRules.ifTrue(
SurfaceRules.isBiome(ModBiomes.ICE_ETHER_FOREST),
SurfaceRules.ifTrue(SurfaceRules.DEEP_UNDER_FLOOR, DIAMOND_BLOCK));

return SurfaceRules.sequence(grassOnTop, dirtBelowTop, iceEtherDeep);
}
}

SurfaceRules.state(方块状态) 是「这里放这个方块」

ifTrue(条件, 规则) 是「满足条件才用这条」

sequence 是「从上往下试,谁先匹配谁生效」

这里用了三个深度条件,得分开理解

ON_FLOOR:只匹配地表最上面那一格,放草

stoneDepthCheck(0, true, 3, CaveSurface.FLOOR):地表往下 3 格以内,放泥土

DEEP_UNDER_FLOOR:地表往下 6 格以外,换成钻石块

ABOVE_WATER 是「水位以上」,这样水下那一格不会长草,而是泥土(原版也是这么干的)

iceEtherDeep 外面那个 isBiome 一定要加,不加的话整个主世界的地下都会变成钻石

这里踩过一个坑

之前的教程草方块的生成有问题,水面以上的草方块直接一层一层堆下去了,往下挖十几格(如果地表够厚的话)还是草方块,底下不是泥土或者石头

abovePreliminarySurface() 这个名字有点误导,它只是个「高度判断」

它回答的是「这个位置在地表之上吗」,不管深度

真正管深度的是 ON_FLOOR、UNDER_FLOOR、DEEP_UNDER_FLOOR 这一类条件

少了深度检查,那个柱体里所有「在地表以上 + 在水位以上」的方块都会被换成草,于是堆上去了

所以写表面规则之前,先想清楚这次要改的是「哪一层」,再挑对应的深度条件

注册

1
2
3
4
5
6
7
8
9
10
11
12
public class ModTerraBlenderApi implements TerraBlenderApi {
@Override
public void onTerraBlenderInitialized() {
Regions.register(new ModOverworldRegion(
Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, "overworld"), 4));

SurfaceRuleManager.addSurfaceRules(
SurfaceRuleManager.RuleCategory.OVERWORLD,
TutorialMod.MOD_ID,
ModSurfaceRules.makeRules());
}
}

Regions.register 注册区域,第二个参数是权重

命名空间不用怕撞:TerraBlender 自己有个 minecraft:overworld,我们的是 tutorial:overworld

addSurfaceRules 把表面规则挂到主世界

测试

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

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

  • 日志里没有 Registered region:fabric.mod.json 少了 terrablender 入口点,或者类名写错
  • 群系进了世界但地表没变:表面规则没注册,或者 isBiome 里的 key 写错
  • 草方块一层层堆到地下,草下面没有泥土和石头:表面规则少了深度条件(ON_FLOOR),abovePreliminarySurface() 只管高度不管深度
  • 整个主世界地下都变成冰以太:表面规则少了 isBiome 限定
  • /locate biome 说找不到:可能这片区域离得太远,往一个方向多走几个区块再试;也可能是替换的群系(雪原针叶林)本来就在很冷的地方
  • /fillbiome 说找不到群系:数据没进 jar(buildRegistry 漏挂 Registries.BIOME),或者 json 序列化失败了
  • 数据生成目录里没有 biome 文件:去翻日志,大概率是 minCount needs to be smaller or equal to maxCount,SpawnerData 的参数写反了
  • 天空颜色没变:写进了 specialEffects,26.1 得写进 EnvironmentAttributeMap
  • 群系里一棵树都没有:addFeature 传成了 ConfiguredFeature,要传 PlacedFeature
  • 群系里没有矿:addDefaultOres 没加,或者矿的 placed feature 没挂在群系上
  • 改了群系颜色但游戏里没变:旧区块缓存了,新建世界
  • 温度设了 0.0F 却不结冰:结冰还跟降水、光照有关,光看温度不够
  • 想加点新的而不是替换:Region 里用 addBiome(...) 自己给参数,别用 replaceBiome