22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unity の Android Plugin を Kotlin で作る

Last updated at Posted at 2017-02-26

Kotlinとは

JetBrainsって会社が作った"プログラム言語"で、Java言語の問題を解決しつつJava言語と同等の速度と互換性を保っていると宣伝されています

以外に昔(2011発表みたい)から存在していて、現在はオープンソース化されていて、最近は情報や本も増えてきています

しかもAndroid StudioのベースとなっているIntelliJ IDEAを作っているのがJetBrainsなんです
つまりAndroid StudioでKotlinは使い易いはず
そしてUnityのAndroid Pluginも作れるはず
と言う投稿です(笑)

2017/05/18 追記
Google I/O 2017 で Kotlin が Android の正式開発言語になったとの発表が!
Android Studio 3.0 Canary 1(Beta?) でサポートも(Pluginではなくなる?)

UnityにおけるAndroid Plugin作成

一方でUnity環境は、ソーシャルゲームをターゲットとした場合にiOS/Androidの開発/Buildがワンコードで可能で、全盛期どころか今だ改良/進化し続けるイケイケの環境です
(最近はUnity社さん主催のセミナーとか良く行くんで、ほんとUnityの中の人ありがとうございます、なんですよね)

が、iOS/Androidネイティブの環境に目を向けると

  • iOSはObjective-CからSwiftと言う言語に舵を切りました
  • AndroidはEclipse環境からAndroid Studio環境、Ant/MavenなどのBuildプロセスからGradleになりつつあります

そうです、UnityのAndroid Plugin環境は変わりつつあるのです
(と言うか"つつ"では無く変わったのに、互換やら移行の問題で古い環境が残りまくっていると言うイメージ)
更にWeb上で検索できるUnityのPlugin情報は、ちと古くありませんか?って思える

個人的にはUnity4.Xとか5.2未満時代の、当時動いたPluginをそのまま使いまわしているんじゃ?ってほど情報が出てないと感じる

なんでKotlinやるついでにUnity Android Pluginについても語れれば良いかな?と言う体で進めます
(この行は最後に記述しまたが、なので超々長文になってしまっています、、、)

iOSのSwift(と言うかApple系全般)は門外漢なんで今回は取り上げない方向です
(職場はMacでiOS Pluginも作っているんですが、、)

Android Studio

まずEclipse時代と変わった点を、個人の感覚ですが

  • Android SDK Managerがメニューの一部ように動く

"Android" Studioって名前なんですから当然と言えば当然なんですが、Eclipse時代はSDK ManagerはEclipse Pluginだったんで拡張機能って感じがすごかった(SDKのDLの遅さも相まって)

いまでもSDKは別途のDLなんですが、Android StudioがUpdate(SDK Versionが上がると)を促してくれたり、デフォルトのままならSDKへのPathを通す必要もないです!
(adbツールをコマンドで使いたいなら、それは自身のPC環境のパスを通す必要はあります)

  • Build環境はGradle一択状態

Eclipse時代はAntだ、Mavenだ、と他にも様々なBuild構築環境があり情報がカオスと思えていました
(あくまで個人的に)
それだけEclipse環境が成熟していたとも言えなくはないのですが、初めてやるには定番というかコレ覚えれば良いんだよねってのが欲しいじゃないですか?(笑)
Android StudioだとGradle一択でOKです!

まあ、Visual StudioやXcodeからするとBuildするのにGradleって言語記述がいる時点で、Unity(ゲーム)アプリ開発者には敷居が高いと言わざるを得ないんですが、、

  • ライブラリ形式は .jar から .aar に

今でも .jar 形式のPluginは使用出来るんですが、断然 .aar 形式をお勧めします
.jar とはJavaのライブラリ形式だと思ってもらえればよく、.aarはAndroidのライブラリ形式と言えます
この時点でAndroid専用の方が良さげじゃありませんか!(笑)

具体的には、.aarは自身専用のRファイルとAndroidManifest.xmlが入れ込まれます
これだけ聞いてUnity Android Plugin開発におていて、おっー!ってなった人は職人です(笑)

Rファイルとは、R.xxxみたいなリソースIDの事でJava Android開発ではIDE(EclipseやAndroid Studio)のプロジェクトツリーで示される画像や文字列などのリソースと紐づくやつです

