1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは!!
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へ依存を追加します。

build.gradle.kts
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を置いています。
スクリーンショット 2025-12-07 14.59.55.png

次に、KMP公式サイトを参考にしてUnityFrameworkをKMPで扱えるようにします。

build.gradle.kts
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で、共通部分を定義します。

Platform.kt
@Composable
expect fun UnityComposable()

このようにexpect/actualでComposable関数を定義することで、Android、iOSで個別に定義したComposable関数を使用することが出来ます。

そして、App.ktで使用して共通UIと合わせて表示します。

App.kt
@Composable
fun App() {
    MaterialTheme {
        Box(
            modifier = Modifier
                .fillMaxSize()
        ) {
            // Unityを表示するComposable
            UnityComposable()

            // 共通UIのComposable
            // 省略
        }
    }
}

Androidの実装

Androidでは、AndroidViewを使用してUnityのシーンを表示させます。

Platform.android.kt
@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クラスとして実装しようと考えましたが、うまく出来ませんでした。

test.swift
@main
struct iOSApp: App {
    
    init(){
        // swift側でUnityの起動を行います
        Unity.shared.start()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

KotlinでもUnityクラスを作成します。
本来であれば、Swift側でもこのKotlinクラスを利用すれば良いと思いましたがうまく実行出来ませんでした。
今回は、Unityのviewを使用するために利用します。

Unity.kt
@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を利用します。

Platform.ios.kt
@Composable
actual fun UnityComposable() {
    UIKitView(
        factory = {
            UnityHolder.view
        },
        modifier = Modifier.fillMaxSize()
    )
}

完成

ちょっとUnityのシーンが違いますが、Android,iOSで表示出来ました!!

Android

iOS

感想

今回は、KMPでUaaLを利用できるか検証して見ました。
結果は、Composeでも表示することは出来ましたが、メリットをあまり感じることが出来ませんでした。
ネイティブの要素を深く使用しているので、共通化する苦労がありました。
技術的には、面白いですが業務で使用できるかと考えると難しそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?