vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity】AndroidでNativeのGUIやら何やら追加したい - Androidその2 Advent Calendar 2016

はじめに

この記事はAndroidその2 Advent Calendar 2016の16日目の記事です。

UnityでAndroid用のアプリを作る場合に、Unity側で用意されている機能だけでは足りないなどの理由で、ネイティブのプラグインを作成したい場合があります。

今回はこの辺りの記事で書いたようなClassのファイルだけでなく、Layoutや画像などを追加する方法を調べてみました。

jarに画像を追加する

この記事と同じように、jarファイルをまず作成してプラグインとして利用する方法を試してみました。

例えば プロジェクトのルートフォルダ/app/src/main/assets に「images」というディレクトリを作成して、その中に入っているJPGファイルをjarファイルに追加したい場合は プロジェクトのルートフォルダ/app/src/build.gradle を下記のように作成します。

build.gradle

apply plugin: 'com.android.library'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 25
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
    sourceSets {
        main {
            resources{
                srcDir "src/main/assets/images"
                resources.includes = ['**.jpg', '**.JPG', '**.png', '**.PNG']
            }
        }
    }
    
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile files('libs/classes.jar')
}
task clearJar(type: Delete) {
    delete 'release/' + 'androidplugin01.jar'
}
task exportJar(type: Copy) {
    from('build/intermediates/bundles/release/')
    into('release/')
    include('classes.jar')
    rename('classes.jar', 'androidplugin01.jar')
}
exportJar.dependsOn(clearJar, build)

ビルド後に作成されるandroidplugin01.jarの容量が、画像分増えていれば成功で、
あとは「AssetManager assetManager = currentActivity.getResources().getAssets();」などでアクセスできます。

ただし、layout.xmlだのあれこれ追加したいと思い始めると、やっぱりこれは面倒な気がします…。
(これはこれで使い方によっては便利なのだとは思うのですが)

UnityでAndroidProjectをエクスポートする

Unity5から(多分)Androidのビルドとして、apkファイルを直接出力するだけでなく、AndroidProjectをエクスポートすることができるようになりました。

方法はBuild Settingsで2つの設定をするだけでできます。

  1. 「Build System」を「Gradle(New)」に変更する
  2. 「Export Project」にチェックを入れて「Export」ボタンを押す

するとフォルダ指定を求められるので、適当にフォルダを作成して、それを指定します。
なおUnityProjectと同じフォルダにすることはできません(UnityProject内のフォルダとかならOK)。

f:id:mslGt:20161216233047j:plain

ネイティブの関数を呼ぶ

さて、出力されたAndroidProjectを開いてみると、 プロジェクトのルートフォルダ/src/main/java/jp/masanori/plugintest (jp/masanori/plugintestはパッケージ名)に、UnityPlayerActivity.javaがあります。

これはWindowsだと下記の場所にあるファイルと同じ内容です。

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player UnityPlayerActivity.java

プラグインの関数を呼ぶときと同じく、このUnityPlayerActivityをUnity側から呼んでやればアクセスできそうです。

Unity

ということでまずはUnity側。

PluginCtrl.cs

using UnityEngine;

namespace Assets.Scripts
{
    public class PluginCtrl : MonoBehaviour
    {
        private readonly string PluginClassPath = "jp.masanori.plugintest.UnityPlayerActivity";

        public void RequestFilePathList()
        {
            using (var androidJavaClass = new AndroidJavaClass(PluginClassPath))
            {
                androidJavaClass.CallStatic("requestFilePathList");
            }
        }
    }
}

このような関数を、例えばボタンが押されたなどのタイミングで呼ぶことにします。

MainCtrl.cs

using UnityEngine;
using UnityEngine.UI;

namespace Assets.Scripts
{
    public class MainCtrl : MonoBehaviour
    {
        public GameObject PluginCtrlObject;
        public Button ImageButton;
        private PluginCtrl pluginCtrl;

        private void Start ()
        {
            pluginCtrl = PluginCtrlObject.GetComponent();
            ImageButton.onClick.AddListener(OnImageButtonClicked);
        }

        private void OnImageButtonClicked()
        {
            pluginCtrl.RequestFilePathList();
        }
    }
}

AndroidManifest.xml