これが .jarのUnity Pluginの場合はコードでは記述禁止なんです

理由はRファイルってのはアプリで1つだったからです、つまりUnity(Android)アプリでも存在して、.jar(Plugin)が複数でもそれぞれに存在して、でもそれはその時点のR.xxxで示されるID値で、最後に.apkになった時点でマージされるんです
つまり最終的にID値がズレるんですね

なのでPlugin職人はR.xxxって楽なコード記述をせずに、ID値をシンボル的なものから取得するコードを強いられていた訳です
しかし、.aarなら1WordでR.xxxって書けるんですよ!

AndroidManifest.xmlもそうです、Unityのプロジェクトインスペクタに置いたxmlにアプリ全てのPluginのパーミッションを書くなんていらんのです、それぞれの.aarが持てるんですよ!

android環境が、専用の.aarなら宜しくやってくれるって事です
やはりカタストロフィ的には、.jar から .aar に変わったのが一押しで大きいですね

UnityのPlugin環境

  • Pluginsフォルダ及び以下のAndroid/iOSフォルダと言う縛りがなくなった

これはUnity5の時点なんで2014~2015年にはこの仕様のはずです

それにともないPluginインスペクターが表示されるようになり、そこで色々な指定が出来るようになりました
Pluginが対象とするプラットフォーム指定とか(なんで特殊フォルダが要らないんです)

余談ですがiOS Pluginの場合依存するFrameworkの指定も出来て、それは出力されるXcodeに反映されます
UnityがXcodeを出力するときにフックしてゴニョゴニョするなんて不要なんです(流石に古い情報か)

更に余談で現状のXcode開発(2017/02 8.2.1)では、ローカルビルド(?)ならAppleのDeveloper契約(p12ファイルとか)が不要でAppleIDのみで自身のオレオレ署名でアプリをビルド&インストールできます(Androidとほぼ同じ)

これは凄い便利で素晴らしい変更だとおもうのですが一点だけ注意が!
なんとBundle Identifier(リバースドメインとかのjp.co.xxx)が例え個人のローカルビルドだろうとユニークらしいのです!
つまり、リリース予定のBundle Identifierをうっかり個人のAppleIDや違うTeamIDとかで使用すると、本番で同じBundle Identifierが使えないんです(既に使用されているとなる)
これチームの人がやらかして今のプロジェクトはxx.xxx.xxx2とかカッコ悪くなってます(笑)

やっとAndroid Studio で Kotlin

ここまで長くなってしまいましたが、上記の内容はどうしても言いたかったのでご容赦を、、
しかし、Kotlinの前にまだまだAndroid StudioでUnity Pluginを作るまでの説明がもう少しあります(笑)

まず、本日の日付とAndroid StudioのVersion等を書きます

  • 2017年2月27日
  • Windows 10 環境
  • Android Studio Version 2.2.3
  • Gradle 2.2.3
  • Android SDK Target Version 25
  • Unity 5.5.1p3
  • kotlin 1.0.6

理由はAndroid Studio等は日々進化していて、操作どころかGUIすら激変する可能性があるからです
実際に現状の私の使用Versionは上記の通りなんですが、検索で古いVersionの頃のWeb情報(と思われる)しかヒットしなくて困ったのと、貴方の情報は何時の話なのってWebサイトに困ったからです(内容に不満ではなく時系列が不明なのが、、)

これでいよいよAndroid StudioでUnity Plugin作成の話に入ります

プロジェクトの作成

Android Studioでは.aar作成専用プロジェクトってのは現状ありません
よって、Start a new Android Studio project(メニュー)から、Add No Activityの
プロジェクトを作成します
(minimum SDK Versinとかは自身のUnityアプリやPlugin機能が要求するVersionとかで適宜指定)

この状態だと、デフォルトのActivityが追加されてないAndroidアプリのプロジェクトなんで、さらにメニューの File -> New -> New Module... から Android Library を選びます

これでプロジェクトツリー上は、app(今回は空)と追加したmodule(これが.aar)の2つのモジュールがあり、build.gradleはプロジェクトとappと追加したモジュールの3つになっているはすです

この状態でapp(アプリは作らない)を放置して、追加したモジュール部分で.aarを作るようにコードやGradleを記述していきます

