基於FML的MinecraftMod製作教程(2) - 建立一個基於Forge的Mod

我決定以一個開發實例作為教程,我們要使用Forge來創建一個叫做Diracon的mod,它將具有如下功能.
(1)一個新的礦物(磚塊):Diracium
(2)一個新的礦錠(物品):Diracium Ingot
(3)一個新的物品:Dirac Wand
(4)一種新的Mob:Dirac Pig
(5)添加新的功能,使Dirac Wand具有瞬間轉移箱子內的物品的能力
(6)修改地形生成器,使新礦物可以生成
(7)一個以土塊為燃料的新爐子Unthinkable Furnace
(8)支持多人遊戲!

這8個功能基本涵蓋了基礎mod開發的全部,我計劃分為三個部分,(1)(2)(3)作為第一部分,(4)(5)作為第二部分,(6)(7)和mod的編譯發布作為第三部分.而(8)會作為貫穿全篇的一個目標,自始至終都在進行.

這裡說明一下本教程中不同顏色的字體所代表的含義.
藍色:一個章節
紅色:知識點,相當於一個章節所講內容的概括
灰色:原理,想深入學習MCP和Forge的人可以讀讀,普通MODer可以無視了.

再統一一下今後教程中將使用的術語.
B8
從上往下看.
紅1:這個類所處的包(Package).
藍1:導入的包.
綠1:類,類的概念我實在難以解釋,熟悉Java的人應該都知道吧...
紫1:繼承,派生.正規的說法是一個基類派生出子類,子類繼承自基類.然後我們並不在乎這上面的文字遊戲,漢語本來就是個不規範的語言呢...我們說子類從基類派生而來,或子類繼承自基類不也一樣嗎...所以本文不會太注重派生/繼承的用詞.但我保證會留下足夠的邏輯線索讓你一眼看出我所表達的意思.
青1:基於XXX接口創建類.
紅2:類的空位,即方法與方法間的空位,如果我說"在XX類中添加這些代碼"就是指在這些位置添加代碼.
藍2:構造函數.如果一個方法的名字和類相同,那麼它就是構造函數.它會在類實例化時執行.
綠2:方法/函數,我習慣叫它方法.這個是最準確最規範的叫法.
紫2:返回值.
青2:參數.

最後再向新手程序員解釋一下何為重寫,重寫(Override)是讓子類的方法覆蓋父類的方法,重寫的辦法是讓子類方法的名字和父類方法一致.唯一特例是構造函數,它的名字依然和子類名字保持一致,但必須調用父類的構造函數(使用super).

創建一個Package(包)
Java以Package的形式來管理代碼,它的作用類似於C++和C#的命名空間,優點是便於將代碼分類管理,使程序更加直觀;方便控制代碼的作用範圍;解決類的重名現象(比如下一章要用到的Block類,有6個類都叫Block,但只有net.minecraft.block包中的Block才是真正需要的).MDK自帶的範例里已經自帶了一個包和一段程序,不過這裡我們還是新建一個.
Package的命名很隨意,Forge建議的命名規範是"作者名.mod名",Java建議的Package命名規範是Package的製作公司的網站的域名的倒寫,例如"com.sun","com.google"等...
比如:
fanhua.minelogin
szszss.eracraft
net.mcbbs.multicraft
net.areazero.tohoskyarena
總之不必在這個的命名上費太多腦筋,但是不要有太奇怪的命名,比如:
pan.1c0xr8Hy.h0yx.SSTM
就是不行的,首先包中的每一段必須是字母或下劃線開頭,不能是數字,其次不建議包名使用大寫.

首先你要為服務器端與客戶端的公用代碼創建一個package,右鍵項目中的src/main/java,選擇New->Package
ngt21

(關於目錄結構,這個跟Gradle的設計有關,Gradle將目錄分為兩個部分:源代碼(我們使用Java來編程,因此自然叫java)和資源文件(resource),因此我們以後將程序放在src/main/java里,資源文件放在src/main/resource里)

然後輸入Package的名字.
B2-3n
創建完畢後,這個Package今後將用於放置你的代碼.

