本篇教程由作者设定使用 CC BY-NC 协议。

本教程为TerraBlender开发教程,目的在于帮助读者增加对此mod的了解,并提供一种更为清晰的自定义群系制作方法。本教程以Minecraft Forge1.20 Mojmap版本,TerraBlender3+作为样例,其它版本中对于此mod的使用方式原则上大同小异,但由于minecraft在洞穴更新版本中对世界生成等内容的重写,这导致世界生成的整体逻辑出现了较大的变化,因此在不由mod提供的部分(如SurfaceBuilder)部分会存在一定差异。
本教程内内容仅代表个人对游戏本体与mod提供的接口的理解,不具有权威性解释,不代表游戏相关开发方与mod制作方的观点,内容仅供参考且具有针对版本特异性,不确保针对每个版本及相应特殊条件下都能适用。由于模组提供的接口特性,本教程需要依赖于mod而不能仅使用数据包实现,本教程将默认读者已掌握对于java与mod开发的基本内容。
您可以在此页面查看此mod提供的源码,其中mod作者提供了使用样例,您可以在对应的版本分支的Example文件夹下找到它们。

准备工作

TerraBlender的用途(选读)

在minecraft1.18版本之前,有关群系生成时的各项内容,比如地形的起伏属性,含有的结构(Structure)或特征(Feature),地表生成(SurfaceBuilder)等。而在1.18版本中,为了适应洞穴更新,Mojang修改了Minecraft的生成逻辑,并将地形的属性与地表生成从群系中独立了出去。前者由维度(Level/World)本身配置,由相应的噪声设置(Noise Setting)一体生成,这一部分具有较低的可控性。后者则同样被整体化储存为一个表面规则资源(SurfaceRules.RuleSource)并由SurfaceRuleData下的overworldlike方法提供。但这种变更一言难尽,这里展示出原版中构造主世界SurfaceRules的重要部分代码:

SurfaceRules.ConditionSource surfacerules$conditionsource = SurfaceRules.yBlockCheck(VerticalAnchor.absolute(97), 2);
[省略部分条件资源变量的创建]
SurfaceRules.ConditionSource surfacerules$conditionsource17 = SurfaceRules.noiseCondition(Noises.SURFACE, 0.5454D, 0.909D);
SurfaceRules.RuleSource surfacerules$rulesource8 = 
             SurfaceRules.RuleSource surfacerules$rulesource8 = SurfaceRules.sequence(
            SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR, SurfaceRules.sequence(
            SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.WOODED_BADLANDS), SurfaceRules.ifTrue(surfacerules$conditionsource, SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfacerules$conditionsource15, COARSE_DIRT),
            SurfaceRules.ifTrue(surfacerules$conditionsource16, COARSE_DIRT),
            SurfaceRules.ifTrue(surfacerules$conditionsource17, COARSE_DIRT),
            surfacerules$rulesource
            ))),
            SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.SWAMP), SurfaceRules.ifTrue(surfacerules$conditionsource5, SurfaceRules.ifTrue(SurfaceRules.not(surfacerules$conditionsource6), SurfaceRules.ifTrue(SurfaceRules.noiseCondition(Noises.SWAMP, 0.0D), WATER)))),
            SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.MANGROVE_SWAMP), SurfaceRules.ifTrue(surfacerules$conditionsource4, SurfaceRules.ifTrue(SurfaceRules.not(surfacerules$conditionsource6), SurfaceRules.ifTrue(SurfaceRules.noiseCondition(Noises.SWAMP, 0.0D), WATER))))
            )),
            SurfaceRules.ifTrue(SurfaceRules.isBiome(Biomes.BADLANDS, Biomes.ERODED_BADLANDS, Biomes.WOODED_BADLANDS), SurfaceRules.sequence(
            SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR, SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfacerules$conditionsource1, ORANGE_TERRACOTTA),
            SurfaceRules.ifTrue(surfacerules$conditionsource3, SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfacerules$conditionsource15, TERRACOTTA),
            SurfaceRules.ifTrue(surfacerules$conditionsource16, TERRACOTTA),
            SurfaceRules.ifTrue(surfacerules$conditionsource17, TERRACOTTA),
            SurfaceRules.bandlands()
            )),
            SurfaceRules.ifTrue(surfacerules$conditionsource7, SurfaceRules.sequence(
            SurfaceRules.ifTrue(SurfaceRules.ON_CEILING, RED_SANDSTONE),
            RED_SAND
            )),
            SurfaceRules.ifTrue(SurfaceRules.not(surfacerules$conditionsource10), ORANGE_TERRACOTTA),
            SurfaceRules.ifTrue(surfacerules$conditionsource9, WHITE_TERRACOTTA),
            surfacerules$rulesource2
            )),
            SurfaceRules.ifTrue(surfacerules$conditionsource2, SurfaceRules.sequence(
            SurfaceRules.ifTrue(surfacerules$conditionsource6, SurfaceRules.ifTrue(SurfaceRules.not(surfacerules$conditionsource3), ORANGE_TERRACOTTA)),
            SurfaceRules.bandlands()
            )),
            SurfaceRules.ifTrue(SurfaceRules.UNDER_FLOOR, SurfaceRules.ifTrue(surfacerules$conditionsource9, WHITE_TERRACOTTA))
            ))......[省略很多内容]