ちなみにプロジェクトモジュールのsrc以下には main, androiTest, test と3つの階層があると思いますが、main にコードのファイルは作っていきます
Gradleの各所にも記述があるんですが、テスト環境もデフォルトで作られるんです

Kotlin環境の構築

やっとタイトルらしい処にきましたが、、
最初に説明した通り、JetBrainsパワーでさらっと流せるほどの作業しかありません

メニューの Help -> Find Action から "Plugins" って打ってヒットした項を選ぶとPluginsウィンドウが開きます

このウィンドウの左下の方に、Install JetBrains plugin... ってボタンがあるので押すと、Browse JetBrains Pluginsウィンドウが更に開きます

この中に Kotlin があるので Install します
しばらくすると Install ボタンの文字が Restart って変わって(インストール完了)促されるので押して再起動します

これで再起動すれば、Kotlin環境がインストールされています
が、もう一つだけ設定がいります

GradleをKotlinに対応させる必要あるのですが、これもメニュー一発です

メニューの Tools -> Kotlin(増えてるはず) -> Configure Kotlin in Project を選びます、すると小さいウィンドウでChoose Configuratorウィンドウがでるので、Android with Gradle を選びます

更にConfigure Kotlin in ProjectウィンドウがでるのでAll modulesとKotlinのVersion(現状の最新)がデフォルトでチェックされているはずなので OK ボタンを押す

これだけで、全てのbuild.gradleに必要な記述が自動で書き込まれます

あとは、Android Studioの仕様なんですが、Gradleが変更されたときにでる
Gradle files have changed since last project sync. A project may be necessa... Sync Now
の Sync Now を押して適応をさせます

Kotlin

ここでは、あまり言語的な解説はしません

  • Kotlinのファイル拡張子は .kt
  • Kotlinにはstaticフィールドが言語的に無いと言われているが、、

static では少々嵌りました
(Unity側でAndroidJavaObject.CallStaticで呼びたかったので)
companion object で @JvmStatic アノテーションだとか色々やったんですが
結局は、出来上がった .aarの中からクラスファイルを拾ってデコンパイル(JADで)して中身を見るはめに(笑)

最終的には、Kotlinの仕様にある package に class 無しでいきなり関数(メソッド)を置けるというスタイルにしました
その場合のクラスパスは ファイル名 + Kt になります
Plugin.kt ファイルなら "PluginKt" がクラス名
関数(メソッド)はちゃんと static になります

今回実験につかった添付メールを送るメーラー起動プラグインのコードです
(内容に責任は持ちません、あくまでお手軽な実験なので)
ファイル名は AttachmentMail.kt です

AttachmentMail.kt
package mm.attachmentmail

import java.io.File
import android.content.Intent
import android.net.Uri
import com.unity3d.player.UnityPlayer

fun StartSelect(address : String, subject : String, body : String, mimeType : String, chooser : String, attachmentPath : String) {
    val intent = Intent()

    intent.setAction(Intent.ACTION_SEND)
    intent.setType(mimeType)
    intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(address))
    intent.putExtra(Intent.EXTRA_SUBJECT, subject)
    intent.putExtra(Intent.EXTRA_TEXT, body)

    val file = File(attachmentPath)

    if (file.exists() && file.isFile()) {
        intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file))
    }

    UnityPlayer.currentActivity.getApplicationContext().startActivity(Intent.createChooser(intent, chooser))
}

このクラスファイルをデコンパイルしてみると

package mm.attachmentmail;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcelable;
import com.unity3d.player.UnityPlayer;
import java.io.File;
import kotlin.jvm.internal.Intrinsics;

public final class AttachmentMailKt
{

    public static final void StartSelect(String address, String subject, String body, String mimeType, String chooser, String attachmentPath)
    {
        Intrinsics.checkParameterIsNotNull(address, "address");
        Intrinsics.checkParameterIsNotNull(subject, "subject");
        Intrinsics.checkParameterIsNotNull(body, "body");
        Intrinsics.checkParameterIsNotNull(mimeType, "mimeType");
        Intrinsics.checkParameterIsNotNull(chooser, "chooser");
        Intrinsics.checkParameterIsNotNull(attachmentPath, "attachmentPath");
        Intent intent = new Intent();
        intent.setAction("android.intent.action.SEND");
        intent.setType(mimeType);
        String as[] = {
            address
        };
        String s = "android.intent.extra.EMAIL";
        Intent intent1 = intent;
        Object aobj[] = (Object[])as;
        intent1.putExtra(s, (String[])aobj);
        intent.putExtra("android.intent.extra.SUBJECT", subject);
        intent.putExtra("android.intent.extra.TEXT", body);
        File file = new File(attachmentPath);
        if(file.exists() && file.isFile())
            intent.putExtra("android.intent.extra.STREAM", (Parcelable)Uri.fromFile(file));
        UnityPlayer.currentActivity.getApplicationContext().startActivity(Intent.createChooser(intent, (CharSequence)chooser));
    }
}

