CrossRoad

XRを中心とした技術ブログ。 Check also "English" category.

【2017/7更新】Android StudioでUnity向けmodule開発時のTips

前回と前々回で、Android nativeの関数をUnityから呼び出す方法について紹介してきました。

Android StudioでUnity向けmoduleを作る方法 - CrossRoad

Android Studioで作ったmoduleをUnityから呼ぶ方法 - CrossRoad

この方法によりJavaで書いたコードをUnityで呼べるのは間違いありません。しかし、2回目で書いたように、実際に呼びたいAndroidの側のコードはもっと複雑であることが多く、この方法だけではうまくいかないことがあります。

たとえば、下記の記事で試している音声認識のソースコード(Android Studio)をUnityで使おうとすると、色々なエラーがでてしまい、使うことができません。

【2016/11/27更新】【Android】Bing Speech APIを使う手順とトラブル対応法をまとめました - CrossRoad

そこで、この記事では、AndroidのnativeコードをUnityで使うとき、私が困ったことと対応方法について整理したいと思います。



1. Android StudioでUnity系のpackageを書くとエラーになる

たとえば、Unityの関数(例:UnitySendMessage)をAndroidから呼び出したり、Unity上のAndroid向けActivityを取得するためには、下記のようなpackageをインポートする必要があります。

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;
import com.unity3d.player.UnityPlayerNativeActivity;

これをソースコードに書くとエラーになります。回避するには、Unityアプリケーションの中にあるclasses.jarをAndroid Studioのプロジェクトにインポートする必要があります。

classes.jarはMacとWindowsで置き場所が異なります。下記はUnity5.3.5f1のときの配置です。Unityのバージョンによって場所が変わることがあるようなのでご注意ください。

Macの場合

/アプリケーション/Unity/PlaybackEngine/AndroidPlayer/Variations/mono/Release/Classes/classes.jar

Windowsの場合

C:\Program Files\Unity\Editor\Data/PlaybackEngines/AndroidPlayer/Varitations/mono/Release/Classes/classes.jar

インポートする場所は下記です。

Android Studioで開いたプロジェクトにclasses.jarをインポートする場所

Warningなどが1,2回出ますが、OKを押して進めます。

Android Studioのgradleの場所

この後、gradleのsyncを実行すればエラーが消えます。




2. manifest.srcFileを書くと、AndroidManifest.xmlがない、というエラーが出る

build.gradleには、srcやAssetsフォルダなどを認識させるためのオプション "sourceSets"があります。下記のブログを書かれた方の動機から推測すると、このオプションはEclipse上での構成を移行するときに使うようです。

AndroidStudioでmainのディレクトリ構成を使わない方法 - Qiita

今回、MicrosoftのBingSpeechAPIをmodule化するために、新規プロジェクトを作って試しました。そのとき、元のMSのサンプルコードにあるbuild.gradleを書き写したところ、下記でエラーになりました。

