第一個物品

模組的架構好了,那我們現在試著加新物品來看看吧
我想想喔來做個布丁如何?(〃∀〃)

在本章節你會了解

  • 如何創建一個物品實例(Instance)
  • 如何註冊物品
  • 幫物品顯示名稱寫語言檔
  • 幫物品加上材質
  • 在程式碼中獲取你註冊的物品
  • 將物品加入創造模式物品欄
  • 將物品設為食物
  • 將物品Tooltip加上敘述
  • Item與ItemStack

如何創建一個物品實例(Instance)

我們來創建一個套件(Package)com.github.immortalmice.modtutorialim.item
未來所有的物品類別會放在這裡

那我們就建立一個Pudding的類別,並繼承minecraft原生的net.minecraft.item.Item類別

public class Pudding extends Item{ public Pudding(){ } }

好的,這時候編譯器應該會告訴你Item類別並沒有宣告沒有參數的建構子,要你處理
那我們就來看一下Item這個類別有哪些建構子可以用吧

很好,只有一個_(:3 ⌒゙)_

public Item(Item.Properties);

那這個Item.Properties是何方神聖呢?
簡單來說,他用來告訴建構子一些物品可以有的基礎屬性
Ex. 最大物品堆疊數、可否被修理、稀有度、是否是個食物

