このウェブページには広告が含まれています。

MOD 作り小技集・マイクラJava版

マインクラフト

この記事はマインクラフト(マイクラ)Java版で使う Fabric を前提とした MOD を作る際に使える小技を集めたものです。

MOD を作ろうとすると、ちょっとした機能を実現するにも、他の人が作った MOD のソースコードを読んだりして、その方法を調べなければなりません。これは結構時間のかかる作業です。

そこで、私自身が MOD を作る際に調べたことをこの記事にまとめておきます。系統だった解説ではなく、脈絡のない知識の寄せ集めです。個人的な備忘録のようなものですが、何かの拍子に MOD を作る誰かの役に立てばいいな、というつもりで書いています。

次の記事で MOD 作りの初心者向けの解説をしているので、必要な場合は参考にしてください。

この記事の内容は、Windows 上で Fabric を前提とした MOD を作ることを想定しています。しかし、Java によるプログラミングでは、OS による違いはあまりないと思います。対象となるマイクラのバージョンは JE 1.21.3 です。

なお、この記事で示されているコードは、あくまでどのように機能を実現するかを示すだけで、例外などが考慮されていません。実装を行う際は状況に応じて例外処理を行ってください。

ワールドの名前

シングルプレイの場合はワールドのセーブ名を、次のコードで読み出すことができます。

String string = MinecraftClient.getInstance().getServer().getOverworld().toString();

string の中には

ServerLevel[WorldName]

という形で WorldName が記録されるので、[] で囲まれた部分を取り出します。

なお、マルチプレイでは getServer() = null となるので、この方法は使えません。

サーバーの URL

マルチプレイの場合は、ワールドのセーブ名の代りにサーバーの URL でプレイしているワールドを識別することができます。

String string = MinecraftClient.getInstance()
.getNetworkHandler().getConnection().getAddress().toString();

上のコードで string に URL を含む文字列が得られるので、そこから URL を取り出します。

ワールドの時刻と月齢

ワールド内の時刻と月齢は次のコードで取得できます。

int  moonPhase = MinecraftClient.getInstance().world.getMoonPhase();
long timeOfDay = MinecraftClient.getInstance().world.getTimeOfDay();
long hour = timeOfDay / 1000L % 24 + 6L;

時刻はゲーム内の朝6時をゼロとして、現実の 20分の1秒 を単位とする Tick で測られます。1000 Tick で1時間、1日24時間は 24000 Tick です。これは、現実時間で 1200秒 = 20分 です。

バイオーム、明るさ、ブロック名

ワールドの任意のブロックでの情報は、ブロック単位の座標 blockPos を与えると得ることができます。バイオームや明るさ、ブロック名は

String biome   = MinecraftClient.getInstance().world.getBiome(blockPos).getIdAsString();
int blockLight = MinecraftClient.getInstance().world.getLightLevel(LightType.BLOCK, blockPos);
int skyLight = MinecraftClient.getInstance().world.getLightLevel(LightType.SKY, blockPos);
BlockState blockState = MinecraftClient.getInstance().world.getBlockState(blockPos);
String blockName = blockState.getBlock().toString();

といったコードで得られます。

プレイヤーがいるディメンション

私は今のところ次のようにしていますが、たぶんもっとカッコいい方法があります。

String string = MinecraftClient.getInstance().world.getDimensionEntry().getIdAsString();

とすると、string に

minecraft:overworld
minecraft:the_nether
minecraft:the_end

のうちのいずれかが入ります。この : の後の5文字目が、w、n、e のどれであるかでディメンションを判別します。

プレイヤーのリスポーン地点

プレイヤーのリスポーン地点の情報はサーバー側にあるので、マルチプレイの場合、クライアント側では見ることができません。シングルプレイの場合は、ローカルにサーバーが立つので、そこから読み出すことができます。

ClientPlayerEntity clientPlayer = MinecraftClient.getInstance().player;
ServerPlayerEntity serverPlayer = MinecraftClient.getInstance().getServer()
.getPlayerManager().getPlayer(clientPlayer.getUuid());
TeleportTarget target = serverPlayer.getRespawnTarget(serverPlayer.isAlive(), null);
Vec3d vec3d = target.position();
String string = target.world().getDimension().effects().toString();

