基于FML的MinecraftMod制作教程(3) – 创建新的砖块,物品和冶炼

上一章我们创建了一个基于ModLoader的mod,现在我们要来为它添加功能.

本章我们要进行:
创建一个新的矿物(砖块):Diracium
创建一个新的矿锭(物品):Diracium Ingot
创建一个新的工具(物品):Dirac Omni Tool

创建一个新的砖块

知识点:创建一个新砖块
///////////////////////////////////////////////////////////////////////////////////////////////////
在旧教程中,这一部分几乎是一笔带过的,然而现在我却费了很大功夫,反复思考该如何阐述清这个概念,因为Minecraft的砖块机制跟很多新人想象中的不一样.这里我只能简述一些,更详细的原理会在别处讨论.

Minecraft中,一个Block的子类的实例即一种砖块,比如游戏中的石砖就是BlockStone类的一个实例,而BlockStone类又继承自Block类.若一个砖块类有多个实例的话,那么每个实例都代表一种独立的砖块,比如铁矿和煤矿都是BlockOre类的实例,它们也拥有独特的砖块ID.

如果难以理解这种设计的话,不妨逆向思考,假如让你设计Minecraft,考虑到游戏内会有数十万个砖块同时存在,而这些砖块都可根据其特性分为几类,那么你该如何储存这些数据?一个很好的办法是创建这几类砖块的"原型"(或"模板"),然后游戏中只储存每个砖块对应哪个"原型".这样资源占用远比每个砖块都储存完整的数据要小.所以上文中创建一个实例就相当于创建一个"原型".

第一步:创建砖块类

