第一個物品
===
模組的架構好了,那我們現在試著加新物品來看看吧
恩...我想想喔...來做個布丁如何?(〃∀〃)
在本章節你會了解
---
- 如何創建一個物品實例(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)中查看*