初心者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データを使った構造物の追加を行ないます。

5日目 Mobの追加 (1.15 ver)

前回の終わりにストラクチャーブロックを使った構造物の追加について書くと言ったのですが、いろいろ忘れそうなので少し予定を変更してMobEntityの追加を先に書きます。

Mobの追加は1.12以前と比べると特に登録の仕方が結構変わっていました。

1. ~Entity.class
2. ~Model.class
3. ~Renderer.class
の3つのクラスを作成する点は1.12以前と同様です。
これらのクラスの詳細は後述しますので、先に登録について書いておきます。

まずなにかクラスを作成し、そこで宣言および登録をします。今回はModEntitiesというクラスを作成しました。
そして、1.13以降では登録するのはEntityではなくEntityTypeなのでEntityTypeを宣言して、EntityType.Builderで作成します。
その際にEntityClassificationやサイズ等を設定します。
その後、registerEntities内で、registryNameを設定してから、登録します。この辺はアイテムやブロックの登録と同様だと思います。


続いて、Entityの場合にはレンダリングの登録が必要になります。
レンダリングはクライアントでのみ行なわれるので、登録用のメソッドはMainModクラスのclientRegistriesで呼びだします。
登録自体は比較的単純で
RenderingRegistry.registerEntityRenderingHandler(EntityType, ~~Renderer::new);
と打ち込めばOKです。

1.13および1.14では、第一引数がEntityTypeではなく~~Entity.classかもしれません (未検証)。



ModEntities.class

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModEntities
{
	public static final EntityType<ExampleEntity> EXAMPLE_ENTITY = EntityType.Builder.<ExampleEntity>create(ExampleEntity::new,EntityClassification.CREATURE)
			.size(1.0f, 2.2f)
			.setShouldReceiveVelocityUpdates(false)
			.build("example_entity");

	public static final EntityType<TestEntity> TEST_ENTITY = EntityType.Builder.create(TestEntity::new,EntityClassification.CREATURE)
			.size(1.0f, 1.5f)
			.setShouldReceiveVelocityUpdates(false)
			.build("test_entity");

	@SubscribeEvent
	public static void registerEntities(final RegistryEvent.Register<EntityType<?>> event)
	{
		EXAMPLE_ENTITY.setRegistryName(TestMod.MODID, "example_entity");
		TEST_ENTITY.setRegistryName(TestMod.MODID, "test_entity");

		event.getRegistry().registerAll(EXAMPLE_ENTITY, TEST_ENTITY);
	}

	//called by clientRegistries method in MainMod class
	@OnlyIn(Dist.CLIENT)
	public static void clientInit()
	{
		RenderingRegistry.registerEntityRenderingHandler(EXAMPLE_ENTITY, ExampleRenderer::new);
		RenderingRegistry.registerEntityRenderingHandler(TEST_ENTITY, TestRenderer::new);
	}
}

TestMod.class

~~~
private void clientRegistries(final FMLClientSetupEvent event)
	{
		ModEntities.clientInit();
	}
~~~


以上が登録なので、続いて作成する3つのクラスについて書いていきます。




まず1つ目の~Entity.classは、そのEntityの挙動等を決めるメインとなるクラスです。
registerGoals()で基本となる行動を決めたり、registerAttributes()で体力や速度を決めます。
このクラスは他にも設定できる項目が非常にたくさんあるので、今回は牛の行動パターンをまねたシンプルなMobを追加します。
自分の追加したい挙動に近いバニラに存在するEntityのコードを参考に編集するのが良い気がします。


TestEntity.class

public class TestEntity extends AnimalEntity
{

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

	@Override
	protected void registerGoals()
	{
		this.goalSelector.addGoal(0, new SwimGoal(this));
		this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D));
		this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
		this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.fromItems(Items.WHEAT), false));
		this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D));
		this.goalSelector.addGoal(5, new WaterAvoidingRandomWalkingGoal(this, 1.0D));
		this.goalSelector.addGoal(6, new LookAtGoal(this, PlayerEntity.class, 6.0F));
		this.goalSelector.addGoal(7, new LookRandomlyGoal(this));
	}

	protected void registerAttributes()
	{
		super.registerAttributes();
		this.getAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(10.0D);
		this.getAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.25D);
	}

	@Override
	public TestEntity createChild(AgeableEntity ageable)
	{
		return ModEntities.TEST_ENTITY.create(world);
	}

}


続いて、Modelを設定します。Modelに関しては1.12以前と同様にTabulaを使って作成するのが良いと思います。

Tabulaに関する説明は長くなるので後述し、とりあえず作成したコードを乗せておきます。


TestModel.class

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.textureHeight = 64;
		this.textureWidth =128;

        this.body = new ModelRenderer(this, 0, 0);
        this.body.setRotationPoint(0.0F, 3.6F, 0.0F);
        this.body.func_228301_a_(-5.0F, -5.0F, -5.0F, 10, 10, 10, 0f);

        this.arm1 = new ModelRenderer(this, 40, 0);
        this.arm1.setRotationPoint(-7.7F, 10.0F, 0.0F);
        this.arm1.func_228301_a_(-0.5F, 0.0F, -0.5F, 1, 10, 1, 0f);
        this.setRotationOffset(arm1, 0.0F, 0.0F, 0.3490658503988659F);

        this.arm2 = new ModelRenderer(this, 44, 0);
        this.arm2.setRotationPoint(7.7F, 10.0F, 0.0F);
        this.arm2.func_228301_a_(-0.5F, 0.0F, -0.5F, 1, 10, 1, 0f);
        this.setRotationOffset(arm2, 0.0F, 0.0F, -0.3490658503988659F);

        this.head = new ModelRenderer(this, 40, 0);
        this.head.setRotationPoint(0.0F, 16.0F, 0.0F);
        this.head.func_228301_a_(-7.5F, -7.5F, -7.5F, 15, 15, 15, 0f);
	}

	//This method is setRotationAngles
	@Override
	public void func_225597_a_(TestEntity p_225597_1_, float p_225597_2_, float p_225597_3_, float p_225597_4_, float p_225597_5_, float p_225597_6_)
	{
		float f = MathHelper.sin(this.swingProgress * (float)Math.PI);
		float f1 = MathHelper.sin((1.0F - (1.0F - this.swingProgress) * (1.0F - this.swingProgress)) * (float)Math.PI);
		this.arm1.rotateAngleZ = 0.0F;
		this.arm2.rotateAngleZ = 0.0F;
		this.arm1.rotateAngleY = -(0.1F - f * 0.6F);
		this.arm2.rotateAngleY = 0.1F - f * 0.6F;
		float f2 = -(float)Math.PI / 2.25F;
		this.arm1.rotateAngleX = f2;
		this.arm2.rotateAngleX = f2;
		this.arm1.rotateAngleX += f * 1.2F - f1 * 0.4F;
		this.arm2.rotateAngleX += f * 1.2F - f1 * 0.4F;
		this.arm1.rotateAngleZ += MathHelper.cos(p_225597_4_ * 0.09F) * 0.05F + 0.05F;
		this.arm2.rotateAngleZ -= MathHelper.cos(p_225597_4_ * 0.09F) * 0.05F + 0.05F;
		this.arm1.rotateAngleX += MathHelper.sin(p_225597_4_ * 0.067F) * 0.05F;
		this.arm2.rotateAngleX -= MathHelper.sin(p_225597_4_ * 0.067F) * 0.05F;
	}

	//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_)
	{
		body.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_);
		head.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_);
		arm1.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_);
		arm2.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_);
	}

	private void setRotationOffset(ModelRenderer renderer, float x, float y, float z)
	{
		renderer.rotateAngleX = x;
		renderer.rotateAngleY = y;
		renderer.rotateAngleZ = z;
	}

}