理论上讲,砖块类并不是必须的,如果只是想实现一种无功能的装饰性砖块的话,直接创建Block类的实例就行了,然而在大部分情况下我们都会为每种或多种相似特性的新砖块创建一个砖块类,创建一个砖块类就是创建一个类并继承Block类,同时在调用父类构造函数时传入材质类型,比如:

 public class MyBlock extends Block{
 public MyBlock(Material material) {
 super(material);
 }

第二步:创建砖块实例

在创建实例之前,我们还需要一个能记录该实例的字段(否则在别处你该如何找到你创建的新砖块呢),比如在Mod主类中添加:

public static MyBlock myBlock;

这个静态公共变量myBlock便代表你的新砖块.然后要开始准备初始化砖块了,初始化砖块要在Mod预初始化(preLoad)时进行,在Mod主类的preLoad方法(即一个拥有@EventHandler,拥有一个FMLPreInitializationEvent参数的方法)中添加:

final String myModId = "diracon";
final String myBlockId = "myblock";
myBlock = new MyBlock(Material.ROCK);
myBlock.setUnlocalizedName(myModId + "." + myBlockId);
myBlock.setRegistryName(myModId, myBlockId);
myBlock.setCreativeTab(CreativeTabs.BUILDING_BLOCKS);
GameRegistry.register(myBlock);

这包含了创建一个砖块的全部过程,myModId代表Mod的ID;myBlockId代表砖块的ID,砖块ID在Mod内不能重复,也就是说同一个Mod内不能有两个砖块具有相同的ID.
new MyBlock(Material.rock)是创建一种基于MyBlock类的材质类型为石材质的砖块.
setUnlocalizedName是设置砖块在语言文件中的键,在游戏中系统会根据这个键从相应的语言文件中找到砖块的名称.为了避免多个Mod间潜在的键冲突问题,我们这里将ModID也作为键的一部分,将冲突的可能性降到最低.
setRegistryName是设置砖块ID,setRegistryName包含两个参数,第一个参数是ModID,第二个参数是砖块ID.
setCreativeTab是设置在创造模式中它在哪个菜单分类里.
GameRegistry.registerBlock是注册一个砖块,在过去(1.8之前)完成这一步便代表砖块创建完毕了,然而在1.9中,还有额外的操作要完成,比如创建砖块对应的物品.

顺便一提,利用生成器模式(Builder,设计模式的一种),以上一堆代码可以被简写为.

final String myModId = "diracon";
final String myBlockId = "myblock";
myBlock = (MyBlock)GameRegistry.register(
	new MyBlock(Material.ROCK).setUnlocalizedName(myModId+"."+myBlockId)
				.setRegistryName(myModId, myBlockId)
				.setCreativeTab(CreativeTabs.BUILDING_BLOCKS));

第三步:创建砖块对应的物品

为什么创建砖块还要再创建一个物品?不要忘了砖块不光是放在地上的,还有被玩家拿在手里,装在包里的,Block类仅仅是指放在地上的.被玩家捡起来带在身上的是物品,不属于Block类的管辖范畴.在MC1.8之前创建砖块对应的物品是由FML自动完成的,但在1.9中需要开发者手动实现(FML也提供了一个自动创建的备用方案,然而那个...并不太靠谱,也不是"正规"的做法)
由于还未讲到物品,因此这里只简单说明一下,一种砖块对应的物品是一个ItemBlock类的实例,比如:

GameRegistry.register(new ItemBlock(myBlock).setRegistryName(myModId, myBlockId));

就是为myBlock砖块创建对应的物品.

第四步:语言文件

第五步:砖块模型

这两步先待会再说,我们先演示一个前三步的实例:制作DiracOre.

至面向对象编程控们:
也许你会想当然的认为Minecraft中每一个砖块就是它的类的实例,可事实上如果这样做,地图上同时出现的几万个类的实例会把Java虚拟机爆掉的,别忘了Java可没结构体(Struct)啊...所以Minecraft中的砖块类的实质比较倾向于享元模式,一个砖块类的实例仅代表一种砖块,你在游戏中看到的一个个砖块只是在地图数据中批量储存的砖块ID而已...
另外你想知道为什么砖块要被注册后才能使用吗?想知道注册砖块时都做了什么吗?在物品部分我会解答...
///////////////////////////////////////////////////////////////////////////////////////////////////

在你的package中新建一个类,Minecraft对Block类的命名规范是BlockXXX,所以我取名叫BlockDiracOre.
C1
创建完毕后,添加这些代码.

public class BlockDiracOre extends Block {
	
	public BlockDiracOre() {
		super(Material.ROCK);
		setHardness(1.5f);
		setResistance(10.0f);
		setLightLevel(0.0f);
		setHarvestLevel("pickaxe", 0);
		setSoundType(SoundType.STONE);	
	}
}

这一次我们在构造函数中设置了砖块的一些特殊属性,如:
setHardness是设置砖块的硬度,这个硬度是相对于徒手而言的,泥土是0.5,石头是1.5,大部分矿石是3.0.
setResistance是设置对爆炸的抗性,石头是10.0.
setLightLevel是设置发光亮度,范围是0.0~1.0,南瓜灯,萤石和岩浆是1.0,通往下界的传送门是0.75.采集中的红石是0.625.
setHarvestLevel是设置开采砖块时需要使用的工具,可以是"pickaxe"(镐), "shovel"(铲子)或"axe"(斧头).后面的数值为工具材质要求,-1(默认值)为可以直接手撕,0是木质和金质,1是石质,2是铁质,3是钻石质.
setSoundType是设置踩在上面的脚步声.默认值就是SoundType.STONE(石头地的声音.) 这里是为了演示这个方法的用途.

另外,上述除setSoundType以外的方法,以及之前提到的setUnlocalizedName、setRegistryName和setCreativeTab,都是既可以在类中调用,也可以是由外部调用,这意味着你可以选择将它们都写在类的构造函数中,也可以选择都写在Mod主类的preLoad中,也可以像我这样选择混合使用,这纯粹是个设计理念的问题.

这一次Eclipse也会报错说没有导入相关的Package,让它自动修正.不过需要注意的是,我们使用的Block是net.minecraft.block包中的Block.

B3-1

然后在Mod主类中添加

private static final String MODID = "diracon";
private static final String DIRACORE = "diracOre";
public static BlockDiracOre diracBlock;

在preLoad方法中添加

diracBlock = new BlockDiracOre();
diracBlock.setUnlocalizedName(MODID + "." + DIRACORE);
diracBlock.setRegistryName(MODID, DIRACORE);
diracBlock.setCreativeTab(CreativeTabs.BUILDING_BLOCKS);
GameRegistry.register(diracBlock);
GameRegistry.register(new ItemBlock(diracBlock).setRegistryName(MODID, DIRACORE));

B3-4

现在我们创建了一个砖块,但还有个问题,它没有模型和语言文件!我们接下来得为他制作模型文件.

知识点:模型、素材集与Blockstate
///////////////////////////////////////////////////////////////////////////////////////////////////
老MODer应该会知道Minecraft在1.5.0以前采用纹理集的形式储存纹理,在1.5.0之后,Minecraft改为使用单个文件储存纹理,对开发者而言确实方便了不少.因此这个知识点也缩水了许多.
不过呢,从1.8开始MC开始使用外置的模型文件,又让这个知识点的内容膨胀了回来...

从Minecraft1.6开始,MC的资源文件都被放入了assets目录内,FML则为了防止文件之间冲突,对应引入了素材集的设定,素材集就是assets文件夹下的目录,比如一个叫diracon的素材集就是assets/diracon文件夹.强烈建议将ModID作为素材集的名称.

B3-5
[图:无比丧心病狂的文件架结构]

最初Minecraft的砖块所必需的外置资源只有纹理贴图,渲染代码是被硬编码到程序中的,然而从1.8开始,MC引入了外置的模型文件的概念,砖块和物品的渲染现在完全由一段外置的json文件来控制,这也就是设计上所谓的"数据驱动(Data Drive)",通过外置的数据文件来控制程序的行为,而不是在代码中硬编码.这样做的好处大概是能实现美术和程序的分离,同时让代码更加规整简洁,坏处嘛...就是现在即使想创建一种最简单的新砖块也至少需要创建3个新的外部文件(不包括纹理),跟过去只需要一行代码就能指定纹理相比麻烦了很多.

对于砖块来说,还有一个问题就是如何实现拥有多种外观的子砖块,比如石头现在就有普通、花岗岩、闪长岩、安山岩等多种外观,在1.8之前子砖块通过Metadata来区分,Metadata是一个范围在0~15的半字节整数,它是一个普通砖块所能存储的唯一的自定义信息,在1.8启用了数据驱动的渲染后,就面临一个问题是如何根据砖块的Metadata为它指定不同的模型,MC采用的方案是引入Blockstate系统,Blockstate(砖块状态)是对Metadata的一个封装,Minecraft通过一个外部文件,根据一个砖块的Blockstate来为它指定不同的渲染模型.对于Blockstate的详细解释将放在本篇附录,现在先可以跳过.

介绍完这几个名词后,就可以说明创建砖块模型的步骤了:

第一步:创建Blockstate文件

上文提到Minecraft通过一个文件来决定一个砖块在不同的状态下该使用何种模型进行渲染,这是一个Json文件,名称为"[砖块id].json",放置在"src/main/resources/assets/[素材集]/blockstates/"文件夹中,它的格式详解会放在附录中,这里只说明对于一个只有一种状态的砖块,它的Blockstate文件内容是:

{
    "variants": {
        "normal": { "model": "[素材集]:[模型名]" }
    }
}

其中"normal"代表一个没有任何特殊状态的砖块的默认状态.

第二步:创建砖块的模型文件

接下来就要创建描述砖块该如何渲染的模型文件了,模型文件同样是个Json文件,名称为"[模型名].json",砖块的模型放置在"src/main/resources/assets/[素材集]/models/block/"文件夹中,物品的模型放置在"src/main/resources/assets/[素材集]/models/item/"文件夹中,它的格式详解同样在本章附录里,这里先简要介绍一下它的特点.

想要从零设计一个模型十分复杂,不过幸好模型拥有继承机制,继承机制可以让一个模型继承父模型的所有属性,并重写特定属性,这里我们使用的父模型是"block/cube_all",所有面都是一个样子的砖块.
(注意,在Blockstate文件中指定模型时可以省略路径,因为它会强制指定为models/block中的文件,而在模型文件中引用其他模型时,需要填写以models文件夹为根目录的相对路径,比如在Blockstate中指定model1和model2两个位于models/block中的砖块模型的话,直接填写"[素材集]:model1"和"[素材集]:model2"就行了.但是当在model2中引用model1,比如将model1作为父模型时,需要填写"[素材集]:block/model1")

对于一个所有面都是一个纹理的模型,模型文件的内容是:

{
	"parent": "block/cube_all",
	"textures": {
		"all": "[素材集]:[纹理名]"
	}
}

第三步:创建砖块对应的物品的模型文件

我们已经创建完了砖块的模型文件,然而之前提到砖块和砖块的物品是严格区分开的两个东西,因此我们还得为砖块的物品创建模型...其实这完全是多此一举,因为如果你看过Minecraft的block/block.json的话,就会发现所有的跟物品有关的东西都已经在砖块模型中写好了,我们在这里创建的物品模型纯粹是重新引用一遍砖块模型.

这里介绍对于只有一种模型的砖块的创建砖块物品的模型的方法,在"src/main/resources/assets/[素材集]/models/item/"中创建一个名为"[砖块id].json"的文件,内容填上:

{
    "parent": "[素材集]:block/[模型名]"
}

没错,其实就是引用之前的砖块模型...

然后还要在代码中注册砖块物品的下面加上:

ModelLoader.setCustomModelResourceLocation([砖块物品], 0, new ModelResourceLocation([素材集]+":"+[砖块id], "inventory"));

这段代码作用类似于一个硬编码的Blockstate(哈,看来MC还没完全根除硬编码的渲染啊),只不过它是针对物品的,其中第二个参数为物品的"ItemDamage","ItemDamage"其实就是物品版的Metadata,只不过它没有0~15的限制,由于最初是被用于记录物品的耐久度因此被称为ItemDamage,实际上可用来存储任何数据,比如对于砖块物品来说,它就是用来存储该砖块的Metadata的,没有指定特殊状态的砖块的Metadata始终是0,因此这里填0就行了.

第四步:添加纹理

这一步就没有什么特别需要说明的了,把你准备好的纹理丢到"src/main/resources/assets/[素材集]/textures/"中吧.注意纹理使用.png或.jpg格式.

///////////////////////////////////////////////////////////////////////////////////////////////////

首先在src/main/resources/assets/diracon/blockstates/文件夹中创建一个叫diracOre.json的Blockstate文件,你可以直接在IDE中右键src/main/resources,点New->Folder创建文件夹,然后New->File创建任意后缀名的文本文件.

B3-2

在文件中填入:

{
    "variants": {
        "normal": { "model": "diracon:diracOre" }
    }
}

然后在assets/diracon/models/block/文件夹中创建砖块的模型文件diracOre.json,这一次填入:

{
	"parent": "block/cube_all",
	"textures": {
		"all": "diracon:diracOre"
	}
}

然后在assets/diracon/models/item/文件夹中创建砖块物品的模型文件diracOre.json,填入:

{
	"parent": "diracon:block/diracOre"
}

接下来该制作纹理了,用绘图工具(比如PS)随便画一个纹理...我是在官方的矿石的基础上瞎涂的.(我使用的纹理尺寸是16x16,不过1.5.0以后MC已经支持大尺寸的高清纹理了.)

C8

之后将它保存为diracOre.png,并放到src/main/resources/assets/diracon/textures/文件夹中.

A3-4

最后就该在代码中添加砖块物品的模型映射了,在preLoad中加上:

ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(diracBlock), 0, new ModelResourceLocation(MODID + ":" + DIRACORE, "inventory"));