創建一個Mod主類
Mod主類是負責進行Mod的初始化,並且可以被Forge所識別的一個類(Class).
關於Mod主類的命名,在ModLoader時代,它的名字必須以"mod_"開頭,但在Forge中它叫什麼都可以.所以我將其命名為Diracon.
在你的package中創建一個類.(右鍵你的package,選擇New->Class)
B4
點擊Finish開始創建,創建完後我們便有了一個空蕩蕩的文件,我們要讓它能被Forge找到,方法是為類添加一段Annotation.

Annotation是Java的一種增強型注釋,它和普通注釋(雙斜杠)的區別是,普通注釋在編譯階段就會被刪除,而Annotation會保留下,Annotation能儲存數據,能在程序運行階段通過某些方法(如反射)被讀取.

在public class Diracon的上一行添加:

@Mod(modid="diracon", name="Diracon", version="1.0.0")

知識點:@Mod
///////////////////////////////////////////////////////////////////////////////////////////////////////////
@Mod必須標註在類的上方,它的含義是告訴Forge"這是一個Mod主類".
它有1個必要參數是字符串類型的modid,代表Mod的id號,它將用於內部識別,請確保它不包含特殊字符,並且不會經常變動,還有一個忠告,確保它是全小寫.此外還有一系列可選參數,比如name是顯示給玩家看的Mod名;version是版本號,對於聯機Mod來說不要亂填,保守的話A.B.C這樣的格式肯定是沒問題的,其中ABC均為數字,A代表主版本號,B代表次版本號,C是沒有功能性更新,純粹是Bug緊急修復時遞增的版本號.如果想更簡單的話可以只留A.B兩位,再懶一些的話哪怕單獨一個數也行,主要能用於區分開新舊版本就行.關於version格式的具體要求可以見擴展閱讀"@Mod的各個參數".
在過去,還有一個叫@NetworkMod的註解,但它在1.7中被去除了.想了解它的替代品可以看這裡.
關於@Mod的其他參數,可以見本篇的擴展閱讀.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

A2-1
之後靜等兩秒,你的Eclipse就開始報錯了...因為你沒有導入相關的package,將鼠標移到紅波浪線上,等提示框出來後選擇"Import ..."來自動導入相關package.

此時你的mod大概是這樣子.
B2-1n

之後我們要添加用於初始化Mod的方法(Method,不理解的自行補Java...)在ModLoader時代,我們通過重寫(Override)基類的方法來實現,現在沒有基類了,所以我們使用Annotation來實現.

在你的類中添加這些代碼.

@EventHandler
public void preLoad(FMLPreInitializationEvent event)
{
}

@EventHandler
public void load(FMLInitializationEvent event)
{
}

@EventHandler
public void postLoad(FMLPostInitializationEvent event)
{
}

知識點:@EventHandler與歷史的傷痕
///////////////////////////////////////////////////////////////////////////////////////////////////////////
看上去這個標題很嚴肅...事實上是我為了搞笑故意起的...
如果你是個老Modder(MC1.6版以前就開始做Mod)那麼你應該記得那時Forge沒有@EventHandler這個註解,取而代之的是@Init,@PreInit和@PostInit這三個分工詳細的註解.
在最初的ModLoader時代,所有的Mod主類都通過重寫load方法來實現初始化,到了ForgeModLoader後,Forge(最初)通過帶有@Init的方法來實現初始化,同時它還提供了@PreInit(初始化前)和@PostInit(初始化後)兩個方法來為初始化掃清障礙或收尾.
然而現在FML使用@EventHandler來泛指所有參與Mod初始化工作的方法,區分這些方法的唯一方式是它們的參數,根據他們的參數的類型,FML會利用事件系統在不同的時段調用他們.