最後にテクスチャーを設定するためのRendererクラスを作成します。
このクラスはそんなに設定することはないです。


TestRenderer.class

public class TestRenderer extends MobRenderer<TestEntity, TestModel>
{
	public TestRenderer(EntityRendererManager p_i50961_1_)
	{
		super(p_i50961_1_, new TestModel(), 0.5f);
	}

	@Override
	public ResourceLocation getEntityTexture(TestEntity entity)
	{
		return new ResourceLocation(TestMod.MODID, "textures/entity/test_entity.png" );
	}
}

スーパーコンストラクターで第二引数のModelと第三引数の影サイズを設定し、 getEntityTextureをオーバーライドしてテクスチャのファイルを設定します。

以上の3つのクラスを作成して、登録すればMobが追加できると思います。まだスポーンエッグは作成していないので、sumommコマンドでMobを出現させました。

f:id:json_fileman:20200416200825p:plain

今回は自作のモデルとバニラのスケルトンを元に編集したモデルの2匹のMobを追加しました。
スポーンエッグや、自然スポーンについては次回以降に書こうと思います。

                                                                                                                                                                                                                                                                  • -

Tabulaの使い方ですが、まず以下のサイトからTabulaをダウンロードします。

Tabula – Minecraft Modeler

Tabulaを使うためにはiChunUtilという前提Modが必要になるので、これもTabulaのバージョンと合ったものをダウンロードします。

iChunUtil - Mods - Minecraft


残念ながら、Tabulaの最新版は1.12.2なので出力されるコードの形式が少し違うものになってしまいますが、とりあえず1.12.2のバージョンを使うのが良いと思います。




この2つのModのjarファイルをバージョンのあったForgeのmodsフォルダに入れて起動すると、タイトル画面のModsボタンの横にTと書かれたボタンが現れるので、これをクリックするとTabulaが起動します。

f:id:json_fileman:20200415233422p:plain


起動すると画像のような画面になります。


f:id:json_fileman:20200415234152p:plain


少し見えづらいかもしれませんが、まず赤いまるで囲った+マークからプロジェクトを作成します。名前等はなんでも良いと思います。テクスチャのサイズは後で変えることもできますが、小さめのMobなら64×32、大きめなら128×64程度でいいのではないでしょうか。スケールは基本的に全部1でいいと思います。

青いまるで囲った部分はインポートで、自作のModelまたはバニラのModelをインポートすることが出来ます。バニラのMobの亜種みたいなものを作るときは、ここでインポートすると非常に楽になります。

次に、右下の紫で囲った部分から直方体の追加、削除、グループ化が出来ます。まとめて角度を変えたい時なんかは、グループボタンを押して作成されたフォルダに、グループ化したい直方体をいれてフォルダごと角度を変えるようなことができます。

直方体を追加したら、左のたくさん数字が並んでいるパラメーターを設定します。Dimensionが直方体の大きさ、Posisionが位置です。次にOffsetですが、これは回転の中心だと思っています。なので、頭などは青い球体が頭の中心にくるように、手足などは球体が始点にくるようにoffsetをずらせばいいと思います。

そんな感じで直方体をいくつか追加して目的のMobの形を作ればいいのですが、右したに竈門が出ているのでこれを見ながらMobの向きが正しいかどうかを確認しながら作ります。間違った向きで作成すると、例えばエサに寄ってくるときに横向きで寄ってきたり不自然な挙動になります。

完成したら、黄色で囲った4色のジグソーパズルのようなアイコンをクリックします。これでテクスチャを1枚の画像内に分配します。このときテクスチャサイズに十分な余分がないとfailed~~と出るので、その場合はテクスチャサイズを大きくします。赤丸の横のアイコンから変更できます。変更後、もう一度ジグソーパズルをクリックして、うまくいっていればメッセージは出ずに右上のテクスチャマップが更新されます。

ここまで行なったら、緑の丸で示した赤い矢印のアイコンをクリックしてエクスポートします。
テクスチャマップとjavaクラスをそれぞれエクスポートします。

その後、右上の「Open Working Directory」をクリックし、exportフォルダを開くと、作成したpng画像とjavaファイルが入っているはずです。


f:id:json_fileman:20200416001244p:plain


無事、出力されていればTabulaでの作業は以上です。

続いてModelクラスを使うのですが、1.12と1.13以降で少し変わっていて、そのままでは使えないため少し変更します。

コンストラクタの中身はメソッド名が変わっているだけで、大きな変化はありません。

次に、func_225597_a_とfunc_225598_a_をオーバーライドします。

func_225597_a_は1.14でのsetRotationAnglesに対応するメソッドで、歩くときに腕を振るような動的なパーツの動き等を設定できます。
今回はゾンビの動きをコピペしました。


func_225598_a_は1.14のrenderに相当するメソッドで、これによって壁画されます。
基本的には、それぞれのModelRendererのfunc_228309_a_ (これはModelRendererの中のrenderメソッド)にそのまま仮引数を渡せば良いと思います。ただ、子供状態などが存在する場合には、少し数値を変換してから渡す必要があるようです。バニラのウサギのモデル等が参考になりそうです。

renderまで設定すればModelクラスの作成は終わりです。
あとはテクスチャをペイントソフト等で編集して、assets.MODID.textures.entityに入れます。


