はじめに
こんにちは!!
Androidエンジンになりたい、藤さんです。
今回は、KMPでUnity as a Library(UaaL)を使えるのか検証して行きたいと思います。
本記事の内容
- KMPプロジェクトにUaaLを追加
- Androidでの表示
- iOSでの表示
本記事は、KMPとUaaLで遊ぶことがテーマです。
記載するコードや実装方法は、すべての環境で動作することを保証していません。
検証環境
- AndroidStudio:
Otter 2025.2.1 - Xcode:
26.1.1 - Unity:
6000.2.10f1 - Kotlin:
2.2.20 - composeMultiplatform:
1.9.1
Unity as a Libraryとは
Unity as a Libraryとは、Unityで開発した3D/2DレンダリングやAR機能などを、Android、iOSのライブラリとして実行させる機能です。
これを使用することで、3DアバターやARなどのアニメーションを簡単に実装することが出来ます。
Unity as a Libraryについて
UaaLの準備
まずは、Unityのシーンを作成しましょう。
下記の記事を参考にして、Android、iOS用のビルドを作成します。
Android
このサイトを参考にして、Androidプロジェクトを作成して下さい。
iOS
このサイトを参考にして、UnityFrameworkを作成して下さい。
KMPプロジェクトへの追加
早速、KMPプロジェクトにUaalを追加していきましょう!!
追加の手順ですが、AndroidとiOSそれぞれでUaaLを追加する必要があります。
Androidプロジェクトへの追加
基本的には、通常のAndroidプロジェクトと同様に追加して使用することが出来ます。
まず、rootディレクトリにunityLibraryを追加します。
そして、composeAppのbuild.gradle.ktsに依存関係を追加する。
androidMainへ依存を追加します。
sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
// ↓ unityLibraryを追加
implementation(projects.unityLibrary)
implementation(fileTree(File(rootProject.projectDir, "unityLibrary/libs")) {
include("*.jar")
})
}
commonMain.dependencies {
// 省略
}
commonTest.dependencies {
// 省略
}
}
iOSプロジェクトへの追加
まず、iosAppにUnityFrameworkを追加します。
今回は、FrameworksディレクトリにUnityFramework.frameworkを置いています。

次に、KMP公式サイトを参考にしてUnityFrameworkをKMPで扱えるようにします。
listOf(
iosArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
freeCompilerArgs += listOf(
"-Xbinary=bundleId=com.fujisue.koki.composeapp"
)
}
iosTarget.compilations.getByName("main") {
val unityFramework by cinterops.creating {
// .def ファイル
definitionFile.set(
project.file("src/nativeInterop/cinterop/UnityFramework.def")
)
includeDirs("${project.rootDir}/iosApp/Frameworks/UnityFramework.framework")
// コンパイル時の Framework 参照
compilerOpts(
"-framework", "UnityFramework",
"-F${project.rootDir}/iosApp/Frameworks" // ←フレームワークの親ディレクトリ
)
}
}
iosTarget.binaries.all {
linkerOpts(
"-framework", "UnityFramework",
"-F${project.rootDir}/iosApp/Frameworks/UnityFramework.framework"
)
}
}
これで、 KMPからUnityFrameworkを扱えるになります。
以上でAndroidとiOSへの導入は完了です。
Unityシーンの表示
今回は、ComposeでUIを共通化したいと考えています
KMPの実装
expectで、共通部分を定義します。
@Composable
expect fun UnityComposable()
このようにexpect/actualでComposable関数を定義することで、Android、iOSで個別に定義したComposable関数を使用することが出来ます。
そして、App.ktで使用して共通UIと合わせて表示します。
@Composable
fun App() {
MaterialTheme {
Box(
modifier = Modifier
.fillMaxSize()
) {
// Unityを表示するComposable
UnityComposable()
// 共通UIのComposable
// 省略
}
}
}
Androidの実装
Androidでは、AndroidViewを使用してUnityのシーンを表示させます。
@Composable
actual fun UnityComposable() {
val lifecycleOwner = LocalLifecycleOwner.current
val configuration = LocalConfiguration.current
val context = LocalContext.current
val unityPlayer = remember {
UnityPlayerForActivityOrService(context as Activity)
}
SideEffect {
unityPlayer.configurationChanged(configuration)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> unityPlayer.onStart()
Lifecycle.Event.ON_RESUME -> unityPlayer.onResume()
Lifecycle.Event.ON_PAUSE -> unityPlayer.onPause()
Lifecycle.Event.ON_STOP -> unityPlayer.onStop()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
unityPlayer.destroy()
}
}
AndroidView(
factory = {
unityPlayer.frameLayout.apply {
viewTreeObserver.addOnWindowFocusChangeListener { hasFocus ->
unityPlayer.windowFocusChanged(hasFocus)
}
}
}
)
}
iOSの実装
iOS側の実装は、間違っている可能性が大きいです。
自分自身もなぜ、動作しているのか完全には理解できていません。
参考にする際は、注意して下さい。
このサイトを参考にして、Unity.swiftを作成します。
はじめは、Unity.swiftをKotlinクラスとして実装しようと考えましたが、うまく出来ませんでした。
@main
struct iOSApp: App {
init(){
// swift側でUnityの起動を行います
Unity.shared.start()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
KotlinでもUnityクラスを作成します。
本来であれば、Swift側でもこのKotlinクラスを利用すれば良いと思いましたがうまく実行出来ませんでした。
今回は、Unityのviewを使用するために利用します。
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
@ExportObjCClass
class Unity :
NSObject(),
UnityFrameworkListenerProtocol{
private val unityFramework: UnityFramework
init {
val bundlePath = NSBundle.mainBundle.bundlePath
val frameworkPath =
(bundlePath as NSString).stringByAppendingPathComponent("Frameworks/UnityFramework.framework")
val bundle = NSBundle.bundleWithPath(frameworkPath)
?: error("UnityFramework.framework が見つかりません: $frameworkPath")
if (!bundle.loaded) {
bundle.load()
}
val frameworkClass = bundle.principalClass() as UnityFrameworkMeta
val framework = frameworkClass.getInstance()
?: error("UnityFramework.getInstance() が null を返しました")
if (framework.appController() == null) {
framework.setExecuteHeader(_mh_execute_header.ptr)
}
unityFramework = framework
}
fun start() {
unityFramework.registerFrameworkListener(this)
memScoped {
val bundleId = "com.unity3d.framework".cstr.ptr
unityFramework.setDataBundleId(bundleId)
}
unityFramework.runEmbeddedWithArgc(
argc = 0,
argv = null,
appLaunchOpts = null
)
}
val view: UIView
get() {
val appController = unityFramework.appController()
?: error("Unity appController が初期化されていません。Unity.start() したか確認してください。")
return appController.rootView ?: error("Unity rootView が null です")
}
}
val UnityHolder: Unity by lazy { Unity() }
作成した、UnityHolderを使用してUIViewを利用します。
@Composable
actual fun UnityComposable() {
UIKitView(
factory = {
UnityHolder.view
},
modifier = Modifier.fillMaxSize()
)
}
完成
ちょっとUnityのシーンが違いますが、Android,iOSで表示出来ました!!
Android
iOS
感想
今回は、KMPでUaaLを利用できるか検証して見ました。
結果は、Composeでも表示することは出来ましたが、メリットをあまり感じることが出来ませんでした。
ネイティブの要素を深く使用しているので、共通化する苦労がありました。
技術的には、面白いですが業務で使用できるかと考えると難しそうです。