10日目 Tile EntityへのGUIの追加
先日作成したインベントリ付きタイルエンティティにGUIを実装します。
GUIを持たせるためには、Containerを継承したクラスとScreenを継承したクラスが必要になります。
まず、Containerクラスから作成します。
public class TestTileContainer extends Container { public TestTileContainer(int windowId, PlayerInventory playerInv, PacketBuffer extraData) { this(windowId, playerInv, playerInv.player.world, extraData.readBlockPos()); } protected TestTileContainer(int id, PlayerInventory playerInv, World world, BlockPos pos) { super(ModTileEntities.TEST_TILE_CONTAINER, id); TileEntity te = world.getTileEntity(pos); te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) .ifPresent(h -> { for(int i = 0; i < 3; ++i) { for(int j = 0; j < 3; ++j) { addSlot(new SlotItemHandler(h, j + i * 3, 62 + j * 18, 17 + i * 18)); } } }); for(int k = 0; k < 3; ++k) { for(int i1 = 0; i1 < 9; ++i1) { this.addSlot(new Slot(playerInv, i1 + k * 9 + 9, 8 + i1 * 18, 84 + k * 18)); } } for(int l = 0; l < 9; ++l) { this.addSlot(new Slot(playerInv, l, 8 + l * 18, 142)); } } @Override public boolean canInteractWith(PlayerEntity playerIn) { return true; } @Override public ItemStack transferStackInSlot(PlayerEntity playerIn, int index) { ItemStack itemstack = ItemStack.EMPTY; Slot slot = this.inventorySlots.get(index); if (slot != null && slot.getHasStack()) { ItemStack itemstack1 = slot.getStack(); itemstack = itemstack1.copy(); if (index == 0) { if (!this.mergeItemStack(itemstack1, 1, 37, true)) { return ItemStack.EMPTY; } slot.onSlotChange(itemstack1, itemstack); } else if (this.mergeItemStack(itemstack1, 0, 1, false)) { //Forge Fix Shift Clicking in beacons with stacks larger then 1. return ItemStack.EMPTY; } else if (index >= 1 && index < 28) { if (!this.mergeItemStack(itemstack1, 28, 37, false)) { return ItemStack.EMPTY; } } else if (index >= 28 && index < 37) { if (!this.mergeItemStack(itemstack1, 1, 28, false)) { return ItemStack.EMPTY; } } else if (!this.mergeItemStack(itemstack1, 1, 37, false)) { return ItemStack.EMPTY; } if (itemstack1.isEmpty()) { slot.putStack(ItemStack.EMPTY); } else { slot.onSlotChanged(); } if (itemstack1.getCount() == itemstack.getCount()) { return ItemStack.EMPTY; } slot.onTake(playerIn, itemstack1); } return itemstack; } }
まずコンストラクタですが、引数が(int, PlayerInventory, PacketBuffer)のコンストラクタ (3~6行目) を作成しておくと、登録時にコンストラクタ参照で渡せるため楽です。このコンストラクタから実際に処理を行なうコンストラクタ (8~34行目) へ、引数を渡せば良いと思います。
PacketBufferのread~~メソッドを使うことで、呼び出し側の対応するwrite~~の情報を受け取ることが出来ます。
今回はTileEntity側でBlockPosを書き込み、Container側で読み込んでいます。
実際に処理を行なうコンストラクタの中身では、まずタイルエンティティを取得し、続いてタイルエンティティの持つItemStackHandlerをこのコンテナーのスロットとして登録します。
SlotItemHandlerの引数は(ItemHandler, Handlerの番号, コンテナ画像上でのx座標, コンテナ画像上でのy座標) です。
今回はインベントリを9つに増やそうと思うので、それらを3×3で表示しています (13~23行目)。
同様にプレイヤーのインベントリも設定します。ここの処理はバニラのチェスト、かまど、ホッパー等と同様です (24~34行目)。
次にcanInteractWithをtrueにして、transferStackInSlotを実装します。
transferStackInSlotはシフトキーを押しながら、アイテムを転送したりする処理に関する内容で、設定しておかないとシフトキーを押してアイテムを選択するとクラッシュしてしまいます。
バニラと同じ動作で良いならバニラのコンテナーを実装している機能が近いクラスからコピペでいいと思います。
次に、Screenを作成します。
public class TestTileScreen extends ContainerScreen<TestTileContainer> { private static final ResourceLocation BACKGROUND_TEXTURE = new ResourceLocation(TestMod.MODID, "textures/gui/dispenser.png"); public TestTileScreen(TestTileContainer screenContainer, PlayerInventory inv, ITextComponent titleIn) { super(screenContainer, inv, titleIn); this.xSize = 176; this.ySize = 166; } @Override public void render(final int mouseX, final int mouseY, final float partialTicks) { this.renderBackground(); super.render(mouseX, mouseY, partialTicks); this.renderHoveredToolTip(mouseX, mouseY); } @Override protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { super.drawGuiContainerForegroundLayer(mouseX, mouseY); this.font.drawString(this.title.getFormattedText(), 8.0f, 5.0f, 0x000000); this.font.drawString(this.playerInventory.getDisplayName().getFormattedText(), 8.0f, 72.0f, 0xffffff); } @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { RenderSystem.color4f(1.0f, 1.0f, 1.0f, 1.0f); this.minecraft.getTextureManager().bindTexture(BACKGROUND_TEXTURE); int x = (this.width - this.xSize) / 2; int y = (this.height - this.ySize) / 2; this.blit(x, y, 0, 0, this.xSize, this.ySize); } }
まず、3行目でGUIに使用するテクスチャのロケーションを宣言しておきます。
今回はディスペンサーのテクスチャをそのまま使います。
コンストラクタで、pngファイル上の使用する部分のサイズを設定します。
他の3つのメソッドはバニラのホッパーからコピペしてきました。
drawStringを無くせば、GUIの文字を消せるし、第一引数にStringを渡して別の文字を表示させることもできます。
drawGuiContainerBackgroundLayerでは設定した画像を表示させます。
bindTextureで画像をセットして、blitで表示させます。
blitの引数は(画面上のx座標, 画面上のy座標, 画像上の開始x座標, 画像上の開始y座標, Δx: 画像上の開始点からx方向にどれだけ幅をとるか, Δy)
になっています。
すこし話が逸れますが、Minecraft.ingameGUIに画像をblitすることでゲーム画面上に画像を貼ることもできます。
ContainerとScreenが完成したので、これらを登録し、TileEntityに紐付けます。
まず登録を行ないます。
@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD) public class ModTileEntities { public static final TestTileBlock TEST_TILE_BLOCK = new TestTileBlock(Block.Properties.create(Material.PLANTS), "test_tile"); public static final TileEntityType<TestTileEntity> TEST_TILE = TileEntityType.Builder.create(TestTileEntity::new, TEST_TILE_BLOCK).build(null); public static final ContainerType<TestTileContainer> TEST_TILE_CONTAINER = IForgeContainerType.create(TestTileContainer::new); @SubscribeEvent public static void registerBlocks(final RegistryEvent.Register<Block> event) { event.getRegistry().register(TEST_TILE_BLOCK); } @SubscribeEvent public static void registerBlockItems(final RegistryEvent.Register<Item> event) { event.getRegistry().register(new BlockItem(TEST_TILE_BLOCK, new Item.Properties().group(TestItemGroups.TEST_GROUP)).setRegistryName(TEST_TILE_BLOCK.getRegistryName())); } @SubscribeEvent public static void registerTileEntity(final RegistryEvent.Register<TileEntityType<?>> event) { TEST_TILE.setRegistryName(TestMod.MODID, "test_tile"); event.getRegistry().register(TEST_TILE); } @SubscribeEvent public static void registerContainer(final RegistryEvent.Register<ContainerType<?>> event) { TEST_TILE_CONTAINER.setRegistryName(TestMod.MODID, "test_tile_container"); event.getRegistry().register(TEST_TILE_CONTAINER); } @OnlyIn(Dist.CLIENT) @SubscribeEvent public static void registerScreen(final FMLClientSetupEvent event) { ScreenManager.registerFactory(TEST_TILE_CONTAINER, TestTileScreen::new); } }
6行目でコンテナをContainerTypeとして宣言します。
IForgeContainerType.createにコンテナのコンストラクタ参照を渡します。
コンテナのコンストラクタに一致するものがない場合は、ラムダ式で記入します。
宣言したコンテナはブロック等と同様に、registryNameを設定し、登録します。
スクリーンは35~40行目で登録しています。スクリーンはクライアント側のみに登録するので、FMLClientSetupEventを呼び出し、
そこでScreenManager.registerFactoryにコンテナータイプとスクリーンのコンストラクタ参照を渡します。
これで登録は完了です。
最後にこれらをTileEntityに紐付け、GUIを開くための機能をBlockに実装します。
public class TestTileEntity extends TileEntity implements INamedContainerProvider { private int count = 0; private LazyOptional<ItemStackHandler> itemHandler = LazyOptional.of(() -> new ItemStackHandler(9)); public TestTileEntity() { super(ModTileEntities.TEST_TILE); } @Override public CompoundNBT write(CompoundNBT compound) { compound.putInt("T_count", count); itemHandler.ifPresent(h -> { compound.put("Items", h.serializeNBT() ); }); System.out.println("write"); return super.write(compound); } @Override public void read(CompoundNBT compound) { System.out.println("read"); super.read(compound); count = compound.getInt("T_count"); itemHandler.ifPresent(h -> { if(compound.contains("Items")) { h.deserializeNBT(compound.getCompound("Items")); } }); } public void increment() { count++; } public int getCount() { return count; } @Override public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) { if(cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) { return itemHandler.cast(); } return super.getCapability(cap, side); } @Override public Container createMenu(int windowId, PlayerInventory inv, PlayerEntity player) { return new TestTileContainer(windowId, inv, world, pos); } @Override public ITextComponent getDisplayName() { return new TranslationTextComponent("Test tile entity with GUI!"); } }
コンテナと紐付けるためには INamedContainerProviderをimplements します。
また、4行目でItemStackHandlerの数を9に変更しています。
INamedContainerProviderの抽象メソッド、createMenuとgetDisplayNameをそれぞれ58~62行目、64~68行目で定義しています。
createMenuでは実際に処理をコンストラクタの引数を渡してnewします。
getDisplayNameではTranslationTextComponentに任意の文字列を渡すと、それが表示される名前になります。
最後にブロックを右クリックの挙動にGUIを開く機能を追加します。
@Override public ActionResultType func_225533_a_(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult result) { TileEntity te = world.getTileEntity(pos); if(te instanceof TestTileEntity && !world.isRemote) { ((TestTileEntity) te).increment(); te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).<ItemStackHandler>cast() .ifPresent(h -> { h.setStackInSlot(0, new ItemStack(Items.DIAMOND, ((TestTileEntity) te).getCount())); } ); NetworkHooks.openGui((ServerPlayerEntity) player, (TestTileEntity) te, b -> b.writeBlockPos(pos)); System.out.println(((TestTileEntity) te).getCount() +", "+pos); } return ActionResultType.SUCCESS; }
NetworkHooks.openGuiでGUIが開きます。
引数はそれぞれ、(サーバープレイヤー、GUIと紐付けたこのブロックのタイルエンティティ、PacketBuffer型のConsumer)です。
ここのConsumerの処理でwriteしたものを、コンテナーのコンストラクタのPacketBufferから読み込みます。
今回は、BlockPosをwriteしています。
以前に、MobEntityと紐付いたGUIコンテナを作成したときはentityIdをint型としてwriteして渡しました。
ついでに、ItemStackHandlerの数を9個にしたので、ブロックが壊されたときに9個分のインベントリをドロップするように修正します。
if(world.getTileEntity(pos) instanceof TestTileEntity && !world.isRemote) { TestTileEntity te = (TestTileEntity) world.getTileEntity(pos); te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).ifPresent(h -> { for(int i=0; i < h.getSlots(); i++) { world.addEntity(new ItemEntity(world, pos.getX(), pos.getY(), pos.getZ(), h.getStackInSlot(i))); System.out.println(h.getStackInSlot(i).getItem()+", "+h.getStackInSlot(i).getCount()); } }); } super.onReplaced(state, world, pos, newState, isMoving); }
これで、GUIが開けるようになったので、実際に起動します。
GUIが開けます。
また破壊されたときにも、ちゃんとすべてのアイテムをドロップしています。
今回は以上です。