今回は以上です。
次回は、元々予定していたストラクチャーブロックを使った構造物の追加か、Mobの追加の続き(スポーンエッグなど)について書こうと思います。

1.15~まとめ

0日目 環境設定(Forge1.13.2) - 初心者modderの備忘録

1日目 Modメインファイル(Forge1.15.2) - 初心者modderの備忘録

2日目 構造物の生成 - 初心者modderの備忘録

3日目 アイテムの追加とアイテムグループの追加 (追記あり) - 初心者modderの備忘録

4日目 ブロックの追加 (単純なブロックとカスタムモデルを持つブロック) - 初心者modderの備忘録

5日目 Mobの追加 (1.15 ver) - 初心者modderの備忘録

6日目 Mobの追加2 (スポーン関連、loot_tableなど) - 初心者modderの備忘録

7日目 nbtデータを使った構造物の追加 - 初心者modderの備忘録

8日目 Tile Entityの追加 (1.15) - 初心者modderの備忘録

9日目 Tile Entityへのインベントリの追加 - 初心者modderの備忘録

10日目 Tile EntityへのGUIの追加 - 初心者modderの備忘録

4日目 ブロックの追加 (単純なブロックとカスタムモデルを持つブロック)

今日はブロックの追加について書きたいと思います。
前半で基本的なブロックの登録、後半でカスタムモデルを持つブロックの登録について書いていきます。

~Part 1~
基本的なブロックの宣言と登録は次のようになります。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModBlocks
{
	public static final Block TEST_BLOCK = new Block(Block.Properties.create(Material.IRON).lightValue(15).sound(SoundType.METAL).hardnessAndResistance(1.5f, 2.5f));

	@SubscribeEvent
	public static void registerBlocks(final RegistryEvent.Register<Block> event)
	{
		TEST_BLOCK.setRegistryName(TestMod.MODID, "test_block");
		
		event.getRegistry().registerAll(TEST_BLOCK);
	}

	@SubscribeEvent
	public static void registerBlockItems(final RegistryEvent.Register<Item> event)
	{
		event.getRegistry().register(new BlockItem(TEST_BLOCK, new Item.Properties().group(TestItemGroups.TEST_GROUP)).setRegistryName(TEST_BLOCK.getRegistryName()));
	}
}


BlockのコンストラクタのPropertiesでマテリアルやサウンドタイプ明るさ、堅さなどを設定できます。ブロックを登録する場合は、ブロックの登録に加えて、アイテムブロックの登録が必要になります。したがって、何個もブロックを登録すると、その度にアイテムブロックの作成をしなければならず、面倒くさいのに加えてミスも増えます。

なので私は、registryNameとitemGroupをコンストラクタに加え、Blockを継承したModBlockというクラスを作成し、ModBlocksクラスでnewした後にリストに渡すというやり方でBlockとItemBlockを一括登録するようにしています。



ModBlock.java

public class ModBlock extends Block
{
	private ItemGroup group;

	public ModBlock(Properties properties, String name, ItemGroup group)
	{
		super(properties);
		this.group = group;
		setRegistryName(TestMod.MODID,name);
	}

	public ItemGroup getGroup()
	{
		return group;
	}
}
@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModBlocks
{
	public static final ModBlock TEST_BLOCK = new ModBlock(Block.Properties.create(Material.IRON).lightValue(15).sound(SoundType.METAL).hardnessAndResistance(1.5f, 2.5f), "test_block", TestItemGroups.TEST_GROUP);
	public static final ModBlock EXAMPLE_BLOCK = new ExampleBlock(Block.Properties.create(Material.WOOD).lightValue(10).sound(SoundType.WOOD).hardnessAndResistance(0.5f), "example_block", TestItemGroups.EXAMPLE_GROUP);
	public static final List<ModBlock> LIST = new ArrayList<>(Arrays.asList(TEST_BLOCK,EXAMPLE_BLOCK));

	@SubscribeEvent
	public static void registerBlocks(final RegistryEvent.Register<Block> event)
	{
		LIST.forEach(event.getRegistry()::register);
	}

	@SubscribeEvent
	public static void registerBlockItems(final RegistryEvent.Register<Item> event)
	{
		LIST.forEach(b -> event.getRegistry().register(new BlockItem(b, new Item.Properties().group(b.getGroup())).setRegistryName(b.getRegistryName())));
	}
}


これがベストな方法かどうかは分かりませんが、こうすることで、登録するブロックが増えてもリストにさえ入れてしまえば、BlockItemの登録漏れは起きないと思います。
ちなみにブロックもアイテム同様、簡単なブロックは宣言時の設定だけで十分でしょうし、複雑なブロックであれば専用のクラスを作るといいと思います。今回で言うと、TEST_BLOCKはModBlocksの宣言のみで作成し、EXAMPLE_BLOCKはクラス(後述)を作成しました。


これで登録はできたので、アイテム同様テクスチャ等の設定を行います。

ブロックはアイテムよりも作成しなければならないjsonファイル等の数が多く、
1. lang
2. blockstates
3. models
4. textures
5. loot_tables
の5つになります。



f:id:json_fileman:20200413232022p:plain





1. まずlangから、

{
	"item.testmod.test_item": "Test Item",
	"item.testmod.example_item": "Example Item",
	"block.testmod.test_block": "Test Block",
	"block.testmod.example_block": "Example Block",
	"itemGroup.test": "Test Item Group",
	"itemGroup.example": "Example Item Group"
}

”block.MODID.registryName":登録したい名前"
で登録できます。

2. 続いてblockstates

{
    "variants":
    {
        "": { "model": "testmod:block/test_block" }
    }
}

登録したいmodelのjsonファイルのパスを打ち込みます。
単純なブロックでは、今回の例のように1つのモデルの登録で済みますが、様々な状態を持つ複雑なブロック(例えばドアなど)は非常に複雑になります。以下のサイトからアセットをダウンロードして見てもらえば分かりますが、ドアには、ドアの向き、上下のブロック、右付き左付き、開閉など多くの状態があるので、その状態の数だけモデルを登録しなければならず、複雑になります。


MC Assets - Browser for Minecraft Asset Files


3. models

これも単純なブロックの場合は簡単です。

{
    "parent": "block/cube_all",
    "textures":
    {
        "all": "testmod:block/test_block"
    }
}

cube_allのところをcube、cube_column、などに変更することでそれぞれ6面や側面と上下面に個別のテクスチャを当てられるようになります。
これもバニラのアセットを参考にするといいでしょう。

例えば、バニラのスイカjsonファイルは以下の通りです。