你可以看一下Item.Properties裡有的方法,看看有沒有你需要的
這邊我會先示範設定其中兩個屬性

  • 最大物品堆疊數為1,布丁這麼脆弱,你居然想堆疊他!不可原諒!(#`Д´)ノ
  • 稀有度為Epic,布丁這麼神聖的東西,你居然敢懷疑他的地位嗎?天都要塌下來了(メ ゚皿゚)メ

最後,你可有發現這些方法都是回傳一個Item.Properties嗎?
是的,玩串串樂的時間到了 ( ・・)つ―{}@{}@{}-

public Pudding(){ super((new Item.Properties()) .maxStackSize(1) .rarity(Rarity.EPIC)); }

這邊的Raritynet.minecraft.item套件(Package)下的一個列舉(enum)
他總共有四個Rarity.COMMONRarity.UNCOMMONRarity.RARERarity.EPIC

這樣一個簡單的物品就完成了,接下來我們要用Forge註冊它ლ(╹◡╹ლ)

如何註冊物品

(以下提供的範例可以只當參考,M鼠只是考慮到未來開發的擴張和管理程式碼上的選擇,所以你會發現M鼠好像創了很多套件,繞了有點多路,搞得很複雜,最最最簡單的完整註冊方法Forge已經寫在DeferredRegister的註解中了,強烈建議一定要看過)

目前1.15.2 Forge建議的註冊方式是使用net.minecraftforge.registries.DeferredRegister<T>
雖然過去的方法net.minecraftforge.event.RegistryEvent.Register<T>並未被刪除,但使用此事件來註冊已經不是Forge推薦的方式了

這是他們的說法

DeferredRegister makes it impossible to do things at the wrong time.

這句話我覺得是事實,但個人感覺這方法稍微少了些彈性,不過那就是其他話題了( ´∀`)つt[ ]

DeferredRegister可以註冊的東西你可以去net.minecraftforge.registries.ForgeRegistries
其實種類很多,物品、方塊、藥水效果、生態系、附魔,一直到世界維度都在使用範圍內

那開始來寫程式碼吧(¯﹃¯)

首先,我們一樣在剛剛創建的套件com.github.immortalmice.modtutorialim.item中建立一個Items的類別
這個類別可以想像是一個列表,裡面會放所有我們模組的物品

這邊我會用內部類別(Inner Class),把註冊相關用的東西放在裡面₍₍ ◝('ω'◝) ⁾⁾ ₍₍ (◟'ω')◟ ⁾⁾

public class Items{ public static DeferredRegister<Item> getRegister(){ return Items.ItemRegistry.REGISTER; } @SuppressWarnings("unused") public static class ItemRegistry{ public static final DeferredRegister<Item> REGISTER = new DeferredRegister<Item>(ForgeRegistries.ITEMS, ModTutorialIM.MODID); } }

DeferredRegister的建構子傳進兩個東西,一個是你要註冊的種類
根據你要註冊的東西(Ex.物品or方塊or),在前面提到過的ForgeRegistries找到相對應的物件傳進去,這邊是ForgeRegistries.ITEMS
第二個就是你的modid,不解釋(つ´ω`)つ

好的,開始把我們的布丁註冊上去吧
com.github.immortalmice.modtutorialim.item.ItemRegistry類別裡加入一個欄位

public static final RegistryObject<Item> OBJ_PUDDING = ItemRegistry.REGISTER.register("pudding", () -> new Pudding());

net.minecraftforge.fml.RegistryObject<T>是用來包裝一個可以被註冊的物件
當註冊完成,裡面的物件就會被更新

你可以透過RegistryObject.get()就可以獲得裡面的物件
但請注意,它可能處於還沒註冊,也有可能已經註冊了( ×ω× )
如果在還沒註冊的時候就呼叫RegistryObject.get()會丟出一個NullPointerException例外

RegistryObject建議使用DeferredRegister.register()來獲取,他會幫你做好所有事
DeferredRegister.register()傳入兩個參數

  • 一個String,就是註冊名稱
    這會被Forge自動加上你modid的前綴,也稱為命名空間(Namespace)
    比如說這邊傳進的"pudding"會變成"modtutorialim:pudding"

    如果處理後的註冊名稱和先前註冊的東西重複的話,會丟出IllegalArgumentException例外
    簡單來說,如果你是在你的模組中註冊,你只要保證你模組中不會有兩個東西擁有相同的註冊名就好了

    註冊名稱一率全小寫,若需要斷詞,請使用下劃線替代(╭ ̄3 ̄)╭♡
    Ex. "the_holy_pudding"

  • 一個java.util.function.Supplier<T>,T必須要是你註冊類型的子類別
    比如說這邊PuddingItem的子類別

    傳入的Supplier必須每次都創建出一個新的實例(Instance)
    對,新的,我再說一次,新的,否則你可能會遇到無法解釋的Bug,因為M鼠做過 (つд⊂)
    像這邊寫的() -> new Pudding()就是一個最簡單的實作方法

一整個下來,其實你會發現DeferredRegister不過就是一個裝著一堆Supplier的列表,等著把這些Supplier在註冊時期執行並完成註冊的動作

現在,我們要把這個DeferredRegister綁定到我們的Mod Event Bus上
(關於事件系統的教學部分我放在後面,請看本教學中的這篇文
或是你也可以先照做,未來再了解Mod Event Bus是什麼即可)

這個動作要在非常早期就完成,基本上建議在你的模組主類別建構子中完成

不過,在模組主類別建構子中加上程式碼之前,我們先新增一個套件com.github.immortalmice.modtutorialim.handlers,未來所有的handlers類型的類別會放在這,Ex.材質處理、地圖生成處理、指令處理etc

現在我們需要的是一個幫我們處理註冊的類別(包括物品、方塊、藥水效果總之一切要用DeferredRegister註冊的都會放在這)
所以我們在這個套件中建立RegistryHandler類別

public class RegistryHandler{ private static IEventBus BUS = FMLJavaModLoadingContext.get().getModEventBus(); }

其中,FMLJavaModLoadingContext.get().getModEventBus()就是用來獲取我們Mod Event Bus的方法

再來,我們就只要把這個Bus傳給DeferredRegister即可
RegistryHandler類別中新增registAll方法
registAll未來也會負責註冊方塊之類的東西(〃 ̄ω ̄)人( ̄︶ ̄〃)

public static void registAll(){ Items.getRegister().register(RegistryHandler.BUS); }

這邊我們透過剛才寫好的Items類別來獲取物品的DeferredRegister

最後的最後,我們在模組主類別中呼叫這個方法吧

public ModTutorialIM(){ RegistryHandler.registAll(); }

現在打開遊戲,輸入以下指令
/give @p modtutorial:pudding

你應該可以拿到一個物品
圖1-3-1 獲得物品

這代表你的物品已經成功進入遊戲系統中了
只是這個東西現在沒有材質、只能用give指令獲得、甚至沒有名字,我們後面就來解決這些事吧

幫物品顯示名稱寫語言檔

你知道的,世界在各國都有人玩Minecraft
因此,除了有名字以外,我們還要考慮到翻譯的問題

每個物品都有一個TranslationKey,他用來尋找遊戲當前語言設定中對應的字串
這個key可以透過覆寫(Override)net.minecraft.item.Item.getTranslationKey()來更改
但其實,除非真的有需要,不然不會覆寫這個方法,不然有可能會造成其他人幫你寫語言檔時的困擾

有注意到我們剛剛在遊戲中拿到的東西上面寫著item.modtutorialim.pudding嗎?
M鼠第一次註冊名打錯字ㄌ,所以上面的圖片(´∩ω∩`)

這就是剛剛提到的getTranslationKey中幫你預設好的key
由於我們現在沒有任何語言檔,他找不到對應的東西顯示,因此就直接把key顯示出來了

到現在,你的專案src資料夾應該是長這樣

  • src
    • main
      • java
        • com
          • github
            • 以下略
      • resources
        • META-INF
          • mods.toml
        • pack.mcmeta

有看到那個resource資料夾了嗎?我們曾經改過裡面的mods.toml
這個資料夾如果你有做過材質包應該不陌生
一些跟java程式碼無關的東西都會放在這裡面

我們要在resources資料夾中建立這樣的資料夾結構 *ଘ(੭*ˊᵕˋ)੭* ੈ✩‧₊˚

  • resources
    • assets
      • modtutorialim
        • lang
    • META-INF
      • mods.toml
    • pack.mcmeta

這樣的資料夾結構是Minecraft在找東西時會遵循的路徑
可惜的是,這個路徑是寫死的,所以除非想找麻煩,還是照著順序建吧 (╥﹏╥)
資料夾名字一字不能差,modtutorialim請改為你的modid,assets最後面有個s

那我們現在要建兩個檔案

  • en_us.json
  • zh_tw.json

這兩個分別是英文(美國)和繁體中文的檔案
想知道有支援那些語言,你可以到Minecraft Wiki中看
檔案名稱是被指定的,請參照Wiki中 "可用語言" 區塊的 "代碼" 欄位

好了我們就在檔案裡面寫上資料吧

  • en_us.json
    ​​​​{ ​​​​ "item.modtutorialim.pudding": "Pudding" ​​​​}
  • zh_tw.json
    ​​​​{ ​​​​ "item.modtutorialim.pudding": "布丁" ​​​​}

Json中前面的key是TranslationKey,後面的值則是在該語言中該顯示的字串

現在打開遊戲,你手中的物品應該有可愛的名字了 (๑´ㅁ`)
圖1-3-2 英文語言檔成果

記得切換語言來確定你另一個語言有沒有正確載入喔
圖1-3-3 遊戲語言切換頁面
圖1-3-4 中文語言檔成果

幫物品加上材質

一個物品要有外觀,基本上需要兩個東西

  • 模型Json檔

    這個檔案包含了許多資訊,這些資訊用來建構一個物品最後會長成什麼樣子
    可能是指定當物品在不同狀態時,轉而使用另一個模型Json檔
    也可能是設定在不同攝影機角度時,該如何縮放、移動、旋轉
    當然還有最重要的,指定該使用的圖像資源在哪裡
    詳細你可以查看Minecraft Wiki的介紹

  • 圖片

    這還要解釋嗎?_(:3 ⌒゙)_

首先,我們的物品模型Json檔要放在resources/assets/modtutorialim/models/item
恩,我說過了,這是寫死的,請一字不差的建立,特別注意哪些名稱後面有s哪些沒有

好了之後就在裡面建立一個pudding.json,pudding請改成你物品的註冊名

{
    "parent": "item/generated",
    "textures": {
        "layer0": "modtutorialim:item/pudding"
    }
}

parent就像是繼承一樣,他會沿用你指定的模型的東西
這邊填入的值是"item/generated"
你可以同時先看一下我在layer0中填的值modtutorialim:item/pudding

這個用來表示檔案位置的格式是 命名空間:路徑/檔案名稱

  1. modtutorialim:item/pudding為例
    命名空間就是modtutorialim,通常是你的modid
    路徑會被程式自動加上這個值被使用的地方的類型,這邊的類型是材質檔,所以他會被加上textures資料夾
    檔案名稱不用寫附檔名,因為這邊的類型是材質檔,所以會被程式加上.png

    所以這個modtutorialim:item/pudding表示法最後指出的位置就是resources/assets/modtutorialim/textures/item/pudding.png

  2. 那來看看item/generated
    他的命名空間被省略了,當沒寫命名空間時一律都當作是minecraft,所以這個值的全名其實是minecraft:item/generated
    路徑跟上面一樣,但要注意這邊值被使用的地方類型是模型檔,所以他會被加上models資料夾
    檔案名稱,這邊是模型檔,所以預設會被加上.json,但除了Json寫的模型檔,現在Minecraft可以載入OBJ和B3D模型檔了,如果要用這兩種方式載入,請寫上附檔名

    所以這個item/generated表示法最後的路徑就是resources/assets/minecraft/models/item/generated.json

你可以去拆原版Minecraft的包,你會在上面的路徑找到這個generated.json
打開來你會看到他幫你定義了光的來源,還有在地上、玩家頭上、第三人稱視角右手、第一人稱視角右手、物品展示框中模型該如何縮放、旋轉和移動 ( Φ ω Φ )

簡單來說,當我們用這個generated.jsonparent之後,我們的模型就可以直接沿用這些數據

至於textures就是定義材質檔案的地方了
而如果你剛剛有拆Minecraft包來看,也許你會發現一件事情
item/generated中也有parent,裡面的值是builtin/generated,但你不會在這路徑中找到Json檔,別說檔案了,連builtin這個路徑都沒有
因為這個builtin/generated是黑魔法所在,他不是意指一個模型Json檔,他是由Java程式碼來接手處理的 σ ゚∀ ゚) ゚∀゚)σ
詳細你可以看net.minecraft.client.renderer.model.ItemModelGenerator這個類別

