AndroidアプリにUnity as a Libraryでリッチな3Dを簡単に組み込む

本記事は、 [Android Advent Calendar 2021](https://qiita.com/advent-calendar/2021/android) 17日目の記事です。
PureなAndroidというよりUnity組み込むという飛び道具的な話

# Unity as a Libraryとは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/acc135c3-17e0-4228-21f1-4be213987f11.png)


> 拡張現実（AR）、3D/2D リアルタイムレンダリング、2D ミニゲームなど、Unity で開発した機能を、ネイティブモバイルアプリケーションに直接挿入。

[Unity as a Library](https://unity.com/ja/features/unity-as-a-library) はゲームエンジンであるUnityで開発した機能を、モジュールとしてAndroidアプリケーションへ組み込むことが出来る仕組みである。 従来の3Dグラフィックのように、自分で1から`OpenGLES`などのグラフィックライブラリを使って描画することは無い。そのため、簡単に3D表現をアプリに組み込むことができる。

なぜ組み込みができるかというと、UnityからAndroid向けに吐き出されるアプリケーションが次のような構成になっているからです。

> もともと Unity は昔から、エンジン部分をライブラリとしてアプリケーション本体から切り離すような構成をしていました。具体的に Android では、エンジンのエントリーは libmain.so 、ビューとして SurfaceView （本体はVulkanあるいはGLSL）、ラッパーとしての UnityPlayer（extends FrameLayout）があり、これを使うためのアプリケーションとして MainActivity がある、という構成です。
>
*from https://tech.mirrativ.stream/entry/2020/10/20/100000
【Unity】MirrativのEmbedding Unityを更新した話: 実践 Unity as a Library*

**Android側から見ると簡単に3D描画できるSurfaceView(GLSurcefaceView)として開発ができる。**

# Unityで作ったシーンをAndroidアプリに組み込む
以下の環境で作業をしていきます。

- MacBook Pro (13-inch, M1, 2020) MacOS Big Sur 11.5.1（20G80）
- Unity 2021.2.0b9
- Android Studio Arctic Fox 2020.3.1 Patch 2

※ `Rosetta 2`を使わないarm64対応のラインナップです。

## Unityのシーンを用意する
Sampleアプリケーションとして、Cubeを原点(position X:0 Y:0 Z:0)に置いたシーンを用意しました。加えて、Unityを象徴する青と灰色のSkyBoxは残しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/aaeff723-0ac2-bc75-3b6c-d67737aa704c.png)

## UnityプロジェクトのExport
`File > Build Settings` を開き、`Platform` の `Android` を選択されていることを確認します(UnityマークがAndroidの欄にない場合は、 `Switch Platform` を押す)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/61aa1ce3-4177-1627-ca92-1355db69b021.png)

### IL2CPPの設定
`PlayerSettings` を押して、 `Setting for Android > Other Settings` を開き `Configuration > Scripting Backend` を **Mono** から **IL2CPP** に変えておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/a5011734-8e4d-2974-3c50-8f6fc03559d5.png)

変えると `Configuration > Target Architecture` の `ARM64` にチェックできるので、チェックを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/3fbd3b7a-144a-8885-f705-985cf0acce8c.png)

`Build Settings`にもどり、`Export Project`にチェックを入れて`Export`を押すと、UnityからGradleプロジェクトが書き出されます。(ちょっと時間かかる)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/f1f44d83-9bfc-48ff-4d49-95adb3799e5e.png)

UnityEditorでやる作業はここまでです。

## unityLibraryをインポート
サンプルアプリのプロジェクトとして `UaaLSample` というプロジェクトを用意した。プロジェクト直下に先程 **ExportされたunityLibraryフォルダをコピーする**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/b3cbfdab-742d-7f7b-d1c4-4c323f0ffd5a.png)

Android Studioを開き以下の修正を加える。

`:unityLibrary` という名前のモジュールをAndroid Studioが認識できるにように、`settings.gradle` に追加する。

```gradle:settings.gradle
include ':app', ':unityLibrary'
```

`:app`で利用できるように`dependencies`に追加。Unity関連のクラスファイルを使えるように `jar`　ファイルを追加する。

```gradle:build.gradle(app)
dependencies {
    implementation project(':unityLibrary')
    implementation fileTree(dir: project(':unityLibrary').getProjectDir().toString() + ('\\libs'), include: ['*.jar'])
    ...
}
```

`:unityLibrary` 側の `AndroidManifest.xml` から以下の項目を削除する。昔はホーム画面にUnityだけ起動するアイコンが追加されたが、今は違うみたいでビルドが通らない。

```xml:AndroidManifest.xml
<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
```

NDKを使ってUnity側のソースもAndroid Studio側でビルドされるため、NDKの場所を明記する。

```gradle:loacl.properties
ndk.dir=/Users/XXXXXXXXXXXX/Library/Android/android-ndk-r21d
```

## UnityPlayerをViewに挿入する

上半分にUnityで描画(SurfaceViewを子に持つFrameView)されるViewを置いた画面を用意した。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:ignore="HardcodedText">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/unity"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/unity">

        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button"/>

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button"/>

        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button"/>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
```

基本的には、UnityPlayerをインスタンス化してaddViewするだけである。
しかし、それだけではUnity側が動かないみたいなので、`requestFocus`、`onWindowFocusChanged`、`onResume`は呼び出す必要がある。

```kotlin:MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import com.unity3d.player.UnityPlayer

class MainActivity : AppCompatActivity() {

    var unityPlayer: UnityPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        unityPlayer = UnityPlayer(this)
        window.clearFlags(1024)

        findViewById<ConstraintLayout>(R.id.unity)?.addView(
            unityPlayer, ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        )

        unityPlayer?.requestFocus()
    }

    // Notify Unity of the focus change.
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        unityPlayer?.windowFocusChanged(hasFocus)
    }

    override fun onResume() {
        super.onResume()
        unityPlayer?.resume()
    }
}
```
アプリが起動し、MainActivityのViewが表示されたところで、Unityが描画を開始し、スプラッシュ画面を表示した後に、先程作成したシーンが表示された。
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/80b33d8d-0d0b-e45e-9e10-d7648e3f3f5f.gif)


<!--Unity側のManifestの影響を受けるので、PlayerSettingsで調整していると良い-->
<!-- Unity側にAndroid Nativeからイベントを送る -->

# 最後に
手軽に3Dグラフィックをアプリに実装するには、OpenGLESを素で書くより断然簡単!

※ ただ、プロジェクトがかさばったり、ビルドプロセスが少し増えたり、端末のリソースをUnityのMainThreadに割いたりするので、多少のデメリットもある。
※ Android アドベントカレンダーなのに単語としてUnityの出現頻度が多いなぁという気持ちになった。
※ サンプルのアプリの3Dが全然リッチじゃないので更新したい。