Unity as a Library 再入門 (Android編: Unity 6.3 + Jetpack Compose)
この記事はREALITY Advent Calendar 2025の12/9の記事として書きました。
他の記事もよろしければどうぞ!
はじめに
Unityをライブラリのように扱い、Nativeアプリの一部のViewとして組み込むことができる、Unity as a Library(UaaL)について、改めてゼロから試してみました。
UaaLについて、「興味はあるけど試したことがない」みたいな人にとっての第一歩の手助けになれば幸いです。
repository
本エントリの内容・成果物は、以下のリポジトリで公開しています。将来、iOS編やaar/xcframework運用についての追記を予定しております。
対象読者
- Unityを触ったことがあり、MonoBehaviourとHierarchyをなんとなく知っていて、Android PlatformでUaaLを試したい人
- Android Studioを触ったことがあり、Jetpack Composeをなんとなく知ってる人
確認環境
- Unity 6000.3.0f1
- Android Studio Otter | 2025.2.1 Patch 1
- macOS Squoia 15.7.2
Unity 6.3 のインストールと球が落ちてくるSceneの実装
「球が落ちてくるScene」の完成系
はじめに、「球が落ちてくるScene」の完成系をお見せします。「3Dで何か動いている」「(Unity)SendMessageベースのインタラクティブ性がある」ことを目的として適当にこさえたものです。

球が落ちてきます。ボタンを押すと増やせます。
Unity 6.3 のインストール
- Unity Hubをダウンロードし、インストールします
- 「Install Editor」から Unity 6.3 LTSをインストールします
- 執筆時点では6000.3.0f1が最新でした。おそらく最新で問題ないですが、何かあった際にはバージョンをあわせるとトラブルを回避できるかもしれません。

install時、または後からAdd ModuleでAndroid Build Supportを追加
新規3Dプロジェクトの作成
- Projectsタブ(?)を選択後、「New Project」ボタンから新規3Dプロジェクトを作成しましょう
- Editor Versionは6000.3.0f1、Templatesは「Universal 3D」を選択
- organizationはなんでもいいです。Project nameは"SphereLibrary"、プルダウンでCreate new local Projectを選択。
- Locationは"uaal-example"というフォルダをつくって選択。
- 配下に「SphereLibrary」Unity Projectディレクトリが配置されるパス構成で本エントリは記載します
- 最後に「Create Project」を選択します

「Univarsal 3D」の新規Unity local projectを作成
不要な初期ファイルの削除と、球が落ちてくるSphere生成Main Sceneの作成
細かいですが、Scene作成について操作ひととおりを以下に書きます。
- 「Universal 3D」テンプレートプロジェクトの直下の
READMEを開くとサンプルを削除できるボタンがInspectorに出ますので、選択し、不要な初期ファイルを削除しちゃってください。 - Assets配下に新しく"SphereLibrary"というフォルダを作成し、その直下に"Main"という名前のSceneを作成します。
- SphereLibrary/Materials配下に"WhiteMaterial", "RedMaterial", "BlueMaterial"という名前で白・赤・青のMaterialを作成します。
- SpehreLibrary/Prefabs配下に前述の3 Materialを適用したSphere、"WhiteSphere", "RedSphere", "BlueSphere"という名前の白球・赤球・青球のPrefabを適当に作成します。Rigidbodyコンポーネントも忘れずにつけてください。
- Main SceneのHierarchyのルート直下に、"MessageReceiver"、"SphereSpawner"という名前のGameObjectを配置し、後述のScriptをアタッチ、それぞれScriptとPrefabの参照を設定します。
- Sphereのスポーン元になる
SphereSpawnerの高さ、Y座標は10あたりにしてください。
- Sphereのスポーン元になる
- 生成されたSphereの受け口であるPlane、白い床も適当に配置してください。

"MessageReceiver" GameObjectとScriptコンポーネント、Materialsフォルダの様子

"SphereSpawner" GameObjectとScriptコンポーネント、Prefabsフォルダの様子
SceneとScriptの設定が正常に完了していれば、Sceneを再生すると1秒に1回スポーンされた白い球が落ちてきて、MessageReceiverに設定したコンポーネントのエディタ拡張のボタンより、白・赤・青の球を追加でスポーンさせることができるはずです。

