本篇教程的视频:

本篇教程源代码

GitHub地址:TutorialMod-DataGen-1.21

注意事项!!!

在开始本篇教程之前,请确保你在生成模板文件的时候,勾选了Data Generation,否则你很有可能无法正常使用数据生成

介绍

数据生成(DataGen)是一个很方便的用于生成各类json文件的工具,可以生成包括模型文件、方块状态文件、语言文件、Tags文件、配方文件等等。
而当你有大量模组内容时,手动编写这些文件会非常繁琐,所以我们可以使用数据生成器来生成这些文件

比如说,在我开发的Arknights Furniture模组中,有大量的家具,目前所加入的模型总数为1000+,如果我要手动编写这些模型的数据文件,那我可能已经没了。
而且只是写模型文件和方块状态文件,暂时还没考虑配方文件

Minecraft本身有数据生成器,我们在本期教程中也将使用原版的一些方法。
不过,因为要讲的内容比较多,所以源代码这个东西暂时不讲,我会提供相关的路径,可以自行研究

另外,数据生成这个东西,不仅仅可以使用这里教程中的方法,你可以使用其他的json生成器,也可以使用其他的语言来编写。
Such as,在我之前开发的Arknights Furniture模组中,在尚未使用数据生成之前,是使用Python批量生成对应模型的数据文件(后面实在受不了了,就开始用Python生成Java了)

Tags文件数据生成

虽然说我们的教程中,尚未真正讲到Tags,但是我们之前写战利品列表的时候,已经写了相关的Tags文件,所以这里我们也就讲了

Tags文件其实有很多种,我们这里就暂时指写方块和物品的Tags文件,其他的Tags文件可以自行研究(比如画这种,也是有Tag的)

原版的Tag生成器可见TagProvider

方块Tags文件

创建ModBlockTagsProvider类,继承FabricTagProvider.BlockTagProvider,并实现super函数和configure方法
(是的,我们继承的是Fabric API中的类,因为这样会相对方便一点)

注意,在实现super函数时,参数选择FabricDataOutputCompletableFuture<RegistryWrapper.WrapperLookup>这两个,不要选择三个参数的,那第三个参数是给ItemTag

1
2
3
4
5
6
7
8
9
10
public class ModBlockTagsProvider extends FabricTagProvider.BlockTagProvider {
public ModBlockTagsProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registriesFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {

}
}

configure方法中,我们可以写入我们的Tags文件,这里我们写之前战利品列表中写过的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {
getOrCreateTagBuilder(BlockTags.PICKAXE_MINEABLE)
.add(ModBlocks.ICE_ETHER_BLOCK)
.add(ModBlocks.ICE_ETHER_ORE)
.add(ModBlocks.RAW_ICE_ETHER_BLOCK);

getOrCreateTagBuilder(BlockTags.NEEDS_IRON_TOOL)
.add(ModBlocks.ICE_ETHER_ORE);

getOrCreateTagBuilder(BlockTags.NEEDS_STONE_TOOL)
.add(ModBlocks.RAW_ICE_ETHER_BLOCK);
}

这里我们使用getOrCreateTagBuilder方法来获取一个Tags文件,然后使用add方法来添加我们的方块

我们之前写的Tag是属于BlockTags,所以这里我们使用BlockTags中的相关字段来获取我们的Tags文件

物品Tags文件

创建ModItemTagsProvider类,继承FabricTagProvider.ItemTagProvider,并实现super函数和configure方法

1
2
3
4
5
6
7
8
9
10
11
12
public class ModItemTagsProvider extends FabricTagProvider.ItemTagProvider {


public ModItemTagsProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> completableFuture) {
super(output, completableFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup wrapperLookup) {

}
}

我们暂时还没有写物品的Tags文件,所以就简单创建一下即可,后面我们会写的

语言文件数据生成

在之前我们也写了语言文件,这里我们也可以使用数据生成来生成语言文件。不过,原版是没有语言文件生成器的

en_us

创建ModENUSLanProvider类,继承FabricLanguageProvider,并实现super函数和generateTranslations方法

