本記事は、 Android Advent Calendar 2021 17日目の記事です。
PureなAndroidというよりUnity組み込むという飛び道具的な話
Unity as a Libraryとは
拡張現実(AR)、3D/2D リアルタイムレンダリング、2D ミニゲームなど、Unity で開発した機能を、ネイティブモバイルアプリケーションに直接挿入。
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は残しておきます。
UnityプロジェクトのExport
File > Build Settings
を開き、Platform
の Android
を選択されていることを確認します(UnityマークがAndroidの欄にない場合は、 Switch Platform
を押す)
IL2CPPの設定
PlayerSettings
を押して、 Setting for Android > Other Settings
を開き Configuration > Scripting Backend
を Mono から IL2CPP に変えておきます。
変えると Configuration > Target Architecture
の ARM64
にチェックできるので、チェックを入れます。
Build Settings
にもどり、Export Project
にチェックを入れてExport
を押すと、UnityからGradleプロジェクトが書き出されます。(ちょっと時間かかる)
UnityEditorでやる作業はここまでです。
unityLibraryをインポート
サンプルアプリのプロジェクトとして UaaLSample
というプロジェクトを用意した。プロジェクト直下に先程 ExportされたunityLibraryフォルダをコピーする。
Android Studioを開き以下の修正を加える。
:unityLibrary
という名前のモジュールをAndroid Studioが認識できるにように、settings.gradle
に追加する。
include ':app', ':unityLibrary'
:app
で利用できるようにdependencies
に追加。Unity関連のクラスファイルを使えるように jar
ファイルを追加する。
dependencies {
implementation project(':unityLibrary')
implementation fileTree(dir: project(':unityLibrary').getProjectDir().toString() + ('\\libs'), include: ['*.jar'])
...
}
:unityLibrary
側の AndroidManifest.xml
から以下の項目を削除する。昔はホーム画面にUnityだけ起動するアイコンが追加されたが、今は違うみたいでビルドが通らない。
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
NDKを使ってUnity側のソースもAndroid Studio側でビルドされるため、NDKの場所を明記する。
ndk.dir=/Users/XXXXXXXXXXXX/Library/Android/android-ndk-r21d
UnityPlayerをViewに挿入する
上半分にUnityで描画(SurfaceViewを子に持つFrameView)されるViewを置いた画面を用意した。
<?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
は呼び出す必要がある。
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が描画を開始し、スプラッシュ画面を表示した後に、先程作成したシーンが表示された。
最後に
手軽に3Dグラフィックをアプリに実装するには、OpenGLESを素で書くより断然簡単!
※ ただ、プロジェクトがかさばったり、ビルドプロセスが少し増えたり、端末のリソースをUnityのMainThreadに割いたりするので、多少のデメリットもある。
※ Android アドベントカレンダーなのに単語としてUnityの出現頻度が多いなぁという気持ちになった。
※ サンプルのアプリの3Dが全然リッチじゃないので更新したい。