Main SceneとScript設定後の再生例
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace SphereLibrary
{
public class MessageReceiver : MonoBehaviour
{
// Messageの送信先:Sphere Spawner
[SerializeField] private SphereSpawner sphereSpawner;
/// <summary>
/// NativeからUnitySendMessage
/// </summary>
/// <param name="message"></param>
private void OnMessage(string message)
{
Debug.Log("message=" + message);
sphereSpawner?.Spawn(message);
}
#if UNITY_EDITOR
/// <summary>
/// エディタ拡張としてNativeからくるMessageを擬似発生させるUIを生成する
/// </summary>
[CustomEditor(typeof(MessageReceiver))]
public class MessageReceiverEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var receiver = target as MessageReceiver;
if (GUILayout.Button("SendMessage(\"white\")"))
{
receiver?.OnMessage("white");
}
if (GUILayout.Button("SendMessage(\"red\")"))
{
receiver?.OnMessage("red");
}
if (GUILayout.Button("SendMessage(\"blue\")"))
{
receiver?.OnMessage("blue");
}
}
}
#endif
}
}
using UnityEngine;
namespace SphereLibrary
{
/// <summary>
/// SphereをSpawnする
/// </summary>
public class SphereSpawner : MonoBehaviour
{
/// <summary>
/// Spawn対象のWhite Sphere Prefab
/// </summary>
[SerializeField] private GameObject whiteSpherePrefab;
/// <summary>
/// Spawn対象のRed Sphere Prefab
/// </summary>
[SerializeField] private GameObject redSpherePrefab;
/// <summary>
/// Spawn対象のBlue Sphere Prefab
/// </summary>
[SerializeField] private GameObject blueSpherePrefab;
/// <summary>
/// Spawn interval
/// </summary>
[SerializeField] private float interval = 1.0f;
private float _time;
private void Update()
{
// interval間隔でSpawn
_time += Time.deltaTime;
if (_time > interval)
{
Spawn(whiteSpherePrefab);
_time = 0;
}
}
/// <summary>
/// 指定した色のSphereをSpawnする
/// </summary>
/// <param name="color"></param>
public void Spawn(string color)
{
if (string.IsNullOrEmpty(color) || color == "white")
{
Spawn(whiteSpherePrefab);
}
else if (color == "red")
{
Spawn(redSpherePrefab);
}
else if (color == "blue")
{
Spawn(blueSpherePrefab);
}
}
private void Spawn(GameObject spherePrefab)
{
if (spherePrefab == null)
{
return; // do nothing
}
// SphereをSpawn
var sphere = Instantiate(spherePrefab, transform.position, Quaternion.identity);
// ランダムな回転初速を設定してやることでいい感じに転がるようにする
var rb = sphere.GetComponent<Rigidbody>();
if (rb != null)
{
rb.angularVelocity = Random.insideUnitSphere * 3.0f;
}
}
}
}
ここまで、Unity側の実装を少し細かく説明してきましたが、正直なところ、Nativeアプリに組み込んで動かすUnityプロジェクトとしてはどんなものでもかまいません。
このUnity実装としては、 "MessageReceiver"というGameObjectがHierarchy上に存在し、あとでNativeから(Unity)SendMessageでメッセージを送る先となるメソッド"OnMessage"が存在している が肝であることを覚えておいてください。後ほど説明します。
Project Settings
必須ではないのですが、ちょっとした設定をProject Settingsで加えておきます。
- Resolution And Presentation -> Resolution -> Run Without FocusをチェックON
- UnityPlayerが動いているかどうかが確認しやすくなります
- UaaLとしてpause/resumeの制御は明示的に行うのが望ましく、その際にも必須のチェックです
- Publish Settings -> Build -> Custom Main ManifestをチェックONしたのち、AndroidManifest.xmlの<activity>エントリは2つとも削除
- UaaLとしてNativeに組み込む際には、不要なLauncher iconがインストール時に出てきてしまうので削除しちゃって良いです
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
</application>
</manifest>
Androidビルド(ライブラリプロジェクトのエクスポート)
Unityとして「3Dのそれっぽい絵が動く」Sceneができたところで、Androidビルド(ライブラリとしてのAndroid StudioプロジェクトのExport)に移りましょう。
Build Profiles設定とビルド(Export)
- Build Profiles(Command + shift + b) を開きます
- 1 Scene Listから、"SphereLibrary/Main" SceneのみがScene Listに入ってる状態にします
- 2 Platformsから、「Android」を選択し、Switch Platformを選択します
- 3 「Export Project」にチェックを入れ、「Export」を選択します
- 通常、apkをビルドする「Build」になっているボタンが、「Export」に変化し、Android Studioプロジェクトを生成、Exportするボタンになります。
- 4 Export先パスの指定ダイアログにて、「New Folder」を選択し、直下に"SphereAndroidLibrary"フォルダを作成します。
- 以後、このフォルダをAndroid Platformビルド先フォルダとして毎回指定するものとします。(gitignoreの対象にします)