1
2
3
4
5
6
7
8
9
10
public class ModENUSLanProvider extends FabricLanguageProvider {
public ModENUSLanProvider(FabricDataOutput dataOutput, CompletableFuture<RegistryWrapper.WrapperLookup> registryLookup) {
super(dataOutput, "en_us", registryLookup);
}

@Override
public void generateTranslations(RegistryWrapper.WrapperLookup registryLookup, TranslationBuilder translationBuilder) {

}
}

这里的super可以在中间加入一个"en_us",这个是语言文件的文件名,如果你要写其他语言文件,可以在这里修改。
不写的话,默认就是en_us

然后在generateTranslations方法中,我们可以写入我们的语言文件

1
2
3
4
5
6
7
8
9
10
11
@Override
public void generateTranslations(RegistryWrapper.WrapperLookup registryLookup, TranslationBuilder translationBuilder) {
translationBuilder.add(ModItems.ICE_ETHER, "Ice Ether");
translationBuilder.add(ModItems.RAW_ICE_ETHER, "Raw Ice Ether");

translationBuilder.add(ModBlocks.ICE_ETHER_BLOCK, "Ice Ether Block");
translationBuilder.add(ModBlocks.ICE_ETHER_ORE, "Ice Ether Ore");
translationBuilder.add(ModBlocks.RAW_ICE_ETHER_BLOCK, "Raw Ice Ether Block");

translationBuilder.add("itemGroup.tutorial_group", "Tutorial Group");
}

这里我们使用add方法来添加我们的语言文件,第一个参数是id,第二个参数是翻译的值。
add能够接受的参数很多,比如ItemBlockString等等

其实这里的物品栏,如果你是像原版一样写的,单独为物品栏注册了RegistryKey,那也可以直接引用这个RegistryKey来写入语言文件

zh_cn

中文的语言文件也是一样的,只是文件名不一样,我们创建ModZHCNLanProvider类,继承FabricLanguageProvider,并实现super函数和generateTranslations方法。
然后super中的参数改为"zh_cn",这个是中文的文件名

1
2
3
4
5
6
7
8
9
10
public class ModZHCNLanProvider extends FabricLanguageProvider {
public ModENUSLanProvider(FabricDataOutput dataOutput, CompletableFuture<RegistryWrapper.WrapperLookup> registryLookup) {
super(dataOutput, "zh_cn", registryLookup);
}

@Override
public void generateTranslations(RegistryWrapper.WrapperLookup registryLookup, TranslationBuilder translationBuilder) {

}
}

翻译的东西这里就不再赘述了,和上面的en_us一样

战利品列表数据生成

创建ModBlockLootTableProvider类,继承FabricBlockLootTableProvider(注意中间有Block),并实现super函数和generate方法

原版生成器可见BlockLootTableGenerator,不过注意,我们这里只讲了方块的战利品列表

1
2
3
4
5
6
7
8
9
10
public class ModBlockLootTableProvider extends FabricBlockLootTableProvider {
public ModLootTableProvider(FabricDataOutput dataOutput, CompletableFuture<RegistryWrapper.WrapperLookup> registryLookup) {
super(dataOutput, registryLookup);
}

@Override
public void generate() {

}
}

generate方法中,我们可以写入我们的战利品列表

1
2
3
4
5
6
7
@Override
public void generate() {
addDrop(ModBlocks.ICE_ETHER_BLOCK);
addDrop(ModBlocks.RAW_ICE_ETHER_BLOCK);
addDrop(ModBlocks.ICE_ETHER_ORE, oreDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER));

}

这里我们使用addDrop方法来添加我们的战利品列表,这里单个参数的话是默认掉落其本身

而矿石的掉落是需要额外再使用oreDrops方法来添加的,这个方法接受两个参数,第一个是矿石,第二个是掉落物品

值得一提的是,这个oreDrops方法是只掉落一个物品的,如果你要掉落多个物品,那么我们就要去看铜矿石的掉落列表(青金石、红石亦可),然后自己写了

我们到BlockLootTableGenerator中可以看到原版的战利品列表的各种生成方法,我们找到copperOreDrops方法

1
2
3
4
5
6
7
8
9
10
11
12
public LootTable.Builder copperOreDrops(Block drop) {
RegistryWrapper.Impl<Enchantment> impl = this.registryLookup.getWrapperOrThrow(RegistryKeys.ENCHANTMENT);
return this.dropsWithSilkTouch(
drop,
(LootPoolEntry.Builder<?>)this.applyExplosionDecay(
drop,
ItemEntry.builder(Items.RAW_COPPER)
.apply(SetCountLootFunction.builder(UniformLootNumberProvider.create(2.0F, 5.0F)))
.apply(ApplyBonusLootFunction.oreDrops(impl.getOrThrow(Enchantments.FORTUNE)))
)
);
}

