模組的架構好了,那我們現在試著加新物品來看看吧
恩…我想想喔…來做個布丁如何?(〃∀〃)
我們來創建一個套件(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
裡有的方法,看看有沒有你需要的
這邊我會先示範設定其中兩個屬性
最後,你可有發現這些方法都是回傳一個Item.Properties
嗎?
是的,玩串串樂的時間到了 ( ・・)つ―{}@{}@{}-
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推薦的方式了
這是他們的說法
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必須要是你註冊類型的子類別
比如說這邊Pudding
是Item
的子類別
傳入的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
你應該可以拿到一個物品
這代表你的物品已經成功進入遊戲系統中了
只是這個東西現在沒有材質、只能用give
指令獲得、甚至沒有名字,我們後面就來解決這些事吧
你知道的,世界在各國都有人玩Minecraft
因此,除了有名字以外,我們還要考慮到翻譯的問題
每個物品都有一個TranslationKey,他用來尋找遊戲當前語言設定中對應的字串
這個key可以透過覆寫(Override)net.minecraft.item.Item.getTranslationKey()
來更改
但其實,除非真的有需要,不然不會覆寫這個方法,不然有可能會造成其他人幫你寫語言檔時的困擾
有注意到我們剛剛在遊戲中拿到的東西上面寫著item.modtutorialim.pudding
嗎?
M鼠第一次註冊名打錯字ㄌ,所以上面的圖片…(´∩ω∩`)
這就是剛剛提到的getTranslationKey
中幫你預設好的key
由於我們現在沒有任何語言檔,他找不到對應的東西顯示,因此就直接把key顯示出來了
到現在,你的專案src資料夾應該是長這樣
有看到那個resource資料夾了嗎?我們曾經改過裡面的mods.toml
這個資料夾如果你有做過材質包應該不陌生
一些跟java程式碼無關的東西都會放在這裡面
我們要在resources資料夾中建立這樣的資料夾結構 *ଘ(੭*ˊᵕˋ)੭* ੈ✩‧₊˚
這樣的資料夾結構是Minecraft在找東西時會遵循的路徑
可惜的是,這個路徑是寫死的,所以除非想找麻煩,還是照著順序建吧 (╥﹏╥)
資料夾名字一字不能差,modtutorialim請改為你的modid,assets最後面有個s
那我們現在要建兩個檔案
這兩個分別是英文(美國)和繁體中文的檔案
想知道有支援那些語言,你可以到Minecraft Wiki中看
檔案名稱是被指定的,請參照Wiki中 "可用語言" 區塊的 "代碼" 欄位
好了我們就在檔案裡面寫上資料吧
{
"item.modtutorialim.pudding": "Pudding"
}
{
"item.modtutorialim.pudding": "布丁"
}
Json中前面的key是TranslationKey,後面的值則是在該語言中該顯示的字串
現在打開遊戲,你手中的物品應該有可愛的名字了 (๑´ㅁ`)
記得切換語言來確定你另一個語言有沒有正確載入喔
一個物品要有外觀,基本上需要兩個東西
模型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
這個用來表示檔案位置的格式是 命名空間:路徑/檔案名稱
以modtutorialim:item/pudding
為例
命名空間就是modtutorialim
,通常是你的modid
路徑會被程式自動加上這個值被使用的地方的類型,這邊的類型是材質檔,所以他會被加上textures
資料夾
檔案名稱不用寫附檔名,因為這邊的類型是材質檔,所以會被程式加上.png
所以這個modtutorialim:item/pudding
表示法最後指出的位置就是resources/assets/modtutorialim/textures/item/pudding.png
那來看看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官方吧
這邊會用這張朋友 硯郎 幫忙畫的圖
大小建議 16x16
如果你真的想改,請使用2的指數(Ex. 1x1, 2x2, 8x8, 16x16, 128x128)
如果你想叛逆,請至少用正方形(Ex. 10x10, 30x30)
如果你想翻桌,用的圖片連正方形都不是,Minecraft會直接強制縮放成正方形,同時某些原先貼圖功能會失效,甚至就算你之後改邪歸正,也會有奇怪的Bug追著你跑
16x16在遊戲裡其實就已經是夠清晰的了,少年你火氣還是不要這麼大吧… •_ゝ•
現在開遊戲,你應該可以看到可愛的布丁ㄌ~
在未來我們的程式碼之中,我們會需要使用到這個我們新增的物品
比如說我們需要判斷玩家手上的東西是不是我們新增的布丁
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;
// ...
}
剛剛那堆範例,單純是如果你吹毛求疵,想知道所有的可能性會有什麼結果的話
常用的方法其實就那幾種 (゚∀゚)
@ObjectHolder
指名命名空間,Field名稱直接和註冊名稱相同@ObjectHolder
,Field加上@ObjectHolder
並直接指名命名空間加註冊名稱你說
M鼠~每次我要拿我物品,都要用give
指令,也太麻煩了吧! (╯‵□′)╯︵┴─┴
對阿,好麻煩,我們現在創建一個新的創造模式物品欄,然後放進去吧
創造模式物品欄指的就是這個東西
我們需要的類別是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
現在執行遊戲,打開創造模式背包,你應該可以在第二頁找到你的布丁了
你說,欸這個itemGroup.modtutorial.items
是什麼鬼啊!
沒錯,M鼠忘記帶大家改語言檔了 レ(゚∀゚;)ヘ=З=З=З
不怕不怕,打開你的語言檔吧
你問我TranslationKey是甚麼?看看ItemGroup.getTranslationKey()
吧 (›´ω`‹ )
程式碼說明一切,我們建構子傳進的label前面加上"itemGroup."
,簡單暴力,不拖泥帶水
由於未來語言檔會越來越複雜、越來越長,所以我們順便加上一些註解 (∩^o^)⊃━☆゚.*・。
現在語言檔會長成這樣
{
"_comment": "==================Creative Tabs==================",
"itemGroup.modtutorialim.items": "Immortalmice's Mod Tutorial Items",
"_comment": "=======================ITEM=======================",
"item.modtutorialim.pudding": "Pudding"
}
{
"_comment": "==================創造模式物品欄==================",
"itemGroup.modtutorialim.items": "M鼠的教學模組物品",
"_comment": "======================物品=======================",
"item.modtutorialim.pudding": "布丁"
}
重新打開遊戲,你可愛的創造模式物品欄就躺在那裏囉
同時也記得切另一個語言看有沒有正確載入囉
我 想 吃 布 丁 なの!
布丁果然還是要能吃才行阿…(¯﹃¯)
過去的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
舉例來說
原版的蘋果
那最後的飽食度就是2.4,等於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鼠想要以下的東西
那麼,剛剛有去看過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
然後吃下布丁,這時候你的飢餓條應該會補回兩個雞腿 (´◔∀◔`)
試著讓飢餓值吃滿吧,你應該會發現就算飢餓條是滿的,你還是能吃下布丁~
雖然是都成功了…不過…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
我們需要覆寫Item.addInformation()
,他有四個參數
ItemStack stack
這代表你可以根據ItemStack
,來顯示不同的敘述內容
ItemStack
的靈魂就是NBT標籤,你通常就是根據NBT標籤來顯示對應的敘述
上圖的Tinker Construct工具就是這樣喔
@Nullable World worldIn
這代表你還可以根據世界來顯示不同的敘述內容
Ex. 天氣、時間…如果你需要的話啦…(´_ゝ`)
List<ITextComponent> tooltip
這就是最關鍵的參數啦!
對這個List
你可以新增敘述,以行為單位
每一行都是一個實現了net.minecraft.util.text.ITextComponent
的實例
ITextComponent
用來顯示文字,除了字母符號以外,你還可以有樣式
Ex. 顏色、粗體、斜體、底線、混淆…
關於ITextComponent
後面會有更詳細的介紹 (╯✧∇✧)╯
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鼠只用到了TranslationTextComponent
和StringTextComponent
,上面已經有介紹,這邊就不再做解釋
你可能發現了,是的,TranslationKey是可以自己創建的 ( ♥д♥)
但還是有幾點要注意
請讓他唯一,就算跨模組也是
想想看,你今天要為你的棉花種子設TranslationKey,你用了tooltip.cotton_seed.descript
另一個模組如果有新增棉花種子,也有要加敘述欄的話,你們很容易就撞在一起
所以最好的方法就是在TranslationKey中加入你的modid,通常會加在第二欄
請讓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上有給出舉例,建議可以去看一下
那麼,現在打開遊戲,你就可以看到成果了 _(┐「ε:)_
好的,那所有模組都很愛用的那個 "按下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來切換進階顯示了
那麼,到此為止,你已經學會如何建立一個物品以及許許多多的功能了
別忘了也要打開你的runServer確認有沒有問題喔 (っ´ω`c)
還記得我說過每個人物品欄中所有的東西都是ItemStack
嗎?
更重要的是,每一個被註冊上去的Item
都是唯一的,物理的唯一
也就是說,小美手中的1個蘋果、小明手中的64個蘋果和M鼠摔倒不小心掉在地上的87顆蘋果
這些ItemStack
中的Item
在Java虛擬機中的記憶體位置是一樣的 (*゚∀゚*)
來看一下一個經典的例子
這兩把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中查看