Edited at

Kotlinで簡単なModを作るまで

More than 1 year has passed since last update.

以前あまりに抽象的な記事を書いてしまったので(=> https://qiita.com/Pctg-x8/items/b6536cdbe5a1656585f2 )今回は具体的にModを作ってみたいと思います。

今回は、経験値を消費して土を凍らせる魔法の杖を追加するMod「FrostyRod」を作っていきたいと思います。あまり実用的なModではないんですが解説に使ったコード/リソースはPublic Domainとしておくので自由に改変したり一部切り取ったりそのままパッケージ化して配布したりしてもらってもかまいません。


環境構築

今回はKotlinを使って書いていきます。


1. IntelliJ IDEAの準備

Community Editionで大丈夫です。Arch Linuxの場合は


# pacman -S intellij-idea-community-edition


で入手できます。


2. Minecraft Forgeの準備

今回はせっかくなので1.12.2向けのModを作っていきます。http://files.minecraftforge.net/ で1.12.2向けのMdkを入手します。執筆時点では14.23.1.2555が最新のRecommendedです。

ファイルはzipになっているので任意の場所に展開します。


3. ワークスペースの準備

zipを展開したそのままの状態ではコンパイル時に要求されるMinecraftのソースなどが存在していないため、その準備を追加で行う必要があります。

このとき、展開したフォルダをそのまま開発に使うのではなく別にMod専用のフォルダを作ってそこにファイルをコピーして使うことをおすすめします。

ファイルのコピーが完了したら以下のコマンドを実行します。


gradle setupDecompWorkspace


Gradleをインストールしていない場合はForgeに付属のGradleを使うことができます。上記gradlegradlew(Unix系の場合は./gradlew)に置き換えてください。

上記コマンドが完了したらIntelliJ IDEAを起動し、Import Projectを選択してフォルダ直下のbuild.gradleを指定します。次に出てくるウィンドウはよくわからなければそのままOKで大丈夫です(Auto importだけ有効にしておくと便利かもしれない)。

インポートが完了したらIDEAを閉じ、次のコマンドを実行します。


gradle genIntellijRuns


ここまで完了したらとりあえずIDEAから起動できると思います。IDEAを起動し、Run>Run...(Alt+Shift+F10)を選択後Minecraft Clientを選択します。ここで選択したあとはRun>Run 'Minecraft Client'(Shift+F10)ですぐに起動できるようになります。


4. 言語プラグインを入れる

Kotlinを利用するために、もう少しだけ設定が必要です。

まずはIDEAにKotlinのプラグインを入れましょう。File>Settings...で出るウィンドウのPlugins>Install JetBrains plugin...で入手できます。

それから、Tools>Kotlin>Configure Kotlin in Projectを実行してください。


コーディング はじめの一歩

それではコーディングを始めていきましょう。まずはsrc/main/javaフォルダが不要なので消します。次にsrc/main直下にkotlinフォルダを作り、そこにmain.ktファイルを作成します。ファイル名はJavaと違って特に制限はないので、いかにもエントリポイントって感じの名前であればなんでも大丈夫です。

生成されたら、内容を以下のようにします。


main.kt

package com.cterm2.frostyrod

import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.event.FMLInitializationEvent

@Mod(modid = FrostyRod.ID, name = FrostyRod.Name, version = FrostyRod.Version, modLanguage = "kotlin")
class FrostyRod
{
companion object
{
const val ID = "frostyrod"
const val Name = "FrostyRod"
const val Version = "1.0.1"
}
}


import節は補完を使うと勝手にいい感じにしてくれるので、package節(なくてもいい)を書いたあとはすぐに@ Mod〜を書き始めてしまって大丈夫です。

書いてる最中に赤文字の部分が出てきてしまっても、しばらくじっとしてるとAlt+Enterでimportを追加できる旨が表示されるのでそれに従えば基本大丈夫です。

Mod.modLanguageは、Scalaの場合にはここを適切に設定することでエントリポイントをobjectにできたりしてちょっと嬉しいとかあるんですがKotlinでは特に恩恵がないのでここは指定しなくても大丈夫です。

これで実行すると画面には特に変化はありませんが、特にエラーなどが出ていなければ、あと左下に"5 mods active"とあればとりあえず大丈夫です。


アイテムを追加する

次はアイテムを追加します。新しくファイルを作成し、以下の内容としてください。


item.kt

package com.cterm2.frostyrod

import net.minecraft.item.Item

object ItemRod : Item()
{
init
{
this.maxStackSize = 1
this.unlocalizedName = "rod"
this.registryName = ResourceLocation(FrostyRod.ID, "item_rod")
}
}


Kotlin最大の特徴として、特定の命名規則のメソッドが変数と同じ記法で呼び出せるようになるというものがあります。this.maxStackSize = 1はJavaではthis.setMaxStackSize(1)と全く同じものとなります。

今回の魔法の杖はスタックできないようにしたいので最大スタックサイズを1に設定しています。これはコンストラクタに書く必要があるのですが、Kotlinではinitブロックを用いて記述します。

ついでなのでunlocalizedNameも設定します。Minecraftを日本語にして遊んでいる方はもうご存知かと思いますが、Minecraftには言語設定があり、Mod/ResourcePack側で適切に設定すると表示名を変えることが可能です。Modでは多言語対応はほぼ必須なので忘れずに設定しましょう。

実際の表示名はsrc/main/resources/assets/{modのID}/lang/*.langで設定されます。最低限英語(US)向けの言語ファイルを提供しておけば大丈夫です。今回のmodの場合は以下のようになります。


src/main/resources/assets/frostyrod/lang/en_us.lang

item.rod.name=Frosty Rod


item.{unlocalizedName}.name={表示名}で指定できます。

registryNameはセーブデータ上のIDとレジストリ(後述)を関連付けるためのもので、あと見た目の解決などにも使われます。"item_rod"がmod内でのアイテム固有の名前である必要があります。

アイテムを追加したら登録を行います。main.ktに戻り、以下の内容を追記してください。


main.kt

// @Modのすぐ下

@Mod.EventBusSubscriber

// companion object内
private val ctab = object : CreativeTabs("frostyRod")
{
override fun getTabIconItem() = ItemStack(Items.STICK)
}

@SubscribeEvent @JvmStatic
fun register(e: RegistryEvent.Register<Item>) {
e.registry.register(ItemRod.setCreativeTab(ctab))
}


いつのまにか登録周りがほとんどイベントベースになっていて若干迷いましたが、これでアイテムを登録することが可能です。EventBusSubscriberなクラスのSubscribeEventがついた(RegistryEvent.Register<T>) -> UnitなメソッドがTのサブクラスのオブジェクトの登録フェーズで呼び出されます。TはItemだったりBlockだったりします。

クリエイティブモードでアイテムを取り出せるようにするにはsetCreativeTabでタブを設定する必要があります。既存のタブか、独自でタブを定義して設定しましょう。

クリエイティブタブの名前もローカライズの対象です。忘れずに指定しましょう。


src/main/resources/assets/frostyrod/lang/en_us.lang

itemGroup.frostyRod=Frosty Rod


ここまででとりあえずこんな感じになっていると思います。


アイテムの見た目の設定

このまま機能を作り込んでもいいのですが、先にアイテムの見た目を設定します。

今回はこのアイコンをアイテムに適用します。16x16であればなんでも大丈夫です。

画像ファイルはsrc/main/resources/assets/{modのID}/textures以下に配置されます。

続いてモデルファイルの定義です。これはsrc/main/resources/assets/{modのID}/models以下に配置され、アイテム向けはitemフォルダの中になります。ここでのファイル名はアイテムのregistryNameと同じである必要があり、また全て小文字となります。


src/main/resources/assets/frostyrod/models/item/item_rod.json

{

"parent": "item/generated",
"textures":
{
"layer0": "frostyrod:items/rod"
},
"display":
{
"thirdperson":
{
"rotation": [0, 90, -35],
"translation": [0, 1.25, -3.5],
"scale": [0.85, 0.85, 0.85]
},
"firstperson":
{
"rotation": [0, -135, 25],
"translation": [0, 4, 2],
"scale": [1.7, 1.7, 1.7]
}
}
}

モデルファイルの定義は複雑なのでとりあえずhttps://bedrockminer.jimdo.com/modding-tutorials/basic-modding-1-8/model-files/ を参考にするなりするといいと思います。parentbuiltin/generatedで解説されているところが多いですがitem/generatedにしないと回転が若干おかしい感じになります。

texturesでテクスチャを指定します。先頭にmodのID名、その後ろに:を挟んでtextures以下のパス(拡張子抜き)を指定します。

モデルファイルが用意できたらコードの方で読み込みを行います。


main.kt

        @SubscribeEvent @JvmStatic

fun registerModels(e: ModelRegistryEvent) {
ModelLoader.setCustomModelResourceLocation(ItemRod, 0, ModelResourceLocation(ItemRod.registryName!!, "inventory"))
}

ModelResourceLocationのコンストラクタの第一引数がNotNullを要求してくるので!!で明示しています。

ここまででアイテムに画像がついてよりそれっぽくなったと思います。


右クリックのアクションを設定する

右クリックでのアクションの設定は2通り、onItemUseonItemRightClickがあります。前者はブロックに対してアイテムを使った際に呼び出され、後者はアイテムを持った状態で右クリック(正確には右ボタンを押下)すると呼び出されます。今回はブロックに対してのアクションを指定したいのでonItemUseを使います。


item.kt

    override fun onItemUse(player: EntityPlayer?, worldIn: World?, pos: BlockPos?, hand: EnumHand?,

facing: EnumFacing?, hitX: Float, hitY: Float, hitZ: Float): EnumActionResult {
worldIn?.let { w ->
if(!w.isRemote && pos != null)
{
val b = w.getBlockState(pos).block
if(b == Blocks.GRASS || b == Blocks.DIRT) player?.let { this.freeze(it as EntityPlayerMP, w as WorldServer, pos) }
}
}
return EnumActionResult.SUCCESS
}

playerはわかりませんが、worldInposnullになることはまずないはずなのでletじゃなくて!!で外してしまっても大丈夫です。

アイテム使用時に対象のブロックを取得し、草もしくは土ブロックであった場合に凍らせるアクション(freezeメソッド呼び出し)を行っています。サーバーワールドの場合(!w.isRemoteで判定しています)、playerEntityPlayerMPに、w(worldIn)WorldServerになるのでキャストを行っています。

freezeアクションの内容は以下のようになっています。


item.kt

    private const val CostExp = 1

private fun freeze(player: EntityPlayerMP, w: WorldServer, pos: BlockPos)
{
if(player.capabilities.isCreativeMode || player.experienceLevel >= CostExp)
{
// consuming user's experience in survival/hardcore mode
if(!player.capabilities.isCreativeMode) player.addExperienceLevel(-CostExp)
w.spawnParticle(EnumParticleTypes.EXPLOSION_NORMAL, true,
pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, 20, 0.0, 0.0, 0.0, 0.1)
w.setBlockState(pos, Blocks.ICE.defaultState, 3)
}
}

クリエイティブモードの場合は無制限に、それ以外の場合は経験値レベルを消費して使えるようにしています。消費量はCostExpで調整できるようになっています。

ブロックを変更する際、末尾に3を指定するとブロックの変更イベントとクライアントへのブロックの変更通知が発生するようになります。通常はこれで十分なため3以外を指定することは殆ど無いです。

ブロックを置き換える際に爆発パーティクルを表示するようにしています。これは特に説明はいらないでしょうが、ブロック座標に0.5を足してブロックの中心から発生するようにしています。

わーい


さいごに

以上で一通り完成ですが、最後にひとつだけ、mcmod.infoの編集についても書いておきます。


mcmod.info

[

{
"modid": "frostyrod",
"name": "FrostyRod",
"description": "Example mod",
"version": "${version}",
"mcversion": "${mcversion}",
"url": "",
"updateUrl": "",
"authorList": ["S.Percentage"],
"credits": "",
"logoFile": "",
"screenshots": [],
"dependencies": []
}
]

FrostyRodのmcmod.infoはこんな感じになっています。

これは何かというと、タイトル画面でModsを選んだ際に出る画面に表示される内容になっています。

ここを適切に指定することで、バグレポートやアップデート配信場所などの情報をユーザーにしめすことができます(あんまり見る人はいないと思いますが)。