初心者modderの備忘録

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

6日目 Mobの追加2 (スポーン関連、loot_tableなど)

前回、とりあえずMobだけ追加しましたが、スポーンエッグや自然スポーンを登録していなかったので、それらを行なっていこうと思います。
また、Mobが倒された時にドロップするアイテムの設定などはすべてルートテーブルで行なうので、そちらも簡単に設定していこうと思います。

少し長くなったので、簡単に目次をつけておきます。


①スポーンエッグの登録
②自然スポーンの設定
③ルートテーブルの設定
④子供のモデルの設定
おまけ:コンテナーをもつMob


                                                                                                                                                                                                                                    • -

①スポーンエッグの登録

まず、スポーンエッグの登録から行ないます。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModItems
{
	public static final Item TEST_ITEM = new Item(new Item.Properties().group(TestItemGroups.TEST_GROUP));
	public static final Item EXAMPLE_ITEM = new ExampleItem(new Item.Properties().group(TestItemGroups.EXAMPLE_GROUP));

	public static final Item TEST_EGG = new SpawnEggItem(ModEntities.TEST_ENTITY, 0x25fff6, 0x251184, new Item.Properties().group(TestItemGroups.TEST_GROUP));

	@SubscribeEvent
	public static void registerItems(final RegistryEvent.Register<Item> event)
	{
		TEST_ITEM.setRegistryName(TestMod.MODID,"test_item");
		EXAMPLE_ITEM.setRegistryName(TestMod.MODID,"example_item");
		TEST_EGG.setRegistryName(TestMod.MODID, "test_egg");

		event.getRegistry().registerAll(TEST_ITEM,EXAMPLE_ITEM, TEST_EGG);
	}
}


上のコードの7行目でスポーンエッグのアイテムを宣言しています。
new SpawnEggItemで宣言し、引数は(EntityType, color1, color2, ItemGroup)で、colorはそれぞれスポーンエッグの卵のベース色と斑点の色です。
登録は、その他のアイテムと同様にsetRegistryNameで名前を登録してから、registerAllに渡すことで登録できます。


登録しただけだと、アイテムのテクスチャが表示されないので、jsonでモデルを設定します。

resources/assets/MODID/models/item
に登録名.jsonのファイルを作成します。
今回の例では、test_egg.jsonです。

{
    "parent": "item/template_spawn_egg"
}

内容はこれだけです。
これでjava内で設定した配色のスポーンエッグのテクスチャが表示されます。



f:id:json_fileman:20200422221834p:plain


また、登録名は他のアイテムと同様にlangフォルダのjsonファイルで設定できます。



                                                                                                                                                                                                            • -

②自然スポーンの設定


続いて自然スポーンの設定を行なっていきます。

Modのメインのファイルのsetupメソッド (preInit)で登録します。
直接メインファイルに書くと、メインファイルが複雑になるので、今回はWorldGenManagerというクラスを作成し、その中にaddSpawnというメソッドを作成しました。

public class WorldGenManager
{
	public void addSpawn()
	{
		ForgeRegistries.BIOMES.forEach(biome ->
		{
			biome.getSpawns(ModEntities.TEST_ENTITY.getClassification())
			.add(new SpawnListEntry(ModEntities.TEST_ENTITY, 10, 2, 8));
		});
	}
}


自然スポーンを登録するには、BiomeクラスのgetSpawnを呼び出して、EntityのClassificationを渡してリストを取得します。
そのリストに新たなエントリーをaddすることで追加できます。
SpawnListEntryの引数は(登録したいEntityType, スポーンの重み, 集団スポーンの最小値, 集団スポーンの最大値) です。

重みに関しては具体的な値は分かりませんが、大きい程よりスポーンしやすくなり10でも結構な確率でスポーンしているように感じました。

このaddSpawnメソッドをModのメインクラスで呼び出します。
少し省略していますがコードを乗せます。

public class TestMod
{
	~~
	public static final WorldGenManager WORLD_GEN = new WorldGenManager();

	~~

	private void setup(final FMLCommonSetupEvent event)
	{
		LOGGER.info("Setup method registered.");

		WORLD_GEN.addSpawn();
	}

	~~
}


f:id:json_fileman:20200422221345p:plain



うまく登録されると画像のように自然にスポーンするようになります。

                                                                                                                                                                                                    • -


③ルートテーブルの設定

モンスターのドロップアイテムは基本的にルートテーブルで設定します。
これも一から自分で書くのは大変なので、バニラのjsonファイルを参考にしながら切り貼りするのが良いでしょう。


MC Assets - Browser for Minecraft Asset Files


assets/data/MODID/loot_tables/entitiesに
登録名.jsonjsonファイルを作成します。
今回の例ではtest_entity.jsonになります。

{
  "type": "minecraft:entity",
  "pools": [
    {
      "rolls": 1,
      "entries": [
        {
          "type": "minecraft:item",
          "functions": [
            {
              "function": "minecraft:set_count",
              "count": {
                "min": 1.0,
                "max": 3.0,
                "type": "minecraft:uniform"
              }
            },
            {
              "function": "minecraft:looting_enchant",
              "count": {
                "min": 0.0,
                "max": 1.0
              }
            }
          ],
          "name": "minecraft:slime_ball"
        }
      ]
    }
  ]
}