android{
(途中省略)
    sourceSets {
        main {
         manifest.srcFile 'AndroidManifest.xml'
(途中省略)
        }
}

Error:A problem was found with the configuration of task ':app:checkDebugManifest'.
> File '/Users/UserName/AndroidStudioProjects/Test/app/AndroidManifest.xml' specified for property 'manifest' does not exist.

また、Android Viewからはmanifestフォルダが消えてしまいました。

Android Studioでmanifestフォルダが消えてしまった例

原因は、自分で作ったAndroid StudioプロジェクトとEclipseから移行したプロジェクトではManifestファイルの配置場所が異なっているためです。

Android Studio(2.1.2)で作った場合

app/src/main/AndroidManifest.xml

Eclipseから移行した場合

app/AndroidManifest.xml

十分には確認していないのですが、今回のオプションは Eclipseから移行したプロジェクトでなければ不要です。ちなみに、build.gradleで manifest.srcFile 'AndroidManifest.xml'のオプションを削除すると、Android ViewでmanifestフォルダとAndroidManifest.xmlが復活します。

Android ViewでmanifestフォルダとAndroidManifest.xmlが復活した例

3. aarとjarは、どちらを使えばよいか?

UnityでAndroidのコードを使うには、aarかjarのどちらかをインポートする必要があります。aarとjarの違いはここで解説した通りですが、再掲します。

aar : Android Archive Library (Androidアプリ開発に必要なファイルをまとめたもの)

jar : Java Archive

つまり、aarの方が範囲が広く、aarにはjarだけでなくAndroidManifest.xmlも入っています。自分の実験結果、および購入したアセットのフォルダ構成調査の結果から、Unityではaarもjarのどちらも使えます。(Plugins/Androidに入れるだけです)

しかし、aarを使う場合、Androidmanifest.xmlの記述内容によっては、Unityでビルドした時にコンフリクトエラーになる場合があるため、Unityで準備されているAndroidManifest.xmlと重複記述がないようにする必要があります。

また、Unityで準備されているAndroidManifest.xmlは、MacとWindowsで置き場所が異なります。

Macの場合

/アプリケーション/Unity/PlaybackEngine/AndroidPlayer/Apk/AndroidManifest.xml

Windowsの場合

C:\Program Files\Unity\Editor\Data/PlaybackEngines/AndroidPlayer/Apk/AndroidManifest.xml

Unityで提供されているAndroidManifest.xmlを使うときはコンフリクトに注意します。たとえば、Activityでintent.Action.MAINをAndroid Studio側に書いてしまうと、Unity側とコンフリクトを起こすので、書かないようにします。



4. Unityでビルドすると、Error building Player: IOException: Failed to Move File / Directory from 'aaa/classes.jar' to 'bob/classes.jar' というエラーがでる



2016/11/1に追加しました


jarファイルだけを使う場合、このエラーは発生しません。これはAndroid StudioでUnitySendMessageとか、Unity向けのメソッドを使うために入れたclasses.jarが、Unityに入れたaarに残っており、UnityでAndroid向けのビルドをするときに使われるclasses.jarとかぶってしまうのが原因のようです。

AndroidStudioを使ってUnityとAndroidの連携 - ほげほげ(仮)

解決するには、Android Studio側のbuild.gradleで下記を書いておきます。

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

この記述により、Android Studioでライブラリのパッケージ(aar)を作る時、libs/classes.jarを除きます。

5. activityを呼び出すには?



(2016/11/28に追加しました)


aarの中にActivityがある場合、Unityから呼び出すことができます。それぞれ、下記のコードを書きます。Java側のAcitivityに画面を準備していない場合、startActivityを呼び出した段階で画面が真っ白になります。

Android Studio側

//import宣言などは省略

public void launchActivity(final Activity m_activity){
    Intent i = new Intent();
    i.setClassName(m_activity,"よびだしたいactivityをpackage名を含むフルパスで表記");
    i.setAction(Intent.ACTION_MAIN);
    m_activity.startActivity(i);
}

Unity側

//using宣言などは省略

public void launchActivity(){
  AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
  AndroidJavaObject currentUnityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
  AndroidJavaClass plugin = new AndroidJavaClass("Java側で呼び出したいActivityを持つクラスを、package名を含むフルパスで表記");
  plugin.CallStatic("launchActivity",currentUnityActivity);  // 第一引数は、Java側のメソッド名
}

6. activityを呼び出すコードをtypoなく書いたのに、java.lang.ClassNotFoundExceptionが出る



(2017/7に追加しました)


たとえば、Android Studio側でプラグインを作成し、Unity側でこのようなコードを書いたとします。


gist1c06bc192c8f65903dddb081e1f385b3

そして、bindService_Start()メソッドをUnityで呼ぶとこのようなエラーが出ることがあります。


AndroidJavaException: java.lang.ClassNotFoundException: com.microsoft.CognitiveServicesExample.MainActivity
java.lang.ClassNotFoundException: com.microsoft.CognitiveServicesExample.MainActivity
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:308)
at java.lang.Class.forName(Class.java:272)
at com.unity3d.player.UnityPlayer.nativeRender(Native Method)
at com.unity3d.player.UnityPlayer.a(Unknown Source)
at com.unity3d.player.UnityPlayer$b.run(Unknown Source)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.microsoft.CognitiveServicesExample.MainActivity" on path: DexPathList[[zip file "/data/app/com.tack.example1-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tack.example1-1/lib/arm, /vendor/lib, /system/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)