Scene ListにはMain Sceneのみを設定

AndroidにSwitch Platform

Export Projectにチェックを入れて、Exportを実行

出力先フォルダとして"SphereAndroidLibrary"をNew Folderで作成して、指定
ビルドに成功すると、指定したフォルダが開きます。失敗時には、Consoleウィンドウの赤エラー文言などを確認の上、リトライしましょう。

sharedフォルダとunityLibraryフォルダをコピーして使います
Android Studioのインストールと新規Composeプロジェクトの準備
Android Studioのインストール
- Android Studioをインストールします
- https://developer.android.com/studio
- トップページからはStable最新へのリンクが貼られていますのでそれをインストールします
新規Composeプロジェクトの準備
- New Project -> Empty Activityを選択
- Nameは "AndroidUaaLSphere"を入力、Package nameはなんでもいいです。Save locationはUnityプロジェクトの隣に並ぶように".../uaal-example/AndroidUaaLSphere"を入力します。

Empty Activityを選択
gradle syncが完了後、Run appを選択します。エミュレータでも実機でもHello Android!と表示されればプロジェクトの準備としては完了です。
UnityライブラリのAndroidプロジェクトへの登録
先ほどビルド(Export)したUnityライブラリをAndroidプロジェクトで扱えるようにします。
Unityビルド成果のコピー
まず、Unityビルド先の「SphereAndroidLibrary」フォルダ配下の「shared」「unityLibrary」2フォルダを、Androidプロジェクト「AndroidUaaLSphere」フォルダ直下にコピーします。