之后我们还要制作语言文件,否则当你进入游戏后,砖块的名字会显示的是tile.diracon.diracOre.name.

知识点:语言文件
///////////////////////////////////////////////////////////////////////////////////////////////////
最初FML使用LanguageRegistry来添加文字内容,然而它有个缺点就是不易进行多语言化.从MC1.7开始,FML改为直接使用语言文件来添加文字内容.

语言文件是一个格式为"[语言名].lang"的UTF-8编码的文本文件,比如en_US.lang是默认英文的语言文件,zh_CN.lang是简体中文的语言文件.

语言文件的位置是在src/main/resources/assets/[ModID]/lang下.

语言文件的格式是按照"键=值"的格式来存储,比如一个键为"tile.myMod.myBlock.name",值为"My Block"的字符串,写为:

tile.myMod.myBlock.name=My Block

注意等号两头不要有多余的空格.

常用的键有:
tile.[砖块名].name
item.[物品名].name
///////////////////////////////////////////////////////////////////////////////////////////////////

在src/main/resources/assets/diracon目录下新建一个lang文件夹,然后在Eclipse中新建一个文件.

A3-2

由于美式英语(en_US)是默认语言,并且是在没有对应的语言文件下的缺省语言,所以通常我们都会准备一个en_US.lang.

A3-5

然后按照格式添加内容.

A3-6

然后还可以添加一个简体中文的语言文件.

A3-7

对于中文的语言文件,还不要忘记检查一下编码是不是UTF-8.

A3-8

那么现在你已经制作完一个完整的砖块了!进入游戏测试一下,在创造模式下可以在普通砖块类别中找到它.

A3-9A3-10

如果你要在生存模式下获取砖块的话,可以用Minecraft自带的控制台指令(别忘了在创建世界时选上Allow Cheats:ON),打开对话框,先随便说一句话获得自己的玩家姓名,然后输入/give [你的名字] [你的物品的id] [数量]

A3-11

在这里顺便说一下ID机制.

知识点:物品与砖块ID
///////////////////////////////////////////////////////////////////////////////////////////////////
事实上,这个部分是写给后入坑(从1.7开始接触Mod开发,甚至是才接触MC的)的人看的.老油条们应该对过去MC的数字ID系统相当熟悉...

原先(1.7以前)MC采用的是数字ID系统,比如石砖的ID是1,原木是17,钻石块是57,铁斧是256+2(至于为什么,待会再说).现在MC的物品和砖块的ID系统采用的是格式为"[命名空间]:[ID]"字符串,比如石砖就是"minecraft:stone".因此在以前如果你要通过游戏指令来给予玩家一个石砖的话,是"/give XXX 1 1",现在则是"/give XXX minecraft:stone 1"或"/give XXX stone 1",如果不指明命名空间的话,默认使用minecraft.

使用文本ID代替数字ID的好处有两个,首先是避免了ID冲突,过去经常会发生两个mod的砖块或物品ID相同的情况,如果提供了配置文件,可供修改ID的话,还可以手动解决,如果没有的话,就只能做个"艰难的选择"了.另外文本ID还引入了命名空间,或者说是强制前缀机制,进一步避免了ID冲突,可以说,现在除非是另一个作者和你过意不去,存心找茬的话,是不会遇到ID冲突的...
第二个避免的坑是物品的ID偏移问题,前文已经提到当你创建一个砖块的同时,还需要创造一个与之对应的物品,曾经MC有这样一个要求:砖块的数字ID必须与砖块的物品对应的数字ID相同,因此在数字ID时代,当你在创建物品时,游戏会偷偷对物品ID进行换算,换算公式就是"实际ID=你设定的物品ID+256",换句话说一个初始化时物品ID设定为1的物品,它真正的ID是257,在游戏里你要输入/give xxx 257 1才能获得那个物品.采用文本ID系统后,开发者和玩家都无需了解这个变态的机制了.

