初心者modderの備忘録

マイクラのmodを作りたくて、初めて見たのですが難しくて忘れそうなので自分用の備忘録も兼ねてブログにしようと思います

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ファイル上の使用する部分のサイズを設定します。

f:id:json_fileman:20200514222656p:plain


他の3つのメソッドはバニラのホッパーからコピペしてきました。
drawStringを無くせば、GUIの文字を消せるし、第一引数にStringを渡して別の文字を表示させることもできます。


drawGuiContainerBackgroundLayerでは設定した画像を表示させます。
bindTextureで画像をセットして、blitで表示させます。
blitの引数は(画面上のx座標, 画面上のy座標, 画像上の開始x座標, 画像上の開始y座標, Δx: 画像上の開始点からx方向にどれだけ幅をとるか, Δy)
になっています。


すこし話が逸れますが、Minecraft.ingameGUIに画像をblitすることでゲーム画面上に画像を貼ることもできます。


f:id:json_fileman:20200514223931p:plain



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が開けるようになったので、実際に起動します。


f:id:json_fileman:20200514231027p:plain

GUIが開けます。

また破壊されたときにも、ちゃんとすべてのアイテムをドロップしています。

f:id:json_fileman:20200514231125p:plain


今回は以上です。