是的,繼承了builtin/generated後你可以用layer#來堆疊你的材質
根據ItemModelGenerator這個類別,你一共有layer0~layer4五個層可以用

你說五個層還是不夠你用怎麼辦,挖賽大大你需求量真大啊
雖然M鼠沒做過,但我猜你可以透過Java的反射(Reflect)來達成載入五個以上的層
如果你真的試成功了請告訴我,我要去當觀眾
嗯?你說這麼有趣的事M鼠不做看看嗎?
不要,我好懶
(っ﹏-) .。o

那麼,最後就把你辛苦畫好的PNG檔放進你前面在textures裡寫的路徑吧
對,PNG檔,沒得商量喔不對,你真的要商量的話寄信去找Minecraft官方吧

這邊會用這張朋友 硯郎 幫忙畫的圖
pudding.png

大小建議 16x16
如果你真的想改,請使用2的指數(Ex. 1x1, 2x2, 8x8, 16x16, 128x128)
如果你想叛逆,請至少用正方形(Ex. 10x10, 30x30)
如果你想翻桌,用的圖片連正方形都不是,Minecraft會直接強制縮放成正方形,同時某些原先貼圖功能會失效,甚至就算你之後改邪歸正,也會有奇怪的Bug追著你跑

16x16在遊戲裡其實就已經是夠清晰的了,少年你火氣還是不要這麼大吧 •_ゝ•

現在開遊戲,你應該可以看到可愛的布丁ㄌ~
圖1-3-5 材質成果

在程式碼中獲取你註冊的物品

在未來我們的程式碼之中,我們會需要使用到這個我們新增的物品
比如說我們需要判斷玩家手上的東西是不是我們新增的布丁