{
    "parent": "block/cube_column",
    "textures": {
        "end": "block/melon_top",
        "side": "block/melon_side"
    }
}


また後述のモデルクリエイターを使うことで非キューブ状のカスタムモデルを作ることもできます。


4. テクスチャーの画像を登録します。16×16のpng画像をtextures/blockに入れます。
TestBlockのテクスチャーは氷塊を暗くしたものにしました。

f:id:json_fileman:20200414001902p:plain

六面や上下面と側面で異なるテクスチャーを登録した場合には必要な分だけ画像を入れます(~_top.png, ~_side.pngなど)。


5. ルートテーブルの設定。ここは1.12以前と大きく異なった点の一つです。以前はブロックを破壊した際のドロップアイテムはjavaのクラス内で記述していましたが、1.13からはそれらをルートテーブルで管理するようになりました。jsonファイルを記述するのは面倒ですが、使いようによっては以前より複雑な処理を簡単に実装できるような気もします。ルートテーブルは
resources/data/MODID/loot_tables/blocksにjson形式で設置します。


test_block.json

{
  "type": "minecraft:block",
  "pools": [
    {
      "rolls": 1,
      "entries": [
        {
          "type": "minecraft:item",
          "name": "testmod:test_block"
        }
      ],
      "conditions": [
        {
          "condition": "minecraft:survives_explosion"
        }
      ]
    }
  ]
}


example_block.json

{
  "type": "minecraft:block",
  "pools": [
    {
      "rolls": 1,
      "entries": [
        {
          "type": "minecraft:alternatives",
          "children": [
            {
              "type": "minecraft:item",
              "conditions": [
                {
                  "condition": "minecraft:match_tool",
                  "predicate": {
                    "enchantments": [
                      {
                        "enchantment": "minecraft:silk_touch",
                        "levels": {
                          "min": 1
                        }
                      }
                    ]
                  }
                }
              ],
              "name": "testmod:example_block"
            },
            {
              "type": "minecraft:item",
              "functions": [
                {
                  "function": "minecraft:set_count",
                  "count": {
                    "min": 3.0,
                    "max": 9.0,
                    "type": "minecraft:uniform"
                  }
                },
                {
                  "function": "minecraft:apply_bonus",
                  "enchantment": "minecraft:fortune",
                  "formula": "minecraft:uniform_bonus_count",
                  "parameters": {
                    "bonusMultiplier": 1
                  }
                },
                {
                  "function": "minecraft:explosion_decay"
                }
              ],
              "name": "testmod:example_item"
            }
          ]
        }
      ]
    }
  ]
}

今回は、TestBlockはブロックそのものをドロップ、ExampleBlockはシルクタッチではそのもの、通常破壊でExampleItemをドロップするようにしています。

これも、1からjsonを書くのは大変なのでバニラのルートテーブルから近いものを探して書き換えるのがいい気がします。


これでブロックの基本的な登録は終わりです。



~Part 2~
続いて、カスタムモデルを持つブロックの登録について書いていきます。

例えば、松明、ランタン、ホッパーなどは四角のブロックではなく、それぞれ固有の形を持っています。このようなブロックを作成します。

このようなブロックは基本的にmodels/blockの中のjsonファイルを書くことで作成することが出来ます。example_blockのmodelのjsonファイルを貼っておきます。

example_block.json


かなり長いjsonファイルですが、自分で書いたわけではなくモデルを作成するのに非常に便利なModel Creatorというツールがあるので、それを使用しました。


MrCrayfish's Model Creator


プロフラムはjavaで書かれているのでマイクラが起動できるなら起動できると思います。
作業中のスクリーンショットは以下のような感じです。


f:id:json_fileman:20200413235000p:plain


16×16×16の3次元空間に、ブロックを配置できるので右側のElementのタブでブロックのサイズ、位置を、Rotationで角度を、Facesでテクスチャをそれぞれ設定します。
テクスチャは自作のものでもバニラのものでも構いません。今回は作るのが面倒だったのでバニラのガラスとレッドストーンブロックのテクスチャを使いました。自作のものを使った場合にはassetsのtexturesにpngファイルを入れます。


作成し終えたら、FileからExport JSONを選択すると、modelsに置くことができるjson形式でファイルが出力されます。

このまま使うとパーティクルが設定されていないので、壊すときに紫と黒のパーティクルが出てしまいます。なので、jsonファイルの中のtexturesの項目にパーティクルを追加してmodelsの中に入れます。

~~

"textures": {
        "green_stained_glass": "minecraft:block/green_stained_glass",
        "redstone_block": "minecraft:block/redstone_block",
        "blue_stained_glass": "minecraft:block/blue_stained_glass",
        "particle":"minecraft:block/red_stained_glass"
    },

~~

続いて、javaでブロッククラスの設定をしていきます。

public class ExampleBlock extends ModBlock
{
	private static final VoxelShape SHAPE1 = Block.makeCuboidShape(3.0D, 0.0D, 3.0D, 13.0D, 1.0D, 13.0D);
	private static final VoxelShape SHAPE2 = Block.makeCuboidShape(3.0D, 5.0D, 4.0D, 13.0D, 6.0D, 13.0D);
	private static final VoxelShape SHAPE3 = Block.makeCuboidShape(7.0D, 0.0D, 7.0D, 9.0D, 8.0D, 9.0D);

	public ExampleBlock(Properties properties, String name, ItemGroup group)
	{
		super(properties, name, group);
	}

	@Override
	public VoxelShape getShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context)
	{
		return VoxelShapes.or(VoxelShapes.or(SHAPE1, SHAPE2),SHAPE3);
	}

	@Override
	public VoxelShape getCollisionShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context)
	{
		return VoxelShapes.empty();
	}
}

ここも1.12以前と変わっていて、以前はこのようなカスタムモデルのブロックに対しては
isOpaqueCube() や isFullCube() をfalseに設定していました。
しかし、1.13以降ではこれらのメソッドは削除され、代わりにget~Shape()が追加されています。


VoxelShapeは先程のModelCreatorで使っていたような16×16×16の3次元空間に存在する直方体を表します。Block.makeCuboidShape(x1, y1, z1, x2, y2, z2)で定義できて、引数のx1~z2はそれぞれ16×16×16の三次元空間における直方体の

x1:始点x座標
y1:始点y座標
z1:始点z座標
x2:終点x座標
y2:終点y座標
z2:終点z座標

を表します。

これらの直方体は
VoxelShapes.or(Shape1, Shape2);
で組み合わせることができます。