ImmutableList.Builder<SurfaceRules.RuleSource> builder = ImmutableList.builder();
if (p_198382_) {
   builder.add(SurfaceRules.ifTrue(SurfaceRules.not(SurfaceRules.verticalGradient("bedrock_roof", VerticalAnchor.belowTop(5), VerticalAnchor.top())), BEDROCK));
}

if (p_198383_) {
   builder.add(SurfaceRules.ifTrue(SurfaceRules.verticalGradient("bedrock_floor", VerticalAnchor.bottom(), VerticalAnchor.aboveBottom(5)), BEDROCK));
}

SurfaceRules.RuleSource surfacerules$rulesource9 = SurfaceRules.ifTrue(SurfaceRules.abovePreliminarySurface(), surfacerules$rulesource8);
builder.add(p_198381_ ? surfacerules$rulesource9 : surfacerules$rulesource8);
builder.add(SurfaceRules.ifTrue(SurfaceRules.verticalGradient("deepslate", VerticalAnchor.absolute(0), VerticalAnchor.absolute(8)), DEEPSLATE));
return SurfaceRules.sequence(builder.build().toArray((p_198379_) -> {
   return new SurfaceRules.RuleSource[p_198379_];

这一过程使其可读性极具降低,大大增加了开发难度,同时没有提供群系生成的接口也导致开发者很难为已有的维度添加新的群系并使其按照一定样式生成。而TerraBlender提供了帮助我们向已有维度(截至3.0.0.x版本支持主世界与下届)添加新的群系,与向原有的RuleSource中添加用于构造新群系地表(也可以是地底下,但基础的高度判断方法无法区分地底的巨大洞穴和地表,也就是说这一过程中的失误常常会导致群系地下的巨型洞穴中生成和地表一样的表面)。这两个内容便足以帮助我们实现大部分需要的效果了。具体对于这种更改的前后对比,同mod下的另一位教程制作的前辈已经展示过了,在此便不再赘述。有兴趣的朋友们可以移步此处了解。

gradle与mods.toml配置

打开build.gradle,找到dependencies部分,并添加这样一行内容:

implementation fg.deobf("com.github.glitchfiend:TerraBlender-forge:1.20.1-3.0.0.167")

其中后面的com.github.glitchfiend:TerraBlender-forge:1.20.1-3.0.0.167部分用于指定我们下载的库源,这是一个maven url,如果想要获取一个mod的库源,可以参考这个网站。在对一份由forge下载的开发包中提供的默认build.gradle文件添加完后,dependencies部分应该是这个样子的:

dependencies {
    minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
    implementation fg.deobf("com.github.glitchfiend:TerraBlender-forge:1.20.1-3.0.0.167")
}

这里省略了一些注释。需要注意的是,原则上在添加此依赖库的时候不需要额外再添加mixin的库,本人测试时在注释掉mixin库相关内容后不影响游戏启动,但由于项目中已经导入过mixin,因此我暂时无法保证在不添加mixin库依赖的情况下仍能运行TerraBlender模组。以防万一,在此贴出mixin需要额外添加的语句,内容可能存在累赘,但经测试可以使用:

buildscript {
    repositories {
        maven { url = "https://maven.aliyun.com/repository/public" }
        maven { url = 'https://maven.minecraftforge.net/' }
        maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }
        mavenCentral()
    }
    dependencies {
        classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '6.+', changing: true
        classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
    }
}

apply plugin: 'net.minecraftforge.gradle'
apply plugin: 'idea'
apply plugin: 'org.spongepowered.mixin'
//(这些内容添加在build.gradle的开头位置)

接下来我们配置mods.toml部分,我们需要在这里声明游戏中的mod依赖。在resources.META_INF下可以找到mods.toml文件,我们需要向其中添加以下内容:

[[dependencies.mod_id]]//请将"mod_id"替换为您的mod的id
    modId="terrablender"//这是我们所依赖的库
    mandatory=true
    versionRange="[3.0.0.167,)"//我们依赖的库的版本范围
    ordering="NONE"
    side="BOTH"

在完成这些后,就可以重新加载gradle项目了。此时gradle会自动帮我们下载添加的依赖库并构建gradle。这一过程建议使用魔法,因为过程不稳定且绝大部分情况下比较缓慢。接下来执行gradle中的runClient任务(这一过程会帮我们检查并补全缺少的资源,而直接使用idea(好吧我只用过idea)的runClient则不会经过这一步骤,有可能导致mixin失败报错)。如果游戏顺利启动且在mod列表中有terrablender模组,则说明配置没有问题。

接下来进入正题,让我们开始为主世界世界添加新的群系。

Biome与Region的创建

Region是TerraBlender为我们提供的一个新的类,用于统筹处理多个需要添加至相应维度的群系。Biome则是我们的老朋友生物群系。由于Minecraft的改动,现在生物群系需要以json形式在数据包中添加。当然,我们可以使用dataGenerator来将一个代码形式的Biome转化为群系的json文件。

这里简单说一下DataGeneration的思路:

  • 首先创建一个Biome实例,这一部分和旧版本使用代码创建群系一样:需要一个MobSpawnSettings.Builder来配置群系中生成的生物,以及一个BiomeGenerationSettings.Builder来配置群系中生成的特征。值得注意的是,Features的配置必须按照一定的顺序,即同一个PlacedFeature(注:较旧版本(如1.16.5版本)的ConfiguredFeature在新版本中被拆分为了ConfiguredFeature与PlacedFeature两部分,前者用于配置Feature的配置,后者则负责额外处理Feature在特定条件下是否生成,尝试生成的次数与位置,类似于旧版本中ConfiguredFeature中的decorated方法)只能在一个Step中使用,不能重复,而任意两个同Step的PlacedFeature也不能调换顺序,这将会导致报错。因此建议先找到原版中和想要制作的群系相似的群系并复制已保持原版的PlacedFeature顺序正确,这些存储在OverworldBiomes类中。最后创建一个Biome.BiomeBuilder()来配置特殊的属性,比如群系粒子,着色,声音等,并调用build()方法构造一个Biome。

  • 接下来我们应该创建一个ResourceKey<Biome>作为我们创建的群系的资源路径(ResourceLocation,可以简单理解为对应物件的id),供数据生成以及代码中的调用该群系。要创造一个ResourceKey<Biome>,我们需要使用以下代码:

public static final ResourceKey<Biome> BIOME = register("biome");

private static ResourceKey<Biome> register(String name) {    
return ResourceKey.create(Registries.BIOME, new ResourceLocation("mod_id","biome"));
}

public static void bootstrap(){}

       这段代码创建了一个名字为mod_id:biome的群系,并将其注册进了BIOME的注册器中。最下方的bootstrap方法用于引导java在恰当的时间加载常量并使内容被注册,这一过程应在FMLCommonSetup事件中触发。

  • 在创建BiomeGenerationSettings.Builder时需要传入HolderGetter<PlacedFeature>与HolderGetter<ConfiguredWorldCarver<?>>参数,这两个参数由数据包生成器提供的BootstapContext<Biome>类提供。这里直接展示其对应的方法,您可以将其创建在任何您认为方便的类下:

public static void bootstrap(BootstapContext<Biome> context){
    HolderGetter<ConfiguredWorldCarver<?>> c = context.lookup(Registries.CONFIGURED_CARVER);
    HolderGetter<PlacedFeature> p = context.lookup(Registries.PLACED_FEATURE);

    context.register(BiomeKey.Biome,BiomeMaker.biomeProvider1(p,c));//这里分别是你之前注册的群系ResourceKey与构造群系实例的方法。
}
  • 最后我们需要将其在GatherData事件中使用它。这个事件不会在正式启动游戏后的任何时间触发,只能在开发环境中使用专门的gradle任务来调用。代码如下(假设上一步的方法在BiomeRegistry类中):

private static final RegistrySetBuilder BUILDER = new RegistrySetBuilder()
        .add(Registries.BIOME, BiomeRegistry::bootstrap);

@SubscribeEvent//注:需要在类声明的位置标记@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)即自动将事件注册至MOD线
public static void onGatherData(GatherDataEvent event){
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();

    generator.addProvider(event.includeServer(), new DatapackBuiltinEntriesProvider(output,lookupProvider,BUILDER, Set.of(Utils.MOD_ID)));
}
  • 在gradle的Tasks/forgegradle run中找到runData并运行,即可生成对应的群系的json文件。这些文件将会生成在src/generated/resources文件夹下,位置如图:基于TerraBlender辅助实现的Minecraft Forge1.20 mojmap自定义群系添加教程-第1张图片

接下来说说Region的使用。我们需要创建一个新的类并继承TerraBlender提供的Region类,然后覆写其下的addBiomes方法。

在构造方法方面,需要传入一个资源路径作为注册的region的id,一个RegionType指定这个Region在主世界还是地狱被使用,一个weight表示比重。但是根据测试,weight对群系的生成范围并不具有约束力,推测可能用于TerraBlender注册的不同群系间的比重分配。(待证明)

在addBiomes方法中,我们则需要调用类下的addBiome方法(没有s,这是两个不同的方法)。分别需要提供Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>>,在addBiomes方法中提供了的形参;六个Climate.Parameter分别代表温度,湿度,大陆性,侵蚀度,奇异度,深度,可以使用Climate.Parameter.span生成一个范围,Climate.Parameter.point生成一个定值,或使用mod提供的ParameterUtils类下预定义的参数,这些参数应当在-2.0F到2.0F之间;一个0F到1F的浮点数,表示生成的概率,原则上越大越稀有;一个BiomeKey,我们在之前已经创建过了,现在直接调用即可。如果需要添加多个群系,只需要多次调用addBiome方法即可。 

至于这些参数分别起到什么效果……很遗憾的是,测试发现这些值似乎对群系生成的影响甚微,因此,似乎没有必要对每个数值进行精细的把控。(待证明)

下面给出范例代码:

public class CommonWorldRegion extends Region {
    public CommonWorldRegion(int weight) {
        super(new ResourceLocation(Utils.MOD_ID,"cw_region_provider"), RegionType.OVERWORLD,weight);
    }

    @Override
    public void addBiomes(Registry<Biome> registry, Consumer<Pair<Climate.ParameterPoint, ResourceKey<Biome>>> mapper) {
        super.addBiomes(registry, mapper);
        this.addBiome(mapper,
                Climate.Parameter.span(0.95F, 1.0F),//温度
                Climate.Parameter.span(-2.0F, -1.55F),//湿度
                Climate.Parameter.span(0.165F, 1.8F),//大陆性
                Climate.Parameter.span(1.75F,1.8F),//侵蚀度(起伏)
                Climate.Parameter.span(0.45F, 0.65F),//奇异度
                Climate.Parameter.span(0.25F, 0.8F),//深度
                0.99F,BiomeKey.BIOME);
    }
}

最后我们需要将region注册进对应的事件线:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModBusEventConsumer {
    @SubscribeEvent
    public static void onSetup(FMLCommonSetupEvent event){
        event.enqueueWork(()-> {
            Regions.register(new CommonWorldRegion(4));
            BiomeKey.bootstrap();
        });
    }
}

打开游戏,使用/locate biome mod_id:biome就可以找到我们新创建的群系了。

SurfaceRule注册于使用

SurfaceRule的基本逻辑与使用

在高版本中,SurfaceRule取代了低版本中的SurfaceBuilder。对于世界生成阶段的每一个方块都会经过一次SurfaceRule的判定,来决定相应位置应该生成何种方块。简单来说,我们在上面展示的一长串难读的代码,其基本逻辑就是对于每一个被选中的方块,先根据其所处群系的匹配来进入下一级SurfaceRule,再根据其位置等特性生成对应的方块。如果方块没有被生成(即返回的方块状态(BlockState)为null),则下降到默认的生成规则,并生成基础地形。

SurfaceRules这一体系(以下简称体系)由以下几个类构成:

  • RuleSource规则资源,是体系的基本组成。一个RuleSource下可以存储多个子RuleSource,它们将按照顺序被执行。其本身不具有决定方块生成的作用,更像是作为一个注册有关内容的引导。其需要包含一个自身的解码器Codec<?>用于注册,以及实现一个apply方法来将生成背景(SurfaceRules.Context)转化为所需要的地表规则(SurfaceRule)。

  • SurfaceRule地表规则,是真正负责执行将位置信息转化为BlockState的。这里需要实现一个tryApply方法,提供了运算方块的坐标。但这些数据不一定能满足我们的需求,因此有需要的话可以在构造一个SurfaceRule实例时将Context一并传入。

  • ConditionSource状态资源,类比上述规则资源,但这个应该返回一个boolean来表示是否应该在此位置进行相应活动。相应的,其真正决定坐标的应该是一个Condition类。但由于其作用效果有限,因此二者常被视为一体的。

SurfaceRules内提供了一些基础的方法:

  • ifTrue方法:这需要一个ConditionSource和一个RuleSource,若前者成立则执行后者。ifTrue方法本身也是一个ConditionSource,因而可以嵌套使用。

  • sequence方法:需要多个RuleSource,在前置的条件成立时其存储的多个RuleSource将会轮流被尝试执行。这可以使多个具有相同前提条件的RuleSource被聚集,比如原版中先使用ifTrue判断是否属于指定群系,再使用sequence来添加所有需要在此群系中执行的子RuleSource。

  • not方法:反转一个ConditionSource的结果。

  • makeStateRule方法:需要提供一个Block,这将把Block转化为一个RuleSource,可以上述提及的情况下使用以放置对应方块。

在SurfaceRules类中还提供了其它一些方法或预定义常量,比如isBiome,waterBlockCheck,yBlockCheck,stoneDepthCheck等等,用来检测一个位置的各种属性,大家可以自行查看。

这里展示一个代码实例并解释一下以加深大家对代码的理解:

public static SurfaceRules.RuleSource build(){
    return SurfaceRules.ifTrue(SurfaceRules.isBiome(BiomeKey.BIOME),//如果群系为我们之前创建的自定义群系
            SurfaceRules.ifTrue(SurfaceRules.not(SurfaceRules.hole()),//且如果位置不是矿洞内
                    SurfaceRules.sequence(
                            SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(0, true, 3, CaveSurface.FLOOR),makeStateRule(Blocks.DIRT))//将距离表面3格以内的方块替换为泥土
                            SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(0, true, 5, CaveSurface.FLOOR),makeStateRule(Blocks.COARSE_DIRT))//对于不符合上方“距离表面3格以内”不成立的方块,如果距离表面五格以内,则替换为砂土
    )));
}