if(playerEntity.getItemStackFromSlot(EquipmentSlotType.MAINHAND).getItem() == /* 我們新增的布丁 */){ }

可是,我們剛剛傳給註冊系統的是一個Supplier,而且還限定每次都要回傳一個新的實例
這樣上面的判斷式永遠都會是false,Forge幫我們建立的Item實例在哪裡?(`へ´≠)

第一個方法,你可以透過上面註冊時提到的RegistryObject<T>.get()
但每次都要多寫一個get()有點煩人,而且如果不是單純的Item類別還要手動轉型,並且當裡面的物品還沒註冊就呼叫get()會丟出NullPointerException異常( ˘•ω•˘ ).oOஇ

Forge給出的另一個答案是@ObjectHolder
@ObjectHolder可以用在類別(Class)上,也可以用在類別中的欄位(Field)上
每當註冊系統完成一個階段,有@ObjectHolder的地方就會被注入註冊成功後的物品

因為@ObjectHolder的規則實在有點複雜,直接上M鼠親手寫的可愛範例

@ObjectHolder("namespace_a") class A{ /* * 'Item' with ID "namespace_a:registry_name_a" will be injected to this field */ @ObjectHolder("registry_name_a") public static final Item any_field_name_a = null; /* * 'Block' with ID "namespace_a:registry_name_b" will be injected to this field */ @ObjectHolder("registry_name_b") public static final Block any_field_name_b = null; /* * Item with ID "namespace_a:registry_name_c" will be injected to this field * Note that no ObjectHolder annotation here, so the field name will be used as registry name */ public static final Item registry_name_c = null; /* * Item with ID "namespace_a:registry_name_c" will be injected to this field * It means you can also use the uppercase character as field name * But the Item ID will still be "namespace_a:registry_name_c" * Note that no ObjectHolder annotation here, so the field name will be used as registry name */ public static final Item REGISTRY_NAME_C = null; /* * Item with ID "namespace_b:registry_name_d" will be injected to this field * A namespace in field annotation will overrides class's annotation */ @ObjectHolder("namespace_b:registry_name_d") public static final Item any_field_name_d = null; /* * No Object will be injected to this field * Rule: * No annotation * && * Non-public || Non-static || Non-final * This field will be ignored * It means if a field has no annotation, only public static final will be process */ private Item any_field_name_e = null; /* * Item with ID "namespace_a:registry_name_f" will be injected to this field * Note that it has annotation on this field * So it will be tried to injected something even it's NOT public static final field */ @ObjectHolder("registry_name_f") private Item any_field_name_f = null; /* * Item with ID "namespace_c:registry_name_e" will be injected to this field * It means you can use your custom class which extends a registable class(Ex. Item, Block, Effect...) */ @ObjectHolder("namespace_c:registry_name_e") public static final AnItemChildClass any_field_name_g = null; } /* * Note that no ObjectHolder annotation on class B */ class B{ /* * Item with ID "namespace_d:registry_name_h" will be injected to this field */ @ObjectHolder("namespace_d:registry_name_h") public static final Item any_field_name_h = null; /* * THIS WILL FAILD and throw IllegalStateException * No namespace specified in ObjectHolder on field and no ObjectHolder annotation on class */ @ObjectHolder("registry_name_i") public static final Item any_field_name_i = null; /* * No Object will be injected to this field * No ObjectHolder annotation on class and no ObjectHolder annotation on field * OF COURSE! */ public static final Item any_field_name_j = null; }

ㄏㄏㄏ,我不是說很複雜了嗎? (σ′▽‵)′▽‵)σ (σ′▽‵)′▽‵)σ (σ′▽‵)′▽‵)σ

不過這邊我們的範例布丁其實很簡單,只要在Items類別這樣寫就好了

@ObjectHolder(ModTutorialIM.MODID) public class Items{ public static final Pudding PUDDING = null; // ... }

剛剛那堆範例,單純是如果你吹毛求疵,想知道所有的可能性會有什麼結果的話
常用的方法其實就那幾種 (゚∀゚)

  • Class加上@ObjectHolder指名命名空間,Field名稱直接和註冊名稱相同
  • Class不加@ObjectHolder,Field加上@ObjectHolder並直接指名命名空間加註冊名稱

將物品加入創造模式物品欄

你說

M鼠~每次我要拿我物品,都要用give指令,也太麻煩了吧! (╯‵□′)╯︵┴─┴

對阿,好麻煩,我們現在創建一個新的創造模式物品欄,然後放進去吧
創造模式物品欄指的就是這個東西
圖1-3-6 創造模式物品欄畫面

我們需要的類別是net.minecraft.item.ItemGroup
如果你需要非常客製化的物品欄,那麼建議你繼承這個類,然後覆寫(Override)裡面的方法

這邊提幾個你可能有興趣的類別方法 ┬─┬ ノ( ' - 'ノ)

  • ItemGroup.createIcon()決定了這個物品欄的代表圖標,如上圖中的食品物品欄圖標是蘋果

    有注意到ItemGroup是一個抽象類別(Abstract class)嗎?
    ItemGroup中唯一的抽象方法(Abstract method)就是這一個

    這個方法回傳一個net.minecraft.item.ItemStack,也就是說你將使用一個ItemStack的外觀當你物品欄的圖標
    你可以想像ItemStack就是一個Item再加上數字,"蘋果"(Item) => "五個蘋果"(ItemStack)

    但事實上一個ItemStack中包含的資訊比起Item還要多上許多(Ex. NBT標籤 & NBT標籤 & NBT標籤 & N B T 標 籤)
    玩家物品欄中所有你看到的東西都是ItemStack而不是Item

    你可能會懷疑為啥這方法不是回傳Item而是ItemStack
    這是因為根據ItemStack的不同,Item的材質顯示是會改變的(Ex. 藥水瓶、弓、指南針)
    所以事實上最後決定材質顯示的不是Item而是ItemStack

  • ItemGroup.hasScrollbar()決定了這個物品欄是否有縱軸滾條

    預設是true,繼承後你可以在建構子中使用ItemGroup.setNoScrollbar()修改

  • ItemGroup.hasSearchBar()決定了這個物品欄是否有搜尋欄

    可惜的是這邊原版寫死了,照他的寫法,只有指南針圖標的那個物品欄可以有搜尋欄,而且唯一
    如果你需要的話請覆寫(Override)這個方法,並直接回傳true

  • ItemGroup.getBackgroundImage()決定了物品欄的背景圖片

    雖然有ItemGroup.setBackgroundImageName()
    但如果你仔細看會發現因為ItemGroup.getBackgroundImage()中寫死的關係,你只能選原版textures\gui\container\creative_inventory下既有的圖片
    有教學資料顯示你直接在你的resources資料夾中建立以minecraft為命名空間的資料夾就可以覆蓋原版的檔案,應該也包含新增,但這點M鼠並未實行驗證過

    所以你可以直接覆寫這方法,直接傳回新的net.minecraft.util.ResourceLocation

    ResourceLocation中的建構子包含了public ResourceLocation(String namespaceIn, String pathIn),用法和你前面在模型Json檔的方式很像,這樣你就可以指定你放在自己模組命名空間中的圖片了 (゚∀。)

不過在這邊M鼠要做的範例中,只會需要用到ItemGroup.createIcon()
所以我不會繼承這個ItemGroup類別,而是直接使用匿名內部類別(Anonymous inner class)

直接在我們的套件(Package)com.github.immortalmice.modtutorialim.item中建立一個ItemGroups類別
這類別會像是一個列表,列出我們模組中新增的物品欄們

在這整個教學中,M鼠只會為這個模組新增兩個物品欄,一個是放物品,一個是用來放方塊
你可以根據你模組的需求來決定你需要新增哪些物品欄

這邊我們就只先建立用來放模組物品的物品欄吧

public class ItemGroups{ public static final ItemGroup ITEM_TAB = (new ItemGroup("modtutorialim.items"){ @Override public ItemStack createIcon(){ return new ItemStack(Items.PUDDING); } }); }

上面ItemGroup中建構子傳入的字串是label,未來也會用它來生成我們的TranslationKey
建議一定要用modid最為前綴,否則容易撞名 (´σ-`)