这里我们可以看到,掉落的是Items.RAW_COPPER,掉落的数量是2-5,而且还有FORTUNE附魔(时运)的加成(当然,还有SilkTouch(精准采集)的附魔)。
这个方法我们并不能直接拿来用,因为我们的矿石和掉落物品不一样,所以我们要自己写

其实搬过来,自己改写一下即可

1
2
3
4
5
6
7
8
9
10
11
12
public LootTable.Builder copperOreLikeDrops(Block drop, Item dropItem) {
RegistryWrapper.Impl<Enchantment> impl = this.registryLookup.getWrapperOrThrow(RegistryKeys.ENCHANTMENT);
return this.dropsWithSilkTouch(
drop,
(LootPoolEntry.Builder<?>)this.applyExplosionDecay(
drop,
ItemEntry.builder(dropItem)
.apply(SetCountLootFunction.builder(UniformLootNumberProvider.create(2.0F, 5.0F)))
.apply(ApplyBonusLootFunction.oreDrops(impl.getOrThrow(Enchantments.FORTUNE)))
)
);
}

这里我们只是将Items.RAW_COPPER改为了dropItem,并添加了一个形参Item dropItem,这样我们就可以自定义掉落物品了。
而附魔相关的东西,你可以自行探究

随后,我们就可以使用这个方法来添加我们的掉落物品了

1
addDrop(ModBlocks.ICE_ETHER_ORE, copperOreLikeDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER));

这样,我们的矿石也就可以掉落多个物品了

模型文件数据生成

创建ModModelProvider类,继承FabricModelProvider,并实现super函数、generateBlockStateModelsgenerateItemModels方法

原版模型生成器可见ModelProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModModelsProvider extends FabricModelProvider {
public ModModelsProvider(FabricDataOutput output) {
super(output);
}

@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {

}

@Override
public void generateItemModels(ItemModelGenerator itemModelGenerator) {

}
}

这里我们重写的两个方法,一个是生成方块状态模型,一个是生成物品模型。
生成方块状态文件的同时,对应的物品模型也会生成,所以我们只需要写方块状态文件即可

generateBlockStateModels方法中,我们可以写入我们的方块状态文件

1
2
3
4
5
6
7
@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.ICE_ETHER_BLOCK);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.RAW_ICE_ETHER_BLOCK);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.ICE_ETHER_ORE);

}

我们之前写的都是最简单的方块状态文件,也就是立方体的六个面都一样,所以我们使用registerSimpleCubeAll方法来注册我们的方块状态文件

这里的registerSimpleCubeAll方法接受一个参数,就是我们的方块

generateItemModels方法中,我们可以写入我们的物品模型文件

1
2
3
4
5
6
@Override
public void generateItemModels(ItemModelGenerator itemModelGenerator) {
itemModelGenerator.register(ModItems.ICE_ETHER, Models.GENERATED);
itemModelGenerator.register(ModItems.RAW_ICE_ETHER, Models.GENERATED);

}

这里我们使用register方法来注册我们的物品模型文件,第一个参数是我们的物品,第二个参数是我们的模型的渲染模式,这里我们使用了Models.GENERATED,这个是一般物品的渲染模式(后面讲到工具时,我们还将采用另外的)

配方文件数据生成

创建ModRecipeProvider类,继承FabricRecipeProvider,并实现super函数和generate方法

原版配方生成器可见RecipeProvider

1
2
3
4
5
6
7
8
9
10
public class ModRecipesProvider extends FabricRecipeProvider {
public ModRecipesProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registriesFuture);
}

@Override
public void generate(RecipeExporter exporter) {

}
}

generate方法中,我们可以写入我们的配方文件

合成配方

这里我们先写之前写的1个方块合成9个物品的配方及其逆配方,这个用一个方法即可完成两个配方

1
2
offerReversibleCompactingRecipes(exporter, RecipeCategory.MISC, ModItems.ICE_ETHER,
RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_BLOCK);

