本篇教程的视频:

本篇教程源代码

GitHub地址:

注意事项!!!

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

介绍

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

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

另外,数据生成这个东西,不仅仅可以使用这里教程中的方法,你可以使用其他的json生成器,也可以使用其他的语言来编写(比如Python)

Tags文件数据生成

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

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

原版的Tag生成器可见TagProvider

方块Tags文件

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

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

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

@Override
protected void addTags(HolderLookup.Provider provider) {

}
}

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

1
2
3
4
5
6
7
8
@Override
protected void addTags(HolderLookup.Provider provider) {
valueLookupBuilder(BlockTags.MINEABLE_WITH_PICKAXE)
.add(ModBlocks.RAW_ICE_ETHER_BLOCK)
.add(ModBlocks.ICE_ETHER_ORE);
valueLookupBuilder(BlockTags.NEEDS_IRON_TOOL)
.add(ModBlocks.ICE_ETHER_ORE);
}

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

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

物品Tags文件

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

1
2
3
4
5
6
7
8
9
10
public class ModItemTagsProvider extends FabricTagProvider.ItemTagProvider {
public ModItemTagsProvider(FabricDataOutput output, CompletableFuture<HolderLookup.WrapperLookup> completableFuture) {
super(output, completableFuture);
}

@Override
protected void addTags(HolderLookup.Provider provider) {

}
}

我们暂时还没有写物品的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<HolderLookup.WrapperLookup> registryLookup) {
super(dataOutput, "en_us", registryLookup);
}

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

}
}

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

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

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

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", "Tutorial Tab");
translationBuilder.add("itemGroup.tutorial2", "Tutorial Tab2");
}

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

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<HolderLookup.WrapperLookup> registryLookup) {
super(dataOutput, "zh_cn", registryLookup);
}

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

}
}

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

战利品列表数据生成

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

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

1
2
3
4
5
6
7
8
9
10
public class ModBlockLootTablesProvider extends FabricBlockLootTableProvider {
public ModBlockLootTablesProvider(FabricPackOutput packOutput, CompletableFuture<HolderLookup.Provider> registriesFuture) {
super(packOutput, registriesFuture);
}

@Override
public void generate() {

}
}

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

1
2
3
4
5
6
@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方法是只掉落一个物品的,如果你要掉落多个物品,那么我们就要去看铜矿石的掉落列表(青金石、红石亦可),然后自己写了

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

1
2
3
4
5
6
7
8
9
10
11
12
public LootTable.Builder createCopperOreDrops(final Block block) {
RegistryLookup<Enchantment> enchantments = this.registries.lookupOrThrow(Registries.ENCHANTMENT);
return this.createSilkTouchDispatchTable(
block,
(Builder<?>)this.applyExplosionDecay(
block,
LootItem.lootTableItem(Items.RAW_COPPER)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(2.0F, 5.0F)))
.apply(ApplyBonusCount.addOreBonusCount(enchantments.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 createCopperOreLikeDrops(final Block block, Item item) {
HolderLookup.RegistryLookup<Enchantment> enchantments = this.registries.lookupOrThrow(Registries.ENCHANTMENT);
return this.createSilkTouchDispatchTable(
block,
(LootPoolEntryContainer.Builder<?>)this.applyExplosionDecay(
block,
LootItem.lootTableItem(item)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(2.0F, 5.0F)))
.apply(ApplyBonusCount.addOreBonusCount(enchantments.getOrThrow(Enchantments.FORTUNE)))
)
);
}

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

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

1
add(ModBlocks.ICE_ETHER_ORE, createCopperOreLikeDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER));

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

模型文件数据生成

创建ModModelsProvider类,继承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
@Override
public void generateBlockStateModels(BlockModelGenerators blockModelGenerators) {
blockModelGenerators.createTrivialCube(ModBlocks.ICE_ETHER_BLOCK);
blockModelGenerators.createTrivialCube(ModBlocks.RAW_ICE_ETHER_BLOCK);
blockModelGenerators.createTrivialCube(ModBlocks.ICE_ETHER_ORE);
}

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

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

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