裡面我們覆寫了createIcon()這個方法,用我們的布丁Item建立一個ItemStack
這是建立一個ItemStack最簡單的方法

現在,我們把我們的布丁放進這個物品欄吧
回去Pudding的建構子,修改一下

public Pudding(){ super((new Item.Properties()) .maxStackSize(1) .rarity(Rarity.EPIC) .group(ItemGroups.ITEM_TAB)); }

夠簡單吧,就再串上一個名叫group丸子方法,傳進剛剛建立的ItemGroup

現在執行遊戲,打開創造模式背包,你應該可以在第二頁找到你的布丁了
圖1-3-7 創造模式物品欄成果

你說,欸這個itemGroup.modtutorial.items是什麼鬼啊!
沒錯,M鼠忘記帶大家改語言檔了 レ(゚∀゚;)ヘ=З=З=З

不怕不怕,打開你的語言檔吧
你問我TranslationKey是甚麼?看看ItemGroup.getTranslationKey()吧 (›´ω`‹ )
程式碼說明一切,我們建構子傳進的label前面加上"itemGroup.",簡單暴力,不拖泥帶水

由於未來語言檔會越來越複雜、越來越長,所以我們順便加上一些註解 (∩^o^)⊃━☆゚.*・。
現在語言檔會長成這樣

  • en_us.json
    ​​​​{"_comment": "==================Creative Tabs==================","itemGroup.modtutorialim.items": "Immortalmice's Mod Tutorial Items","_comment": "=======================ITEM=======================","item.modtutorialim.pudding": "Pudding" ​​​​}
  • zh_tw.json
    ​​​​{"_comment": "==================創造模式物品欄==================","itemGroup.modtutorialim.items": "M鼠的教學模組物品","_comment": "======================物品=======================","item.modtutorialim.pudding": "布丁" ​​​​}

重新打開遊戲,你可愛的創造模式物品欄就躺在那裏囉
圖1-3-8 創造模式物品欄名稱成果
同時也記得切另一個語言看有沒有正確載入囉

將物品設為食物

我 想 吃 布 丁 なの

布丁果然還是要能吃才行阿(¯﹃¯)

過去的1.12.2中,如果你的物品要能吃的話,你必須要繼承ItemFood這個類別
但現在已經分家了,至於分家成哪兩個類別呢

  • Item
  • Food

恩,哈哈哈哈,M鼠的笑話真是難笑 (ㆆᴗㆆ)

來看看net.minecraft.item.Food類別吧~
首先來看建構子

什麼!建構子是private!而且只有一個!看來是個注重隱私的朋友呢
不怕不怕,看到有一個叫Food.Builder的內部類別 (inner class)了嗎?
這才是我們該用DER東西,基本上用法和Item.Properties一樣,那有哪些方法呢

  • hunger()
    這東西用來決定一個食物能恢復多少飢餓值
    方法傳進一個整數,2點飢餓值等同遊戲裡飢餓條的一個雞腿
    所以我說為啥飢餓條是用雞腿?布丁不好嗎?

  • saturation()
    這東西用來決定食物的飽食度
    嗯?你說飢餓值和飽食度差在哪?
    詳細你可以看Wiki的介紹

    方法傳入一個浮點數(Float),MCP在這方法參數的翻譯有點不太精確
    這個數值最後會傳進net.minecraft.util.FoodStats.addStats(int, float)
    這個方法參數翻譯就精確多了,是foodSaturationModifier

    對,他是Modifier,而不是和hunger一樣單純的一個數值
    這個Modifier最後依舊會轉換成數值,轉換公式是:
    hunger * saturationModifier * 2.0F

    舉例來說

    • 原版的蘋果

      • hunger = 4
      • saturationModifier = 0.3

      那最後的飽食度就是2.4,等於1.2個雞腿

    • 原版的黃金胡蘿蔔

      • hunger = 6
      • saturationModifier = 1.2

      那最後的飽食度就是14.4,等於7.2個雞腿

  • meat()
    這個方法決定食物是不是
    在原版中唯一用到這個值的地方就是在判斷食物可不可以給狼吃 (´-ω-`)

  • setAlwaysEdible()
    這個方法決定食物的可食性是不是無視當前飢餓值
    原版生存模式中的玩家,若飢餓值是滿的,那他無法吃大部分的食物
    只有金蘋果、附魔金蘋果、歌萊果這三個可以食用
    這三個可以食用的關鍵就是因為呼叫了這個方法

    順帶一提,創造模式的玩家永遠都能吃任何食物 ╮(′~‵〞)╭

  • fastToEat()
    這個方法決定食物是否可以以二倍速食用
    原本吃下一個食物要32 tick,相當於1.6秒
    吃下一個有fastToEat的食物需要16 tick,相當於0.8秒

  • effect()
    這個方法可以為食物新增新的藥水效果
    注意,是新增,也就是你可以多次呼叫,給食物多個藥水效果
    舉例來說就是原版的河豚、金蘋果etc

  • build()
    全部都設定完之後就呼叫這個方法吧
    它會回傳一個Food實例,就是最後的結果 (  ̄ 3 ̄)y