不过需要说明的是,数字ID系统仍未被彻底去除,而是被隐藏了,换句话说,数字ID的分配现在由系统全盘接管了,开发者没法再手动指定;文字ID到数字ID的相互转换也会在幕后自动进行.
///////////////////////////////////////////////////////////////////////////////////////////////////

然而如果你此时尝试启动服务器的话,会出现一个ClassNotFoundError报错,这是因为上文提到的ModelLoader在服务器端并不存在.

知识点:客户端与服务器端
///////////////////////////////////////////////////////////////////////////////////////////////////
严格来说,MC的客户端与服务器端是分离开的,在早年(MCP+ModLoader时代)开发者想制作能在服务器端运行的Mod需要另行开发一份供服务器运行的版本,不用说就知道这很麻烦...FML将客户端和服务器融为了一体,但这就需要一套机制来让Mod在运行时判断哪些代码可以在客户端上运行,哪些代码可以在服务器上运行,这里我们介绍的是代理器(SidedProxy).

用最言简意赅的话解释,"代理器用来在不同的环境下完成不同的工作,Forge会根据当前环境(客户端/服务器端)来挑选合适的代理器"
以文件加载来举例,并不是所有文件在任何时候都需要加载的,图形文件就只有客户端需要加载,代理器(Proxy)的作用便显现出来了,你将加载文件的方案委托给不同的代理器,在实际运行时,Forge会根据当前环境,指派不同的代理器去完成加载任务.

我们主要使用代理器来完成文件加载,目前代理器主要分两种,"通用代理器(或者叫服务器代理器,因为通常来说服务器上需要做的事在客户端里也需要做一遍)"和"客户端代理器",通用代理器加载任何端都需要的文件,客户端代理器在此基础上会额外加载只有客户端需要的文件.

代理器的实现通过@SidedProxy来实现,@SidedProxy是一个Annotation,它有3个参数,但最常用的只有两个:字符串类型的clientSide和serverSide.前者是在客户端中加载的代理器的类,后者是在服务器端中加载的代理器的类.@SidedProxy必须用来修饰一个静态(static)公共变量.比如:

@SidedProxy(clientSide="yourmod.ClientProxy",serverSide="yourmod.CommonProxy")
static public CommonProxy proxy; //其中,ClientProxy派生自CommonProxy

游戏运行时,Forge会判断当前的环境,是客户端的话就实例化yourmod包下的ClientProxy类并赋值给proxy,反之,如果是服务器端的话就实例化CommonProxy类.客户端代理器应派生自通用代理器,并重写基类方法.

如果不手动指定clientSide或serverSide的话,FML会自动加载当前类中的内部类ClientProxy和ServerProxy作为代理器.

此外还有其他手段来实现客户端与服务器端的分离,比如@SideOnly,关于@SideOnly,ACmod的老大WeAthFolD写过一篇详细的介绍http://weathfold.moe/blog/index.php/archives/39/
///////////////////////////////////////////////////////////////////////////////////////////////////

所以接下来我们要制作一个代理器来实现仅在客户端加载模型,在Mod主类中添加两个内部类:ServerProxy和ClientProxy:

@SidedProxy
private static ServerProxy proxy;

public static class ServerProxy {
	public void loadModel() {}
}
	
public static class ClientProxy extends ServerProxy {
	@Override
	public void loadModel() {
		super.loadModel();
		ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(diracBlock), 0, new ModelResourceLocation(MODID + ":" + DIRACORE, "inventory"));
	}
}

其中ClientProxy继承了ServerProxy. loadModel方法用于加载模型,在服务器版本中它什么也不做,在客户端版本中它会为砖块物品加载模型,其中"super.loadModel()"不是必须的,仅仅只是一个习惯.
然后将preLoad中的"ModelLoader.setCustomModelResourceLocation"替换成:

proxy.loadModel();

B3-3

现在你的Mod就可以实现在服务器端正常加载了!启动服务器时依然弹出?这是EULA的锅...将MDK目录中的run/eula.txt里的"eula=false"改成"eula=true".

 

创建一个新的物品

接下来我们要为这个矿创建一个冶炼产物:Diracium Ingot.

知识点:创建一个新物品的流程
///////////////////////////////////////////////////////////////////////////////////////////////////
物品和砖块的原理差不多,同样是一个物品类的实例将会作为一种物品.
一个最简的物品类是这样

public class MyItem extends Item {
}

没错,它甚至连构造函数都不需要...因为Item类的构造函数没有参数,因此这里也可以省略掉,让编译器自动生成一个默认构造函数.

而新建一种物品的代码则是这样,这部分代码添加在你的Mod主类

public static MyItem myItem;

这个静态公共变量myItem便代表你的新物品,然后还要在你的Mod主类的preLoad方法(或者是任意一个拥有@EventHandler并且参数为FMLPreInitializationEvent的方法)中添加

final String myModId = "mymod";
final String myItemId = "myItem";
myItem = new MyItem();
myItem.setUnlocalizedName(myModId  + "." + myItemId );
myItem.setRegistryName(myModId, myItemId);
myItem.setCreativeTab(CreativeTabs.MATERIALS);
GameRegistry.register(myItem);

最后是制作物品的模型文件并注册,大部分物品的模型文件通常是这个格式:

{
    "parent": "item/generated",
    "textures": {
        "layer0": "[素材集]:[纹理名]"
    }
}

最后别忘了在客户端代理器中为物品加载模型:

ModelLoader.setCustomModelResourceLocation(myItem, 0, new ModelResourceLocation(myModId + ":" + myItemId, "inventory"));

///////////////////////////////////////////////////////////////////////////////////////////////////

创建一个叫ItemDiracIngot的类,使它继承Item类.

构造函数采用默认的即可.

A3-12

之后在Mod主类中添加

private static final String DIRACINGOT = "diracIngot";
public static ItemDiracIngot diracIngot;

在preLoad方法中添加