「shared」と「unityLibrary」フォルダをコピー
settings.gradle
次に、Android Studio側の設定ファイルを編集していきます。settings.gradle.ktsの末尾に:unityLibraryinclude行を追加します。
:
rootProject.name = "AndroidUaaLSphere"
include(":app")
include(":unityLibrary")
gradle.properties
続けて、必要なpropertyをgradle.propertiesの末尾に追加します。
Unity 6.0では1行で済んでましたが、Unity 6.3ではSdkPathとNdkPathが必要そうなのでさらに追加します(パス情報はUnityでビルドされた直下のgradle.propertiesから持ってきています)。
/ApplicationsあたりのパスからしてMac固有のため、他の環境でビルドする際にはよしなに読み替えてください。
:
# Unity 6000.0.58f2 requires adding the following 1 line. (from Unity 2020)
unityStreamingAssets=
# Unity 6000.3.0f1 also requires adding 2 lines from the exported build.gradle.
unity.androidSdkPath=/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/AndroidPlayer/SDK
unity.androidNdkPath=/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/AndroidPlayer/NDK
build.gradle
app moduleからUnityLibrary moduleを参照するために依存情報を追加
dependencies {
:
(中略)
:
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation(project(":unityLibrary"))
// ktsでは project(":unityLibrary").getProjectDir と書けなかったので相対パスで解決
implementation(fileTree(project.projectDir.resolve("../unityLibrary/libs")))
}
gradle syncと Run 'app'でとくにエラーが出ていなければ、ここまでは成功です。
ComponentActivityからUnityPlayerを扱う
MainActivity.kt
下準備が終わったところで、ComponentAcitivityからUnityPlayerを扱う部分を実装していきましょう。突然ですがまず全ソースコードをはじめにばーんと紹介し、追って、抜粋しつつ説明を加えていきます。
package app.reality.androiduaalsphere
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import app.reality.androiduaalsphere.ui.theme.AndroidUaaLSphereTheme
import com.unity3d.player.UnityPlayer
import com.unity3d.player.UnityPlayerForActivityOrService
class MainActivity : ComponentActivity() {
private lateinit var unityPlayer: UnityPlayerForActivityOrService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
unityPlayer = UnityPlayerForActivityOrService(this)
// FrameLayoutからComposeに載せ替えるために不要な親FrameLayoutからremoveしておく
unityPlayer.frameLayout?.removeView(unityPlayer.view)
enableEdgeToEdge()
setContent {
AndroidUaaLSphereTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
UnityViewComposable(unityView = unityPlayer.view)
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding),
onButtonClicked = { label -> unitySendMessage(label) }
)
}
}
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
unityPlayer.windowFocusChanged(hasFocus)
}
override fun onResume() {
super.onResume()
unityPlayer.onResume()
}
fun unitySendMessage(message: String) {
UnityPlayer.UnitySendMessage("MessageReceiver", "OnMessage", message)
}
}
@Composable
fun UnityViewComposable(unityView: View) {
AndroidView(factory = { unityView }, update = { view -> })
}
@Composable
fun Greeting(
name: String,
modifier: Modifier = Modifier,
onButtonClicked: (String) -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp), verticalArrangement = Arrangement.Top
) {
Text("Hello Compose $name")
Spacer(Modifier.size(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
listOf("white", "red", "blue").forEach { label ->
Button(onClick = { onButtonClicked(label) }) {
Text(label)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AndroidUaaLSphereTheme {
Greeting("Android")
}
}
Unityライブラリとしての設定が正常に完了していれば、Composeのサンプルとして生成されたMainActivity.ktの中身をまるっと差し替えて、以下のようなアプリが動作するはずです。

white/red/blueのComposeなButtonを押すたびにSphereが降ってきます
UnityPlayerのインスタンス化と親剥がし
unityPlayer = UnityPlayerForActivityOrService(this)
// FrameLayoutからComposeに載せ替えるために不要な親FrameLayoutからremoveしておく
unityPlayer.frameLayout?.removeView(unityPlayer.view)
UnityPlayerのインスタンス化についてはUnity 2023.1で変更が加えられており、通常のActivity向けUnityPlayerForActivityOrServiceとGameActivity向けUnityPlayerForGameActivityに枝分かれしました。
親FrameLayoutからのremoveについては、Android公式の「Using Views in Compose」に基づいて、AndroidViewとしてUnityのView(実体はSurfaceView)を別の親にぶら下げるためにremoveしています。
removeをしておかないとjava.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.と怒られます。
windowFocusChangedとonResume
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
unityPlayer.windowFocusChanged(hasFocus)
}
override fun onResume() {
super.onResume()
unityPlayer.onResume()
}
このへんはミニマム動かすためのおまじないです。実際のアプリケーションではよしなにPause/Resumeを実装する必要があります。
UnitySendMessageによるNative -> Unityの実現
fun unitySendMessage(message: String) {
UnityPlayer.UnitySendMessage("MessageReceiver", "OnMessage", message)
}
このコードが前述した「UnityにおけるHierarchy上の "MessageReceiver" という名前をもつ GameObject の"OnMessage"メソッド 」にメッセージ文字列を送信します。
このコードでは"white","red","blue"といった文字列を送信し、色に対応したSphereをUnity側でスポーンさせています。
余談: 「Views in Compose」 or 「Compose in Views」
Compose UIと従来のxml Viewの共存には、親子関係によって2種類の方法があります。
- 公式docs「Using Views in Compose」
- 公式docs「Using Compose in Views」
Unity(Unity as a Libraryとして使おうとした際も同様です)は、旧来のxml viewでできています(FrameLayoutの中にSurfaceViewをもっています)。そのため、Composeとの同居でいうと本エントリで試している「Views in Compose」は少々無理をしており、「Compose in Views」の方が無難と考えられます。
もう少し複雑な構成のUIを試してみたり、他の情報が手に入った際には、「こういうのは動いた」「こういうのはダメだった」あたりを、随時追記できたらいいなと考えております。
それでは良きUaaLライフを!メリークリスマス!
Appendix
strings.xmlへの"game_view_content_description"リソースの追加は不要になりました
UnityPlayerクラスから参照されていてクラッシュしていたgame_view_content_description文字列リソース追加はUnity 6.3では不要になっていました。Unity 6.0では必要だったのでいつのまにか追加された模様です。

unityLibrary内、strings.xmlに追加されてました
Discussion