好的,那我們就來改我們的布丁吧
M鼠想要以下的東西

  • 飢餓度為4,只是個布丁,填飽不了肚子,但還是神器 ε(*´・∀・`)з゙
  • 飽食修飾值為2.0,雖然吃不飽,但吃了會很高興 (●⁰౪⁰●)
  • 永遠可食,裝甜點是另一個胃,而甜點不會吃飽,這不是Bug,這是上帝的禮物( ゚∀゚) ノ♡

那麼,剛剛有去看過Item.Properties的你應該亳不急待地要告訴我,你看到了一個叫food()的方法對吧
Yes,這方法傳進一個Food實例,就是我們要的,來改寫Pudding的建構子吧

public Pudding(){ super((new Item.Properties()) .maxStackSize(1) .rarity(Rarity.EPIC) .group(ItemGroups.ITEM_TAB) .food((new Food.Builder()) .hunger(4) .saturation(2.0f) .setAlwaysEdible() .build() ) ); }

現在打開遊戲,切回生存模式
輸入指令讓自己飢餓值清空

/effect @p minecraft:hunger 5 255

然後吃下布丁,這時候你的飢餓條應該會補回兩個雞腿 (´◔​∀◔`)
圖1-3-9 食物成果

試著讓飢餓值吃滿吧,你應該會發現就算飢餓條是滿的,你還是能吃下布丁~

雖然是都成功了不過M鼠還是有點不滿意

布丁這麼好吃,應該要吃很快阿,1.6秒對布丁來說也太久了吧? (╯°▽°)╯ ┻-┻

你可能說:"啊!我知道了,就是剛剛的fastToEat()對吧!"

是啦,可是0.8秒也還是很久耶對於布丁這麼好吃的東西來說

既然fastToEat()只能讓你改成16 tick,那麼我們就改用另一個方法吧
我們需要的是在我們的Pudding類別覆寫(Override)Item.getUseDuration()

@Override public int getUseDuration(ItemStack stack){ return 2; }

2 tick,0.1秒,完美 (*≥▽≤)ツ┏-┓

那就打開遊戲囉,你現在應該可以在0.1秒吃下手中的布丁
開啟創造模式後更是舒暢呢~

WEEEEEEEEEEEEEEEE: ♡。゚.(*♡´◡` 人´◡` ♡*)゚♡ °・WEEEEEEEEEEEEEEEE

將物品敘述框加上敘述

敘述框就是指這個東西,英文上叫做Tooltip
圖1-3-10 物品敘述框展示