diracIngot = new ItemDiracIngot();
diracIngot.setUnlocalizedName(MODID + "." + DIRACINGOT);
diracIngot.setRegistryName(MODID, DIRACINGOT);
diracIngot.setCreativeTab(CreativeTabs.MATERIALS);
GameRegistry.register(diracIngot);

然后要制作它的模型文件,在src/main/resources/assets/diracon/models/item/中添加文件diracIngot.json,内容为:

{
    "parent": "item/generated",
    "textures": {
        "layer0": "diracon:diracIngot"
    }
}

然后为矿物画一个纹理,我同样是把官方的铁锭给涂紫了…

diracingot

然后保存文件到src/main/resources/assets/diracon/textures/中,纹理名要设为diracIngot.png.接着在语言文件中为它添加名字.最后是在客户端代理器中为它注册模型:

ModelLoader.setCustomModelResourceLocation(diracIngot, 0, new ModelResourceLocation(MODID + ":" + DIRACINGOT, "inventory"));

A3-14

B3-6

完成这些后就可以测试了.

A3-15

 

创建一个工具

工具是一种特殊的物品,它的定义是能对特定的砖块产生挖掘速度加成,出于这个定义,锄头(Hoe)并不属于工具,因为它不会对挖掘砖块产生任何加成.最初,所有工具的物品类都派生自ItemTool类,但如果通过Forge的话,任何一个物品都能被变成工具.

知识点:利用Forge创建工具
///////////////////////////////////////////////////////////////////////////////////////////////////
Forge为Item类添加了setHarvestLevel方法,它可以使此种物品成为一个工具,它的参数是setHarvestLevel(String toolClass, int level),toolClass是工具类型,用字符串来表示,默认已有的类型是"pickaxe"(镐),"shovel"(铲子)和"axe"(斧子),可以自定义新类型.harvestLevel是采矿强度,当工具的harvestLevel大于等于砖块的HarvestLevel时,这个砖块就可以被加速开采,具体增加的速度由工具的材质和砖块的硬度决定.

例如:

MyTool.setHarvestLevel("pickaxe",4);

它的效果是将MyTool设定为工具,工具类型为pickaxe,采矿强度比钻石还强(但依然采不了基岩).

顺便再温习一下如何让一个砖块能够被采集.

MyBlock.setHarvestLevel("pickaxe", 4);

这个是让MyBlock砖块只能被类型为pickaxe,强度为4的工具采集.由于原版的最强工具钻石工具也只有3强度,所以只有你的新物品能采集它.

Forge添加的这个方法的好处是做到了类型统一,mod作者不用指定自己的砖块能被某几种工具采集,而只需指定自己的砖块能被哪一类工具采集,这样即使是别人开发了新mod,只要双方的工具类型一致,工具就能正常工作.
///////////////////////////////////////////////////////////////////////////////////////////////////

在这里我胡搞了一个叫Dirac Omni Tool的东西,也就是所谓的万能工具.

先要说一下Dirac是什么,Dirac的中文写作迪拉克,读作xi jian(.......),本教程中的Diracium(迪拉克元素)是一种能从无穷无尽的迪拉克之海(反物质世界/纯能量世界)中吸取能量,凭借这些能量来创造各种在我们的宇宙中因为能量守恒定律而无法做到的事情!

首先为物品画一个图,我随便瞎图了一个...

diracomnitool

之后将它存在相应文件夹下,名字叫做diracOmniTool.png,然后创建一个叫ItemDiracOmniTool的物品类.

A3-16

在过去setHarvestLevel有个缺点就是只能一个工具只能有一种类型,想要能加速多种类型的采集就需要自己重写两个方法,不过现在setHarvestLevel已经支持多种工具类型了,因此我们的万能工具的代码也简化了很多:

public class ItemDiracOmniTool extends ItemTool{

	 public ItemDiracOmniTool() {
		 //调用基类的构造函数,参数分别是攻击实体(Entity)造成的伤害加成,
		 //					挥动时的冷却加成(正数减小冷却,负数增加冷却,但建议不要小于或等于-4),
		 //					工具材质(ToolMaterial),能被这种工具加速挖掘的砖块.
		 //其中,第四个参数是原版MC用的,使用Forge的可以无视.
		 super(100f, 10.0f, ToolMaterial.DIAMOND, new HashSet());
		 setHarvestLevel("pickaxe", 3);
		 setHarvestLevel("shovel", 3);
		 setHarvestLevel("axe", 3);
		 setMaxDamage(0); //设置最大耐久度,0的话即为永不损坏
	 }
}

之后在Mod主类中添加:

private static final String DIRACOMNITOOL = "diracOmniTool";
public static ItemDiracOmniTool diracOmniTool;

在preLoad方法中添加:

diracOmniTool = new ItemDiracOmniTool();
diracOmniTool.setUnlocalizedName(MODID + "." + DIRACOMNITOOL);
diracOmniTool.setRegistryName(MODID, DIRACOMNITOOL);
diracOmniTool.setCreativeTab(CreativeTabs.TOOLS);
GameRegistry.register(diracOmniTool);

在客户端代理器中添加:

ModelLoader.setCustomModelResourceLocation(diracOmniTool, 0, new ModelResourceLocation(MODID + ":" + DIRACOMNITOOL, "inventory"));

制作模型文件:

{
    "parent": "item/generated",
    "textures": {
        "layer0": "diracon:diracOmniTool"
    }
}

A3-17

之后进游戏,开一个生存模式的存档,然后用/give指令获得一个新工具(ID应该是diracon:diracOmniTool),之后用它四处敲敲试试.

A3-18

这玩意敲什么动物都是一击必杀的,而且在1.9下几乎没有冷却时间,敲什么砖块也都敲得动(基岩除外...)

接下来我们试试自定义一种工具类型和材质. 首先是自定义的工具类型,工具类型并不需要注册之类的过程,只要两个标识工具类型的字符串相同便会被判定为同一种工具,接下来我们为万能工具添加工具类型"dirac",同时设置新砖块只能被dirac类型的工具开采. 在ItemDiracOmniTool的代码中添加:

setHarvestLevel("dirac", 1);

然后,在BlockDiracOre类中,将

setHarvestLevel("pickaxe", 0);

改为

setHarvestLevel("dirac", 1);

(如果没有的话就自己写上)

为了突显工具对它的开采速度的加成,将它的

diracBlock.setHardness(1.5f);

改为

diracBlock.setHardness(10.0f);