SurfaceRule的注册

TerraBlender为我们提供了将SurfaceRule添加至对应维度的接口,我们仍需要在FMLCommonSetup事件中使用它。在之前代码的基础上变更以下内容:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModBusEventConsumer {
    @SubscribeEvent
    public static void onSetup(FMLCommonSetupEvent event){
        event.enqueueWork(()-> {
            Regions.register(new CommonWorldRegion(4));
            SurfaceRuleManager.addSurfaceRules(SurfaceRuleManager.RuleCategory.OVERWORLD,Utils.MOD_ID,CommonWorldRegion.build());//这是添加的注册SurfaceRule的内容
            BiomeKey.bootstrap();
        });
    }
}

我们可以看到这里需要三个参数,分别是你需要添加到的维度,你的modid,以及你创建的RuleSource内容。

在完成这些后,打开游戏并找到你的群系,就可以看到被修改过的地表了。

SurfaceRule自定义

SurfaceRule的自定义是继承自原版SurfaceRule接口的自建类,在这里我们可以使用代码与给出的数据直接进行类似旧版本中SurfaceBuilder的行为,具有较高的自由度,个人认为对于复杂的地表要求,使用这一功能比堆叠ifTrue与senquence更为便捷与直观。

  • 首先我们需要创建一个类并实现SurfaceRules.RuleSource接口,这需要实现codec方法与apply方法。如果你不需要在创建的时候像ifTrue(从源码中可以看到这其实是TestRuleSource类的构造的快捷方法)等一样传入参数,则可以直接使用public static final Codec<WastelandSurfaceLayer> CODEC = Codec.unit(BiomeSurfaceRule::new)来创建一个对应的Codec,其中BiomeSurfaceRule是你创建的SurfaceRule的类名。

    在codec方法中我们返回new KeyDispatchDataCodec<>(CODEC)即可。而apply方法则需要我们创建并返回一个SurfaceRule。这里按部就班即可,新建SurfaceRule的方法在下一步。

  • 接下来我们应该创建一个类并实现SurfaceRules.SurfaceRule接口。由于其需要实现的tryApply方法只提供了坐标位置而缺少更多信息,因此我强烈建议在构造这个Rule实例时传入SurfaceRules.Context,如下:

public record Rule(SurfaceRules.Context context) implements SurfaceRules.SurfaceRule{......}
  • 创建完成后我们应当实现tryApply方法。根据之前传入的context,我们可以获取到我们需要的各种信息。但由于类中数据的权限设置,对于一些不能访问的值,您需要使用AT来为其手动添加权限。如果安装了Minecraft Develop插件,可以选中需要的方法或类或变量,然后右键,并找到“复制/粘贴特殊”下的“AT Entry”。再在你的accesstransformer.cfg文件中输入public再粘贴刚刚自动复制的内容,并重新加载gradle项目即可。

    至于在这里该如何具体书写代码,大家可以去查看原版已经完成的SurfaceRule的写法。这里仅给出一份参考代码来帮助大家了解基本的逻辑:

public record Rule(SurfaceRules.Context context) implements SurfaceRules.SurfaceRule{
            public BlockState tryApply(int x, int y, int z) {
                //将y大于100的位置均转变为空气 由于在之前额外使用了限制地表范围的SurfaceRules.stoneDepthCheck(0, true, 3, CaveSurface.FLOOR),这些内容仅对地表前3的方块生效。但这一过程容易造成群系衔接出现问题。
                if(context.blockY >= 100) return Blocks.AIR.defaultBlockState();
                //对于高度为99或98的方块,随机变为砂土或菌丝
                if(context.blockY >= 98) return new Random(context.pos.asLong()).nextFloat() >=0.3F ? Blocks.MYCELIUM.defaultBlockState() : Blocks.COARSE_DIRT.defaultBlockState();
                if(context.blockY >= 95) return Blocks.COARSE_DIRT.defaultBlockState();//高度为97到95的方块替换为砂土
                //使用自NoiseThresholdConditionSource,创建噪音函数并获取位置的噪音值。这一值一般位于+-0.75之间,不同噪音可能有所差异
                double noise = context.randomState.getOrCreateNoise(Noises.PILLAR).getValue(context.blockX,0,context.blockZ);
                if (noise > 0.6D) {//根据噪音值的不同范围生成不同的方块
                    //我们创建了getState方法来帮助我们判断目标方块在最上方还是次上方
                    return getState(Blocks.MYCELIUM.defaultBlockState(),Blocks.COARSE_DIRT.defaultBlockState());
                }else if(noise > 0.3D){
                    //在特定区间内进行随机,这使得两种生成之间有过渡效果。
                    return new Random(context.pos.asLong()).nextFloat() * 0.3F < noise - 0.3D ? getState(Blocks.MYCELIUM.defaultBlockState(),Blocks.COARSE_DIRT.defaultBlockState()) : getState(Blocks.GRAVEL.defaultBlockState(),Blocks.ANDESITE.defaultBlockState());
                } else if (noise > 0.2D) {
                    return getState(Blocks.GRAVEL.defaultBlockState(),Blocks.ANDESITE.defaultBlockState());
                }else if (noise > -0.2D){
                    return new Random(context.pos.asLong()).nextFloat() * 0.4F < noise + 0.2D ? getState(Blocks.GRAVEL.defaultBlockState(),Blocks.ANDESITE.defaultBlockState()) : getState(Blocks.ANDESITE.defaultBlockState(),Blocks.DIRT.defaultBlockState());
                } else if (noise > -0.6D) {
                    return new Random(context.pos.asLong()).nextFloat() * 0.5F < noise + 0.8D ? getState(Blocks.ANDESITE.defaultBlockState(),Blocks.DIRT.defaultBlockState()) : null;
                } else {
                    return new Random(context.pos.asLong()).nextFloat() < 0.1F ? getState(Blocks.ANDESITE.defaultBlockState(),Blocks.DIRT.defaultBlockState()) : null;
                }
            }