今回は、スライムのルートテーブルをコピーしました。2種類のアイテムをドロップするようなMobを作るときは牛やニワトリなんかを参考にすれば良いのではないでしょうか。

                                                                                                                                                                                                    • -

④子供のモデルの設定


AnimalEntityを継承したEntityは繁殖で増やすことが出来ます。その場合、Modelクラスで適切に設定することで、バニラのように生まれてくる子供のサイズを小策することができます。

public class TestModel extends EntityModel<TestEntity>
{
    public ModelRenderer body;
    public ModelRenderer head;
    public ModelRenderer arm1;
    public ModelRenderer arm2;

    //func_228301_a_ is addBox
	public TestModel()
	{
		
	~~~

	//This method is render
	@Override
	public void func_225598_a_(MatrixStack p_225598_1_, IVertexBuilder p_225598_2_, int p_225598_3_, int p_225598_4_,
			float p_225598_5_, float p_225598_6_, float p_225598_7_, float p_225598_8_)
	{
		if (this.isChild)
		{
			p_225598_1_.func_227860_a_();
			p_225598_1_.func_227862_a_(0.56666666F, 0.56666666F, 0.56666666F);
			p_225598_1_.func_227861_a_(0.0D, 1.375D, 0.125D);
			ImmutableList.of(body, head, arm1, arm2).forEach((p_228292_8_) -> {
				p_228292_8_.func_228309_a_(p_225598_1_, p_225598_2_, p_225598_3_, p_225598_4_, p_225598_5_, p_225598_6_, p_225598_7_, p_225598_8_);
			});
			p_225598_1_.func_227865_b_();
		} else
		{
			p_225598_1_.func_227860_a_();
			p_225598_1_.func_227862_a_(1.0F, 1.0F, 1.0F);
			p_225598_1_.func_227861_a_(0.0D, 0.0D, 0.0D);
			ImmutableList.of(body, head, arm1, arm2).forEach((p_228290_8_) -> {
				p_228290_8_.func_228309_a_(p_225598_1_, p_225598_2_, p_225598_3_, p_225598_4_, p_225598_5_, p_225598_6_, p_225598_7_, p_225598_8_);
			});
			p_225598_1_.func_227865_b_();
		}
	}

	~~~

}

他の部分は省略してしまいましたが、 func_225598_a_ (renderに相当) 内で設定します。
p_225598_1_.func_227862_a_(x, y, z)はそれぞれx,yおよびz方向のスケールを設定します。今回は子供のサイズは0.566倍に設定しました。
次の行のp_225598_1_.func_227861_a_(x, y, z); は各軸方向のオフセットを補正します。スケールを変更する際に、おそらくmobの中心を起点に拡大または縮小されるので、そのまま表示すると浮いたり地面に埋まったりします。なので、オフセットを変更して適切な位置にMobが表示されるようにします。y座標を最も変更することになるとおもいますが、yは正の値を設定するとオフセットが下にずれます。



f:id:json_fileman:20200423203150p:plain




                                                                                                                                                                                                    • -

おまけ
コンテナーをもつMob


機能として、コンテナー (チェストのような) を持つEntityを追加したので載せておきます。
Entityのクラスに加えて、Containerクラス、Screenクラスを作成します。



ExampleEntity.class

public class ExampleEntity extends AnimalEntity implements  INamedContainerProvider
{
	private ItemStackHandler handler = new ItemStackHandler(9);
	private int count = 0;
	private boolean obtained_flag=false;

	protected ExampleEntity(EntityType<? extends AnimalEntity> type, World p_i48564_2_)
	{
		super(type, p_i48564_2_);
	}

	@Override
	public AgeableEntity createChild(AgeableEntity ageable)
	{
		return null;
	}

	@Override
	public void writeAdditional(CompoundNBT compound)
	{
		super.writeAdditional(compound);
		compound.putInt("count", count);
		compound.putBoolean("flag", obtained_flag);

		ListNBT listnbt = new ListNBT();

		for(int i = 0; i < handler.getSlots(); ++i) {
			ItemStack itemstack = handler.getStackInSlot(i);
			if (!itemstack.isEmpty()) {
				CompoundNBT compoundnbt = new CompoundNBT();
				compoundnbt.putByte("Slot", (byte)i);
				itemstack.write(compoundnbt);
				listnbt.add(compoundnbt);
			}
		}
		compound.put("Items", listnbt);
	}

	@Override
	public void readAdditional(CompoundNBT compound)
	{
		super.readAdditional(compound);

		ListNBT listnbt = compound.getList("Items", 10);

		for(int i = 0; i < listnbt.size(); ++i)
		{
			CompoundNBT compoundnbt = listnbt.getCompound(i);
			int j = compoundnbt.getByte("Slot") & 255;

			handler.setStackInSlot(j, ItemStack.read(compoundnbt));

		}

		obtained_flag = compound.getBoolean("flag");
		count = compound.getInt("count");
	}