1
2
3
4
5
6
@Override
public void generateItemModels(ItemModelGenerators itemModelGenerators) {
itemModelGenerators.generateFlatItem(ModItems.ICE_ETHER, ModelTemplates.FLAT_ITEM);
itemModelGenerators.generateFlatItem(ModItems.RAW_ICE_ETHER, ModelTemplates.FLAT_ITEM);
itemModelGenerators.generateFlatItem(ModItems.CARDBOARD, ModelTemplates.FLAT_ITEM);
}

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

配方文件数据生成

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

原版配方生成器可见RecipeProvider

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

@Override
protected RecipeProvider createRecipeProvider(HolderLookup.Provider provider, RecipeOutput recipeOutput) {
return null;
}

@Override
public String getName() {
return "recipe gen";
}
}

createRecipeProvider方法中,我们需要先实例化一个RecipeProvider,并重写buildRecipes方法

这是相比较之前1.21.1的教程不一样的地方

1
2
3
4
5
6
7
8
9
@Override
protected RecipeProvider createRecipeProvider(HolderLookup.Provider provider, RecipeOutput recipeOutput) {
return new RecipeProvider(provider, recipeOutput) {
@Override
public void buildRecipes() {

}
};
}

合成配方

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

1
nineBlockStorageRecipes(RecipeCategory.MISC, ModItems.ICE_ETHER, RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_BLOCK);

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

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

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

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

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

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

熔炼配方

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

1
private static final List<ItemLike> ICE_ETHER_LIST = List.of(ModItems.RAW_ICE_ETHER, ModBlocks.ICE_ETHER_ORE);

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

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

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

现在我们来写熔炼配方

1
2
oreSmelting(ICE_ETHER_LIST, RecipeCategory.MISC, CookingBookCategory.MISC, ModItems.ICE_ETHER, 0.7f, 200, "ice_ether");
oreBlasting(ICE_ETHER_LIST, RecipeCategory.MISC, CookingBookCategory.MISC, ModItems.ICE_ETHER, 0.7f, 100, "ice_ether");

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

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

营火配方

营火配方也很简单

1
2
simpleCookingRecipe("campfire_cooking", CampfireCookingRecipe::new,
600, ModItems.RAW_ICE_ETHER, ModItems.ICE_ETHER, 0.35f);

这里面的参数也显而易见了吧,就不解释了

举一反三

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

1
2
3
4
5
shaped(RecipeCategory.MISC, Items.SUGAR, 3)
.pattern("###")
.define('#', Items.BEETROOT)
.unlockedBy("has_item", has(Items.BEETROOT))
.save(recipeOutput);

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

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

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

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

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

相关内容会生成在advancement文件夹中

注意!数据生成中是必须写这个解锁条件的

save是输出方法

1
2
3
4
5
6
shapeless(RecipeCategory.BUILDING_BLOCKS, ModBlocks.ICE_ETHER_ORE)
.requires(ModItems.RAW_ICE_ETHER)
.requires(Items.STONE)
.unlockedBy("has_item", has(ModItems.RAW_ICE_ETHER))
.unlockedBy("has_item_stone", has(Items.STONE))
.save(recipeOutput);

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

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

unlockedBy是条件,这里我们有两个条件,一个是RAW_ICE_ETHER,一个是STONE,这里的条件和有序合成一样,也是必须写的

不过是写一个也行,写多个也行,这里的话,只要有一个条件满足,那么这个配方就会解锁

save是输出方法,不再赘述

注册数据生成器

噼里啪啦写了一堆,一共是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(ModBlockLootTablesProvider::new);
pack.addProvider(ModBlockTagsProvider::new);
pack.addProvider(ModEnUsLangProvider::new);
pack.addProvider(ModItemTagsProvider::new);
pack.addProvider(ModModelsProvider::new);
pack.addProvider(ModRecipesProvider::new);
pack.addProvider(ModZhCnLangProvider::new);
}
}

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

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

运行Data Generation

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

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

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

这样想想,假设你和我一样有着千百个模型,纯手写这些数据文件,那得写到猴年马月去了

注意

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

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

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