またカーソルの範囲とあたり判定の範囲は個別に設定することもできて、
getCollisionShapeを設定するとgetShapeで設定した判定とは別に当たり判定用のshapeを設定できます。今回は、当たり判定を無くすように設定しています。

shapeに関するより詳しいことは、以下のサイトに書いてありました。

Minecraft Modding: Block Shapes (VoxelShapes) [1.14.4+]


以上を設定して起動すると、画像のようになります。


f:id:json_fileman:20200414001947p:plain

カスタムモデルが反映されていて、当たり判定はなくなっています。




次回は、ストラクチャーブロックで保存したnbtを使った構造物の生成について書こうと思います。

3日目 アイテムの追加とアイテムグループの追加 (追記あり)

今回はアイテムの登録と、アイテムグループの登録を行いたいと思います。
アイテムはインゴットのような、それ単体では何もできない無機能アイテムと、エンダーパールのように消費することで何かしらの効果を発揮する機能を持ったアイテムをついかしようと思います。

またアイテムグループは、1.12までのクリエイティブタグに相当するものでクリエイティブモードの時や、アイテムを検索するときの分類に使われます。
(20200511 アイテムグループについて追記・修正しました。記事の最後辺り参照。)


まずはアイテムを登録するための、ModItemsというクラスを作成します。
私は、このクラス内で宣言と登録を一緒に行いたいのでEventBusSubscriberアノテーションをつけています。
Registerのようなクラスを作って、そちらでまとめて登録してもいいと思います。どちらがベターなのかはわかりません。

package testmod;

import net.minecraft.item.Item;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@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));

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

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


無機能アイテムの場合、作成は簡単でModItemsの中で宣言してItemをnewするだけで作成できます。(11行目 TEST_ITEM)
1.13からItemのコンストラクタが引数にProperties をとるようになったので、これもnewで作成します。
ここのgroupの引数にアイテムグループを入れます。今回は自分で作成したTEST_GROUPを使用しています。
アイテムグループの登録については、この記事の後ろのほうに書いておきます。
既存のアイテムグループを使用する場合には、net.minecraft.item.ItemGroupをインポートしてItemGroup.MISCのように打ち込めば大丈夫です。

続いて、もう一つ宣言しているEXAMPLE_ITEMは自分で作成したExampleItemクラスをnewしています。
ExampleItemの中身は後述します。
またこちらのアイテムはEXAMPLE_GROUPという、これも自分で作成したアイテムグループに登録します。


次に宣言したアイテムを登録します。
registerItemsメソッドに@SubscribeEventアノテーションをつけます。
このアノテーションをつけたメソッドは引数の型があっていればメソッド名は任意だったような気がします。

registerAllに入れる前に.setRegistryNameで名前を登録しておかないとエラーが出ます。


アイテムの登録は以上です。

ExampleItemの中身を書いておきます。

package testmod;

import java.util.Random;

import net.minecraft.entity.EntityType;
import net.minecraft.entity.item.ItemEntity;
import net.minecraft.entity.monster.HuskEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUseContext;
import net.minecraft.item.Items;
import net.minecraft.util.ActionResult;
import net.minecraft.util.ActionResultType;
import net.minecraft.util.Hand;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvents;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;

public class ExampleItem extends Item
{
	public ExampleItem(Properties properties)
	{
		super(properties);
	}

	@Override
	public ActionResultType onItemUse(ItemUseContext context)
	{
		World world = context.getWorld();
		if (!world.isRemote)
		{
			ItemStack itemstack = context.getItem();
			Vec3d vec3d = context.getHitVec();
			double x = vec3d.getX() + context.getFace().getXOffset()*0.5D;
			double y = vec3d.getY();
			double z = vec3d.getZ() + context.getFace().getZOffset()*0.5D;
			HuskEntity entity = new HuskEntity(EntityType.HUSK, world);

			entity.setPosition(x,y,z);
			world.addEntity(entity);
			itemstack.shrink(1);
		}

		return ActionResultType.SUCCESS;
	}

	@Override
	public ActionResult<ItemStack> onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn)
	{
		ItemStack itemstack = playerIn.getHeldItem(handIn);
		Random rand = worldIn.rand;

		if(!worldIn.isRemote)
		{
			ItemStack stack = new ItemStack(Items.BLUE_WOOL);
			double x = playerIn.getPositionVec().getX();
			double y = playerIn.getPositionVec().getY();
			double z = playerIn.getPositionVec().getZ();
			x = rand.nextInt(100) > 50 ? x+rand.nextInt(5)+5 : x+rand.nextInt(5)-10;
			y = y +rand.nextInt(5)+5;
			z = rand.nextInt(100) > 50 ? z+rand.nextInt(5)+5 : z+rand.nextInt(5)-10;
			ItemEntity entity = new ItemEntity(worldIn, x, y, z, stack);

			worldIn.playSound(null,playerIn.getPosition(), SoundEvents.ENTITY_DRAGON_FIREBALL_EXPLODE, SoundCategory.NEUTRAL, 1.0F, 1.0F);
			worldIn.addEntity(entity);

			itemstack.shrink(1);
		}

		return ActionResult.func_226248_a_(playerIn.getHeldItem(handIn));

	}
}

テスト用で作っただけなので機能は適当ですが、ブロックに向かって右クリックするとハスクをスポーンさせます。
空中に向かって (ブロックが選択されない状況で) 右クリックをすると、爆発音がして自分の周りのどこかに青の羊毛が落ちます。

この例でわかるように、onItemUseはブロックに向かって使ったときの機能です。引数のcontextからget~()でworld等をとってこれます。
同様に、onItemRightClickは空中に向かって使ったときの機能です。こちらは引数にworldやplayerがあるので必要に応じて使えばいいと思います。



アイテムグループの登録について書いておきます。
アイテムグループの登録は1.13以降だとかなり簡潔になっています。
基本的な形を書いておきます。

package testmod;

import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;

public class TestItemGroup extends ItemGroup
{
	public static final ItemGroup TEST_GROUP = new TestItemGroup();

	public TestItemGroup() 
	{
		super("Item Group Name");
	}

	@Override
	public ItemStack createIcon() 
	{
		return new ItemStack(Items.DIAMOND);
	}
}


createIconはタグに表示するアイコンをItemStackの形で渡します。
今回は、2つのアイテムグループを登録したかったので実際には次のクラスを使いました。

package testmod;

import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemStack;

public class TestItemGroups extends ItemGroup
{
	private static final int TEST_ID = 0;
	private static final int EXAMPLE_ID = 1;

