第一個物品 === 模組的架構好了,那我們現在試著加新物品來看看吧 恩...我想想喔...來做個布丁如何?(〃∀〃) 在本章節你會了解 --- - 如何創建一個物品實例(Instance) - 如何註冊物品 - 幫物品顯示名稱寫語言檔 - 幫物品加上材質 - 在程式碼中獲取你註冊的物品 - 將物品加入創造模式物品欄 - 將物品設為食物 - 將物品Tooltip加上敘述 - Item與ItemStack 如何創建一個物品實例(Instance) --- 我們來創建一個套件(Package)`com.github.immortalmice.modtutorialim.item` 未來所有的物品類別會放在這裡 那我們就建立一個Pudding的類別,並繼承minecraft原生的`net.minecraft.item.Item`類別 ```java= public class Pudding extends Item{ public Pudding(){ } } ``` 好的,這時候編譯器應該會告訴你Item類別**並沒有**宣告**沒有參數的建構子**,要你處理 那我們就來看一下Item這個類別有哪些建構子可以用吧 ...很好,只有一個_(:3 ⌒゙)_ ```java= public Item(Item.Properties); ``` 那這個`Item.Properties`是何方神聖呢? 簡單來說,他用來告訴建構子一些物品可以有的基礎屬性 Ex. 最大物品堆疊數、可否被修理、稀有度、是否是個食物 你可以看一下`Item.Properties`裡有的方法,看看有沒有你需要的 這邊我會先示範設定其中兩個屬性 - 最大物品堆疊數為1,**~~布丁這麼脆弱,你居然想堆疊他!不可原諒!(#\`Д´)ノ~~** - 稀有度為Epic,**~~布丁這麼神聖的東西,你居然敢懷疑他的地位嗎?天都要塌下來了(メ ゚皿゚)メ~~** 最後,你可有發現這些方法都是回傳一個`Item.Properties`嗎? 是的,玩串串樂的時間到了 ( ・・)つ―{}@{}@{}- ```java= public Pudding(){ super((new Item.Properties()) .maxStackSize(1) .rarity(Rarity.EPIC)); } ``` 這邊的`Rarity`是`net.minecraft.item`套件(Package)下的一個列舉(enum) 他總共有四個`Rarity.COMMON`、`Rarity.UNCOMMON`、`Rarity.RARE`、`Rarity.EPIC` 這樣一個簡單的物品就完成了,接下來我們要用Forge註冊它ლ(╹◡╹ლ) 如何註冊物品 --- *(以下提供的範例可以只當參考,M鼠只是考慮到未來開發的擴張和管理程式碼上的選擇,所以你會發現M鼠好像創了很多套件,繞了有點多路,搞得很複雜,最最最簡單的完整註冊方法Forge已經寫在`DeferredRegister`的註解中了,**強烈建議一定要看過**)* 目前1.15.2 Forge建議的註冊方式是使用`net.minecraftforge.registries.DeferredRegister<T>` 雖然過去的方法`net.minecraftforge.event.RegistryEvent.Register<T>`並未被刪除,但使用此事件來註冊已經不是Forge推薦的方式了 這是[他們的說法](https://www.minecraftforge.net/forum/topic/79493-1151-deferredregister-vs-registryevent/) > 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),把註冊相關用的東西放在裡面₍₍ ◝('ω'◝) ⁾⁾ ₍₍ (◟'ω')◟ ⁾⁾ ```java= 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`類別裡加入一個欄位 ```java= 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必須要是你註冊類型的子類別 比如說這邊`Pudding`是`Item`的子類別 傳入的`Supplier`必須每次都創建出一個**新的實例**(Instance) 對,**新的**,我再說一次,**新的**,否則你可能會遇到無法解釋的Bug,~~因為M鼠做過~~ (つд⊂) 像這邊寫的`() -> new Pudding()`就是一個最簡單的實作方法 一整個下來,其實你會發現`DeferredRegister`不過就是一個裝著一堆`Supplier`的列表,等著把這些`Supplier`在註冊時期執行並完成註冊的動作 現在,我們要把這個DeferredRegister綁定到我們的Mod Event Bus上 *(關於事件系統的教學部分我放在後面,請看本教學中的[這篇文](https://hackmd.io/@immortalmice/HJ3VSHJ_I)。 或是你也可以先照做,未來再了解Mod Event Bus是什麼即可)* **這個動作要在非常早期就完成,基本上建議在你的模組主類別建構子中完成** 不過,在模組主類別建構子中加上程式碼之前,我們先新增一個套件`com.github.immortalmice.modtutorialim.handlers`,未來所有的handlers類型的類別會放在這,Ex.材質處理、地圖生成處理、指令處理...etc 現在我們需要的是一個幫我們處理註冊的類別(包括物品、方塊、藥水效果...總之一切要用`DeferredRegister`註冊的都會放在這) 所以我們在這個套件中建立`RegistryHandler`類別 ```java= public class RegistryHandler{ private static IEventBus BUS = FMLJavaModLoadingContext.get().getModEventBus(); } ``` 其中,`FMLJavaModLoadingContext.get().getModEventBus()`就是用來獲取我們Mod Event Bus的方法 再來,我們就只要把這個Bus傳給DeferredRegister即可 在`RegistryHandler`類別中新增`registAll`方法 `registAll`未來也會負責註冊方塊之類的東西(〃 ̄ω ̄)人( ̄︶ ̄〃) ```java= public static void registAll(){ Items.getRegister().register(RegistryHandler.BUS); } ``` 這邊我們透過剛才寫好的Items類別來獲取物品的`DeferredRegister` 最後的最後,我們在模組主類別中呼叫這個方法吧 ```java= public ModTutorialIM(){ RegistryHandler.registAll(); } ``` 現在打開遊戲,輸入以下指令 `/give @p modtutorial:pudding` 你應該可以拿到一個物品 ![圖1-3-1 獲得物品](https://i.imgur.com/3M6mKzP.png) 這代表你的物品已經成功進入遊戲系統中了 只是這個東西現在沒有材質、只能用`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,asset**s**最後面有個s 那我們現在要建兩個檔案 - en_us.json - zh_tw.json 這兩個分別是英文(美國)和繁體中文的檔案 想知道有支援那些語言,你可以到[Minecraft Wiki](https://minecraft-zh.gamepedia.com/%E8%AF%AD%E8%A8%80)中看 檔案名稱是被指定的,請參照Wiki中 "可用語言" 區塊的 "代碼" 欄位 好了我們就在檔案裡面寫上資料吧 - en_us.json ```json= { "item.modtutorialim.pudding": "Pudding" } ``` - zh_tw.json ```json= { "item.modtutorialim.pudding": "布丁" } ``` Json中前面的key是TranslationKey,後面的值則是在該語言中該顯示的字串 現在打開遊戲,你手中的物品應該有可愛的名字了 (๑´ㅁ\`) ![圖1-3-2 英文語言檔成果](https://i.imgur.com/gvqlfY7.png) 記得切換語言來確定你另一個語言有沒有正確載入喔 ![圖1-3-3 遊戲語言切換頁面](https://i.imgur.com/USMY4lA.png) ![圖1-3-4 中文語言檔成果](https://i.imgur.com/007zPs5.png) 幫物品加上材質 --- 一個物品要有外觀,基本上需要兩個東西 - 模型Json檔 這個檔案包含了許多資訊,這些資訊用來建構一個物品最後會長成什麼樣子 可能是指定當物品在不同狀態時,轉而使用另一個模型Json檔 也可能是設定在不同攝影機角度時,該如何縮放、移動、旋轉 當然還有最重要的,指定該使用的圖像資源在哪裡 詳細你可以查看[Minecraft Wiki](https://minecraft-zh.gamepedia.com/%E6%A8%A1%E5%9E%8B)的介紹 - 圖片 呃...這還要解釋嗎?\_(:3 ⌒゙)\_ 首先,我們的物品模型Json檔要放在`resources/assets/modtutorialim/models/item` 恩,我說過了,這是寫死的,請一字不差的建立,特別注意哪些名稱後面有s哪些沒有 好了之後就在裡面建立一個`pudding.json`,pudding請改成你物品的註冊名 ```json { "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.json`當`parent`之後,我們的模型就可以直接沿用這些數據 至於`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](https://i.imgur.com/JbLh4uS.png) 大小建議 **16x16** 如果你真的想改,請使用**2的指數**(Ex. 1x1, 2x2, 8x8, 16x16, 128x128) 如果你想叛逆,請至少用**正方形**(Ex. 10x10, 30x30) 如果你想翻桌,用的圖片連正方形都不是,Minecraft會直接強制縮放成正方形,同時某些原先貼圖功能會失效,~~甚至就算你之後改邪歸正,也會有奇怪的Bug追著你跑~~ 16x16在遊戲裡其實就已經是夠清晰的了,少年你火氣還是不要這麼大吧... •\_ゝ• 現在開遊戲,你應該可以看到可愛的布丁ㄌ~ ![圖1-3-5 材質成果](https://i.imgur.com/LCwpItY.png) 在程式碼中獲取你註冊的物品 --- 在未來我們的程式碼之中,我們會需要使用到這個我們新增的物品 比如說我們需要判斷玩家手上的東西是不是我們新增的布丁 ```java= 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鼠親手寫的~~可愛~~範例 ```java= @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`類別這樣寫就好了 ```java= @ObjectHolder(ModTutorialIM.MODID) public class Items{ public static final Pudding PUDDING = null; // ... } ``` 剛剛那堆範例,單純是如果你吹毛求疵,想知道所有的可能性會有什麼結果的話 常用的方法其實就那幾種 (゚∀゚) - Class加上`@ObjectHolder`指名命名空間,Field名稱直接和註冊名稱相同 - Class不加`@ObjectHolder`,Field加上`@ObjectHolder`並直接指名命名空間加註冊名稱 將物品加入創造模式物品欄 --- 你說 **M鼠~每次我要拿我物品,都要用`give`指令,也太麻煩了吧! (╯‵□′)╯︵┴─┴** 對阿,好麻煩,我們現在創建一個新的創造模式物品欄,然後放進去吧 創造模式物品欄指的就是這個東西 ![圖1-3-6 創造模式物品欄畫面](https://i.imgur.com/MKi9Axb.png) 我們需要的類別是`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鼠只會為這個模組新增兩個物品欄,一個是放物品,一個是用來放方塊 你可以根據你模組的需求來決定你需要新增哪些物品欄 這邊我們就只先建立用來放模組物品的物品欄吧 ```java= 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`的建構子,修改一下 ```java= public Pudding(){ super((new Item.Properties()) .maxStackSize(1) .rarity(Rarity.EPIC) .group(ItemGroups.ITEM_TAB)); } ``` 夠簡單吧,就再串上一個名叫`group`的~~丸子~~方法,傳進剛剛建立的`ItemGroup` 現在執行遊戲,打開創造模式背包,你應該可以在第二頁找到你的布丁了 ![圖1-3-7 創造模式物品欄成果](https://i.imgur.com/YvqtaJC.png) 你說,欸這個`itemGroup.modtutorial.items`是什麼鬼啊! 沒錯,~~M鼠忘記帶大家改語言檔了~~ レ(゚∀゚;)ヘ=З=З=З 不怕不怕,打開你的語言檔吧 你問我TranslationKey是甚麼?看看`ItemGroup.getTranslationKey()`吧 (›´ω\`‹ ) 程式碼說明一切,我們建構子傳進的label前面加上`"itemGroup."`,簡單暴力,不拖泥帶水 由於未來語言檔會越來越複雜、越來越長,所以我們順便加上一些註解 (∩\^o\^)⊃━☆゚.\*・。 現在語言檔會長成這樣 - en_us.json ```json= { "_comment": "==================Creative Tabs==================", "itemGroup.modtutorialim.items": "Immortalmice's Mod Tutorial Items", "_comment": "=======================ITEM=======================", "item.modtutorialim.pudding": "Pudding" } ``` - zh_tw.json ```json= { "_comment": "==================創造模式物品欄==================", "itemGroup.modtutorialim.items": "M鼠的教學模組物品", "_comment": "======================物品=======================", "item.modtutorialim.pudding": "布丁" } ``` 重新打開遊戲,你可愛的創造模式物品欄就躺在那裏囉 ![圖1-3-8 創造模式物品欄名稱成果](https://i.imgur.com/II5zmJc.png) 同時也記得切另一個語言看有沒有正確載入囉 將物品設為食物 --- **我 想 吃 布 丁 ~~なの~~!** 布丁果然還是要能吃才行阿...(¯﹃¯) 過去的1.12.2中,如果你的物品要能吃的話,你必須要繼承`ItemFood`這個類別 但現在已經分家了,至於分家成哪兩個類別呢... - `Item` - `Food` 恩,哈哈哈哈,~~M鼠的笑話真是難笑~~ (ㆆᴗㆆ) 來看看`net.minecraft.item.Food`類別吧~ 首先來看建構子... 什麼!建構子是`private`!而且只有一個!~~看來是個注重隱私的朋友呢~~ 不怕不怕,看到有一個叫`Food.Builder`的內部類別 (inner class)了嗎? 這才是我們該用DER東西,基本上用法和`Item.Properties`一樣,那有哪些方法呢 - `hunger()` 這東西用來決定一個食物能恢復多少飢餓值 方法傳進一個整數,2點飢餓值等同遊戲裡飢餓條的一個雞腿 ~~*所以我說為啥飢餓條是用雞腿?布丁不好嗎?*~~ - `saturation()` 這東西用來決定食物的飽食度 嗯?你說飢餓值和飽食度差在哪? 詳細你可以看[Wiki](https://minecraft-zh.gamepedia.com/index.php?title=%E9%A3%9F%E7%89%A9&variant=zh-tw)的介紹 方法傳入一個浮點數(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`的建構子吧 ```java= 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 食物成果](https://i.imgur.com/FF3heVJ.png) 試著讓飢餓值吃滿吧,你應該會發現就算飢餓條是滿的,你還是能吃下布丁~ 雖然是都成功了...不過...M鼠還是有點不滿意... **布丁這麼好吃,應該要吃很快阿,1.6秒對布丁來說也太久了吧? (╯°▽°)╯ ┻---┻** 你可能說:**"啊!我知道了,就是剛剛的`fastToEat()`對吧!"** 恩...是啦,可是0.8秒也還是很久耶...對於布丁這麼好吃的東西來說... 既然`fastToEat()`只能讓你改成16 tick,那麼我們就改用另一個方法吧 我們需要的是在我們的`Pudding`類別覆寫(Override)`Item.getUseDuration()` ```java= @Override public int getUseDuration(ItemStack stack){ return 2; } ``` **2 tick,0.1秒,完美 (\*≥▽≤)ツ┏---┓** 那就打開遊戲囉,你現在應該可以在0.1秒吃下手中的布丁 開啟創造模式後更是舒暢呢~ WEEEEEEEEEEEEEEEE: ♡。゚.(\*♡´◡\` 人´◡\` ♡\*)゚♡ °・WEEEEEEEEEEEEEEEE 將物品敘述框加上敘述 --- 敘述框就是指這個東西,英文上叫做Tooltip ![圖1-3-10 物品敘述框展示](https://i.imgur.com/9R9Sjmp.png) 我們需要覆寫`Item.addInformation()`,他有四個參數 1. `ItemStack stack` 這代表你可以根據`ItemStack`,來顯示不同的敘述內容 `ItemStack`的靈魂就是NBT標籤,你通常就是根據**NBT標籤**來顯示對應的敘述 上圖的[Tinker Construct](https://www.curseforge.com/minecraft/mc-mods/tinkers-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` ```java= @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` ```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` ```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鼠只用到了`TranslationTextComponent`和`StringTextComponent`,上面已經有介紹,這邊就不再做解釋 你可能發現了,是的,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`的宣告是這樣的: ```java= GREEN("GREEN", 'a', 10, 5635925) ``` 那麼`TextFormatting.GREEN`的FormattingCode就是`§a` 關於FormattingCode的規則,[Wiki](https://minecraft-zh.gamepedia.com/%E6%A0%B7%E5%BC%8F%E4%BB%A3%E7%A0%81)上有介紹, > 如果在格式代碼後使用顏色代碼,則格式代碼的作用範圍只能持續到顏色代碼之前。因此,當使用顏色代碼與格式代碼一起使用時,確保首先使用顏色代碼,並在更改顏色時重用格式代碼。 > `§r`可以用於重置文字樣式。 Wiki上有給出舉例,建議可以去看一下 那麼,現在打開遊戲,你就可以看到成果了 \_(┐「ε:)\_ ![圖1-3-11 物品敘述欄成果](https://i.imgur.com/e3vk79X.png) 好的,那所有模組都很愛用的那個 **"按下Shift顯示更多訊息"** 要怎麼做呢? 很簡單,我們只需要透過`net.minecraft.client.gui.screen.Screen.hasShiftDown()`來知道玩家是否按下了Shift鍵就可以了 改一下程式碼 ```java= @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成果](https://i.imgur.com/jLbsYO7.png) ![圖1-3-13 物品敘述欄Shift按下後成果](https://i.imgur.com/YE8CQdC.png) 那麼,到此為止,你已經學會如何建立一個物品以及許許多多的功能了 別忘了也要打開你的runServer確認有沒有問題喔 (っ´ω\`c) Item與ItemStack --- 還記得我說過每個人物品欄中所有的東西都是`ItemStack`嗎? 更重要的是,每一個被註冊上去的`Item`都是唯一的,物理的唯一 也就是說,小美手中的1個蘋果、小明手中的64個蘋果和M鼠摔倒不小心掉在地上的87顆蘋果 這些`ItemStack`中的`Item`在Java虛擬機中的記憶體位置是一樣的 (\*゚∀゚\*) 來看一下一個經典的例子 ![圖1-3-14 Tinker Construct兩把劍展示](https://i.imgur.com/wrqifCi.png) 這兩把[Tinker Construct](https://www.curseforge.com/minecraft/mc-mods/tinkers-construct)的劍 顯示名稱不一樣、Tooltip中的敘述文字不一樣、攻擊力不一樣、外觀不一樣,甚至Tinker Construct還給了他們不一樣的特性,左邊會隨時間回復耐久、右邊有磁吸效果 這樣的兩個東西,物品ID`#5472`是一樣的,連註冊名`tcconstruct:broadsword`也一樣 甚至這樣兩個`ItemStack`中的`Item`,在以下判斷式會通過,因為記憶體位置是一樣的 (o´罒\`o) ```java= if(leftItemStack.getItem() == rightItemStack.getItem()){ // IT'S TRUUUUUUUUUUUUUUUE } ``` 我想你應該已經能夠感受到我要說的了,`ItemStack`不單單只是`Item`加上個數,這麼簡單的東西 而做到這一切的,最大的原因就是`ItemStack`可以擁有`net.minecraft.nbt.CompoundNBT` 這個東西你可以透過`ItemStack.hasTag()`來確認有無,和用`ItemStack.getTag()`獲得 `CompoundNBT`的用法很簡單,真的,你打開類別之後看一下你應該就知道該怎麼用了 ( • ̀ω•́ ) 至於上面作為例子的兩把劍,這些不同的地方式怎麼做到的,這邊不做深入討論 但你只要記得,任何方法(Method),只要他參數中有`ItemStack`,那就恭喜你可以動手拉 這些方法,包含但不限於以下舉例 (つ´ω\`)つ ```java= 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](https://github.com/immortalmice/MinecraftForge1.15.2-ModdingTutorial)中查看*