第一個方塊 === 在遙遠的過去,有一個國度的公主愛上了當地的甜點師 (*´∀\`)~♥ 公主最愛吃那名甜點師做的布丁 然而,這樣的戀情國王是不認同的,並為公主安排了和貴族王子的婚禮 誰知道,公主在婚禮上狠狠地賞了貴族王子一巴掌後和甜點師逃跑了 憤怒的國王派人把公主和甜點師抓來,並在公主面前殺了甜點師 國王同時頒布了新的法令,全國從今以後不准有任何人製作布丁 (╬゚д゚) 傷心的公主婚後仍念念不忘那名甜點師和那為她而製作的布丁 ~~因此就在某天晚上,公主把在床上熟睡的貴族王子做成一台布丁機~~ Σ(゚Д゚;≡;゚д゚) 在本章節你會了解 --- - 如何創建一個~~布丁機~~方塊實例(Instance) - 如何註冊方塊和方塊的物品形式 - 幫方塊加上材質 - 控制方塊掉落 - 幫方塊加上簡單的功能 - Block與BlockState 如何創建一個方塊實例(Instance) --- 和物品很類似,只是這次我們要繼承的是`net.minecraft.block.Block` 我們建立一個套件(Package)`com.github.immortalmice.modtutorialim.block` 然後在裡面創建一個`PuddingMachine.java`並繼承剛剛提到的`Block` (〃∀〃) ```java= public class PuddingMachine extends Block{ public PuddingMachine(){ } } ``` 和物品不太一樣又很像,`Block`建構子傳入的參數是`net.minecraft.block.Block.Properties` `Block.Properties`的建構子有三個 不論哪一個都要傳入一個`net.minecraft.block.material.Material` 所有你可以直接使用的`Material`,都以靜態欄位的方式宣告在`Material`自身的類別裡了 你可以直接調用,像是這樣 ```java= new Block.Properties(Material.IRON); ``` `Material`正如其名,就是一種材料 σ\`∀´)σ 你去查一下,你會發現整個Minecraft程式碼中只有`net.minecraft.block.DoorBlock` 但是Minecraft裡有鐵門和木門啊?怎看不到這兩個類別? 原因就是鐵門和木門其實都是`DoorBlock`的實例(Instance),只是傳進了不同的`Material`而已 相同道理的還有地板門、告示牌、壓力版、階梯...等等 \_(:3 」∠ )\_ 如果你有類似的需求,可以學原版的作法,如果你需要對應不同的材料有不同表現時,你可以透過`Block`自身的保護(Protected)欄位`material`獲得材料,或公開的方法`Block.getMaterial()` 兩個作法會像下面這樣 ```java= //If you are in child class or using reflection this.material; //This is public method, but is @Deprecated currently block.getMaterial(blockStateIn); ``` `Block.Properties`的另外兩個建構子分別是傳入`net.minecraft.block.material.MaterialColor`和`net.minecraft.item.DyeColor` 這兩個東西其實是指同一件事,因為後者可以直接轉換成前者 所有可以直接用的`MaterialColor`都以靜態欄位的方式宣告在`MaterialColor`自身的類別裡了 `MaterialColor`非常不建議自己實例化出一個來,除了它的建構子是私有(Private)的以外 *(這邊指你可以用Reflection去強制使用它)* 原版的這裡寫的程式碼是很不具延展性的 (。ŏ_ŏ) 整個Minecraft裡最多只能有64個`MaterialColor`,而Minecraft自己就已經用掉了52個 就算你繼續用反射(Reflection)大法硬做 如果所有模組作者都這樣想,那遊戲出Bug的機率就非常高 ┐(´д\`)┌ 那麼,獲得了一個`Block.Properties`實例後,我們可以對它做一些設定,和建立物品的方法類似 對於我們即將要做的布丁機,我們要設定幾項數值 (ノ>ω<)ノ - 硬度設為5.0f & 抗爆性設為6.0f,和原版鐵磚相同 這兩個決定了方塊被破壞的速度,以及抗爆的程度 `Block.Properties.hardnessAndResistance()`有多載(Overolad) 其中一個傳入一個浮點數,另一個傳入兩個浮點數 傳入兩個浮點數的就分別是傳入硬度和抗爆性 傳入一個參數的則代表硬度和抗爆性會被設成同一個數值 =v= 如果你想參考原版方塊的數值的話,你可以查看`net.minecraft.block.Blocks`這個類別 或是在Minecraft Wiki的 [挖掘頁面](https://minecraft-zh.gamepedia.com/%E6%8C%96%E6%8E%98) 和 [爆炸頁面](https://minecraft-zh.gamepedia.com/%E7%88%86%E7%82%B8) 查看 - 有效的採集工具設為鎬子(`ToolType.PICKAXE`) `net.minecraftforge.common.ToolType`是Forge額外加上去的一個類別 裡面定義了三個靜態欄位的`ToolType`,他們分別是 - `ToolType.AXE` - `ToolType.PICKAXE` - `ToolType.SHOVEL` - 採集等級設為2,指鐵鎬以上可採集 採集等級在這邊是一個[魔法數字(Magic Number)](https://zh.wikipedia.org/wiki/%E9%AD%94%E8%A1%93%E6%95%B8%E5%AD%97_(%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)),好孩子請不要學 (´σ-\`) 至於實際上的對應是 - 0 : 空手 - 1 : 石製工具可採集 - 2 : 鐵製工具可採集 - 3 : 鑽石工具可採集 - 亮光等級為15 方塊的發光等級,數值範圍0 - 15 要參考原版方塊的數值,你可以看`net.minecraft.block.Blocks`這個類別 或是在Minecraft Wiki的 [亮度頁面](https://minecraft-zh.gamepedia.com/%E4%BA%AE%E5%BA%A6) 查看 (\^\_\^) - 是否為完整固態方塊 決定了一個方塊的四邊若有方塊時,四周的方塊是否會渲染該面的材質 試想,如果你站在一個Minecraft世界的草原上 請問你腳下的草地方塊需要六個面都顯示嗎? 當然是不用,因為有許多面是被遮住的 這些被遮住的面如果也顯示,只是增加程式運算的負擔 我想你應該有用過旁觀模式(`/gamemode spectator`)潛進地下過吧? 其實Minecraft什麼都沒做,因為地下的大部分面是沒被顯示,可以直接看穿的 但是問題來的,像箱子、門、生怪箱這些不是完整被填滿的方塊 如果不顯示某些面(即使和另一個方塊相連),後果就會變成這樣 σ ゚∀ ゚) ゚∀゚)σ ![圖1-4-1 方塊面不被顯示](https://i.imgur.com/lbD1hnK.png) 所以我們必須告訴遊戲說這不是一個完整被填滿的方塊,這樣一來所有面都會顯示了 想當然的,這會增加顯示資源的消耗,請少用 因此,我們最後`PuddingMachine`的類別建構子會長成這樣 (๑´ㅂ\`๑) ```java= public PuddingMachine(){ super(Block.Properties.create(Material.IRON) .hardnessAndResistance(5.0f, 6.0f) .harvestTool(ToolType.PICKAXE) .harvestLevel(2) // Iron pickaxe and above .lightValue(15) .notSolid()); } ``` 如何註冊方塊和方塊的物品形式 --- 方塊的推薦註冊方式和物品一樣,是`net.minecraftforge.registries.DeferredRegister<T>` 因此所有流程和物品十分相似 (✪ω✪) > 以下的某些部分M鼠會快速跳過,這是因為`DeferredRegister`已經在[第一個物品](/@immortalmice/SyGqBSy_I)中介紹過了 > 如果有疑問,或是還沒看過那篇文的話,請先去看 首先,在我們的`com.github.immortalmice.modtutorialim.block`套件下建立一個`Blocks.java` 然後寫進以下程式碼 ```java= @ObjectHolder(ModTutorialIM.MODID) public class Blocks{ public static final PuddingMachine PUDDING_MACHINE = null; public static DeferredRegister<Block> getRegister(){ return Blocks.BlockRegistry.REGISTER; } @SuppressWarnings("unused") private static class BlockRegistry{ public static final DeferredRegister<Block> REGISTER = new DeferredRegister<Block>(ForgeRegistries.BLOCKS, ModTutorialIM.MODID); public static final RegistryObject<Block> OBJ_PUDDING_MACHINE = BlockRegistry.REGISTER.register("pudding_machine", () -> new PuddingMachine()); } } ``` 然後回到我們之前在創建物品時,建立的`com.github.immortalmice.modtutorialim.handlers` 並在以前寫的方法`registAll()`中新增一行 ლ(╹◡╹ლ) ```java= Blocks.getRegister().register(RegistryHandler.BUS); ``` 現在打開遊戲,執行以下指令 ``` /setblock ~ ~ ~ modtutorial:pudding_machine ``` 應該會有一個方塊出現在你的位置上 ![圖1-4-2 setblock指令結果](https://i.imgur.com/FcLscvg.png) 這代表方塊已經成功註冊到遊戲裡了 (⁰▿⁰) 當然,用`setblock`非常煩人,而且我們勢必會需要這個方塊的 **物品形式** 因此我們幫這個方塊建立出一個物品,並註冊它,然後順便放進一個新的創造模式物品欄 回到我們的 `com.github.immortalmice.modtutorialim.item.Items.ItemRegistry` 新增下面這一行 ```java= public static final RegistryObject<Item> OBJ_PUDDING_MACHINE = ItemRegistry.REGISTER.register("pudding_machine", () -> new BlockItem(BlockRegistry.OBJ_PUDDING_MACHINE.get(), new Item.Properties().group(ItemGroups.BLOCK_TAB))); ``` 這邊我們用到了`net.minecraft.item.BlockItem.BlockItem` `BlockItem`的建構子傳入一個`Block`和一個`Item.Properties` 其實你把`BlockItem`當成一個多帶`Block`欄位的`Item`就可以了 ・\*・:≡( ε:) 另外,我們還用到了`RegistryObject<T>.get()`,來獲取被註冊過後的方塊 還記得我之前在[第一個物品](https://hackmd.io/@immortalmice/SyGqBSy_I)中說過的嗎?如果呼叫了這個方法(Method),但是那個東西還沒被註冊的話,這邊會丟出`NullPointerException`異常 。・゚・(つд\`゚)・゚・ 不過好事是我們其實不用擔心這件事,因為註冊的順序,方塊永遠是第一個,物品是第二個 所以這邊的`RegistryObject<T>.get()`理應是不會丟出異常的 最後,我們把這個東西丟進了`ItemGroups.BLOCK_TAB`,但這個東西我們還沒寫 所以在`com.github.immortalmice.modtutorialim.item.ItemGroups`中新增一個欄位`BLOCK_TAB` ```java= public static final ItemGroup BLOCK_TAB = (new ItemGroup("modtutorialim.blocks"){ @Override public ItemStack createIcon(){ return new ItemStack(Blocks.PUDDING_MACHINE.asItem()); } }); ``` 這邊用到了`Block.asItem()`,如果你要拿某個方塊的物品形式,這是一個最簡單的方法 但這個物品必須是已經註冊過的,否則你會拿到`Items.AIR`,也就是空氣方塊的物品形式 最後,讓我們把`ItemGroups`和`PuddingMachine`的語言檔補齊 ( ºωº ) - en_us.json ```json= { "_comment": "==================Creative Tabs==================", ... "itemGroup.modtutorialim.blocks": "Immortalmice's Mod Tutorial Blocks", ... "_comment": "=======================BLOCK======================", "block.modtutorialim.pudding_machine": "Pudding Machine", ... } ``` - zh_tw.json ```json= { "_comment": "==================創造模式物品欄==================", ... "itemGroup.modtutorialim.blocks": "M鼠的教學模組方塊", ... "_comment": "=======================方塊======================", "block.modtutorialim.pudding_machine": "布丁機", ... } ``` 好了之後打開遊戲,現在你應該可以在新的創造模式物品欄找到布丁機了 更重要的是,你可以把布丁機拿到手上,並透過右鍵把它放到地上 ![圖1-4-3 方塊的創造模式物品欄](https://i.imgur.com/OZl0U3N.png) 幫方塊加上材質 --- 一個方塊要有外觀,需要三個東西,和物品有一點不一樣,多出一個Json檔 (´-ω-`) - 方塊狀態Json檔 想想看,Minecraft原版裡的熔爐 裡面有在燒東西和沒在燒東西的時候外觀是不一樣的對吧? (つд⊂) 這個有沒有燒東西的值被存在一個`net.minecraft.block.BlockState`的容器裡 這個方塊狀態Json檔要做的事就是根據`BlockState`裡的值,去選出正確的模型Json檔 同時,你還可以對選中的模型Json檔做簡單的旋轉 詳細你可以看[Minecraft Wiki](https://minecraft-zh.gamepedia.com/%E6%A8%A1%E5%9E%8B)的介紹 - 模型Json檔 這個檔案就是負責把圖片設定到對的位置 (Ex.每個面、破壞粒子效果) 甚至你還可以用多個小的長方體,來組成你的模型形狀和外觀 詳細你可以看[Minecraft Wiki](https://minecraft-zh.gamepedia.com/%E6%A8%A1%E5%9E%8B)的介紹 - 圖片 恩,不解釋 (メ゚Д゚)メ 首先,我們到`resources/assets/modtutorialim`資料夾建立一個叫`blockstates`的資料夾 並在裡面加入`pudding_machine.json`檔案,pudding_machine請改成你方塊的註冊名 ```json= { "variants": { "": {"model": "modtutorialim:block/pudding_machine_model"} } } ``` 一個方塊狀態Json檔裡面需要有`variants`或是`multipart`區塊 一般情況用`variants`,而如果你的模型可以拆成許多小部分,並依據`BlockState`作為是否顯示的條件的話,可以考慮用`multipart` 這邊我只示範`variants`,需要`multipart`的話,可以參考[Minecraft Wiki](https://minecraft-zh.gamepedia.com/%E6%A8%A1%E5%9E%8B) 其實Wiki寫得很仔細,只是頁面資訊量很大會看得有點頭昏而已 ( º﹃º ) 由於我們的布丁現在沒有任何定義的`BlockState`,所以直接加一個空字串`""`為鍵值 而空字串指向的值,裡面可以用`model`來定義你的模型Json檔位置 檔案位置的表示方式和[物品](/@immortalmice/SyGqBSy_I?both#%E5%B9%AB%E7%89%A9%E5%93%81%E5%8A%A0%E4%B8%8A%E6%9D%90%E8%B3%AA)相同,這邊不再贅述 好了之後,我們在原先的`resources/assets/modtutorialim/models`下建立一個`block`資料夾 並在裡面建立`pudding_machine_model.json`的檔案 檔案名稱可以自己選,但必須要和你上面方塊狀態Json檔中的值對上 (ㅅ˘ㅂ˘) ```json= { "parent": "block/block", "textures":{ "particle": "modtutorialim:block/pudding_machine_bottom", "top": "modtutorialim:block/pudding_machine_top", "bottom": "modtutorialim:block/pudding_machine_bottom", "bottomfront": "modtutorialim:block/pudding_machine_bottomfront" }, "elements": [{ "from": [3, 4, 3], "to": [13, 16, 13], "faces": { "up": {"uv": [0, 0, 16, 16], "texture": "#top"}, "north": {"uv": [0, 0, 16, 16], "texture": "#top"}, "east": {"uv": [0, 0, 16, 16], "texture": "#top"}, "west": {"uv": [0, 0, 16, 16], "texture": "#top"}, "south": {"uv": [0, 0, 16, 16], "texture": "#top"} } },{ "from": [3, 0, 3], "to": [13, 4, 13], "faces": { "up": {"uv": [0, 0, 16, 16], "texture": "#bottom"}, "down": {"uv": [0, 0, 16, 16], "texture": "#bottom"}, "north": {"uv": [0, 0, 16, 16], "texture": "#bottomfront"}, "east": {"uv": [0, 0, 16, 16], "texture": "#bottom"}, "west": {"uv": [0, 0, 16, 16], "texture": "#bottom"}, "south": {"uv": [0, 0, 16, 16], "texture": "#bottom"} }}] } ``` 和物品很像,我們用`parent`繼承了另一個模型,只是這次繼承的是原版的`block.json` `block.json`裡面定義了方塊在各種不同視角下的旋轉、位移和縮放,繼承之後我們就可以直接用裡面的東西,而如果你的方塊是一個完整的實心正方體,建議可以繼承`block/cube` `textures`裡面是用來定義變量的,你可以偷瞄一下下面的部分,你應該可以看到`#top`這種東西,而這個`top`就是我們在`textures`定義的變數 也就是我們把`top`定義成了`modtutorialim:blocks/pudding_machine_top`這個路徑 接下來,我們的布丁機不是一個完整實心的正方體,我把它切成了兩部分,寫在`elements`裡 座標的數值XYZ,若以一個完整的實心正方體而言,是從`[0, 0, 0]`到`[16, 16, 16]` 但其實這邊的數值範圍可以是 -16 ~ 32 也就是說你可以延伸到其他方塊去 (\^ρ\^)/ 布丁機的第一個子方塊是從座標`[3, 4, 3]`到`[13, 16, 13]` 第二個子方塊是座標`[3, 0, 3]`到`[13, 4, 13]` 而`faces`裡面寫的,就是指定這個子方塊六個面所要用的材質 其中`uv`就是你實際圖片上的座標位置,由於布丁機的圖片都是16\*16,並且整張圖片都使用 所以這邊是從`[0, 0]`到`[16, 16]`,但實際上這邊要寫作`[0, 0, 16, 16]` `texture`就是你圖片的實際位置,請用你剛剛前面定義過的變數填入 ♥(´∀\` )人 好了之後,就把圖片放到你指定的路徑裡吧,這邊M鼠的三張圖片是由朋友硯郎所提供 `pudding_machine_top.png` ![pudding_machine_top](https://i.imgur.com/hQCGggk.png) `pudding_machine_bottomfront.png` ![pudding_machine_bottomfront](https://i.imgur.com/iF9BjhF.png) `pudding_machine_bottom.png` ![pudding_machine_bottom](https://i.imgur.com/lF9WnUU.png) 現在打開遊戲,把布丁機放到地上,你應該可以看到方塊有外觀了 ![圖1-4-4 方塊的第一個外觀](https://i.imgur.com/Wkg18Z0.png) 但是很明顯的,這邊有兩個問題 (◞‸◟) - 手中的物品依舊是紫黑塊 - 放進去的方塊圖片是透明的,但這邊卻是一片白 首先,手中的物品很好解決,你就把它當成一般的物品來寫模型Json檔就好了 `resources/assets/modtutorialim/models/item/pudding_machine` ```json= { "parent": "modtutorialim:block/pudding_machine_model" } ``` 直接繼承我們剛才的方塊模型Json檔,輕鬆 打開遊戲吧,你還可以把物品丟在地上看 (ノ>ω<)ノ ![圖1-4-5 方塊的物品外觀](https://i.imgur.com/u0jVSuc.png) 當然,如果你想要物品的模型有特殊的外觀的話,就別繼承方塊模型,照原本物品模型Json檔寫出自己要的吧 現在的問題就是方塊是不透明的,原因就是Minecraft方塊渲染的預設是不透明的 理由很簡單,就是減輕顯示運算負擔 (\*’ー’\*) 所以我們現在要告訴遊戲這個方塊請以透明的方式渲染 打開我們的`ModEventHandlers`類別 我們要訂閱`net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent`事件 *(關於事件系統的教學部分我放在後面,請看本教學中的[事件系統](https://hackmd.io/@immortalmice/HJ3VSHJ_I)。 或是你也可以先照做,未來再了解事件是什麼即可)* 所以在類別裡加上以下程式碼 (ゝ∀・) ```java= @SubscribeEvent public static void onClientSetup(FMLClientSetupEvent event){ RenderTypeLookup.setRenderLayer(Blocks.PUDDING_MACHINE, RenderType.getCutoutMipped()); } ``` 我們這邊用`net.minecraft.client.renderer.RenderTypeLookup`來為我們的布丁機設定所謂的RenderType,所有可以用的RenderType都可以在自身的類別用公開方法取得 這邊提出四個最常用的RenderType - SOLID 完全不透明,也是所有方塊預設的RenderType (樹葉方塊除外) - CUTOUT 圖片的某些區塊是完全透明的,就像是這些區塊被切掉一樣 - CUTOUT_MIPPED 和上面一樣,但是加上了MipMapping技術 (ㄏ ̄▽ ̄)ㄏ 關於MipMapping技術,你可以看這篇文章[Anti-Aliasing Problem and Mipmapping](https://textureingraphics.wordpress.com/what-is-texture-mapping/anti-aliasing-problem-and-mipmapping/) - TRANSLUCENT 半透明,像是原版的有色玻璃一樣,同時也是這四個中最消耗顯示資源的RenderType 好了之後打開遊戲,你的方塊應該是透明的了 ㄟ( ̄▽ ̄ㄟ) ![圖1-4-6 方塊的透明外觀](https://i.imgur.com/GpFEdbN.png) 你可能從上圖發現了,即使我們的方塊不是完整的16\*16\*16,但滑鼠移到上面的時候,黑框的範圍仍然是16\*16\*16,同時,這個方塊的碰撞箱也是16\*16\*16 要改變這件事,我們要覆寫我們`PuddingMachine`類別的`Block.getShape()` ```java= @Override public VoxelShape getShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context){ return Block.makeCuboidShape(3.0D, 0.0D, 3.0D, 13.0D, 16.0D, 13.0D); } ``` 這邊用到的是`Block`的靜態方法`makeCuboidShape`傳入開始的XYZ和結束的XYZ共六個小數 打開遊戲後你應該可以發現黑框貼齊了方塊模型 同時用`F3+B`查看也可以看到生物的碰撞箱可以貼齊方塊模型 (♡˙︶˙♡) ![圖1-4-7 方塊的碰撞箱展示](https://i.imgur.com/EfjWuYw.png) 控制方塊掉落 --- 雖然距離上面有一點遠了,但還記得我們當初在前面設定了布丁機是鐵鎬以上可以採集嗎? 如果你有認真測試,你應該會發現地上的布丁機就算是生存模式用鑽石鎬也沒辦法掉落物品 Minecraft的方塊掉落在1.14版本之後全面被一個叫 "Loot Table" 的東西接管了 如果一個方塊沒有被定義Loot Table,就不會掉落任何東西 。・゚・(つд\`゚)・゚・ *(其實M鼠這邊覺得很奇怪,應該要有個預設是如果找的到方塊的物品形式,就掉落該物品,否則什麼都不掉,會讓模組開發者感到困惑,特別是大部分方塊的掉落方式都是很簡單的掉落自身物品而已,每個方塊都要寫Loot Table實在過於冗長。你要知道,開發程式中,多寫就是多增加Bug出現的機率,試想如果你今天有100個方塊的情況...)* 雖然說是要用Loot Table,但我這邊要用另一個作法,就是覆寫`Block.getDrops()`這方法 你可以先看一下這方法原本的內容,其實很簡單,就是去找Loot Table 但剛才我不是抱怨過每個方塊都要寫Loot Table太過冗長嗎? ⊂彡☆))д\`) 所以我們要改一下邏輯,改成有找到Loot Table就用Loot Table,找不到就掉落該方塊的物品形式 來人,上程式碼 < ( ̄︶ ̄)> ```java= @Override @SuppressWarnings("deprecation") public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder){ ResourceLocation resourcelocation = this.getLootTable(); LootTableManager manager = builder.getWorld().getServer().getLootTableManager(); if(manager.getLootTableFromLocation(resourcelocation) != LootTable.EMPTY_LOOT_TABLE){ return super.getDrops(state, builder); } return Arrays.asList(new ItemStack(Items.PUDDING_MACHINE)); } ``` 首先,我們透過一系列的get,來去拿到這個方塊的`LootTable`,然後檢查它是不是有被載入的 你可以看`LootTableManager.getLootTableFromLocation()`這個方法,你會發現它在找不到`LootTable`的時候會回傳一個預設的空`LootTable` 因此我們只要來比較他們是否一樣就知道這個方塊的`LootTable`是否有被載入 ლ(╹ε╹ლ) *(記得別忘了去`Items`用`@ObjectHolder`把布丁機的物品形式加進去喔)* 這邊我們用到了一個被棄用(Deprecated)的方法,就是當我們呼叫父層的`getDrops()`的時候 雖然是被標示棄用了,但M鼠這邊還是決定用`@SuppressWarnings`壓過 Forge曾經在[這篇帖子](https://www.minecraftforge.net/forum/topic/80107-1144-onblockactivated-deprecated/?tab=comments#comment-381578)提到他們認為Mojang濫用(Abuses)了`@Deprecated`這個註解 M鼠承認自己對Mojang的程式碼不夠熟,沒辦法判定Forge這話可不可信 (´・_・\`) 但整份Minecraft原始碼概略的看下來,用了`@Deprecated`這個註解的地方的確是有些過多的 事實上,對於`getDrops()`這個方法,有更推薦的版本就是`BlockState.getDrops()` 當然,沒有被棄用,但去看一下`BlockState`裡的方法,其實它不過就是呼叫了`Block.getDrops()` 如果M鼠這邊採用`BlockState.getDrops()`,會造成程式無限循環的問題 Σ(\*゚д゚ノ)ノ 最後,打開遊戲,現在布丁機應該可以透過鐵鎬破壞來獲取了 關於`LootTable`,我們會在未來[物品掉落和寶箱生成](https://hackmd.io/@immortalmice/rJ0y8Sk_8)這篇文做更詳細的介紹 幫方塊加上簡單的功能 --- 哀,布丁機果然就是要能做出布丁才是好的布丁機啊... இдஇ 但是我們現在還沒學會怎麼做一個[圖形使用者介面](https://hackmd.io/@immortalmice/S14dOhwq8)還有用[網路與封包](https://hackmd.io/@immortalmice/H17ru3wqU)來同步資料 所以我們先來用個很簡單的方式:右擊布丁機就會跳出一個布丁來 d(\`・∀・)b 關於要控制玩家對方塊右鍵的動作,我們只要覆寫`onBlockActivated()`就可以了 ```java= @Override public ActionResultType onBlockActivated(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn, BlockRayTraceResult hit){ if(worldIn.isRemote) return ActionResultType.SUCCESS; if(!player.isSneaking()){ ItemEntity itemEntity = new ItemEntity(worldIn, pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5, new ItemStack(Items.PUDDING)); worldIn.addEntity(itemEntity); return ActionResultType.SUCCESS; } return ActionResultType.PASS; } ``` 首先,我們先來了解回傳的`net.minecraft.util.ActionResultType`是什麼吧 `ActionResultType`是一個列舉,裡面有4個元素 - `ActionResultType.SUCCESS` 代表所有的檢查都通過了,並且已經完成了該做的任務,會有揮手動畫 (\*´艸\`\*) - `ActionResultType.FAIL` 代表有些條件檢查並未通過,所以這個動作的結果失敗了 (´∩ω∩`) - `ActionResultType.PASS` 代表這個動作跟我無關,並且我並未對任何資料做任何的改變 (つд⊂) - `ActionResultType.CONSUME` 這和`ActionResultType.SUCCESS`基本上是一樣的 目前唯一的區別就是玩家是否會有揮手的動畫 (●` 艸 ´) 要注意的是,只要伺服器或客戶端任一是`ActionResultType.SUCCESS`,就會有揮手動畫 *(更詳細的說,客戶端若是`SUCCESS`會根據滑鼠輸入事件的設定(通常是True)來揮手 伺服器端若是`SUCCESS`會傳封包通知客戶端進行揮手動畫)* 首先,我們透過`worldIn.isRemote`這個布林值來確定執行端 若是`true`,代表當前環境是邏輯客戶端,相反就是邏輯伺服器端 (ゝ∀・) *(想知道更多關於執行端的東西,請看[客戶端與伺服器端](https://hackmd.io/@immortalmice/rJKayrf9U)這篇文)* 由於我們的客戶端不需要做任何事,可以視為任務已完成,所以直接回傳`ActionResultType.SUCCESS` ☆⌒(゜-゜)v 再來,確定是伺服器端後,這邊我會把玩家是否是蹲下來做個區別,並且如果是蹲下,就回傳`ActionResultType.PASS` 也就是我只關注玩家不是蹲下的情況下的右鍵行為 那麼,就是建立一個`net.minecraft.entity.item.ItemEntity`(就是物品掉在地上時的形式) 把他設定在布丁機的XZ平面正中央,Y向上一格 最後就把布丁正式的加進世界囉~ 。:.゚ヽ(\*´∀\`)ノ゚.:。 打開遊戲,現在你應該可以~~嘗到公主黑暗又深奧的手藝囉~~ Σ(゚Д゚;≡;゚д゚) Σ(゚Д゚;≡;゚д゚) Σ(゚Д゚;≡;゚д゚) ![圖1-4-8 最終成果展示](https://i.imgur.com/utDuonU.gif) Block與BlockState --- 和`Item`很像,所有被註冊的`Block`都是唯一的 但我們總是要讓地上的方塊有著一些不一樣的資料,總不能世界上所有的馬鈴薯作物方塊的當前生長階段都一樣 (◞‸◟) 因次,和`ItemStack`很像,`Block`有所謂的`net.minecraft.block.BlockState` 玩家物品欄裡的所有東西都是一個`ItemStack` 而所有被放置在世界中的方塊,都有它自己的`BlockState` (儘管有可能是空的) 要獲得一個方塊的當前`BlockState`,你需要用世界和位置去獲得 (゚3゚)~♪ ```java= public BlockState World.getBlockState(BlockPos); ``` 和`CompoundNBT`不太一樣,`BlockState`這東西用起來有點複雜 為了來示範一下,就來改一下我們的布丁機吧 你有注意到我們的布丁機,不管玩家面向哪裡放下,它的正面永遠會是面對北方嗎? 我們現在希望他可以像熔爐一樣,玩家把它放下時,方塊的正面就會面向玩家 (∂ω∂) 首先,你必須要覆寫(Override)`Block.fillStateContainer()` 這個方法會在`Block`中的建構子被呼叫,目的是把所有這個方塊會用到的`BlockState`登記起來 Ex.作物方塊需要記錄生長階段、熔爐需要紀錄是否正在燒東西、紅石中繼器需要記錄延遲設定 這個`fillStateContainer()`的參數接收一個`net.minecraft.state.StateContainer.Builder` 你只要盡情地往這個`StateContainer.Builder`塞你需要紀錄的東西就可 ( ~'ω')~ ```java= @Override protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder){ builder.add(BlockStateProperties.HORIZONTAL_FACING); } ``` 這個`StateContainer.Builder.add`可以傳入 **一個或多個** `IProperty<?>` `net.minecraft.state.IProperty<?>`,追朔到最尾端,主要有3個實作類別 - `net.minecraft.state.BooleanProperty` - `net.minecraft.state.IntegerProperty` - `net.minecraft.state.EnumProperty<T>` 很好理解,其實就是記錄的資料種類,他們分別可以記錄布林值、正整數、列舉(Enum) 比如說熔爐是否正在燒東西,就是一個`BooleanProperty` 或是說作物的生長階段,就是一個範圍0~7的`IntegerProperty` 而我們需要的面向,就會用到`EnumProperty<T>`,這邊的`T`就是`net.minecraft.util.Direction` 你可以透過這三種`IProperty<?>`的實作類別,來去建立你要的東西 詳細用法請自行參閱各別的建構子 嗯?你說M鼠不是要做示範?為什麼草草帶過? **因為我要的這東西已經有人幫我寫好了啊~** (๑´ㅁ\`) 打開`net.minecraft.state.properties.BlockStateProperties` 你可以看到琳瑯滿目的靜態`IProperty<?>`等著你用,我們要的就是其中之一`HORIZONTAL_FACING` 事實上,你可以發現這些`IProperty<?>`建構時傳入了一個字串 有沒有覺得和我們在註冊方塊時似曾相識? 事實上他的確就像是一個註冊名,所以有幾點需要注意一下 (\*゚ー゚) 1. 如果原版已經有可以用的,請用原版 2. 盡量讓它唯一 雖然和方塊註冊不一樣,你給一樣字串它不會丟出錯誤 但在你寫方塊狀態Json檔時,撞名有可能會有問題 特別是你想取的名字和原版的已經相撞的話 雖然Forge官方沒說,但M鼠建議你可以加個命名空間Ex:`"modtutorialim:age"` 3. 如果你打算讓這個`IProperty<?>`和其他模組共享,請把宣告放進你模組的API裡 剛才說過了,`fillStateContainer()`會在`Block`的建構子中被呼叫 之後,在`Block`的建構子中會根據你經手的這個builder創建出一個所謂的defaultState 這個defaultState會在一個方塊被放置到世界中時套用 (・ε・) 如果你需要修改defaultState,你可以在你的建構子中呼叫`Block.setDefaultState`來去修改 當然,你其實可以覆寫`Block.getStateForPlacement()`,這也是我們這邊要用的 ```java= @Override public BlockState getStateForPlacement(BlockItemUseContext context){ return this.getDefaultState().with(BlockStateProperties.HORIZONTAL_FACING, context.getPlacementHorizontalFacing().getOpposite()); } ``` 其實蠻簡單的,拿到defaultState,然後用`with()`去把裡面的值修改就可以了 接下來,我們只要改一下BlockState的JSON檔就可以了 ```json= { `"variants":{ "facing=north": {"model": "modtutorialim:block/pudding_machine_model"}, "facing=east": {"model": "modtutorialim:block/pudding_machine_model", "y": 90}, "facing=south": {"model": "modtutorialim:block/pudding_machine_model", "y": 180}, "facing=west": {"model": "modtutorialim:block/pudding_machine_model", "y": 270} }` } ``` 還記得剛剛說過,創立一個新的`IProperty<?>`需要傳進一個字串嗎? 是的,這邊的BlockState JSON會需要用到這個字串 隨便拿一組Key-Value來看一下 ◝( ゚∀ ゚ )◟ ```json= "facing=west": {"model": "modtutorialim:block/pudding_machine_model", "y": 270} ``` 首先,Key是由一個或多個BlockState的判斷組成 等號前是`IProperty<?>`的辨別字串,等號後是列舉(Enum)的比對 剛剛說了 "多個" ,所以如果你有一個以上的BlockState要判斷,需要用`,`分隔 ```json= "facing=west,lit=true,east=false" ``` 當然了,你可能發現了,如果你今天有3個BlockState,分別有4、2、2種可能的值 你這邊就要寫出4 * 2 * 2 = 16組Key-Value,可以說是有一點...甚至是非常沒有效率的 (´゚д゚\`) 過往的版本中,有Forge BlockState可以使用,大幅降低這件事的發生 但在1.15中,[這個東西被移除了](https://gist.github.com/williewillus/30d7e3f775fe93c503bddf054ef3f93e),理由以及未來該怎麼做,這邊節錄一下 > Forge Blockstate jsons are now removed as having model logic being was always a massive hack Instead, the vanilla model json format has been expanded. You can now specify a "loader" field in the json and register a custom loader to process the model in code. 其實M鼠不是很贊同這件事,或是其實我對這件事情有所誤會 簡單來說,我並不了解,儘管他可能是個`massive hack` 但在沒有很好的替代方案時,移除這東西真的是利大於弊嗎? 雖說可以使用客製化Loader,但建立一個客製化Loader需要花的功夫其實非常多 ( ×ω× ) *(我們未來會在[特殊模型顯示](https://hackmd.io/@immortalmice/SywvF2D98)這篇文聊到如何寫客製化Loader)* 扯遠了,剛剛講的是Key,現在來講Value ```json= {"model": "modtutorialim:block/pudding_machine_model", "y": 270} ``` 也就是說,你可以直接映射到一個新的model檔,或是用現有的model檔做一些簡單的變換 這邊只是很簡單的以Y軸做旋轉 打開遊戲,你應該可以看到所有布丁機在放到地面時會面向你了 (\*´∀\`)~♥ ![圖1-4-9 BlockState展示](https://i.imgur.com/tCyQzMC.png) --- *本頁面撰寫於2020/06/05,目前最後更新日期為2020/06/08* *若上述時間與你閱讀的時間相距過遠,請自行斟酌是否採用本頁面的資訊* *完整的程式碼可以到本教學文的[Github Repo](https://github.com/immortalmice/MinecraftForge1.15.2-ModdingTutorial)中查看*