这里我们使用offerReversibleCompactingRecipes方法来添加我们的配方,
这个方法有很多参数,注意不要漏

第一个是exporter

第二个是配方的类型,是后者(Block/Item)合成该参数的后一个产物(Item/Block)的分类

第三个是合成的物品(或方块

第四个是配方的类型,是前者(Item/Block)合成该参数的后一个产物(Block/Item)的分类

第五个是合成的方块(或物品)

有点小绕,不过你可以看看相关的源代码,就能理解了

熔炼配方

我们写之前的熔炉和高炉配方,但在此之前,我们需要一个List类型的字段来存放我们需要燃烧的物品

1
private static final List<ItemConvertible> ICE_ETHER = List.of(ModItems.RAW_ICE_ETHER, Items.ICE);

这里我们使用List.of方法来创建一个List,这个方法接受一个或多个参数,这里我们传入了两个参数,一个是我们的物品,一个是原版的物品。
后面你像继续加更多的物品,也是可以的

注意,一个List对应一个熔炼后的产物,也就是说这里的RAW_ICE_ETHERICE熔炼后会得到同一个产物

如果你要得到不同的产物,那么你就要创建多个List,然后在generate方法中写入多个熔炼配方

现在我们来写熔炼配方

1
2
3
4
offerSmelting(exporter, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER,
0.7f, 200, "ice_ether");
offerBlasting(exporter, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER,
0.7f, 100, "ice_ether");

这里我们使用offerSmelting方法来添加我们的熔炉配方,
offerBlasting方法来添加我们的高炉配方

这两个方法的参数本质上是一样的,其中它们的第二个参数是我们的List
第三个参数是配方的分类,第四个参数是配方的产物
第五个参数是熔炼经验,第六个参数是熔炼时间
第七个参数是配方所属的

营火配方

前面我们没讲营火配方,但曾经有人问过营火的配方怎么使用数据生成,很简单啦,看源代码先

可能你会发现,怎么没有类似于高炉和熔炉的方法呢?没有offerCampfire啥啥啥的。
但是,我们能发现一个offerFoodCookingRecipe的方法,这个方法是用来添加食物的烹饪配方的,而绝大多数的食物不仅有高炉和熔炉的配方,还有营火的配方(以及烟熏炉)

那好了,这个东西我们能不能挖出点什么来呢?我们先看看其中一个例子

1
offerFoodCookingRecipe(exporter, cooker, serializer, recipeFactory, cookingTime, Items.BEEF, Items.COOKED_BEEF, 0.35F);

它是generateCookingRecipes方法下的一条语句,而除了后面的具体的物品,前面那一堆直接使用形参的是什么呢?

这里我们进一步溯源,一路找到VanillaRecipeProvider中,它有这么两条语句

1
2
generateCookingRecipes(exporter, "smoking", RecipeSerializer.SMOKING, SmokingRecipe::new, 100);
generateCookingRecipes(exporter, "campfire_cooking", RecipeSerializer.CAMPFIRE_COOKING, CampfireCookingRecipe::new, 600);

So?我们找到了烟熏炉和营火的配方生成器的写法,而照着offerBlastingofferSmelting的写法,我们可以写出营火的配方生成器

1
2
3
4
public static void offerCampfireCooking(RecipeExporter exporter, List<ItemConvertible> inputs, RecipeCategory category, ItemConvertible output, float experience, int cookingTime, String group) {
RecipeProvider.offerMultipleOptions(exporter, RecipeSerializer.CAMPFIRE_COOKING, CampfireCookingRecipe::new,
inputs, category, output, experience, cookingTime, group, "_from_campfire_cooking");
}

无非将高炉和熔炉里的参数换成了营火的参数

然后我们就可以使用这个方法来添加我们的营火配方了

1
2
offerCampfireCooking(exporter, ICE_ETHER_LIST, RecipeCategory.MISC, ModItems.ICE_ETHER,
0.35f, 600, "ice_ether");

不过,我到后来才发现,其实还有更简单的方法,那就是直接使用offerFoodCookingRecipe方法

1
2
offerFoodCookingRecipe(exporter, "campfire_cooking", RecipeSerializer.CAMPFIRE_COOKING, CampfireCookingRecipe::new,
600, ModItems.RAW_ICE_ETHER, ModItems.ICE_ETHER, 0.35f);

而只是将其中的形参换成了具体的实参而已。
当然,如果你有大量的配方需要添加,我的建议还是提炼个方法出来。
不然每次都要写一大堆,不仅麻烦,而且容易出错

举一反三

前面的教程举了另外两个有序合成和无序合成的例子,这里我们也来写一下

1
2
3
4
5
ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, Items.SUGAR,3)
.pattern("###")
.input('#', Ingredient.ofItems(Items.BEETROOT))
.criterion("has_item", RecipeProvider.conditionsFromItem(Items.BEETROOT))
.offerTo(exporter, Identifier.of(TutorialMod.MOD_ID, "beetroot_to_sugar"));