在过去这样就可以了,然而在1.9中Minecraft存在一段硬编码,使得镐(pickaxe)对任何石制材质的砖块都会产生开采加成,因此我们要自定义一种新的砖块材质类型.

创建一个砖块材质

砖块材质(Material)是对砖块的一个分类,对它的具体定义请看Plus篇的Block部分.

知识点:创建砖块材质
///////////////////////////////////////////////////////////////////////////////////////////////////
一个材质是一个Material类的实例,原版MC已有的材质全部在Material下,可以通过诸如Material.ROCK之类的来调用.
理论上说,材质可以通过生成器模式来方便地创建,但是由于Minecraft的制作组脑子长炮了,那群傻子将生成器的参数方法全设为了内部保护(protected),迫使我们即使是创建一个最简单的材质也不得不自己动手新建一个材质类,或者使用反射.
Material类的构造函数的参数是一个MapColor,它是指在游戏内置的物品地图中此砖块显示的颜色.
Material在创建时的参数方法包括:
setTranslucent 使其可透过光
setRequiresTool 需要有正确的工具才会有掉落
setBurning 可以被点燃
setReplaceable 让这个砖块可以被其他砖块直接取代,比如雪,你直接往雪上盖个砖块就会自动把雪覆盖消失.
setNoPushMobility 让这个砖块无法被活塞推动,活塞的塞子会直接穿过它.
setImmovableMobility 让这个砖块无法被活塞推动,并且会挡住活塞的塞子.
setAdventureModeExempt 让砖块无节操化,可以被玩家用任意东西破坏,成为可以被任何人用任何东西推倒的街角自行车,妖の惨剧に濡れて...

此外,除了Material类以外,还有几个派生自Material的类:
MaterialLogic 没有碰撞体积的砖块,不会影响它的正下方的草,可以被玩家用任意物品破坏.
MaterialLiquid 液体
MaterialPortal 传送门的材质
MaterialWeb 蜘蛛网的材质
MaterialTransparent 火的材质
///////////////////////////////////////////////////////////////////////////////////////////////////

首先创建一个材质类,我给它起名叫MaterialDirac.

之后让它继承Material类.并为它添加上代码.

public class MaterialDirac extends Material {
	public MaterialDirac() {
		 super(MapColor.PURPLE); //设置它在地图上的颜色
		 setRequiresTool(); //让它只能被特定工具采集
	}
}

A3-20

接下来在mod主类中添加:

public static Material diracMaterial;

然后在preLoad方法的开头添加:

diracMaterial = new MaterialDirac();

然后将BlockDiracOre中的:

super(Material.ROCK);

改为:

super(Diracon.diracMaterial);

然后再进游戏测试,用/give刷出迪拉克矿石,然后摆在地上,然后分别拿迪拉克工具和石镐敲一敲,万能工具很快就能敲碎矿石,而镐很难敲动,即使敲碎了也掉不了矿石.

然而这个东西很无聊,所以我就去掉了,让任何工具都能挖掘它,因为待会我们要制作万能工具的合成,它的合成需要用到迪拉克矿锭,迪拉克矿锭通过烧迪拉克矿石取得,如果矿石需要万能工具才能挖的话....就成一个无解的循环了.

 

物品栈(ItemStack)

知识点:物品栈的概念
///////////////////////////////////////////////////////////////////////////////////////////////////
在继续下面的内容之前,我们先得说明物品栈的概念.

物品栈是游戏中玩家实际拿在手里的物品,例如"10个煤","1把铁镐","1辆尚未放置的矿车".
一个物品栈主要由4个参数组成:物品类型,数量,损伤度(ItemDamage),NBT数据.
物品类型即该物品栈中包含的是什么物品.
物品数量更好理解...
物品损伤度有两种作用,对于工具武器装甲等可以使用的物品来说,它就是代表当前物品的损伤度,当损伤度超过最大耐久后,就会坏掉.而它的另一种作用,就是对于拥有子类型(Subtype)的物品来说,代表当前为哪种类型,比如煤炭就有2个子类型,0为煤矿掉出的煤炭,1为烧木头烧出的木炭.
NBT数据目前教程还未涉及到,因此先暂不详细介绍,简单地说,NBT就是用来存储任何自定义数据的东西.不过这个参数是可选的.

举例,创建一个数量为16的木炭的物品栈的代码是:

new ItemStack(Items.COAL, 16, 1)

创建一个数量为1的煤炭的物品栈的代码是:

new ItemStack(Items.COAL, 1, 0);

对于物品损伤度为0的物品栈来说,代码可以简写为:

new ItemStack(Items.COAL, 1)

对于物品损伤度为0,数量为1的物品栈,还可简写为:

new ItemStack(Items.COAL)

如果你传入的是一个砖块的话,系统也会自动转换为砖块对应的物品:

new ItemStack(Blocks.STONE)

它等效于手动获取砖块对应的物品,即:

new ItemStack(Item.getItemFromBlock(Blocks.STONE))

///////////////////////////////////////////////////////////////////////////////////////////////////

 

添加一个冶炼公式

知识点:添加一个冶炼公式
///////////////////////////////////////////////////////////////////////////////////////////////////
FML的GameRegistry类提供了addSmelting方法来添加冶炼公式,它的代码是:

GameRegistry.addSmelting(myBlock, new ItemStack(myItem), 100f);

其中,myBlock是你的矿石砖块,myItem是冶炼产物,100f是冶炼后获得的经验.Minecraft中烧一个金矿才1f,烧一个碎石只有0.1f...)

顺便一提,在将拥有子类型的砖块或物品用于冶炼原料时,直接将砖块或物品作为第一个参数传进去会让所有类型都适用于这个冶炼公式,如果你希望只有特定的类型能被用于冶炼,或者不同的类型拥有不同的产物的话,就需要将第一个参数改为:

new ItemStack(myBlock, 1, [子类型id])

其中子类型id对物品就是Metadata,对物品就是损伤度.有个特殊数值是OreDictionary.WILDCARD_VALUE,代表任何值,也就是上文提到的所有类型...
///////////////////////////////////////////////////////////////////////////////////////////////////

我们要做一个迪拉克矿石冶炼成迪拉克矿锭的冶炼公式,在mod主类的load方法(即任何具有@EventHandler和FMLInitializationEvent参数的方法,没错这次我们不在preLoad里弄了)内添加:

GameRegistry.addSmelting(diracBlock, new ItemStack(diracIngot), 100f);

A3-21

之后进游戏试试.

A3-22

