基於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這兩個類. (更新中,未完★)