我們需要覆寫Item.addInformation(),他有四個參數

  1. ItemStack stack
    這代表你可以根據ItemStack,來顯示不同的敘述內容
    ItemStack的靈魂就是NBT標籤,你通常就是根據NBT標籤來顯示對應的敘述
    上圖的Tinker Construct工具就是這樣喔

  2. @Nullable World worldIn
    這代表你還可以根據世界來顯示不同的敘述內容
    Ex. 天氣、時間如果你需要的話啦(´_ゝ`)

  3. List<ITextComponent> tooltip
    這就是最關鍵的參數啦!
    對這個List你可以新增敘述,以行為單位
    每一行都是一個實現了net.minecraft.util.text.ITextComponent的實例

    ITextComponent用來顯示文字,除了字母符號以外,你還可以有樣式
    Ex. 顏色、粗體、斜體、底線、混淆
    關於ITextComponent後面會有更詳細的介紹 (╯✧∇✧)╯

  4. ITooltipFlag flagIn
    原版唯一實現了ITooltipFlag的是net.minecraft.client.util.ITooltipFlag.TooltipFlags
    他用來表示遊戲現在是否開啟了進階模式的敘述框顯示
    進階模式預設由F3 + H開啟和關閉
    請注意這和許多模組會寫的 "按下Shift顯示更多訊息" 是不同的東西 ( • ̀ω•́ )

那現在來說說ITextComponent吧,直接實現了此介面的類別是net.minecraft.util.text.TextComponent,是一個虛擬類別(Abstract Class)

而直接繼承了TextComponent的類別有六個,我只挑出三個常用的來說明

  • net.minecraft.util.text.KeybindTextComponent
    玩家是有可能會改按鍵綁定的,比如說把蹲下改成Ctrl,把跳躍改為滑鼠中鍵
    那如果你要顯示請按下XXX蹲下,如果你XXX寫死,那就會誤導改了按鍵的玩家
    KeybindTextComponent可以幫你自動轉換成玩家綁定的按鍵並顯示

  • net.minecraft.util.text.StringTextComponent
    最純粹的ITextComponent,給予字串,他就會顯示

  • net.minecraft.util.text.TranslationTextComponent
    給予TranslationKey,他會轉換出當前語言設定中對應的字串

好了之後我們就來為布丁加上敘述吧(ゝ∀・)b
com.github.immortalmice.modtutorialim.item.Pudding

@Override public void addInformation(ItemStack stack, @Nullable World worldIn, List<ITextComponent> tooltip, ITooltipFlag flagIn){ tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_1")); tooltip.add(new StringTextComponent("")); tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_2")); tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_3")); tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_4")); tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_5")); tooltip.add(new StringTextComponent("")); tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_6")); tooltip.add(new StringTextComponent("")); tooltip.add((new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_7")) .setStyle((new Style()) .setBold(true) .setItalic(true) .setColor(TextFormatting.GOLD) ) ); }

resources/assets/modtutorialim/lang/zh_tw.json

{ "_comment": "=====================敘述欄======================", "tooltip.modtutorialim.pudding.descript_1": "§7曾經有個老鼠,他想要做§b五§r§7件事", "tooltip.modtutorialim.pudding.descript_2": "§7首先是他想要寫Forge模組開發教學", "tooltip.modtutorialim.pudding.descript_3": "§7他也想要吃布丁,還有吃布丁", "tooltip.modtutorialim.pudding.descript_4": "§7以及吃下那顆美味可口的布丁", "tooltip.modtutorialim.pudding.descript_5": "§7至於最後的一件事,那就是吃布丁", "tooltip.modtutorialim.pudding.descript_6": "§7可是時間有限,所以他做了個決定", "tooltip.modtutorialim.pudding.descript_7": "他 全 都 要" }

resources/assets/modtutorialim/lang/en_us.json

{ "_comment": "======================TOOLTIP=====================", "tooltip.modtutorialim.pudding.descript_1": "§7There was a mouse. He wanted to do §bFIVE§r §7things.", "tooltip.modtutorialim.pudding.descript_2": "§7First, he wanted to write a tutorial.", "tooltip.modtutorialim.pudding.descript_3": "§7He also wanted to eat pudding, and eat pudding.", "tooltip.modtutorialim.pudding.descript_4": "§7Then, eat the delicious pudding.", "tooltip.modtutorialim.pudding.descript_5": "§7The last thing is, eat pudding.", "tooltip.modtutorialim.pudding.descript_6": "§7But he had limited time, so he made a decision.", "tooltip.modtutorialim.pudding.descript_7": "HE WANTED THEM ALL" }

參數tooltip是一個java.util.List,直接呼叫add()就可以新增一行敘述了
這邊M鼠只用到了TranslationTextComponentStringTextComponent,上面已經有介紹,這邊就不再做解釋

你可能發現了,是的,TranslationKey是可以自己創建的 ( ♥д♥)
但還是有幾點要注意

  1. 請讓他唯一,就算跨模組也是
    想想看,你今天要為你的棉花種子設TranslationKey,你用了tooltip.cotton_seed.descript
    另一個模組如果有新增棉花種子,也有要加敘述欄的話,你們很容易就撞在一起
    所以最好的方法就是在TranslationKey中加入你的modid,通常會加在第二欄

  2. 請讓TranslationKey可以很好的表達這個Key會用在哪個地方

而直接新增一個new StringTextComponent("")就可以做到空行的作用囉 ´-ω-)b

我在tooltip增加的最後一行,我為他新增了樣式net.minecraft.util.text.Style
裡面我設定了粗體斜體金色,而其他可以用的樣式你可以自行參考Style類別中的方法宣告

這邊有一個很特別的地方就是setColor(),這個方法傳入一個net.minecraft.util.text.TextFormatting

TextFormatting包辦的並不只有顏色,剛剛的粗體、斜體,還有底線、混淆之類的都在裡面
所以理論上你是可以透過setColor()去把樣式設成粗體的,但是這樣做會讓程式碼使人誤會,建議不要這樣使用,估計是MCP翻譯時出的小差錯 (´・_・`)