上のコードで vec3d にプレイヤーの位置が、string にプレイヤーがいるディメンション名が入ります。string からディメンションを判別する方法は プレイヤーがいるディメンション の項目と同じです。

なお、マルチプレイでは getServer() = null となります。

プレイヤーの死亡地点

プレイヤーが最近に死亡した場所についての情報はクライアントから

Optional<GlobalPos> lastDeathPos = MinecraftClient.getInstance().player.getLastDeathPos();
if (!lastDeathPos.isEmpty()) {
BlockPos blockPos = lastDeathPos.get().pos();
String string = lastDeathPos.get().dimension().toString();
}

で取り出すことができます。上のコードでは、blockPos に死亡地点のブロック単位での座標、string にディメンションの情報が入ります。string は

ResourceKey[minecraft:dimension / minecraft:overworld]

という形になっているので、後半の minecraft:overworld の部分を プレイヤーがいるディメンション の項目と同様に処理します。

プレイ中のワールドで過去に一度も死亡していない場合は、lastDeathPos.isEmpty() = true となります。

プレイヤーのステータス効果

ポーションの効果のような、プレイヤーのステータス効果はコレクションに入った状態で取り出せます。

Collection<StatusEffectInstance> statusEffects
= MinecraftClient.getInstance().player.getStatusEffects();
Object[] effectArray = statusEffects.toArray();
for (int i = 0; i < effectArray.length; i++) {
int duration = ((StatusEffectInstance) effectArray[i]).getDuration();
String string = ((StatusEffectInstance) effectArray[i]).getTranslationKey();
}

Duration はマイクラの Tick 単位で測られるので、秒に直すには duration/20 とします。string には、ローカル言語に翻訳される前の効果名が effect.minecraft.night_vision といった形式で入ります。

ターゲットされたブロック

プレイヤーがターゲットをとっているブロックは HUD で強調表示されるので、その情報はマイクラのシステムにあるはずです。しかし、どうやって読み出せばよいのか分からないため、私は今のところ下のようにしています。

ClientPlayerEntity player = MinecraftClient.getInstance().player;
double range = player.getBlockInteractionRange();
HitResult hitResult = player.raycast(range, 0F, false);
if (hitResult != null) {
if (hitResult.getType().equals(HitResult.Type.BLOCK)) {
BlockPos blockPos = ((BlockHitResult) hitResult).getBlockPos();
}
}

プレイヤーから射線を放ち、ヒットしたブロックの座標を得る、という仕掛けになっています。

チャットを送る

チャットを送るには以下のメソッドを使います。

MinecraftClient.getInstance().player.networkHandler.sendChatMessage(string);

このメソッドとは別に、いかにもそれらしい名前の player.sendMessage というメソッドがあるのですが、これは MOD からプレイヤーへのメッセージを表示するだけで、サーバーにはチャットを送りません。

チャットを受け取る

チャットにはシステムから送られるものとプレイヤーから送られるものの2種類があります。これらのチャットをクライアントが受信すると、それぞれに対応したイベントが発生します。これらのチャットの内容を知るには、MOD の初期化をする部分で次のようにイベントハンドラーを登録します。

ClientReceiveMessageEvents.GAME.register(this::readSystemMessage);
ClientReceiveMessageEvents.CHAT.register(this::readPlayerMessage);

これらのハンドラーは次の引数を取ります。

void readSystemMessage(Text text, boolean b)
void readPlayerMessage(Text text, SignedMessage signedMessage, GameProfile gameProfile,
MessageType.Parameters parameters, Instant instant)

クリップボードにコピーする

OS のクリップボードに文字列をコピーする場合、普通は java.awt.Toolkit などを使うのですが、これはマイクラではエラーを起こします。マイクラは下のように、独自にクリップボードにアクセスする方法を提供しています。

MinecraftClient.getInstance().keyboard.setClipboard(string);

キーを割り当てる

ゲーム中にキーを押したときに何らかの機能を起動するには、MOD の初期化時にキーの割り当てを行います。初期化を行うクラスなどで

public static KeyBinding KEY_BINDING;