这里我们使用ShapedRecipeJsonBuilder来创建有序合成配方

pattern是合成表格,这里只有一行,如果是一个完整的九宫格,那么就是像下面这样

1
2
3
.pattern("###"
.pattern("###"
.pattern("###"

input是输入,也就是你九宫格中单字符对应的物品,这里我们使用#来代表BEETROOT
而有多个字符时,也是多写几个input即可

criterion是条件,这个属于隐形进度,也就是不同于“石器时代”、“获得升级”这种有类似于任务书一样可见进度。
配方在游戏中是按照你游戏时触发的条件解锁的,比如你入水了,会解锁和船有关系的配方;获得了原木,则会解锁和木头相关的配方等等。
数据生成中是必须写的

offerTo是输出,里面的Identifier是配方的命名空间id,命名空间不写的话是默认在minecraft下的

1
2
3
4
5
6
7
```json
ShapelessRecipeJsonBuilder.create(RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_ORE)
.input(ModItems.RAW_ICE_ETHER)
.input(Items.STONE)
.criterion("has_item", RecipeProvider.conditionsFromItem(ModItems.RAW_ICE_ETHER))
.criterion("has_item", RecipeProvider.conditionsFromItem(Items.STONE))
.offerTo(exporter, Identifier.of(TutorialMod.MOD_ID, "ice_ether_ore"));

这里我们使用ShapelessRecipeJsonBuilder来创建无序合成配方

input是输入,这里我们有两个输入,一个是RAW_ICE_ETHER,一个是STONE,最多也不能超过9个

criterion是条件,这里我们有两个条件,一个是RAW_ICE_ETHER,一个是STONE,这里的条件和有序合成一样,也是必须写的.
不过是写一个也行,写多个也行,这里的话,只要有一个条件满足,那么这个配方就会解锁

offerTo是输出,不再赘述

注册数据生成器

噼里啪啦写了一堆,一共是7个类(实际上视频教程只有6个,中文的语言文件生成器没写),但现在我们还要注册这些数据生成器,不然它们是不会生效的

我们找到TutorialModDataGenerator,在它的onInitializeDataGenerator注册我们的数据生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TutorialModDataGenerator implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();

pack.addProvider(ModBlockTagsProvider::new);
pack.addProvider(ModItemTagsProvider::new);
pack.addProvider(ModENUSLanProvider::new);
pack.addProvider(ModZHCNLanProvider::new);
pack.addProvider(ModBlockLootTableProvider::new);
pack.addProvider(ModModelsProvider::new);
pack.addProvider(ModRecipesProvider::new);
}
}

当然了,这里我们把fabricDataGenerator.createPack()拿出来了,单独赋给了一个Pack,这样我们就可以使用pack来添加我们的数据生成器了

然后确保我们的所有数据生成器都是public的,不然是无法正确引用的

运行Data Generation

找到右上角Minecraft Client的下拉框,选择Data Generation,然后等待数据生成完成即可

或者你也可以找GradleTasks/fabric中的runDatagen,也是可以的

数据生成的速度很快,基本上几十毫秒就可以搞定,不过如果你的模组内容很多,那么可能会慢一点

这样想想,假设你和我一样有着千百个模型,纯手写这些数据文件,估计Minecraft都更新了好几代了

注意

另外,请在正式运行游戏之前,将原本你写的这些数据文件删除,不然会出现重复的数据文件,导致游戏无法正常运行

当然你不放心的话,可以先找个地方放一下,然后运行游戏,看看是否正常

之后的教程除了特殊情况,基本上会采用数据生成来写数据文件,这样不仅方便,而且还能够减少出错的概率