	public static final ItemGroup TEST_GROUP = new TestItemGroups("test", TEST_ID);
	public static final ItemGroup EXAMPLE_GROUP = new TestItemGroups("example", EXAMPLE_ID);

	private int id;

	public TestItemGroups(String name, int id)
	{
		super(name);
		this.id = id;
	}

	@Override
	public ItemStack createIcon()
	{
		switch(id)
		{
		case TEST_ID:
			return new ItemStack(ModItems.TEST_ITEM);
		case EXAMPLE_ID:
			return new ItemStack(ModItems.EXAMPLE_ITEM);
		default:
				return null;
		}
	}
}


すこしややこしくなりますが、こうすればこのクラス内でいくつでも新しいアイテムグループを登録できます。
ちなみにより簡潔に書くために、コンストラクタの引数を(String neme, ItemStack icon)として、宣言時に("test", new ItemStack(ModItems.TEST_ITEM))としてみたのですがItemStackのnewがModItemsの登録より先に実行されるみたいで起動はできましたがアイコンが表示されませんでした (バニラの既存のアイテムは可能です)。
そのため、switch文をつかって、createIconメソッドの中身でItemStackをnewしています。


これらのクラスがあればエラーは起きずに実行できると思いますが、テクスチャ等を登録していないので紫と黒の変なアイテムが変な名前で登録されていると思います。
なので、テクスチャや言語ファイルを作成します。


f:id:json_fileman:20200401223232p:plain


ファイルの構成は画像のようにします。
/src/main/resources 以下に assets/MOD_ID (今回の場合はtestmod)、その下にlang、models、texturesなど必要なパッケージを作成します。

上から順に見ていきます。まずlangから。
1.12までは言語ファイルは.langだったのですが、1.13以降はすべてjsonファイルになりました。
英語の場合はen_us.json、日本語はja_jp.jsonで登録します。その他の言語もファイルを作れば登録できますが、通常は英語と日本語で十分だと思います。
英語の方のみ載せます。


en_us.json

{
	"item.testmod.test_item": "Test Item",
	"item.testmod.example_item": "Example Item",

	"itemGroup.test": "Test Item Group",
	"itemGroup.example": "Example Item Group"
}

アイテムの名前は
"item.MOD_ID.registryName" : "表示したい名前"
アイテムグループは
"itemGroup.registryName": "表示したい名前"
になります。
jsonファイルは一カ所でも間違えていると(カンマの打ち忘れ、消し忘れなどよくやってしまいます)、読み込まれないので以下のサイトで正しいjsonファイルかどうかをチェックしてから保存するといいと思います。

JSONLint - The JSON Validator


続いてmodels/itemの中身です。
シンプルなアイテムの場合は以下の通りになります。

{
    "parent": "item/generated",
    "textures":
    {
        "layer0": "testmod:item/test_item"
    }
}

layer0にMOD_id:item/テクスチャのファイル名
を書けば大丈夫です。
もし複雑な (例えば時計のようにインベントリ内でテクスチャが変化するような) ものを作りたい場合は、以下のサイトからバニラのjsonファイルを参考にすればいいと思います。

MC Assets - Browser for Minecraft Asset Files


最後にテクスチャですが、これは16×16の背景を透過させたpngファイルをmodelsで指定したファイル名でtextures/itemに入れれば大丈夫です。
複雑なことをしないのであれば、名前はregistryNameと同じにしておくと安心だと思います。
今回はペイントで適当に作ったものをテクスチャにしました。

f:id:json_fileman:20200401230046p:plain
f:id:json_fileman:20200401230100p:plain


以上すべてを作成し、起動すると以下のようになります。

f:id:json_fileman:20200401231052p:plain


次回は、ブロックの追加をしたいと思います。


(2020年5月11日 追記)
アイテムグループについて。

本記事で、コンストラクターでItemStackを渡すと、アイテム登録のタイミングより先にItemGroupのコンストラクターが呼ばれてしまうため、固有のidとswitch-caseで実装していたのですが、関数型インターフェースのSupplierを使うことで簡潔にできました。

public class TestItemGroups extends ItemGroup
{
	public static final ItemGroup TEST_GROUP = new TestItemGroups("test", () -> new ItemStack(ModItems.TEST_ITEM));
	public static final ItemGroup EXAMPLE_GROUP = new TestItemGroups("example", () -> new ItemStack(ModItems.EXAMPLE_ITEM));

	private String name;
	private Supplier<ItemStack> itemStackSupplier;

	public TestItemGroups(String name, Supplier<ItemStack> supplier)
	{
		super(name);
		this.name = name;
		this.itemStackSupplier = supplier;
	}

	@Override
	public ItemStack createIcon()
	{
		return itemStackSupplier.get();
	}

	@Override
	public String toString()
	{
		return this.name;
	}
}

Supplierは一般的に「遅延評価」に用いられるようで、今回の場合でもItemStackのnewされるタイミングをcreateIcon()が呼ばれるまで遅らせています。
したがって、ItemStackの生成がアイテム登録より後のタイミングになり、無事にアイコンが登録できます。

1.13以降、バニラやForgeのコードの所々に関数型インターフェースやラムダ式が使われており、例えば階段ブロックのコンストラクターにもSupplierが使われています。関数型やラムダ式については、まだ分からない部分も多いので少しずつ勉強していこうと思います。

2日目 構造物の生成

ちょっと前回の予定と変わってしまったのですが、せっかく調べたのに忘れてしまいそうなのでいろいろ飛ばして構造物の生成について書いておきたいと思います。

ここでいう構造物はIFeatureConfigを継承したもので、バニラでは海底神殿、ネザー要塞のような大きな物から、かぼちゃやスイカのような小さな物まで全てFeatureとして登録されています。バニラのFeature.classを見るとすべての構造物がわかります。

このような構造物は1.12以前ではWorldGeneratorを使って生成することが多かったと思いますが、1.13以降ではWorldGeneratorが削除されています。
代わりにBiome.addFeatureで似たようなことができるみたいです。ちなみにまだちゃんと調べていないので詳しいやり方等はわからないのですが、ブロックの集合体だけではなく地下の洞窟なんかも生成できるみたいです。参考にしたリンクを張っておきます。

MC 1.13 Worldgen · GitHub

さて、このaddFeatureなのですが実は1.14と1.15でも微妙に仕様が変更されています。ここで少し戸惑ったので忘れないうちに書いておきます。
以下、具体的なコードを記述していきます。

今回は、例として"樹氷 (Ice Spike) "を色んな場所に生成したいと思います。

まず構造物の内容を記述します。