と定義しておきます。何らかの機能を J キー に割り当てる場合は初期化のメソッド内で

KEY_BINDING = KeyBindingHelper.registerKeyBinding(
new KeyBinding("Invoke", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_J, "My MOD"));

とします。キーが押されたかどうかは

KEY_BINDING.wasPressed();

で判定することができます。機能を起動するメソッドを定期的に呼び出しておき、キーが押されたときに起動するようにします。

HUD を表示する

HUD に情報を追加表示するには、MOD を初期化する部分で描画用のイベントハンドラーを登録しておきます。

HudRenderCallback.EVENT.register(this::render);

テキストを表示するためのハンドラーは次のようになります。

void render(DrawContext drawContext, RenderTickCounter renderTickCounter) {
TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
String string = "Hello, World!";
int x = 0; int y = 0;
int color = 0xFFFFFFFF;
drawContext.drawText(textRenderer, string, x, y, color, false);
}

スクリーンを表示する

スクリーンを開くには、スクリーンのインスタンスを

MinecraftClient.getInstance().setScreen(new MyScreen(Text.of("My Screen")));

でクライアントに登録します。null を登録するとスクリーンが閉じます。MyScreen は Screen クラスの継承クラスです。null を登録するほか、親クラスの close メソッドを呼ぶことでもスクリーンを閉じることができます。

ゲーム中に何らかの条件でスクリーンを表示させるには、MOD の初期化時に、次のイベントを追加します。

ClientTickEvents.END_CLIENT_TICK.register(MyScreen::open);

このイベントはゲームの時間単位である Tick の終了時に発生します。open メソッドの中にスクリーンを開く条件を記述します。

例えば、何らかのキーを押したときにスクリーンを開くには、open メソッドを次のようにします。

public static void open(MinecraftClient minecraftClient) {
while (MyMODClient.KEY_BINDING.wasPressed()) {
minecraftClient.setScreen(new MyScreen(Text.of("My Screen")));
}
}

スクリーンのサイズが変わった場合は init メソッド、描画には render メソッドが呼ばれるので、これらのオーバーライドを記述します。 render メソッドの引数は

void render(DrawContext drawContext, int mouseX, int mouseY, float delta)

となっているので、drawContext を使ってスクリーンの描画をします。

外部のウェブサイトを開く

スクリーンからは次のコードで外部のウェブサイトを開くための確認画面を開くことができます。

ConfirmLinkScreen.open(this, "https://somewhere.net");

これをスクリーンのボタンなどに割り当てておきます。この画面で、リンクを信頼して開く、という選択をすると、規定に設定したウェブブラウザでウェブサイトが開きます。

描画のクリッピングとスケール

DrawContext には、描画の範囲を制限する、描画の大きさを変える、といった機能があります。クリッピングは scissor で、スケールは matrix で行います。

drawContext.enableScissor(x1, y1, x2, y2);
MatrixStack matrixStack = drawContext.getMatrices();
matrixStack.push();
matrixStack.scale(0.5F, 0.5F, 1F);
// Draw something.
matrixStack.pop();
drawContext.disableScissor();

ここでの matrix は3次元のものですが、画面の描画では2次元分しか意味がないので、scale(0.5F, 0.5F, 1F) というように3番目のスケールを1とします。

テクスチャーの描画

テクスチャーを描画するには、まず、テクスチャーの画像ファイルに対する Identifier を設定します。MOD に独自のテクスチャーは src/main/resources/assets/my-mod フォルダーに置きます。このフォルダーとファイル名を次のように指定します。

Identifier texture = Identifier.of("my-mod", "texture.png");

マイクラで使われているテクスチャーを流用して描画する場合は

Identifier texture = Identifier.of("minecraft", "textures/mob_effect/night_vision.png");

などとします。

次に、テクスチャーは専用のレイヤーに描画されるので、このレイヤーを指定するメソッドを用意します。

private RenderLayer renderLayers(Identifier identifier) {
return RenderLayer.getGuiTextured(identifier);
}

これらの準備をすれば、次のコードでテクスチャーを描画できます。

drawContext.drawTexture(this::renderLayers, texture, x, y, u, v, du, dv, width, height);