Kotlin
minecraft
minecraftmodding
minecraftforge

MCDevNight KotlinModding講座まとめ

More than 1 year has passed since last update.

始めに

この記事は、Minecraft Advent Calender 2017の1日目の記事です。
MCDevNightという、マイクラ関連のデベロッパー向けイベントにおける、Kotlin Modding講座の内容をまとめ&補足したものになります。
MinecraftForgeにおけるModdingとKotlinの文法をある程度理解している人向けの内容になります。
また、適宜当時使ったサンプルコードを参照してください。

要求環境

  • MinecraftForgeのModding環境(ここでは1.10.2を用いています)
  • IntelliJ IDEA

何故Kotlin?

これは改めて説明する必要があるか、と言われるとそうでもないので簡単にまとめます。
Better Java
この一言で終わりです。

環境構築

MinecraftForge環境におけるModdingには、ビルドツールにGradleが使われているため、非常に簡単にKotlinを利用できます。
以下では、build.gradleの書き換えが必要な部分について説明します。

build.gradle
buildscript {
    ext.kotlin_version = "1.1.51"
    repositories {
        jcenter()
        maven {
            name = "forge"
            url = "http://files.minecraftforge.net/maven"
        }
    }
    dependencies {
        classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'kotlin'

注目すべき点は3つです。
まず、1つ目の ext.kotlin_version=1.1.51 ですが、ここでKotlinのバージョンを指定しています。
そして、classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"という部分でkotlinのgradle用プラグインを依存として追加しています。
そして、最後のapply plugin: kotlinでkotlinのgradle pluginを適用しています。

次に、dependencies句です。

build.gradle
dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
}

kotlinのstdlib(標準ライブラリ)を依存として追加します。

最後に、これはこのままコピペして追加する部分です。

build.gradle
compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

コンパイル時にjava8をターゲットに指定しているだけですね。

Kotlinにstaticが無い問題

Kotlinでは、javaのようなstatic修飾子がありません。
ですが、Moddingにおいては様々な場所でstaticな変数が要求されます。(SidedProxy,ModMetadata等)
ここでは、kotlinにおけるstaticの代用機能(コンパイル時にstaticに変換されるもの)を紹介し、Moddingにおけるそれぞれの用例を示します。

Kotlinにおける、staticの代用機能

まず、Kotlinにおいてstaticの代用となるのは以下の3つです。

  • Companion Object
  • Top-level Declarations(Package-level Object)
  • Object Declarations

以下で、それぞれの項目について紹介していきます。

Companion Object

これが、Javaにおけるstaticに一番近い書き方です。
クラスに紐付けられた、Companion Objectというシングルトンオブジェクト内に変数等を定義します。
実際の挙動としては、

  1. Companionという名前の、シングルトンオブジェクトがstaticでクラス内に定義される
  2. Companion Object内に定義されたフィールドやメソッドは、このシングルトンオブジェクトに格納される。
  3. Kotlinにおけるcompanion objectへのアクセスは クラス名.定義されたオブジェクト の形で行われる
  4. これはコンパイル時に クラス名.Companion.定義されたオブジェクト の形に変換される
  5. @JvmStaticアノテーションをつけると、通常のstaticフィールドになる

重要なのは、最後です。
Companion Objectはそのままではstaticではありません
@JvmStaticをつけた時だけ、staticになります。

そして、Companion Objectの用途ですが、基本的にそのクラスに関連性が高いオブジェクトに使うべきである、と考えています。
Moddingにおいては、@Modアノテーションをつけたクラスに使うことが多いと思います。

用例:

ExampleModCore.kt
@Mod(modid = MOD_ID, name = MOD_NAME, version = VERSION, dependencies = DEPENDENCIES, useMetadata = true, acceptedMinecraftVersions = "1.10.2")
class ExampleModCore {

    companion object {
        @JvmStatic
        @Metadata
        lateinit var META: ModMetadata

        @JvmStatic
        @SidedProxy(serverSide = "mcDevNight.example.common.CommonProxy", clientSide = "mcDevNight.example.client.ClientProxy")
        lateinit var PROXY: CommonProxy

        @JvmStatic
        @Instance
        lateinit var INSTANCE: ExampleModCore
}
//以下略
}

Top-level Declarations (Package-level Object)

この機能、Kotlinの公式のドキュメントではTop-level Declarations表記なんですが、個人的にはPackage-level Objectって言い方のほうが好きなんですよね。
まぁここでは公式表記のTop-level Declarationsを使いますが。

この機能は、恐らくKotlinで最も多く使われているstatic表記です(詳しく調べたことはないから誰か統計取ってみてほしい)。
STDLibの拡張関数群なんかは大体コレで定義されていますね。
というかKotlinのmain関数もコレです。

fun main(args: Array<String>) {
    println("Hwello, World!")
}

このような、クラス外での定義がTop-level Declarationsです。
これは、全てがデフォルトでstaticです。
コンパイル時に専用クラスが生成され、その中にstaticな変数/メソッドとして定義されます。
個人的には、Top-level Declarationsだけを記述するファイルを作ると読みやすいです。

ちなみに、importしておけば、どのパッケージからでもそれぞれの変数/メソッド名だけを書いて呼び出すことができます。
同じパッケージ内であればimportする必要はありません。

以下、用例です。

ExampleModConsts.kt
const val MOD_ID = "examplemod"
const val MOD_NAME = "ExampleModCore"
const val VERSION = "1.0.0"
const val DEPENDENCIES = "required:forgelin"

val tabExampleMod = object : CreativeTabs("exampleMod") {
    override fun getTabIconItem(): Item {
        return MCItem.APPLE
    }
}

//Item  ===========================================================================================
val exampleItem = createItem("exampleItem", "example_item")

//Block ===========================================================================================
val exampleBlock = createBlock("exampleBlock", "example_block", Material.ROCK, 20f, 20f).setCreativeTab(tabExampleMod)



//Register
fun register() {
    register(exampleItem)
    register(exampleBlock.initItemBlock())
    register(exampleBlock)
}

便利関数なんかは全部コレでいいと思います。

Object-Declarations

これは、シングルトンオブジェクトを作成する機能です。
基本的な挙動はCompanion Objectと同様ですが、以下の点で異なります。

  • 名前をつけて、通常のクラスと似たような振る舞いができる
  • 名無しの状態で変数に代入可能
  • クラスに紐付けされない

ちなみにstaticにするには@JvmStaticアノテーションが必要です。

用例:

ExampleModRegistry.kt
object ExampleModRegistry {
    // Items ======================================================================================
    val exampleAxe = ItemExampleAxe()
    val enchantedAxe = ItemExampleEnchantedAxe()

    // Blocks =====================================================================================


    // Registers ==================================================================================

    fun preinit(event: FMLPreInitializationEvent) {
        register(exampleAxe)
        register(enchantedAxe)

        if (event.side.isClient) {
            setTexture(exampleAxe)
            setTexture(enchantedAxe)
        }
    }

    fun init(event: FMLInitializationEvent) {
        addRecipe(ItemStack(exampleAxe), "EE", "ES", " S", 'E', exampleItem, 'S', MCItem.STICK)
        addRecipe(enchantedAxe.getEnchanted(), " B ", "BAB", " B ", 'B', exampleBlock, 'A', exampleAxe)
    }
//以下略
}

普通のクラスみたいに書けますね。

Item/Blockのインスタンスをどのように保持するか

1.11.2までの時点で、ItemとBlockのインスタンスの保持はstaticな変数で行われているのが主流です。
それでは、Kotlinにおいてはどの方法を用いるべきでしょうか?

結論から言うと、最終的には「好み」になると思います。
Companion Objectは、クラスに関連性の高いオブジェクトに使用するべきと言う(個人的な)考えから、あまりオススメしません。
ただ、Top-level DeclarationsとObject Declarationsには両方長所と短所があるため、最終的な判断は個人の好みだと思います。

Top-level Declarations

長所

  • 記述が簡潔になる
  • 複数ファイルに定義を分割しても呼び出し側への影響が少ない
  • デフォルトでstatic #### 短所
  • フィールド名被りが発生すると対処が面倒
  • 1.12.2でのForgeの仕様に対応するにはちょっと汚い書き方が必要になる

Object Declarations

長所

  • 呼び出し側の記法がJavaにおけるstaticなフィールドと同じ
  • なのでフィールド名被っても無問題
  • 1.12.2でのForgeの仕様変更への対応も楽である #### 短所
  • static化するには@JvmStaticが必要
  • 大量の変数定義が1つのobjectに並びがち

好きな方を選んでください。
ちなみに俺は前までObject Declarationsを使っていましたが、今はTop-level Declarationsを使っています。
1.12.2だとForgeの仕様に対応するための登録メソッドとCreativeTabsのインスタンス書くだけのobjectとか作らないといけないんですけどね。

KotlinでModを書くときの便利技

今までmoddingしてて、Kotlin関連で便利だな、と思ったものをいくつか紹介します。

@Modアノテーションの引数をconst valな変数で定義する

ModIDなんかは、割と参照する機会が多いと思います。
const valは、Javaのpublic static finalなプリミティブと同様に、コンパイル時にリテラルに変換される変数です。
Top-level Declarations使って定義しておきましょう。

ItemBlockの取得関数を拡張関数化

Item.getItemFromBlock(block)って長いし、直感的じゃないですよね。
ついでに言うと、どこかのバージョンでか、ItemBlockをGameRegistry.register()してないと取得できないなんてことが発生していました。
ついでに言うと、ItemBlockのテクスチャのjsonファイルはサブフォルダに隔離したいし、Modelの登録はRegistryName使う事で簡易化したいしで、RegistryNameをModId:block/blockNameってしたいんですよね。
それで作ったのがコレ

ExampleModConsts.kt
private fun Block.getItemBlock(): Item {
        return Item.getItemFromBlock(this)!!
    }

    private fun Block.initItemBlock(registryName: ResourceLocation = this.registryName!!): Item {
        val name = if (!registryName.resourcePath.startsWith(block)) {
            ResourceLocation(registryName.resourceDomain, block/${registryName.resourcePath})
        } else {
            registryName
        }
        return ItemBlock(this).setRegistryName(name)
    }

これにより、GameRegistry.register(block.initItemBlock())と書いて登録、以降の呼び出しはblock.getItemBlock()でできます。
拡張関数って便利!!

もっと拡張関数!

テンプレ処理はとりあえずTop-level Declarationsにしてしまう人

fun EntityPlayer.sendMessageOnlyServer(component: ITextComponent) {
    if (!world.isRemote) sendMessage(component)
}

fun EntityPlayer.addAndSpawnXpOrb(value: Long, world: World) {
    var over = if (value > Int.MAX_VALUE) {
        value - Int.MAX_VALUE
    } else {
        0
    }
    val xp = value - over
    over += Math.max(0, experienceTotal + xp - Int.MAX_VALUE)
    addExperience((value - over).toInt())
    if (over > 0 && !world.isRemote) {
        world.spawnXpOrb(over, posX, posY, posZ)
    }
}

fun BlockPos.serializeNBT(): NBTTagCompound {
    val tag = NBTTagCompound()
    tag.setInteger("x", x)
    tag.setInteger("y", y)
    tag.setInteger("z", z)
    return tag
}

fun BlockPos.deserializeNBT(tag: NBTTagCompound): BlockPos =
        BlockPos(tag.getInteger("x"), tag.getInteger("y"), tag.getInteger("z"))

ぶっちゃけ最後は拡張関数にする意味がほとんどない
まぁメソッド名汚染対策で拡張関数にしてるようなもの

最後に

MCDevNightで講演した内容は以上になります。
Kotlin、使いこなせればすごく便利なので是非ともModdingにも活用してください。
Kotlinの機能で、こうすればModdingの便利なのでは?というものがあれば是非とも教えてください