	@Override
	public boolean processInteract(PlayerEntity player, Hand hand)
	{
		if(!obtained_flag && !world.isRemote)
		{
			handler.setStackInSlot(0, new ItemStack(Items.EMERALD, 20));
			obtained_flag = true;
		}

		if(!world.isRemote())
		{
			count++;

			NetworkHooks.openGui((ServerPlayerEntity) player, (INamedContainerProvider)this,buf -> buf.writeInt(getEntityId()));
			return true;
		}
		return false;
	}

	@Override
	public Container createMenu(int p_createMenu_1_, PlayerInventory p_createMenu_2_, PlayerEntity p_createMenu_3_)
	{
		  return new TestContainer(p_createMenu_1_, p_createMenu_2_, getEntityId());
	}

	@SuppressWarnings("unchecked")
	@Nonnull
	@Override
	public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side)
	{
		if (cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
		{
			return LazyOptional.of(() -> (T)handler);
		}
		return super.getCapability(cap, side);
	}
}


インベントリの内容はnbtデータで管理しますが、Entityは既にたくさんのnbtデータ (体力、座標など) を持っているので、writeやreadをオーバーライドすることが出来ません。
なので、wrtie/read additionalで追加のnbtデータを管理します。


続いてコンテナークラスを作成します。

TestContainer.class

public class TestContainer extends Container
{

	public static ContainerType<TestContainer> TESTBLOCK_CONTAINER = IForgeContainerType.create(TestContainer::new);

	public TestContainer(int windowId, PlayerInventory playerInventory, PacketBuffer extraData)
	{
		this(windowId, playerInventory, extraData.readInt());
	}

	public TestContainer(int windowId, PlayerInventory playerInventory, int id)
	{
		super(TESTBLOCK_CONTAINER, windowId);

		ExampleEntity entity = (ExampleEntity) playerInventory.player.world.getEntityByID(id);

		//Obtained entity's itemHandler, and set Slot.
		entity.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));
				}
			}
		});

		//Set player's inventory Slot.
		for(int k = 0; k < 3; ++k) {
			for(int i1 = 0; i1 < 9; ++i1) {
				this.addSlot(new Slot(playerInventory, i1 + k * 9 + 9, 8 + i1 * 18, 84 + k * 18));
			}
		}

		for(int l = 0; l < 9; ++l) {
			this.addSlot(new Slot(playerInventory, 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 < 9) {
				if (!this.mergeItemStack(itemstack1, 9, 45, true))
				{
					return ItemStack.EMPTY;
				}
			} else if (!this.mergeItemStack(itemstack1, 0, 9, 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;
	}

}

このクラスでは、iTemHandelerを受け取り、それぞれにスロットを設定します。GUI上での二次元座標を与えてインベントリの表示位置を決定します。




そして、どこのクラスでもいいので、 @SubscribeEventで登録を行ないます。

	@SubscribeEvent
	public static void onContainerRegistry(final RegistryEvent.Register<ContainerType<?>> event)
	{
		TestContainer.TESTBLOCK_CONTAINER.setRegistryName("test_container");
		event.getRegistry().register(TestContainer.TESTBLOCK_CONTAINER);
	}


最後に、Screenクラスを作成します。

public class TestScreen extends ContainerScreen<TestContainer>
{

	private ResourceLocation GUI = new ResourceLocation(TestMod.MODID, "textures/gui/dispenser.png");

	public TestScreen(TestContainer container, PlayerInventory inv, ITextComponent name) {
		super(container, inv, name);
	}

	@Override
	public void render(int mouseX, int mouseY, float partialTicks)
	{
		this.renderBackground();
		super.render(mouseX, mouseY, partialTicks);
		this.renderHoveredToolTip(mouseX, mouseY);
	}

	@Override
	protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY)
	{
	}

	@Override
	protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
		GlStateManager.func_227637_a_(1.0f, 1.0f, 1.0f, 1.0f);;
		this.minecraft.getTextureManager().bindTexture(GUI);
		int relX = (this.width - this.xSize) / 2;
		int relY = (this.height - this.ySize) / 2;
		this.blit(relX, relY, 0, 0, this.xSize, this.ySize);
	}
}


このクラスの主な役割は、guiのテクスチャー設定です。
今回はディスペンサーのGUIに合わせました。


スクリーンはクライエント側にしか必要ないので、MODのメインクラスのクライエント処理で登録を行ないます。

private void clientRegistries(final FMLClientSetupEvent event)
{
	ScreenManager.registerFactory(TestContainer.TESTBLOCK_CONTAINER, TestScreen::new);
}

以上を登録してEntityに右クリックを行なうとGUIが開いてインベントリを扱えます。

f:id:json_fileman:20200423223003p:plain



長くなりましたが、今回は以上です。



次回は元々予定していた、nbtデータを使った構造物の追加を行ないます。