
本期我們繼續探討Minecraft Forge Mod的開發。在前面的教程中,我們已經介紹了方塊狀態(Block State),它可以用來存儲簡單的參數。但是,如果我們想要存儲更復雜的參數類型,該怎麼辦呢?
舉個例子,假設我們想要創建一個展示桌,這個展示桌可以展示Minecraft遊戲中的任意物品,並將其放置在桌面上。如果我們使用方塊狀態來實現,我們需要自己實現一個參數來存儲物品類型,這樣遊戲就會為每種物品都創建一個方塊狀態。而且,如果物品擁有耐久度等其他參數,這樣的處理方式可能會變得更加複雜。

因此,對於這種情況,我們需要使用方塊實體(Block Entity)。方塊實體可以簡單理解為附著在方塊上的實體,它並不是遊戲系統內真正的實體,而是一種以實體的方式來擴展方塊功能的機制。
我已經為展示桌寫好了相應的類,但是它現在還沒有方塊實體。接下來,我們要實現這個方塊實體,讓我們的方塊擁有這個功能。我們需要實現一個名為BlockEntity的接口,並重寫兩個方法:newBlockEntity 和 getTicker。

然而,我們暫時無法實現這兩個方法,因為它們需要返回BlockEntity和BlockEntityTicker類型的對象,這些對象需要我們自己實現。所以,我們首先要創建一個方塊實體的類。
我們可以在這裡新建一個Java類,命名為DeskBlockEntity。根據文檔,要創建方塊實體,我們需要繼承BlockEntity類。接著,我們創建構造函數。
讓我們回顧一下我們需要實現的功能:我們需要在展示桌上展示一種物品。首先,我們需要有一個物品的存儲空間。當然,我們也可以自己實現這個存儲空間,畢竟只有一個物品,我們只需要一個變量就可以了。但是,如果我們要存儲多個物品,或者考慮與其他Mod的兼容性等問題,這種方式可能就不夠靈活了。
對於這種情況,Forge為我們提供了一個解決方案:我們可以直接使用它提供的一些類和接口。
首先,我們來實現我們的存儲空間。我們使用私有 final 字段 `itemStackHandler`,這是 Forge 為我們提供的 `ItemStackHandler`。我們只需要保存一樣物品,所以我們創建一個新的 `ItemStackHandler` 實例。
privatefinalItemStackHandler itemStackHandler =newItemStackHandler(1);
但是在這裡我們還需要寫一些額外的東西。我們可以看到,在 `ItemStackHandler` 中,有兩個方法沒有實現,一個是 `onContentsChanged`,另一個是 `unload`。我們需要重寫這兩個方法。
@OverrideprotectedvoidonContentsChanged(intslot){super.onContentsChanged(slot);// 在物品發生變化時執行的操作}// 這裡不實現 unload 方法,因為我們暫時不需要在方塊卸載時執行操作
另外,我們需要一個 `lazyOptionalItemHandler` 字段,它是一個懶加載的可選項,用於存儲我們的物品。我們需要在以後的代碼中使用它。
privatefinalLazyOptional<IItemHandler> lazyOptionalItemHandler = LazyOptional.of(() -> itemStackHandler);
然後,我們需要實現 `getCapability` 方法,這個方法和之前寫的類似。接著,我們需要編寫保存和讀取數據的方法 `saveData` 和 `loadData`。這些方法都需要用到 `CompoundTag`,而這個標籤是經常要用到的。
@OverridepublicvoidsaveData(CompoundTag tag){super.saveData(tag);tag.put("TECHNITEMS", itemStackHandler.serializeNBT());}@OverridepublicvoidloadData(CompoundTag tag){super.loadData(tag);itemStackHandler.deserializeNBT(tag.getCompound("TECHNITEMS"));}
接著,根據文檔的要求,我們需要實現同步數據的方法 `syncData` 和 `syncUpdates`。此外,還需要實現存儲和讀取數據的方法。
@OverridepublicvoidsyncData(){super.syncData();if(world !=null&& !world.isClient) {BlockState state = world.getBlockState(pos);world.updateListeners(pos, state, state, Block.NOTIFY_ALL);}}@OverridepublicvoidsyncUpdates(){super.syncUpdates();if(world !=null&& !world.isClient) {world.updateListeners(pos, world.getBlockState(pos), world.getBlockState(pos), Block.NOTIFY_ALL);}}
至此,我們的類基本完成了。`saveData` 和 `loadData` 方法的實現方式基本固定,其他方法也都是類似的。當然,如果你想插入其他邏輯,也可以在相應的位置額外編寫。在沒有其他邏輯的情況下,代碼的基本結構就是這樣的。接下來,我們需要實現 `BlockEntity` 的邏輯。
publicvoiduseBlockEntity(ServerPlayerEntity player){// 在服務器端執行的操作// 這裡我們只是展示物品,不需要任何特殊的邏輯}
以上就是我們的代碼,接下來我們可以繼續實現其他的邏輯。
首先我們來實現方塊實體的每課邏輯。在服務端,我們需要在 `tick` 方法中執行一些操作,但是在客戶端我們並不需要。所以,我們需要對客戶端和服務端分別處理。
@Overridepublicvoidtick(){super.tick();// 在服務器端執行的每課邏輯//TODO:在這裡實現服務器端的邏輯}
然後是客戶端的邏輯,一般情況下我們不需要在客戶端執行太多邏輯,因為客戶端的邏輯會導致數據不同步。但如果你有一些不會影響遊戲世界的邏輯,可以在這裡執行。
@OverridepublicvoidtickClient(){super.tickClient();// 在客戶端執行的每課邏輯//TODO:在這裡實現客戶端的邏輯}
有了以上的代碼,我們就可以在服務器端和客戶端分別處理每課邏輯了。接下來,我們需要在 `BlockEntityType` 中返回我們的方塊實體,並且註冊這個方塊實體的標籤。
publicstaticfinalRegistryObject<BlockEntityType<DeskBlockEntity>> DESK_BLOCK_ENTITY = Registration.BLOCK_ENTITIES.register("desk_block_entity", () ->BlockEntityType.Builder.create(DeskBlockEntity::new, ModBlocks.DESK_BLOCK.get()).build(null));publicstaticfinalDeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MOD_ID);publicstaticfinalRegistryObject<Item> DESK_BLOCK_ITEM = ITEMS.register("desk_block", () ->newBlockItem(ModBlocks.DESK_BLOCK.get(),newItem.Properties().group(ItemGroup.DECORATIONS)));publicstaticfinalDeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITIES, MOD_ID);
以上就是註冊方塊實體和方塊項目的代碼。我們還需要在客戶端和服務端分別獲取方塊實體的標籤,這樣我們就可以在遊戲中正確地處理方塊實體了。
我們需要為我們的桌子創建一個渲染器,以便能夠展示物品。我們可以通過 `BlockEntityRenderer` 類來實現這個渲染器。以下是實現的代碼:
publicclassDeskBlockEntityRendererextendsBlockEntityRenderer<DeskBlockEntity>{publicDeskBlockEntityRenderer(BlockEntityRenderDispatcher dispatcher){super(dispatcher);}@Overridepublicvoidrender(DeskBlockEntity blockEntity,floatpartialTicks, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider,intlight,intoverlay){super.render(blockEntity, partialTicks, matrixStack, vertexConsumerProvider, light, overlay);ItemStack itemStack = blockEntity.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).map(itemHandler -> itemHandler.getStackInSlot(0)).orElse(ItemStack.EMPTY);if(!itemStack.isEmpty()) {matrixStack.push();matrixStack.scale(0.5f,0.5f,0.5f);matrixStack.translate(1.0,3.0,1.0);// 調整物品的位置MinecraftClient.getInstance().getItemRenderer().renderItem(itemStack, ModelTransformation.Mode.FIXED, light, overlay, matrixStack, vertexConsumerProvider);matrixStack.pop();}}}
在這個渲染器中,我們首先獲取方塊實體上的物品。然後,我們對物品進行渲染,使用 `MinecraftClient.getInstance().getItemRenderer().renderItem` 方法。在渲染之前,我們進行了一些參數的設置,比如縮放和位置偏移。
最後,我們需要註冊這個渲染器,以確保在遊戲中生效:
@Environment(EnvType.CLIENT)publicclassModClientSetup{publicstaticvoidinit(){BlockEntityRendererRegistry.INSTANCE.register(ModBlocks.DESK_BLOCK_ENTITY.get(),newDeskBlockEntityRenderer());}}
以上就是為我們的桌子創建一個渲染器的代碼。現在,我們的桌子應該能夠正確地展示放置在上面的物品了。
這個渲染器的功能還可以進一步拓展,大家可以根據自己的需求進行修改和優化。另外,我們還可以參考原版遊戲中類似的渲染邏輯,瞭解更多的渲染技巧和效果。記得,渲染器是在客戶端執行的,所以需要將其註冊到客戶端代碼中。
@Environment(EnvType.CLIENT)publicclassModClientSetup{publicstaticvoidinit(){// 註冊方塊實體渲染器BlockEntityRendererRegistry.INSTANCE.register(ModBlocks.DESK_BLOCK_ENTITY.get(),newDeskBlockEntityRenderer());}@SubscribeEventpublicstaticvoidregisterBlockEntityRenderers(EntityRenderersEvent.RegisterBlockEntityRenderersevent){// 註冊方塊實體渲染器供客戶端使用event.registerRenderer(ModBlocks.DESK_BLOCK_ENTITY.get(),newDeskBlockEntityRenderer());}}
在註冊渲染器時,我們還需要注意確保在客戶端代碼中註冊,因為渲染器只在客戶端執行。另外,我們還需要在方塊實體的 setChanged 方法中發送區塊更新以確保數據同步。這樣,我們就能夠在遊戲中展示放置在桌子上的物品了。如果想要進一步完善功能,比如右鍵交互放置物品等,可以繼續實現相應的邏輯。