初心者modderの備忘録

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

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が使われています。関数型やラムダ式については、まだ分からない部分も多いので少しずつ勉強していこうと思います。