            private BlockState getState(BlockState surfaceState,BlockState groundState){
                if(context.stoneDepthAbove > 3 || context.blockY <= 56){//如果位置深度大于3或者位置低于y=56,则返回null,这将使其掉入默认生成
                    return null;
                }else if(context.stoneDepthAbove <= 1){//如果是表层方块,返回第一种
                    return surfaceState;
                }else {//如果是次表层方块,返回第二种
                    return groundState;
                }
            }
        }
  • 最后我们需要把新创建的RuleSource的CODEC注册。创建以下方法并在FMLCommonSetup中调用即可:

public static void ruleSourceRegBootstrap(){
    Registry.register(BuiltInRegistries.MATERIAL_RULE,new ResourceLocation("mod_id","biome_surface"),BiomeSurfaceRule.CODEC);
}

这样我们的注册就完成了。在之前提到的创建RuleSource组的过程中在对应群系调用它,并启动游戏,这时你就可以看到根据我们创建的新SurfaceRule而生成的群系了。

基于TerraBlender辅助实现的Minecraft Forge1.20 mojmap自定义群系添加教程-第2张图片

我不理解为什么要把画质压这么低才能上传,将就着看吧

最后我们在这里总结一下我们做的所有事情:

  1. 创建生物群系,我们使用的是DataGenerator,需要创建怪物数据,创建顺序不能错的特征数据,创建群系实例,创建资源键,注册群系至生成器,监听事件并在gradle任务中生成。

  2. 创建region,配置region参数,添加群系并配置群系有关参数,注册region实例。

  3. 创建RuleSource组,完成内部内容并注册进对应维度。

  4. 创建自定义RuleSource,制作对应Codec,创建自定义SurfaceRule,修改需要使用的类的AT,自定义要在表面生成的效果,注册Codec。

以上便是关于TerraBlender基础使用以及帮助开发者实现群系地表生成的主要内容了,内容均为个人对mod的浅薄理解,如有错误希望可以得到您的包涵与指正,感谢您的观看。

另外,在此提供一些便利的用于群系开发的链接或教程:

https://misode.github.io/worldgen/biome/ 你可以在此网站中快速生成需要的群系数据包,而不必使用data generator或手动配置json文件。

https://www.youtube.com/watch?v=enzKJWq0vNI&list=PLKGarocXCE1H9Y21-pxjt5Pt8bW14twa-&index=13 你可以在此网站中找到详细的data generator的使用方法教程。

https://github.com/Glitchfiend/TerraBlender/tree/TB-1.20.2-3.1.x/Example/Forge 你可以在此网站中看到TerraBlender作者提供的1.20版本Forge版开发样例。同样的,你可以在这一项目下的其它分支或文件夹中找到其它版本或Fabric版本的开发样例。