< ?xml version="1.0" encoding="utf-8"? >
< manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.masanori.plugintest"
    xmlns:tools="http://schemas.android.com/tools"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0" >
    < permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/ >
    < permission android:name="android.permission.READ_EXTERNAL_STORAGE" / >
    < supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/ >
    < application
        android:theme="@style/UnityThemeSelector"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true" >
        < activity android:name=".UnityPlayerActivity"
                  android:label="@string/app_name" >
            < intent-filter >
                < action android:name="android.intent.action.MAIN" / >
                < category android:name="android.intent.category.LAUNCHER" / >
            < /intent-filter>
            < meta-data android:name="unityplayer.UnityActivity" android:value="true" / >
        < /activity >
    < /application >
< /manifest >

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Apk にあるAndroidManifest.xmlをベースにしていますが、UnityProject側に入れる必要は無いかもしれません。

Android

AndroidProjectとして出力したあと、UnityPlayerActivity.javaを編集します。

UnityPlayerActivity.java

package jp.masanori.plugintest;

import com.unity3d.player.UnityPlayer;
import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

public class UnityPlayerActivity extends Activity
{
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code


    private static void requestFilePathList(){
        // TODO: 画像ファイルへのアクセス.
    }

    // Setup activity layout
    @Override protected void onCreate (Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        getWindow().setFormat(PixelFormat.RGBX_8888); // <--- This makes xperia play happy

        mUnityPlayer = new UnityPlayer(this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }
~省略~

これでUnity側でボタンが押されたときに「requestFilePathList()」が呼ばれるようになるので、あとは通常のAndroidアプリと同じようにassetsフォルダの画像にアクセスすればOKです。

もし画像をUnity側から操作したい場合は、Filesフォルダなどに画像をコピーする必要があるかもしれません。

DataBindingを使う

エクスポート後のProjectは通常のAndroidアプリのProjectとほぼ同じです。

なので、DataBindingを使うこともできます。

まずはLayoutを作成します。

native_button_layout.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android" >
< RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    < Button
        android:text="@string/native_button_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_marginStart="65dp"
        android:layout_marginTop="82dp"
        android:id="@+id/native_button" / >
< /RelativeLayout >
< /layout >

DataBindingを有効にします。

build.gradle

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

allprojects {
   repositories {
      flatDir {
        dirs 'libs'
      }
   }
}

apply plugin: 'com.android.application'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}
android {
    compileSdkVersion 25
    buildToolsVersion '25.0.0'

    defaultConfig {
        targetSdkVersion 25
    }

    lintOptions {
        abortOnError false
    }
    signingConfigs { release {
        storeFile file('C:/Users/masanori/.android/debug.keystore')
        storePassword 'android'
        keyAlias 'androiddebugkey'
        keyPassword 'android'
    } }

    buildTypes {
        debug {
            jniDebuggable true
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
            signingConfig signingConfigs.release
        }
    }
    dataBinding {
        enabled = true
    }
}

UnityのViewに追加します。

~省略~

import jp.masanori.plugintest.databinding.NativeButtonLayoutBinding;

public class UnityPlayerActivity extends Activity
{
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    private static void requestFilePathList(){
        // TODO: 画像ファイルへのアクセス.
    }
    // Setup activity layout
    @Override protected void onCreate (Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        getWindow().setFormat(PixelFormat.RGBX_8888); // <--- This makes xperia play happy

        // DataBindingを取得してmUnityPlayerに追加.
        NativeButtonLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.native_button_layout);
        mUnityPlayer.addView(binding.getRoot());

        mUnityPlayer = new UnityPlayer(this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }

ポイントとしてはDataBindingはmUnityPlayerに追加する、ということでしょうか。

「setContentView(mUnityPlayer);」の後にDataBindingの処理を行うと、Unity側の画面が消えてネイティブ側の画面のみが表示され、悲しい気持ちになります。

また、下記はプラグイン作成時の注意ではありますが、今回の場合も気をつけた方が良いかもしれません。

おわりに

iOSのネイティブプラグインは以前からUnityのビルド後に追加する形であったため、Androidでも同じようにできる、というのは便利ですね。

ただ多くの場合jarファイルのように一つにファイルがまとまることがないため、複数プロジェクトで使いまわしする場合などは不便かも?

そもそもUnity側で解決できるならそちらでやったほうが…。

とまぁ微妙なところもありますが、適材適所、便利に問題に対応していくのが良いかと思います。

参考