ClassNotFoundException (指定したクラスは存在しない)というエラーです。ただし、Unity側で指定したpackage名は合っています。

今回の私の場合、原因はSpeechSDK.jar(Android Studio側のサンプルに含まれていたプラグインや、ヘッダファイル的なもの)をUnity側にインポートしていないことが原因でした。

もう少し汎用的に言うと、Android Studio側で呼び出したいクラスにimplementの記載があると、implementで宣言されたメソッドの実体が入っているjarファイルがUnityにないと、package名が合っていてもClassNotFoundExceptionになるようです。

この現象は、1からAndroid Studioで作った機能をUnityで使う場合には発生しないかもしれませんが、今回のように既存のAndroidの機能(そこそこ複雑なもの)をUnityで使えるようにしたい、というときにありえます。




7. AndroidプロジェクトにC/C++ネイティブライブラリを含むとき、Unityではどこに配置するか?



(2017/7に追加しました)


AndroidはJavaですが、時にはC/C++で書かれたライブラリを使いたい時があります。このような外部ライブラリは一般的にPluginsディレクトリ以下に配置しますが、Android向けのライブラリファイルは配置するディレクトリが決まっています。

特定の Android プラットフォーム (armv7、x86) では、ライブラリ (lib*.so) を以下のように配置する必要があります:

Assets/Plugins/Android/libs/x86/

Assets/Plugins/Android/libs/armeabi-v7a/

もしこのルールを守らず、例えば、

Assets/Plugins/Android/aaa.so

と配置すると、以下のようなエラーが出ます。

Caused by: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tack.test111-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tack.test111-1/lib/arm, /vendor/lib, /system/lib]]] couldn't find "aaa.so"

Unityではディレクトリ構成が比較的自由ですが、プラグインについては記述を守る必要があります。

8. Serviceを使うと、 android.os.BinderProxy cannot be cast to aaa というエラーが出る



(2017/7に追加しました)


たとえば、このようなコードを書いてServiceとしてAndroidの別機能を呼び出すとします。


gistd4f26eb7a4434747971dedbf4f5517d9

すると、こんなエラーが出ることがあります。(一部だけ載せています)


Caused by: java.lang.ClassCastException: android.os.BinderProxy cannot be cast to com.microsoft.CognitiveServicesExample.MainActivity$MyServiceLocalBinder
at com.microsoft.CognitiveServicesExample.NativeBridge$1.onServiceConnected(NativeBridge.java:25)
at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1209)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1226)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)

原因はAndroidManifest.xmlにあります。私の場合は、Serviceの使い方を参考にしたサンプルコードにisolatedProcessというタグが入っていました。つまり、isolatedProcessを抜くと解決します。


gistbfaee572dd60f7ca96b0dd76a956f143

9. Unityで現在のAcitivityを取得するには?



(2017/7に追加しました)


Unity側のC#でこのようなコードを書きます。

AndroidJavaObject currentUnityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); 

Unityで作ったAndroidアプリは、Unityの準備したActivityがメインのActivity以前になります。書いたブログでは、currentUnityActivity変数を使って現在のActivityを取得し、これを元にServiceを起動していました。

public void bindService_Start(){
		Debug.Log("Unity test bindService Start");
		AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 
		AndroidJavaObject currentUnityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); 
		AndroidJavaClass plugin = new AndroidJavaClass("com.microsoft.CognitiveServicesExample.NativeBridge");
		plugin.CallStatic("startBindService",currentUnityActivity);  //ここで、現在のActivityを引数に入れて、Android側で準備したservice起動メソッドを呼んでいる
}

10. 終わりに

ひとまず、思いつく限りのトラブル対応集を書き出してみました。今回書いた内容は、インターネットで直接の情報が得られず、解決まで時間を要したものが多いです。今回の情報が、UnityでAndroidのネイティブプラグインを使う方の参考になれば幸いです。