UnityのスマホネイティブプラグインをKotlin/Nativeで共通化する


Unityのスマホネイティブプラグインを言語統一できないか?

昨今のスマホネイティブプラグインの言語の選択肢としてはJava, Kotlin, Objective-c, Swiftがあげられるかと思います。
たまに自分も趣味などでプラグインを使うことがあるのですが同じような処理を別で書かないといけないのが手間だと思っていました。
そこで昨年くらいからスマホのネイティブ界隈で話題になっていたKotlin/Nativeに目をつけてUnityで実行させてみたという記事です。

そもそもKotlin/Nativeとは…?
まとめている記事もありましたので参照させて頂きます。

基本的にはスマホネイティブの共通化できるロジックをKotlinで書いて共通化させようぜ!ってことなのですがAndroidでは.jarとしても吐き出せますし、iOSは.frameworkとして吐き出せるのでネイティブにとって扱いやすいものになっています。

さて、今回検証に使用したリポジトリです。動かして見たい人は是非ご活用ください。

実行した結果

Android iOS

文字列をOS毎に変えるという処理ですが呼び出すメソッドは1つにしてあります。

準備

Kotlin/Native自体の作成方法は既にわかりやすい記事がありますのでそちらを参照させていただきます。
Kotlin/Nativeチュートリアル Android, iOS編
自分はここを参考にさせて頂きました。そのため今回上記の記事をベースに進めます。

Androidのネイティブが自分はよくわからなかったので、最初は基本的にコピペで作って必要な箇所を変えていきました。

今回、上記記事の「Common moduleの解説」の章まで出来たら一旦は大丈夫です。
上記の記事ほぼそのままですがソースコードを載せておきます。

共通

common.kt

package com.sample.mizotake.kotlinnativeforunity

expect fun platformName(): String

public class common {
    public fun createApplicationScreenMessage(): String {
        return "Call Kotlin Native on ${platformName()}"
    }
}

Android

actual.kt

package com.sample.mizotake.kotlinnativeforunity

actual fun platformName(): String {
    return "Android"
}

iOS

actual.kt

package com.sample.mizotake.kotlinnativeforunity

import platform.UIKit.UIDevice

actual fun platformName(): String {
    return UIDevice.currentDevice.systemName() +
            " " +
            UIDevice.currentDevice.systemVersion
}

共通処理にUnityで呼び出すクラスとメソッドを定義します。OS毎に変える処理はexpect actual処理でinterfaceのように切り出して呼べるようです。

Unityへの導入

Android

自分もよく把握できていませんがGradle Syncをするとbuildというディレクトリができて

プロジェクト名 + android.jarができていました。
もし出来ていない場合は

右端にGradleというタブがあるのでそこからbuildの項目を見るとbuildの詳細一覧があるのでandroidJarをダブルクリックすれば走り出してjarができるかと思います。

吐き出されたjarをUnityのPlugin/Androidに放り入れるだけです。
これでUnityへの導入は完了です。

iOS

こちらは先ほどのKotlin/Nativeチュートリアル Android, iOS編の「iOSアプリ」の章にあるbuild.gradleの追記だけ行いましょう。

/common/build.gradle
...

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'

    inputs.property "mode", mode
    dependsOn kotlin.targets.iOS.compilations.main.linkTaskName("FRAMEWORK", mode)

    from { kotlin.targets.iOS.compilations.main.getBinary("FRAMEWORK", mode).parentFile }
    into frameworkDir

    doLast {
        new File(frameworkDir, 'gradlew').with {
            text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
            setExecutable(true)
        }
    }
}

tasks.build.dependsOn packForXCode

ここの部分ですね。これを追記して./gradlew buildすることで

main.frameworkができます。これをUnityのPlugin/iOSに放り込めばframeworkの導入は大丈夫ですが、iOSの場合もう一手間必要です。
externの実装がないとC#では呼び出せませんそのためPlugin/iOSフォルダに

common.mm
#import <main/main.h>

extern "C" {
    const char* createApplicationScreenMessage() {
        NSString *message = [[Maincommon alloc] init].createApplicationScreenMessage;
        return strdup([message UTF8String]);
    }
}

を追加しましょう。これを追加することで先ほど作ったKotlinで書いたコードのframeworkを参照できます。
ここでcommonというkotlinファイルを作ったがMaincommonって何だろう?ってなると思います。どうやらframeworkに吐き出す時に変換されているようです。
それを確認するにはAndroidStudioでframeworkのHeaderを見ると一番下の行に自分で実装した処理が追記されていると思います。

これを参考にしてObjective-c++でインターフェースを定義する必要があります。
ちなみにSwiftだとMaincommonという変換名ではなくcommonで呼び出せそうですがSwiftを使うために手間をかけるよりObjective-c++を書いた方が早いと自分は思うのでこのまま進めます。

C#で呼び出す

事前準備は終わりました。
UnityではuGUIのTextにネイティブで呼び出した文字列を表示させます。

CallKotlinNative.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Runtime.InteropServices;

public class CallKotlinNative : MonoBehaviour
{

#if UNITY_IOS
    [DllImport("__Internal")]
    private static extern string createApplicationScreenMessage();
#endif

    private Text viewableText;

    void Start()
    {
        var pluginMessage = "";
#if UNITY_ANDROID
        using (var plugin = new AndroidJavaObject("com.sample.mizotake.kotlinnativeforunity.common"))
        {
            pluginMessage = plugin.Call<string>("createApplicationScreenMessage");
            Debug.Log(pluginMessage);
        }
#elif UNITY_IOS
        pluginMessage = createApplicationScreenMessage();
#endif
        viewableText = GetComponent<Text>();
        viewableText.text = pluginMessage;
    }
}

C#側は普通にネイティブプラグインを呼び出すだけですね。
これを実機ビルドまたはシミュレータービルドすることで動作の確認ができると思います。

終わりに

Kotlin/NativeのUnityProjectへの導入は手間が必要かと思っていましたが思った以上に簡単でした。ただ、iOSの導入のexternだけどうにかならなかいとAndroidProject内に.mm入れてみて.frameworkだけ吐き出して更新させるなどをしようと思いましたがうまく行きませんでした…Androidネイティブのディレクトリ構成やtaskのカスタムに詳しければどうにかできるのかな?と思っています。
Kotlin/Nativeを使えば基本的に言語はKotlinひとつに統一できますし、共通処理やOS依存処理も問題ないのではない気がしています。個人的にKotlinでUIKitなどもimportして使えることに驚きました。
何にせよ扱う言語は少ないに限ると思っていますのでKotlin/Nativeは良いものだと思います。ただ現在betaなので書き方や吐き出し方が変わる可能性は高いです。

追記 (2019/11/26)

先日「Gotanda.unity」というイベントで「いつか使える Kotlin/Native With Unity」というタイトルでLTさせていただきました。スマホだけではなくデスクトップのネイティブプラグインも視野に入れた検証結果です。スライドとリポジトリはブログにまとめています → Gotanda.unity #14 でLTしてました!