数据生成 1.21 Fabric
本篇教程的视频:
本篇教程源代码
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
函数时,参数选择FabricDataOutput
和CompletableFuture<RegistryWrapper.WrapperLookup>
这两个,不要选择三个参数的,那第三个参数是给ItemTag
的
1 | public class ModBlockTagsProvider extends FabricTagProvider.BlockTagProvider { |
在configure
方法中,我们可以写入我们的Tags文件,这里我们写之前战利品列表中写过的文件
1 |
|
这里我们使用getOrCreateTagBuilder
方法来获取一个Tags文件,然后使用add
方法来添加我们的方块
我们之前写的Tag
是属于BlockTags
,所以这里我们使用BlockTags
中的相关字段来获取我们的Tags文件
物品Tags文件
创建ModItemTagsProvider
类,继承FabricTagProvider.ItemTagProvider
,并实现super
函数和configure
方法
1 | public class ModItemTagsProvider extends FabricTagProvider.ItemTagProvider { |
我们暂时还没有写物品的Tags文件,所以就简单创建一下即可,后面我们会写的
语言文件数据生成
在之前我们也写了语言文件,这里我们也可以使用数据生成来生成语言文件。不过,原版是没有语言文件生成器的
en_us
创建ModENUSLanProvider
类,继承FabricLanguageProvider
,并实现super
函数和generateTranslations
方法
1 | public class ModENUSLanProvider extends FabricLanguageProvider { |
这里的super
可以在中间加入一个"en_us"
,这个是语言文件的文件名,如果你要写其他语言文件,可以在这里修改。
不写的话,默认就是en_us
然后在generateTranslations
方法中,我们可以写入我们的语言文件
1 |
|
这里我们使用add
方法来添加我们的语言文件,第一个参数是id
,第二个参数是翻译
的值。add
能够接受的参数很多,比如Item
、Block
、String
等等
其实这里的物品栏,如果你是像原版一样写的,单独为物品栏注册了RegistryKey
,那也可以直接引用这个RegistryKey
来写入语言文件
zh_cn
中文的语言文件也是一样的,只是文件名不一样,我们创建ModZHCNLanProvider
类,继承FabricLanguageProvider
,并实现super
函数和generateTranslations
方法。
然后super
中的参数改为"zh_cn"
,这个是中文的文件名
1 | public class ModZHCNLanProvider extends FabricLanguageProvider { |
翻译的东西这里就不再赘述了,和上面的en_us
一样
战利品列表数据生成
创建ModBlockLootTableProvider
类,继承FabricBlockLootTableProvider
(注意中间有Block
),并实现super
函数和generate
方法
原版生成器可见BlockLootTableGenerator
,不过注意,我们这里只讲了方块的战利品列表
1 | public class ModBlockLootTableProvider extends FabricBlockLootTableProvider { |
在generate
方法中,我们可以写入我们的战利品列表
1 |
|
这里我们使用addDrop
方法来添加我们的战利品列表,这里单个参数的话是默认掉落其本身
而矿石的掉落是需要额外再使用oreDrops
方法来添加的,这个方法接受两个参数,第一个是矿石,第二个是掉落物品
值得一提的是,这个oreDrops
方法是只掉落一个物品的,如果你要掉落多个物品,那么我们就要去看铜矿石的掉落列表(青金石、红石亦可),然后自己写了
我们到BlockLootTableGenerator
中可以看到原版的战利品列表的各种生成方法,我们找到copperOreDrops
方法
1 | public LootTable.Builder copperOreDrops(Block drop) { |
这里我们可以看到,掉落的是Items.RAW_COPPER
,掉落的数量是2-5
,而且还有FORTUNE
附魔(时运)的加成(当然,还有SilkTouch
(精准采集)的附魔)。
这个方法我们并不能直接拿来用,因为我们的矿石和掉落物品不一样,所以我们要自己写
其实搬过来,自己改写一下即可
1 | public LootTable.Builder copperOreLikeDrops(Block drop, Item dropItem) { |
这里我们只是将Items.RAW_COPPER
改为了dropItem
,并添加了一个形参Item dropItem
,这样我们就可以自定义掉落物品了。
而附魔相关的东西,你可以自行探究
随后,我们就可以使用这个方法来添加我们的掉落物品了
1 | addDrop(ModBlocks.ICE_ETHER_ORE, copperOreLikeDrops(ModBlocks.ICE_ETHER_ORE, ModItems.RAW_ICE_ETHER)); |
这样,我们的矿石也就可以掉落多个物品了
模型文件数据生成
创建ModModelProvider
类,继承FabricModelProvider
,并实现super
函数、generateBlockStateModels
和generateItemModels
方法
原版模型生成器可见ModelProvider
1 | public class ModModelsProvider extends FabricModelProvider { |
这里我们重写的两个方法,一个是生成方块状态模型,一个是生成物品模型。
生成方块状态文件的同时,对应的物品模型也会生成,所以我们只需要写方块状态文件即可
在generateBlockStateModels
方法中,我们可以写入我们的方块状态文件
1 |
|
我们之前写的都是最简单的方块状态文件,也就是立方体的六个面都一样,所以我们使用registerSimpleCubeAll
方法来注册我们的方块状态文件
这里的registerSimpleCubeAll
方法接受一个参数,就是我们的方块
在generateItemModels
方法中,我们可以写入我们的物品模型文件
1 |
|
这里我们使用register
方法来注册我们的物品模型文件,第一个参数是我们的物品,第二个参数是我们的模型的渲染模式,这里我们使用了Models.GENERATED
,这个是一般物品的渲染模式(后面讲到工具时,我们还将采用另外的)
配方文件数据生成
创建ModRecipeProvider
类,继承FabricRecipeProvider
,并实现super
函数和generate
方法
原版配方生成器可见RecipeProvider
1 | public class ModRecipesProvider extends FabricRecipeProvider { |
在generate
方法中,我们可以写入我们的配方文件
合成配方
这里我们先写之前写的1个方块合成9个物品的配方及其逆配方,这个用一个方法即可完成两个配方
1 | offerReversibleCompactingRecipes(exporter, RecipeCategory.MISC, ModItems.ICE_ETHER, |
这里我们使用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_ETHER
和ICE
熔炼后会得到同一个产物
如果你要得到不同的产物,那么你就要创建多个List
,然后在generate
方法中写入多个熔炼配方
现在我们来写熔炼配方
1 | offerSmelting(exporter, ICE_ETHER, RecipeCategory.MISC, ModItems.ICE_ETHER, |
这里我们使用offerSmelting
方法来添加我们的熔炉配方,
而offerBlasting
方法来添加我们的高炉配方
这两个方法的参数本质上是一样的,其中它们的第二个参数是我们的List
,
第三个参数是配方的分类
,第四个参数是配方的产物
,
第五个参数是熔炼经验
,第六个参数是熔炼时间
,
第七个参数是配方所属的组
营火配方
前面我们没讲营火配方,但曾经有人问过营火的配方怎么使用数据生成,很简单啦,看源代码先
可能你会发现,怎么没有类似于高炉和熔炉的方法呢?没有offerCampfire
啥啥啥的。
但是,我们能发现一个offerFoodCookingRecipe
的方法,这个方法是用来添加食物的烹饪配方的,而绝大多数的食物不仅有高炉和熔炉的配方,还有营火的配方(以及烟熏炉)
那好了,这个东西我们能不能挖出点什么来呢?我们先看看其中一个例子
1 | offerFoodCookingRecipe(exporter, cooker, serializer, recipeFactory, cookingTime, Items.BEEF, Items.COOKED_BEEF, 0.35F); |
它是generateCookingRecipes
方法下的一条语句,而除了后面的具体的物品,前面那一堆直接使用形参的是什么呢?
这里我们进一步溯源,一路找到VanillaRecipeProvider
中,它有这么两条语句
1 | generateCookingRecipes(exporter, "smoking", RecipeSerializer.SMOKING, SmokingRecipe::new, 100); |
So?我们找到了烟熏炉和营火的配方生成器的写法,而照着offerBlasting
和offerSmelting
的写法,我们可以写出营火的配方生成器
1 | public static void offerCampfireCooking(RecipeExporter exporter, List<ItemConvertible> inputs, RecipeCategory category, ItemConvertible output, float experience, int cookingTime, String group) { |
无非将高炉和熔炉里的参数换成了营火的参数
然后我们就可以使用这个方法来添加我们的营火配方了
1 | offerCampfireCooking(exporter, ICE_ETHER_LIST, RecipeCategory.MISC, ModItems.ICE_ETHER, |
不过,我到后来才发现,其实还有更简单的方法,那就是直接使用offerFoodCookingRecipe
方法
1 | offerFoodCookingRecipe(exporter, "campfire_cooking", RecipeSerializer.CAMPFIRE_COOKING, CampfireCookingRecipe::new, |
而只是将其中的形参换成了具体的实参而已。
当然,如果你有大量的配方需要添加,我的建议还是提炼个方法出来。
不然每次都要写一大堆,不仅麻烦,而且容易出错
举一反三
前面的教程举了另外两个有序合成和无序合成的例子,这里我们也来写一下
1 | ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, Items.SUGAR,3) |
这里我们使用ShapedRecipeJsonBuilder
来创建有序合成配方
pattern
是合成表格,这里只有一行,如果是一个完整的九宫格,那么就是像下面这样
1 | .pattern("###") |
input
是输入,也就是你九宫格中单字符
对应的物品,这里我们使用#
来代表BEETROOT
;
而有多个字符时,也是多写几个input
即可
criterion
是条件,这个属于隐形进度,也就是不同于“石器时代”、“获得升级”这种有类似于任务书一样可见进度。
配方在游戏中是按照你游戏时触发的条件解锁的,比如你入水了,会解锁和船有关系的配方;获得了原木,则会解锁和木头相关的配方等等。数据生成中是必须写的
offerTo
是输出,里面的Identifier
是配方的命名空间
和id
,命名空间不写的话是默认在minecraft
下的
1 | ```json |
这里我们使用ShapelessRecipeJsonBuilder
来创建无序合成配方
input
是输入,这里我们有两个输入,一个是RAW_ICE_ETHER
,一个是STONE
,最多也不能超过9个
criterion
是条件,这里我们有两个条件,一个是RAW_ICE_ETHER
,一个是STONE
,这里的条件和有序合成一样,也是必须写的.
不过是写一个也行,写多个也行,这里的话,只要有一个条件满足,那么这个配方就会解锁
offerTo
是输出,不再赘述
注册数据生成器
噼里啪啦写了一堆,一共是7个类(实际上视频教程只有6个,中文的语言文件生成器没写),但现在我们还要注册这些数据生成器,不然它们是不会生效的
我们找到TutorialModDataGenerator
,在它的onInitializeDataGenerator
注册我们的数据生成器
1 | public class TutorialModDataGenerator implements DataGeneratorEntrypoint { |
当然了,这里我们把fabricDataGenerator.createPack()
拿出来了,单独赋给了一个Pack
,这样我们就可以使用pack
来添加我们的数据生成器了
然后确保我们的所有数据生成器都是public
的,不然是无法正确引用的
运行Data Generation
找到右上角Minecraft Client
的下拉框,选择Data Generation
,然后等待数据生成完成即可
或者你也可以找Gradle
中Tasks/fabric
中的runDatagen
,也是可以的
数据生成的速度很快,基本上几十毫秒就可以搞定,不过如果你的模组内容很多,那么可能会慢一点
这样想想,假设你和我一样有着千百个模型,纯手写这些数据文件,估计Minecraft都更新了好几代了
注意
另外,请在正式运行游戏之前,将原本你写的这些数据文件删除,不然会出现重复的数据文件,导致游戏无法正常运行
当然你不放心的话,可以先找个地方放一下,然后运行游戏,看看是否正常
之后的教程除了特殊情况,基本上会采用数据生成来写数据文件,这样不仅方便,而且还能够减少出错的概率