FMLPreInitializationEvent 預初始化
CPW建議開發者在預初始化時進行讀取配置,創建物品磚塊,以及註冊相關信息等操作,另外你還能在這時候獲得FML給你傳來的配置文件.(Configuration,但是我並不太願意在基礎篇講這個東西...)
從1.7開始,磚塊和物品的初始化必須在這個階段進行!
FMLInitializationEvent 初始化
配置Mod設置,添加合成表...另外CPW建議Mod間通訊(通過FMLInterModComms類完成)應當在此時進行.
FMLPostInitializationEvent 初始化後
CPW認為初始化後是供Mod間相互交互的時候,此時該載入的Mod都已經載入了,因此開發者們可以在此時為實現Mod間聯動的操作做準備.順便一提,FMLPostInitializationEvent事件的buildSoftDependProxy方法可以獲取一個類的實例,它會先判斷你要求的mod是否存在,如果存在則返還給你你要求的類的實例.
IMCEvent 接收Mod間通訊
這個事件會排在FMLInitializationEvent之後,它會附帶一個保存着其他Mod發來的信息的列表.

此外,還有幾個服務器事件,但這個就不在基礎篇的範圍內了...(其實是我急着去玩Civ5懶得繼續碼字了)以後我會單開一個篇幅講.
///////////////////////////////////////////////////////////////////////////////////////////////////////////

然後導入相關的包,你的第一個Mod就已經能運行了...保存,選擇Run - Client開始運行吧!
A2-3

B2-2n
於是就是這樣...你的第一個Mod已經能被載入了,雖然它還什麼都沒有,但千里之行始於足下,你已經走出了第一步,不是嗎?
(好奇為什麼我的Minecraft Forge旁邊有個綠點?因為我現在用的是測試版的Forge...當你開發Mod時應該已經有穩定版了)

擴展閱讀

最初我計劃將一些更深入的東西寫在單獨的章節里,但總是感覺有些突兀,而且有些東西往往不足以寫成一篇單獨的文章,再考慮到很多好♂學的人希望能立刻讀到這些內容,因此便有了擴展閱讀,擴展閱讀的內容往往高於甚至遠高於基礎篇的內容,屬於選讀部分.

@Mod的各個參數

@Mod註解除了modid這個必選參數以外,還有很多可選參數,這些參數包括:

參數 類型 描述 默認值
modid String 用於FML內部識別的ID,ID絕不能和其他Mod重複. 無,此為必要參數
name String 人類可讀的Mod名. modid
version String 用於內部識別的版本號,別被文檔騙了,它可不是為人類可讀而設計的,它的格式有嚴格要求,具體見附文-version的格式. 見附文-version的默認值
dependencies String 描述Mod依賴和加載順序,其格式是"[順序]:[Mod名]<[版本]>",如果有多個的話使用";"分割.
[順序]可以是"required-before","required-after","before"或"after".前兩者代表硬性依賴,如果不存在被依賴Mod(或版本不正確)的話此Mod不會被加載,其後綴表示此Mod是在被依賴的Mod之前還是之後加載.後兩者代表可選依賴,僅用於表示加載順序.
[Mod名]為被依賴Mod的modid,如果[順序]是"before"或"after"的話,這裡可以填"*"(星號),代表在所有Mod的最後或最初加載.(當然,我們都知道如果有一堆Mod爭當第一的話,總會有個先後順序,所以別太依賴這個.)
<[版本]>是可選值,格式為"@[值域]",比如"@[1.0, 2.0)"為版本大於等於1.0,小於2.0的版本;"@[2.3.3,)"代表版本大於等於2.3.3的版本.
實例:"required-after:parentmod@[1.5,)"依賴parentmod1.5或更高的版本,且在它之後加載.
空字符串
useMetadata boolean 字面上是指"是否允許mcmod.info中的設定覆蓋@Mod中的設定".這是因為Mod的信息可以來自於兩處:@Mod註解和mcmod.info文件,毫無疑問mcmod.info是最正式的也是內容最豐富的,然而兩者之間的內容可能存在重複甚至矛盾,因此就有了這個選項,用於指定優先使用哪裡的設定.
然而實際上它的功能比較有效,並非所有mcmod.info中的設定都能覆蓋@Mod,只有以下設定可以覆蓋:
依賴:當useMetadata為false(默認值),或mcmod.info中的useDependencyInformation為false(默認值)時,會使用@Mod中的dependencies作為Mod的依賴;否則會使用mcmod.info中的requiredMods、dependencies和dependants來作為Mod依賴.需要注意的是@Mod中的dependencies和mcmod.info中的dependencies作用完全不同.
Mod順序:準確地說,它和上面的"依賴"應該是一體的,當"依賴"滿足使用@Mod的條件時,FML會將@Mod中的dependencies作為排序依據;否則會使用...一個空字符串作為排序依據?也許這是個Bug?
false
clientSideOnly boolean 是否為客戶端專用Mod,若為true,則不會在獨立服務器被加載. false
serverSideOnly boolean 是否為獨立服務器專用Mod,若為true,則不會在客戶端被加載. false
acceptedMinecraftVersions String 表明Mod可以運行在哪些MC版本上,默認空字符串表示不檢查,裡面填的內容採用Maven Version Range Specification規則. 空字符串
acceptableRemoteVersions String 表明聯機Mod的服務器端所允許的客戶端Mod版本,比如服務器端運行着1.1.5版,默認情況下客戶端運行的Mod也必須是1.1.5版,然而通過設置acceptableRemoteVersions,可以讓運行着其他版本Mod的服務器端也能連入遊戲,比如1.1.0~1.1.4.設置格式依然採用Maven Version Range Specification規則,不過如果填"*"(星號)則是允許一切版本.此外,如果Mod主類中有一個方法帶有@NetworkCheckHandler註解的話,這一項會被忽略. 空字符串
acceptableSaveVersions String 大概是對遊戲存檔所使用的Mod版本號的限制,然而怎麼看都感覺像是一個尚未被實現的功能... 空字符串
certificateFingerprint String 用來實現對Mod的Jar包簽名驗證,大概是要求Mod的Jar包必須有一個SHA-1格式的驗證簽名,否則就會拒絕加載,然而我不是很了解這個 233 空字符串
modLanguage String 描述該Mod所使用的編程語言,可以是"java"或"scala". "java"
modLanguageAdapter String 如果你的Mod是使用其他JVM語言(也就是除Java和Scala以外的語言)編寫的話,你需要手動寫一個語言適配器,這個適配器需要實現ILanguageAdapter接口,這裡填入的是適配器的類名,也就是形如"package.MyClass"的格式. 空字符串
canBeDeactivated boolean 是否可以在Mod菜單中被關閉,又是一個尚未實現的功能... false
guiFactory String 指示一個實現了IModGuiFactory接口的類,這個類主要是負責創建Mod菜單中的Config界面,具體可以參考ForgeGuiFactory類和FMLConfigGuiFactory類. 空字符串
updateJSON String 標示獲取升級信息Json的URL,關於這個Json文件的格式可以參考https://gist.github.com/LexManos/7aacb9aa991330523884. 空字符串
customProperties @CustomProperty[] 標明Mod的自定義屬性,例如@Mod(modid="XXX", customProperties = {@CustomProperty(k = "鍵", v = "值")}) 獲取屬性可以通過Loader.instance().getCustomModProperties([Mod ID]).get([鍵]) 不過這裡有個隱患就是如果那個Mod不存在的話會直接NullPointerException.此外它也只能存字符串,因此實在不明白它能拿來幹什麼. 空數組

注:如果你給一個參數指定了null或空字符串(對於String類型)的話,也會被置為默認值.
version的格式:version的格式採用Maven的版本號規則,簡單地說,它最多包含5部分:主版本號(Major),次版本號(Minor),無功能性更新純粹是修復Bug時的遞增版本號(Incremental),累計構建編號(Build)以及不限數量的文字修飾符(Qualifier).在不引入修飾符時一切都好說,比較版本時直接從前往後比,不相等就返回結果,相等就對比下一位,然而在引入修飾符後情況就複雜了很多,那篇文章幾乎有一半的內容在描述如何處理修飾符,有興趣的話可以看看那篇文章.
version的默認值:version的默認值規則比較複雜,如果你沒有給version指定值,或者指定一個空值或null的話,系統會先在Mod包的根目錄尋找"version.properties"這個文件,然後按照標準的Java Properties文件的格式從中讀取"[modid].version"這個條目,比如"diracon.version";如果沒有找到文件、文件格式錯誤或沒有此條目的話,系統會去使用Metadata中的version;如果還是沒有值的話,則此處的version和Metadata中的version都會被置為"1.0".