当你将产物从炉子里取出来时,能获得不少经验.

 

添加一个合成

知识点:添加一个合成公式
///////////////////////////////////////////////////////////////////////////////////////////////////
FML的GameRegistry提供了addRecipe方法来添加合成.
addRecipe的参数是(ItemStack itemstack, Object... aobj)
ItemStack是物品栈的实例,代表产物.
Object aobj[]是一个Object数组,你可以理解为它是一个可以供开发者按照一定规则随意书写的脚本,Minecraft中的一个解释器会解释这个脚本,翻译成一个合成配方. 然而想要仅凭文字来解释它的使用规范实在太难了.所以我以牌子的合成为例来解释.

addRecipe(new ItemStack(Item.sign, 3), new Object[] {"###", "###", " X ", '#', Block.planks, 'X', Item.stick});

如你所见,首先它创建了一个牌子的物品栈,并将数量设为3,这样每次合成完后能获得3个牌子,之后新建了一个Object数组.这个数组描述了一个合成图,并解释了合成图的内容.
首先,这个数组开头由1~3个字符串组成,分别代表合成表的第1~3行.每个字符串至少有3个字符.分别代表本行的第1,2,3个位置.空位使用空格来填补.
之后,是对合成图的解释,解释的格式是"被解释的字符,含义".如此重复直到所有的字符都被解释完毕.
'#', Block.planks 就是将'#'字符解释为木头砖块.
'X', Item.stick 是将'X'字符解释为木条物品.
因此,你在游戏中可以按照
木块 木块 木块
木块 木块 木块
.       木条
的方式来合成牌子.

另外,由于参数是"Object...",也就是变长参数,因此"new Object[] { ... }"其实可以省略,上文保留"new Object[] { ... }"纯粹是为了便于区分两个部分.

对于拥有子类型(Subtype)的原料来说,还可以用这种方式来解释:

addRecipe(new ItemStack(Block.planks, 4, 0), new Object[] {"#", '#', new ItemStack(Block.wood, 1, 0)});
 addRecipe(new ItemStack(Blocks.planks, 4, 1), new Object[] {"#", '#', new ItemStack(Blocks.wood, 1, 1)});

这样就可以让不同的子类型合成出不同的产物,或者强制要求只有特定的子类型才能参与合成.

此外,FML还提供了一种方式来添加合成,它允许你创建一个专门的类来判断合成条件,功能更加强大,但在这里它超出了"速成班"的范围...所以就先不说了.
///////////////////////////////////////////////////////////////////////////////////////////////////

在mod主类的load方法内,我们之前添加冶炼公式的地方的下面,添加:

GameRegistry.addRecipe(new ItemStack(diracOmniTool, 1), "###", "#X#", " X ", '#', diracIngot, 'X', Items.STICK);

之后进游戏测试.

A3-23

至此,第三章结束.(断断续续坑了3个多月,终于结束了!)

扩展阅读

Blockstate与Property系统


(注:此片完全照抄自"Minecraft常见问题"的"1.8的BlockState和Property",已经看过的人就不用再看了.)
在1.8之前,我们如果想在砖块中存储额外信息的话只能使用4位的Metadata.而从1.8开始,Minecraft提供了BlockState系统.
BlockState系统的总体思想是提供一种更好地表达一个砖块的状态的方案,这个方案既要能直白地表达要存储的状态;又要足够的小,不至于让内存或硬盘爆掉;同时也要足够的快.最终产物就是BlockState.
原版BlockState系统(Forge对它做了扩展,这个以后有时间再写)主要由这三部分组成:
BlockState类:描述一种砖块(即一个Block类的实例)所有可能的状态.
IBlockState接口:代表一种状态.
IProperty接口:代表状态中的一种属性.
我们以1.8的石头砖块的BlockStone类为例来解释各部分的作用和使用方法.
首先它先声明了一种属性:

public static final PropertyEnum VARIANT = PropertyEnum.create("variant", BlockStone.EnumType.class);

从代码中可以很直观地看到VARIANT是一种枚举类型的属性,PropertyEnum继承自PropertyHelper,而PropertyHelper则实现了IProperty.Minecraft默认提供了这三种属性类型的实现:连续整数(PropertyInteger),枚举(PropertyEnum)和布尔值(PropertyBool),其中枚举还有一个专门用于描述方向的封装PropertyDirection.
然后我们要声明BlockState,这部分的代码在稍微靠下的地方:

protected BlockState createBlockState()
{
    return new BlockState(this, new IProperty[] {VARIANT});
}

它重写了基类的createBlockState,声明了一个拥有VARIANT属性的BlockState,它的构造函数会自动计算所有属性能组成的全部状态,用数学的说法是求它们的笛卡尔积,比如如果一个BlockState有一个取值范围在0~2的连续整数属性和一个布尔值属性,那么它就有6种状态,分别是{0 False, 0 True, 1 False, 1 True, 2 False, 2 True}.
我们之前提到IBlockState接口的实例代表一种状态,首先我们先说明如何修改状态,修改状态是通过IBlockState的withProperty方法,这个方法的参数是给定一个待更新属性和它的待更新值,返回值是更新后的状态,还是以刚才的为例,假如那个整数属性叫INT,那么要将一个值为{0 False}的状态改成{2 False}的话,代码是:

IBlockState state = oldState.withProperty(INT, Integer.valueOf(2)); //oldState为旧状态,state为修改值后的新状态

需要注意的是,withProperty不会对旧状态产生任何影响,上述代码在运行完后oldState不会有任何改变,因此你永远无需担心调用withProperty会破坏旧状态,你可能会认为Minecraft默认的IBlockState实现采用的是不变量(Immutable)设计,即每次修改值时始终返回一个新实例而不会改变原实例,这并不是完全正确的,实际上是BlockState已经预先生成好了所有可能的状态,每次调用withProperty时只是根据一个状态转移表查找到应转移到的状态,然后返回那个状态的实例.
通过withProperty,我们可以将任何一个已有状态修改成我们想要的状态,但是我们该如何获取"第一个"状态,也就是如何凭空获取一个IBlockState实例? BlockState提供了getBaseState方法可以获取一个空的状态,然而这个空状态的值是不确定的,每次手工赋值必然很麻烦,因此Block类允许配置一个默认状态,设置默认状态的办法是调用Block类的setDefaultState,以BlockStone为例:

this.setDefaultState(this.blockState.getBaseState().withProperty(VARIANT, BlockStone.EnumType.STONE));

这个代码是设置该砖块的默认状态为{STONE},以后如果要获取默认状态,直接调用Block类的getDefaultState方法即可.
最后我们要考虑的是如何保存状态,现在我们又要和老朋友Metadata打招呼了,MC1.8仍然使用Metadata系统,并且也依然受最多16种取值的限制,所以说你的BlockState虽然最多可能有超过16种状态,但在保存时必须只保留最关键的属性,将它简化到16种以下,比如说如果你设计了一种管道砖块,它有2种属性,一个有16种取值的用于标识管线类型的枚举属性,和一个范围在0~63用于标识管道与周围连接情况的整数属性(如果你好奇0~63是怎么来的话...使用6个二进制位来标识这个砖块与周围6个方向的连通情况),显然它们的笛卡尔积有多达1024种情况,然而我们可以只保存标识管线类型的那个属性,标识连接情况的属性可以在运行时通过判断周围砖块的类型来推算出来.
保存、读取和计(bu)算(wan)非关键属性的方法以及它们的使用范例分别是:

public int getMetaFromState(IBlockState state)
{
    //返回值为该状态的Metadata的值,不需要的话就返回0或者干脆不重写这个方法
    return ((EnumPipeType)state.getValue(pipeType)).getMetadata(); //模仿BlockStone的写法
}
public IBlockState getStateFromMeta(int meta)
{
    //返回值为该Metadata对应的状态,不需要的话就返回getDefaultState或者干脆不重写这个方法
    return getDefaultState().withProperty(pipeType, EnumPipeType.byMetadata(meta)); //模仿BlockStone的写法
}
public IBlockState getActualState(IBlockState state, IBlockAccess worldIn, BlockPos pos)
{
    //返回值为补全了非关键属性的状态,如果不需要补全数据的话,直接"return state;"即可;或者干脆不重写这个方法.
    int i = 0;
    i |= (worldIn.getBlockState(pos.up()   ).getBlock() == this ? 1 : 0) << EnumFacing.UP   .getIndex();
    i |= (worldIn.getBlockState(pos.down() ).getBlock() == this ? 1 : 0) << EnumFacing.DOWN .getIndex();
    i |= (worldIn.getBlockState(pos.east() ).getBlock() == this ? 1 : 0) << EnumFacing.EAST .getIndex();
    i |= (worldIn.getBlockState(pos.west() ).getBlock() == this ? 1 : 0) << EnumFacing.WEST .getIndex();
    i |= (worldIn.getBlockState(pos.north()).getBlock() == this ? 1 : 0) << EnumFacing.NORTH.getIndex();
    i |= (worldIn.getBlockState(pos.south()).getBlock() == this ? 1 : 0) << EnumFacing.SOUTH.getIndex();
    return state.withProperty(linkingState, Integer.valueOf(i));
}

原版BlockState系统的内容便就此结束了,如果你问BlockState系统的作用的话,那就是它创造了数据驱动渲染的可能,数据驱动就是指通过外部数据来决定程序运作流程,而不是通过硬编码的代码,比如BlockModelShapes类中的这行代码:

this.registerBlockWithStateMapper(Blocks.stone, (new StateMap.Builder()).setProperty(BlockStone.VARIANT).build());

这样便建立了砖块状态与渲染的映射关系,游戏会根据VARIANT属性的值从assets/minecraft/bloackstates中选择合适的数据文件用于渲染.

Blockstate JSON文件格式 - 更新中

模型JSON文件格式 - 更新中

OreDictionary系统


矿物字典(OreDictionary)这个名字其实起的不是很好,称其为原料字典会更好一些,因为它包含的不只是矿物.

我们知道在Minecraft中有些合成的原料是可以替换的,比如原木分解成木板既可以用旧的四种树木:橡树、云杉、桦树、丛林树(它们都是一种砖块),也可以用新的两种树木:金合欢和黑橡树(这两种是另一种砖块),如果制作一个以原木为原料的合成的话,想要让新树木也能被用于合成就必须为每种原木砖块都指定一遍合成,如果合成需要用到3个原木,那么组合下来就是2x3=6个合成配方,如果需要更多的原料,且原料又包括更多的替代品的话,那么就会有多达几十种合成配方的组合爆炸. 此外,还要考虑到多种Mod之间的联动,如果有一个树木Mod(比如林业Mod)提供了新的原木砖块的话,你该怎么让它的新砖块也能用于你的合成?早年很多工业类Mod都会包含铜矿这一矿物(比如IC和RP,现在什么样不知道了),那么它们之间的合成兼容又该如何实现呢?

于是Forge就提供了矿物字典这个东西,它允许开发者为砖块或物品注册一个字符串别名,与砖块/物品ID不同的是,这个字符串别名是可以重复的,而且它就是被设计用来重复的 - 任何拥有相同别名的砖块或物品都有机会在合成中互相代替(我只是说"有机会",实际上你可以制作不能被代替的合成),比如上述的两种原木砖块都会在矿物字典中被注册为"log",开发者在编写合成配方时引用"log"作为原料就可以实现任意原木间的相互代替,避免了组合爆炸,也允许其他Mod提供的新原木被用于合成.

矿物字典OreDictionary类全部由静态方法组成,它提供的几个常用的方法包括:
registerOre 注册一种原料,或者说是在矿物字典中为一种砖块或物品的默认类型(砖块的Metadata 0,物品的损伤度 0)添加别名,如果你希望是任何类型的话,就需要仿照上文"添加冶炼"提到的那样,改成new ItemStack(...),在子类型ID那填OreDictionary.WILDCARD_VALUE,其实这个名称叫的太专业或者说太装逼了...如果叫ANY或DC(Don't Care)的话会更好理解一些.
itemMatches 心疼智障Mojang没给ItemStack重写equals? Lex替你做了,itemMatches用于对比两个物品栈是否相同,参数strict代表是否判断损伤度相等. (不过你应该注意到了这个不管NBT的判断...)
containsMatch 判断两组物品栈中是否存在相同的物品栈,好像不是很常用...

此外矿物字典引入了一种新的ID:矿物ID,任何在矿物字典中拥有相同别名的砖块或物品都会拥有相同的矿物ID,不过它并不常用,因此这里就不过多介绍了.

制作基于矿物字典的合成需要用到ShapedOreRecipe或ShapelessOreRecipe这两个类. (更新中,未完★)