時空M鼠:MCP新的Mapping已經把這方法的名字改成applyTextStyle,明確多了

TextFormatting的建構子是private的,你只能使用他列舉(enum)中既有的成員

最後,你有看到我的Json檔中用了§7§r§b這些奇怪的東西嗎?
這個叫做FormattingCode,他用來表示上面的一個TextFormatting
比如說TextFormatting.GREEN的宣告是這樣的:

GREEN("GREEN", 'a', 10, 5635925)

那麼TextFormatting.GREEN的FormattingCode就是§a

關於FormattingCode的規則,Wiki上有介紹,

如果在格式代碼後使用顏色代碼,則格式代碼的作用範圍只能持續到顏色代碼之前。因此,當使用顏色代碼與格式代碼一起使用時,確保首先使用顏色代碼,並在更改顏色時重用格式代碼。
§r可以用於重置文字樣式。

Wiki上有給出舉例,建議可以去看一下

那麼,現在打開遊戲,你就可以看到成果了 _(┐「ε:)_
圖1-3-11 物品敘述欄成果

好的,那所有模組都很愛用的那個 "按下Shift顯示更多訊息" 要怎麼做呢?
很簡單,我們只需要透過net.minecraft.client.gui.screen.Screen.hasShiftDown()來知道玩家是否按下了Shift鍵就可以了

改一下程式碼

@Override public void addInformation(ItemStack stack, @Nullable World worldIn, List<ITextComponent> tooltip, ITooltipFlag flagIn){ boolean isShiftDown = Screen.hasShiftDown(); if(!isShiftDown){ tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.hold_shift_descript")); }else{ tooltip.add(new TranslationTextComponent("tooltip.modtutorialim.pudding.descript_1")); /* ... */ } }

現在打開遊戲,你應該就可以用Shift來切換進階顯示了
圖1-3-12 物品敘述欄Shift成果
圖1-3-13 物品敘述欄Shift按下後成果

那麼,到此為止,你已經學會如何建立一個物品以及許許多多的功能了
別忘了也要打開你的runServer確認有沒有問題喔 (っ´ω`c)

Item與ItemStack

還記得我說過每個人物品欄中所有的東西都是ItemStack嗎?

更重要的是,每一個被註冊上去的Item都是唯一的,物理的唯一
也就是說,小美手中的1個蘋果、小明手中的64個蘋果和M鼠摔倒不小心掉在地上的87顆蘋果
這些ItemStack中的Item在Java虛擬機中的記憶體位置是一樣的 (*゚∀゚*)

來看一下一個經典的例子

圖1-3-14 Tinker Construct兩把劍展示

這兩把Tinker Construct的劍

顯示名稱不一樣、Tooltip中的敘述文字不一樣、攻擊力不一樣、外觀不一樣,甚至Tinker Construct還給了他們不一樣的特性,左邊會隨時間回復耐久、右邊有磁吸效果

這樣的兩個東西,物品ID#5472是一樣的,連註冊名tcconstruct:broadsword也一樣
甚至這樣兩個ItemStack中的Item,在以下判斷式會通過,因為記憶體位置是一樣的 (o´罒`o)

if(leftItemStack.getItem() == rightItemStack.getItem()){ // IT'S TRUUUUUUUUUUUUUUUE }

我想你應該已經能夠感受到我要說的了,ItemStack不單單只是Item加上個數,這麼簡單的東西

而做到這一切的,最大的原因就是ItemStack可以擁有net.minecraft.nbt.CompoundNBT
這個東西你可以透過ItemStack.hasTag()來確認有無,和用ItemStack.getTag()獲得

CompoundNBT的用法很簡單,真的,你打開類別之後看一下你應該就知道該怎麼用了 ( • ̀ω•́ )

至於上面作為例子的兩把劍,這些不同的地方式怎麼做到的,這邊不做深入討論
但你只要記得,任何方法(Method),只要他參數中有ItemStack,那就恭喜你可以動手拉
這些方法,包含但不限於以下舉例 (つ´ω`)つ

public void Item.addInformation(ItemStack, /*...*/); public float Item.getDestroySpeed(ItemStack, /*...*/); public ITextComponent Item.getDisplayName(ItemStack); public boolean Item.hasEffect(ItemStack); public Rarity Item.getRarity(ItemStack); public IBakedModel ItemOverrideList.getModelWithOverrides(/*...*/, ItemStack, /*...*/);

如果沒有傳入ItemStack怎麼辦?
別忘了,條條大路通NBT,你可能可以從玩家的物品欄或是傳入的參數中找到ItemStack
想辦法拿到ItemStack吧! σ`∀´)σ


本頁面撰寫於2020/05/11,目前最後更新日期為2020/06/06
若上述時間與你閱讀的時間相距過遠,請自行斟酌是否採用本頁面的資訊
完整的程式碼可以到本教學文的Github Repo中查看