クラス名はちゃんと ファイル名 + "Kt" の AttachmentMailKt になってます
関数(メソッド)も static です

あとはKotlinの売りの一つであるNull安全がメソッド先頭で関係してそうです(笑)
そもそもNull安全はコンパイル時の話なんで、実行時とは関係ないかもですが、、

あと便利だった機能は

  • メニューの Code -> Convert Java File to Kotlin File

もともとは既存のJavaファイルを変換する機能だとおもいますが、Javaでこう記述したのはどうなる?的に実験で約に立ちました

Android Studio で Unity Plugin

そう避けて通れないアイツです、Unity の classes.jar です
こいつを取り込まなないと
import com.unity3d.player.UnityPlayer
が出来ません(笑)

Win/MacともにUnityのドキュメントに場所が記載あるのでコピーして持ってきます
Android Studio側は、今回は
ProjectRooT/AddModule(追加した)/libs/
にコピーしました

これはデフォルトのGradleファイルの依存関係部分に下記のように、libsディレクトリ以下の.jarファイルを取り込んでくれるって記述があるからです

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

しかし現状のAndroid Studioの罠がコレなんです
このUnity Plaginの .aar で実現したいことは、Build時にclasses.jarは参照するけど、.aarには含めて欲しくない、なんです
(Unity側で重複してしまう)

これは、何もUnityのclasses.jarに限らず、android-support-v4.jar や android-support-v7.jar 等も同じなんで、Pluginを作る方は注意すると良いです
つまり、android-support-vX.jar とかはバージョン管理したものを Unity Project側で登録するスタイルとか
(何処の誰かの .jar や .aar がサポートライブラリ持ってるとかはNGだと思います)

サポートライブラリはMD5とかでバージョンがわかるので適宜上げていくのが良いかと
(特定のサポートライブラリバージョンに依存とか良くありませんよ!)

で通常のGradle(2系)だと compileOnly って正にコンパイル時だけ参照して成果物には含めないってコマンドがあるんですが、、なんとAndroid StudioのGradleだと使えないんですよ、、
(正確にはGradleとAndroid StudioのGradleは違うらしい)

2017/05/18 追記
Gradle には Maven と同じ provided 指定があるらしいので使うだけで済むようです
Gradle(2系) の compileOnly は Maven での provided となるみたいです
なので Android Studio で provided で使えるなら使えば良い

dependencies {
    provided fileTree(dir: 'libs', include: ['*.jar'])
}

↓↓↓ 以下 忘れて下さい(もしくは何かの Grafle Task 作る際に参考にするなど) ↓↓↓

なので、build.gradleで記述してやる必要があります
下記は私もWebで拾った記述なんで申し訳ないんですが、exclude ってのが排除するって感じで取り除くコマンドみたいです
(そんなにGradleは強くないんです、、)
outputsってのが成果物だと思うんで .aar から取り除くって事だと思います
あとはラムダ風記述をなんとなく推測すれば(笑)

android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

↑↑↑ provided に変更 ↑↑↑

余談にしていいのか分かりませんが、現状のデフォルトのbuild.gradleでは
下記のサポートライブラリを使うようになっていますが、今回のIntentでメーラー起動では不要なのでコメントアウトしています
C++でもWebでも何でも殆どのケースでは不要なIncludeは悪です
この程度の機能な .aar がサポートライブラリに依存するようになったら使いづらいですから
(実はGradleは頭が良くて、コードで使用してないものは取り込まないとかなら失礼しました)

dependencies {
    compile 'com.android.support:appcompat-v7:25.2.0'
}
  • Unity classes.jar の対応
  • Gradle の Kotlin 対応
  • 正しい Kotlin Plugin コードの記述