今回は樹氷をほぼそのまま使うので樹氷が継承しているFeatureを継承した"ModIceSpike"というクラスを作成しました。
コンストラクタはsuperにそのまま引数を渡して、boolean型のplaceメソッドをオーバーライドします。このplaceの中にどのような構造物にするかを記述します。今回は樹氷のplaceをほぼそのままコピペしました。

package testmod;

import java.util.Random;
import java.util.function.Function;

import com.mojang.datafixers.Dynamic;

import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.IWorld;
import net.minecraft.world.gen.ChunkGenerator;
import net.minecraft.world.gen.GenerationSettings;
import net.minecraft.world.gen.feature.Feature;
import net.minecraft.world.gen.feature.NoFeatureConfig;

public class ModIceSpike extends Feature<NoFeatureConfig>
{

	public ModIceSpike(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public boolean place(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generator, Random rand, BlockPos pos, NoFeatureConfig config)
	{
		while(worldIn.isAirBlock(pos) && pos.getY() > 2) {
			pos = pos.down();
		}

		if (worldIn.getBlockState(pos).getBlock() == Blocks.SNOW_BLOCK) {
			return false;
		} else {
			pos = pos.up(rand.nextInt(4));
			int i = rand.nextInt(4) + 7;
			int j = i / 4 + rand.nextInt(2);
			if (j > 1 && rand.nextInt(60) == 0) {
				pos = pos.up(10 + rand.nextInt(30));
			}

			for(int k = 0; k < i; ++k) {
				float f = (1.0F - (float)k / (float)i) * (float)j;
				int l = MathHelper.ceil(f);

				for(int i1 = -l; i1 <= l; ++i1) {
					float f1 = (float)MathHelper.abs(i1) - 0.25F;

					for(int j1 = -l; j1 <= l; ++j1) {
						float f2 = (float)MathHelper.abs(j1) - 0.25F;
						if ((i1 == 0 && j1 == 0 || !(f1 * f1 + f2 * f2 > f * f)) && (i1 != -l && i1 != l && j1 != -l && j1 != l || !(rand.nextFloat() > 0.75F))) {
							BlockState blockstate = worldIn.getBlockState(pos.add(i1, k, j1));
							Block block = blockstate.getBlock();
							if (blockstate.isAir(worldIn, pos.add(i1, k, j1)) || func_227250_b_(block) || block == Blocks.SNOW_BLOCK || block == Blocks.ICE) {
								this.setBlockState(worldIn, pos.add(i1, k, j1), Blocks.PACKED_ICE.getDefaultState());
							}

							if (k != 0 && l > 1) {
								blockstate = worldIn.getBlockState(pos.add(i1, -k, j1));
								block = blockstate.getBlock();
								if (blockstate.isAir(worldIn, pos.add(i1, -k, j1)) || func_227250_b_(block) || block == Blocks.SNOW_BLOCK || block == Blocks.ICE) {
									this.setBlockState(worldIn, pos.add(i1, -k, j1), Blocks.PACKED_ICE.getDefaultState());
								}
							}
						}
					}
				}
			}

			int k1 = j - 1;
			if (k1 < 0) {
				k1 = 0;
			} else if (k1 > 1) {
				k1 = 1;
			}

			for(int l1 = -k1; l1 <= k1; ++l1) {
				for(int i2 = -k1; i2 <= k1; ++i2) {
					BlockPos blockpos = pos.add(l1, -1, i2);
					int j2 = 50;
					if (Math.abs(l1) == 1 && Math.abs(i2) == 1) {
						j2 = rand.nextInt(5);
					}

					while(blockpos.getY() > 50) {
						BlockState blockstate1 = worldIn.getBlockState(blockpos);
						Block block1 = blockstate1.getBlock();
						if (!blockstate1.isAir(worldIn, blockpos) && !func_227250_b_(block1) && block1 != Blocks.SNOW_BLOCK && block1 != Blocks.ICE && block1 != Blocks.PACKED_ICE) {
							break;
						}

						this.setBlockState(worldIn, blockpos, Blocks.PACKED_ICE.getDefaultState());
						blockpos = blockpos.down();
						--j2;
						if (j2 <= 0) {
							blockpos = blockpos.down(rand.nextInt(5) + 1);
							j2 = rand.nextInt(5);
						}
					}
				}
			}

			return true;
		}
	}
}

34行目の != SNOW_BLOCKを == SNOW_BLOCKに変更しました。ここを変更しておかないと結局雪の上にしか樹氷が生成されずaddFeatureが動いたかどうかが分からなくなってしまうので、それを避けるためです。




続いてこの構造物をForgeに登録します。
これはアイテム等とほぼ同じで、登録用のクラスの先頭に"@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)"をつけて、さらに登録用のメソッドの先頭に"@SubscribeEvent"をつけ、RegistryEventsから登録します (まだアイテム登録の記事を書いていませんが...)。

package testmod;

import net.minecraft.world.gen.feature.Feature;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class RegistryEvents
{
	public static final ModIceSpike MOD_ICE_SPIKE = new ModIceSpike(NoFeatureConfig::deserialize);

	@SubscribeEvent
	public static void onItemsRegistry(final RegistryEvent.Register<Feature<?>> event)
	{
		MOD_ICE_SPIKE.setRegistryName("mod_ice_spike");
		
		event.getRegistry().register(MOD_ICE_SPIKE);
	}

}

また構造物に限らず複数を登録するときは、registerの代わりにregisterAllを使ってカンマで区切ることで同時に複数のものを登録できます。


ここまでで登録はできましたが、このままではワールドに生成されないので生成のためのコードを記述します。
ここの一部が1.14と1.15で変わっているので注意が必要です。


まず、生成のためのメソッドをもつクラス、WorldGenManagerを作成します。

package testmod;

import net.minecraft.world.biome.Biome;
import net.minecraft.world.gen.GenerationStage.Decoration;
import net.minecraft.world.gen.feature.IFeatureConfig;
import net.minecraft.world.gen.placement.CountRangeConfig;
import net.minecraft.world.gen.placement.FrequencyConfig;
import net.minecraft.world.gen.placement.Placement;
import net.minecraftforge.registries.ForgeRegistries;

public class WorldGenManager
{
	public WorldGenManager() {}

	public void genSetup()
	{
		for(Biome biome : ForgeRegistries.BIOMES)
		{
			biome.addFeature(Decoration.TOP_LAYER_MODIFICATION, 
					 RegistryEvents.MOD_ICE_SPIKE
					.func_225566_b_(IFeatureConfig.NO_FEATURE_CONFIG)
					.func_227228_a_(Placement.COUNT_TOP_SOLID
					.func_227446_a_(new FrequencyConfig(1))));
		}
	}
}


ちなみに1.14および1.13では

public void genSetup()
{
	for(Biome biome : ForgeRegistries.BIOMES)
	{
		biome.addFeature(
				Decoration.TOP_LAYER_MODIFICATION, 
				Biome.createDecoratedFeature(
				RegistryEvents.MOD_ICE_SPIKE, 
				IFeatureConfig.NO_FEATURE_CONFIG, 
				Placement.COUNT_TOP_SOLID, 
				new FrequencyConfig(1)));
	}
	
	
}

となります。
addFeatureの第一引数はどちらも同じなのですが、第二引数が変わっています。
1.15からBiome.createDecoratedFeatureが削除されているようでFeatureのメソッドを連ねて第二引数にするようです。
また、Forgeが開発途中であるせいなのかメソッド名が番号となっていて分かりづらいです。

Decoration、Placement、Configは今回使ったもの以外にもたくさんあり、また自分で作ることもできるようなので目的に応じて使い分けてください。
とは言っても私はまだ使用がよくわかっていないので、もし詳しい方がいたら教えてほしいです。

今回は詳しくは書きませんが(まだ試していないので)、addFeatureは鉱石の生成にも使うことができます。
コードだけ置いておきます。
1.14

     biomeIn.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, Biome.createDecoratedFeature(Feature.ORE, new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE, Blocks.COAL_ORE.getDefaultState(), 17), Placement.COUNT_RANGE, new CountRangeConfig(20, 0, 0, 128)));

1.15

     biomeIn.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, Feature.ORE.func_225566_b_(new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE, field_226791_az_, 17)).func_227228_a_(Placement.COUNT_RANGE.func_227446_a_(new CountRangeConfig(20, 0, 0, 128))));


少し話がそれましたが、構造物の生成に戻ります。
最後にメインファイルのsetupメソッドでgenSetupを呼び出します。

package testmod;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

@Mod(TestMod.MODID)
public class TestMod
{
	public static TestMod instance;
	public static final String MODID = "testmod";
	public static final WorldGenManager WORLD_GEN = new WorldGenManager();
	public static final Logger LOGGER = LogManager.getLogger(MODID);

	public TestMod()
	{
		instance = this;


		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);


		MinecraftForge.EVENT_BUS.register(this);
	}

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

	private void clientRegistries(final FMLClientSetupEvent event)
	{

		LOGGER.info("clientRegistries method registered.");
	}

}

これで起動してみます。


f:id:json_fileman:20200317024030p:plain


無事、樹氷が生成されました。
ちなみに生成頻度はFrequencyConfig(int)で変えられるのですが1でもかなり多くの数の樹氷が生成されてしまっています。

なので正しい方法かどうかはわかりませんが、ModIceSpikeの34行目のif分に条件をたして
if (worldIn.getBlockState(pos).getBlock() == Blocks.SNOW_BLOCK || rand.nextInt(100) < X)
nextIntがX未満の時は生成しないようにしました。
X = 50とX = 98の場合の画像を添付します。




f:id:json_fileman:20200317024048p:plain
X = 50



f:id:json_fileman:20200317024114p:plain
X = 98




このようにすればnextIntとXの値次第である程度任意に生成頻度を変えられるのではないでしょうか。
ただベストなやり方ではないような気がするので、詳しい方がおられましたら、これに関しても教えてほしいです。

次回からは元々予定していた通り、アイテムの追加、続いてブロックの追加等を行っていきたいと思います。

1日目 Modメインファイル(Forge1.15.2)

前回(約1年前)の記事を書いたときは1.13.2がforgeの最新版だったので環境設定は1.13.2になっていますが1.15.2でも同様の手順で出来ました。

とりあえず今回からは現時点(2020/3/15)で最新の1.15.2に関して更新していこうと思います。
前回までで環境設定が終わったので、今回はModのメインファイルを作っていこうと思います。

まず/src/main/javaにデフォルトで入っているパッケージおよびファイルを削除して、そこに新たにパッケージを作成して、その中にメインとなるクラスを作成します。
今回の場合はtestmodというパッケージにTestMod.javaを作成しています。

f:id:json_fileman:20200315111210p:plain


続いて、TestMod.javaの中身を記述します。

package testmod;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

@Mod(TestMod.MOD_ID)
public class TestMod
{
	public static TestMod instance;
	public static final String MOD_ID = "testmod";
	public static final Logger LOGGER = LogManager.getLogger();

	public TestMod()
	{
		instance = this;

		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);

		MinecraftForge.EVENT_BUS.register(this);
	}

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

	private void clientRegistries(final FMLClientSetupEvent event)
	{
		LOGGER.info("clientRegistries method");
	}
}


1.12以前と1.13以降でこの辺りが結構変わってますね。
setupメソッドが以前のpreinitに当たるらしいです。で、clienetRegistriesがクライアント側のみの処理なので以前のproxy等が必要なくなったようです。

とは言っても、このへん詳しいことは正直よくわかっていません。
詳しくわかったら、また追記するかもしれませんが、それまでは"おまじない"状態です。



これでメインのクラスの最低限の記述は終わったのですが、このまま起動するとエラーが出ます。
/src/main/resources/META-INFに入っているmods.tomlにmodの情報を記載する必要があります。

# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here:  https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="[31,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# A URL to refer people to when problems occur with this mod
issueTrackerURL="http://my.issue.tracker/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="testmod" #mandatory
# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
version="0.1" #mandatory
 # A display name for the mod
displayName="Test Mod" #mandatory
# A URL to query for updates for this mod. See the JSON update specification <here>
updateJSONURL="http://myurl.me/" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
displayURL="http://example.com/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
credits="Thanks for this example mod goes to Java" #optional
# A text field displayed in the mod UI
authors="Love, Cheese and small house plants" #optional
# The description text for the mod (multi line!) (#mandatory)
description='''
This is a long form description of the mod. You can write whatever you want here

全部のせると長くなるので前半を抜粋しました。

とりあえず15行目のmodidだけ変更して自分のmodidと一致していればエラーは出ないと思います。今回はそのほかに17行目のバージョン、19行目の表示名を変更しました。
#から始まっている行はコメントなので消しても問題ありません。また行末に#optinalとなっている行も消して大丈夫です。


これで実行してみてmodの欄に自分のmodが表示されていればうまく動いてるということになります。


f:id:json_fileman:20200315114141p:plain


次回は無機能アイテムの追加をしたいと思います。