これが出来れば、あとは .aar をつくるだけです
Android Studio の Gradleタブの中から追加したモジュール名のTasksのbuildのassembleReleaseを実行します
ProjectRooT/AddModule(追加した)/build/outputs/aar/
に .aar が出来ているはずです
(本来はGradleのタスクで場所やコピー移動など記述して便利にするんです)

kotlin-runtime.jar

これが、今回で一番嵌った部分です
JADでデコンパイルまでするはめになった元凶です(笑)

上記で作成した .aar なんですが Kotlinの機能をつかったコードが入るんですが、そのランタイムは .aar には入ってないんです

上記のデコンパイルコード内のこの行です
import kotlin.jvm.internal.Intrinsics;

Gradleの依存関係ではkotlin-stdkibが入り込むと思ってたんですが、、、
この行をコメントアウトしても .aar は作れたんで機能してない?とか、、、

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

つまり、Unityでしかるべく取り込んで実行しても、Androidでは
java.lang.NoClassDefFoundError:
(Intrinsicsクラス)
的な Kotlin の class が無いぜ!的な例外が飛びます

で、kotlin-runtime.jar があれば良いってわかってからも少し大変でした
ググっても公式的なDLがなかなか見つからない(笑)

オチはkotlin-compiler-X.X.X(Version).zipってセットを落とせば全部入ってました
この中にkotlin-runtime.jarが入ってました
(おそらく一部なんで単発でkotlin-runtime.jarで検索してもダメだったんでしょう)
kotlin-compiler

Unity側で正しく動作させるには

  • 自分で作った Kotlin Plugin .aar
  • kotlin-runtime.jar

の二つをUnityに登録する必要があります

Unity でのプラグイン呼び出し

任意のUnityのプロジェクトインスペクタに .aar と kotlin-runtime.jar を置き
下記のように、お問い合わせメール機能として使うイメージを想定しました

ここで気になるのは、AndroidJavaClass とか AndroidJavaObject ですかね
よくメンバ変数とかにキャッシュするコードを見かけるんですが、、

Unityのドキュメントには

AndroidJavaObject や AndroidJavaClass は、多くのタスクを自動化し Java の呼び出しをより高速で行うためにキャッシュを使用します。

とあります
自分は使用側がクラスをキャッシュしてしまったら何か内部の動きを阻害するんじゃないかと勘ぐっています

なので using を使って IDisposable インターフェイスの動作を実行してもらうコードにしています
メーラー起動の部分をキャッシュして new をケチった所で、違う所をオプティマイズするべきだと考えるからです
(素早くメーラー起動を繰り返す設計などに絶対にならないはずです)

Mailer.cs
using UnityEngine;

public class Mailer  {
  private string AddressCS = "cs@example.com";

  // 言語切り替えで英語等になるような仕組みを入れても良い.
  private string Chooser = "メーラーを選択して下さい";
  private string Subject = "お問い合わせメール";
  private string Body = "テンプレ文章\n\n聞きたい事書いてね";

  // 照準的な添付を想定(特殊な場合は変更する).
  private string MimeType = "message/rfc822";

#if UNITY_EDITOR
#elif UNITY_ANDROID
  private string ClassName = "mm.attachmentmail.AttachmentMailKt";
  private string MethodName = "StartSelect";
#elif UNITY_IPHONE
#endif

  /// <summary>
  /// 添付ファイル付きの問い合わせメールを想定.
  /// </summary>
  /// <param name="attachmentPath">アプリでアクセス権限がある領域のファイルを指定する事.</param>
  public static void StartSelect(string attachmentPath) {
#if UNITY_EDITOR
#elif UNITY_ANDROID
    using (AndroidJavaClass ajc = new AndroidJavaClass(ClassName)) {
      ajc.CallStatic(MethodName, AddressCS, Subject, Body, MimeType, Chooser, attachmentPath);
    }
#elif UNITY_IPHONE
#endif
  }
}

よく読むとドキュメントでも

Mono ガベージコレクタは、作成されたすべての AndroidJavaObject と AndroidJavaClass のインスタンスを使用後すぐに開放する必要があります。ただし、それらを using(){} ステートメント内に置き、できるだけ早く削除されるように確実にしておきます。こうしないと、インスタンスがいつ破棄されるかわかりません。

言ってますね

22
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?