🐙

Unity as a LibraryでAndroidに組み込む時にaarを使う

に公開

概要

Unityで全部の画面を完結するわけではなく、AndroidやiOSなどの通常のアプリの一部だけでUnityの画面を使う、という試みは広く使われています。

  • アプリ起動速度の高速化
  • ネイティブUIを使った文字入力を使う
  • Unityでリッチなグラフィックスを分担

などのメリットがあることから(アプリの複雑さを検討したうえで価値があるなら)Unity as a Libraryを採用することもあります。

参考:Unity as a Libraryをあなたのプロジェクトにおいて採用すべきか
https://zenn.dev/mirrativ_blog/articles/f1e9dc9e55d43c

このUnity as a Libraryを使うとき、問題になるのがビルドの複雑化です。

iOSについてはxcframeworkを用いる方法が広く(!?)使われています https://qiita.com/mao_/items/4da81d6b57ea1a0fe382

この記事ではUaaLのUnity側のビルド成果をAndroidにおけるxcframework的な立ち位置であるaarにすることを試みます。

通常のUaaL(Android)のビルド手法、あるいはなぜaarを採用するのか

通常のUaaL(Android)の作り方についてUnity公式の(ちょっと古いですがUnity2022対応の)UaaLサンプルを見てみましょう。
https://github.com/Unity-Technologies/uaal-example/blob/uaal-example/22LTS/docs/android.md

ここにあるようにExport ProjectしたandroidBuildフォルダを直接NativeAndroid App内で

include ':unityLibrary'
project(':unityLibrary').projectDir=new File('..\\UnityProject\\androidBuild\\unityLibrary')

このようにmodule として取り込んでいます。

この仕組みは公式推奨なだけあって、多くの採用例があります。

一方でUnity部分をmoduleから更にまとめてaarにビルドした上でAndroidアプリに取り込む場合は

  • フォルダ丸ごとではなく単一のファイル参照で済むので簡潔
  • Androidアプリ側のビルド時間の短縮が見込める
  • Androidアプリ開発者のOSが分散している環境でも問題がない

などのメリットがありそうです。

補足:Androidアプリ開発者のOSが分散している環境でも問題がない について

たとえばUnityエンジニアがWindowsでUnityEditorからAndroid Moduleとしてexoportしたフォルダは、中を覗くとgradle関連やdllがwindows向けになっています。

これをMacや他のOSを使っているAndroidアプリ開発者が単純にビルドしようとするとビルドに失敗します。

逆のパターンも同様にビルドに失敗します。つまりUaaLのプロジェクトでAndroidを扱うときは開発者のOSを統一しておく必要がありそうと言えます。

実際のビルドステップを見てみる

たとえば こんな感じにのプロジェクトだったら以下のように書きます。雰囲気で読んでください。

プロジェクト構造の例

project-root/
├── .github/
│   └── workflows/
│       └── build-android-aar.yml
├── unity-impl/                    ← Unity as Library プロジェクト
│   ├── Assets/
│   │   ├── Scripts/
│   │   ├── Scenes/
│   │   └── ...
│   ├── Packages/
│   ├── ProjectSettings/
│   ├── Library/
│   └── build/
│       └── android/              ← Unityビルド出力 (Android)
│           ├── unityLibrary/
│           │   └── build/
│           │       └── outputs/
│           │           └── aar/
│           │               └── unityLibrary-release.aar
│           ├── launcher/
│           ├── build.gradle
│           └── settings.gradle
├── android-impl/                  ← Android Native プロジェクト
│   ├── app/
│   │   ├── src/
│   │   │   └── main/
│   │   │       ├── java/
│   │   │       ├── res/
│   │   │       └── AndroidManifest.xml
│   │   ├── libs/
│   │   │   └── unityLibrary-release.aar  ← ビルドスクリプトによりコピー
│   │   └── build.gradle.kts
│   ├── gradle/
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── gradlew.bat
├── ios-impl/                      ← iOS Native プロジェクト
│   ├── ios-impl.xcodeproj/
│   ├── ios-impl/
│   │   ├── AppDelegate.swift
│   │   ├── ViewController.swift
│   │   ├── Info.plist
│   │   └── UnityFrameworks/       ← Unity iOS ビルド出力配置予定
│   └── Podfile
├── README.md
├── .gitignore
└── LICENSE

ビルドスクリプトの例


namespace UnityBuilderAction
{
    public static class BuildScript
    {
        /// <summary>
        /// Unity as Library用のAndroidビルドを実行
        /// </summary>
        public static void BuildAndroidModule()
        {
        // do something
        }
    }
}
Github Actionsの一例(細かい解説は省略)
name: Build Android AAR

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]
  workflow_dispatch:

env:
  UNITY_VERSION: 2022.3.22f1
  GRADLE_VERSION: '7.6'
  NDK_VERSION: r23b

jobs:
  build:
    name: Build Unity as Library AAR
    runs-on: ubuntu-latest
    
    steps:
      # リポジトリのチェックアウト
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          lfs: true
      
      # Git LFS Pull
      - name: Git LFS Pull
        run: git lfs pull

      # Unityライブラリのキャッシュ
      - name: Cache Unity Library
        uses: actions/cache@v4
        with:
          path: unity-impl/Library
          key: Library-${{ hashFiles('unity-impl/Assets/**', 'unity-impl/Packages/**', 'unity-impl/ProjectSettings/**') }}
          restore-keys: Library-

      # Unityのビルド (Unity as Library)
      - name: Build Unity Project as Library
        uses: game-ci/unity-builder@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
        with:
          projectPath: unity-impl
          unityVersion: ${{ env.UNITY_VERSION }}
          targetPlatform: Android
          buildMethod: UnityBuilderAction.BuildScript.BuildAndroidModule
          versioning: None

      # Java環境のセットアップ
      - name: Set up JDK 11
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '11'
      
      # Gradleのセットアップ
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3
        with:
          gradle-version: ${{ env.GRADLE_VERSION }}

      # Android NDKのセットアップ
      - name: Setup Android NDK
        uses: nttld/setup-ndk@v1
        id: setup-ndk
        with:
          ndk-version: ${{ env.NDK_VERSION }}
          add-to-path: true
          link-to-sdk: true
      
      # Verify NDK installation
      - name: Verify NDK installation
        run: |
          echo "NDK installed at: ${{ steps.setup-ndk.outputs.ndk-path }}"
          # Set environment variables for subsequent steps
          echo "ANDROID_NDK_ROOT=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
          echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV

      # Fix Unity build directory permissions
      - name: Fix Unity build permissions
        run: |
          # Fix ownership of Unity build directory
          sudo chown -R $USER:$USER ./unity-impl/build/android
          
          # Create necessary Gradle directories with proper permissions
          mkdir -p ./unity-impl/build/android/.gradle/${{ env.GRADLE_VERSION }}/fileChanges
          mkdir -p ./unity-impl/build/android/.gradle/vcs-1
          mkdir -p ~/.gradle/caches
          mkdir -p ~/.gradle/wrapper
          
          # Set proper permissions recursively
          chmod -R 755 ./unity-impl/build/android
          chmod -R 755 ~/.gradle
          
          # Ensure gradle wrapper directory exists
          mkdir -p ./unity-impl/build/android/gradle/wrapper

      # Gradle Wrapperの生成とビルド
      - name: Build AAR with Gradle
        working-directory: ./unity-impl/build/android
        env:
          ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
          GRADLE_USER_HOME: ~/.gradle
          GRADLE_OPTS: "-Dfile.encoding=utf-8 -Xmx2g"
        run: |
          # Generate Gradle Wrapper with specific version
          gradle wrapper --gradle-version=${{ env.GRADLE_VERSION }} --distribution-type=bin
          chmod +x ./gradlew
          
          # Verify Gradle version and NDK setup
          ./gradlew --version
          echo "Using NDK at: $ANDROID_NDK_HOME"
          
          # Fix NDK path in Unity-generated build.gradle files
          echo "Fixing NDK paths in build.gradle files..."
          find . -name "build.gradle" -type f | while read gradle_file; do
            # Unity-specific NDK path replacement (this is the effective pattern)
            sed -i 's|/opt/unity/Editor/Data/PlaybackEngines/AndroidPlayer/NDK|$System.env.ANDROID_NDK_HOME|g' "$gradle_file"
          done
          
          # Verify NDK installation
          echo "Verifying NDK installation..."
          if [ -f "$ANDROID_NDK_HOME/ndk-build" ]; then
            echo "✓ NDK is properly installed at: $ANDROID_NDK_HOME"
          else
            echo "✗ NDK not found at: $ANDROID_NDK_HOME"
          fi
          
          # Build AAR with daemon disabled to avoid permission issues
          ./gradlew clean :unityLibrary:assembleRelease --no-daemon --stacktrace

      # AARファイルをandroid-impl/app/libsにコピー (Windows build scriptと同様の処理)
      - name: Copy AAR to android-impl
        run: |
          mkdir -p android-impl/app/libs
          find ./unity-impl/build/android -name "unityLibrary-release.aar" -exec cp {} android-impl/app/libs/ \;
          echo "AAR file copied to android-impl/app/libs/"
          ls -la android-impl/app/libs/

      # AARファイルのアップロード
      - name: Upload AAR artifacts
        uses: actions/upload-artifact@v4
        with:
          name: unity-aar-${{ github.sha }}
          path: android-impl/app/libs/*.aar
          retention-days: 30
      
      # リリースタグがある場合、リリースに追加
      - name: Create Release
        if: startsWith(github.ref, 'refs/tags/v')
        uses: softprops/action-gh-release@v2
        with:
          files: android-impl/app/libs/*.aar
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ここで大切なのは ./gradlew clean :unityLibrary:assembleRelease --no-daemon --stacktrace を行ってaarとしてビルドする部分です。

こうして出来たaarファイルを良い感じにAndroidアプリ側で取り込みましょう。

Androidアプリ側で設定すること

出来上がったunity部分のaarファイルをAndroidアプリ側で認識するために以下のように設定します

関係があるファイルを抜き出し

android-impl/
├── app/
│   ├── libs/
│   │   └── unityLibrary-release.aar  ← Unity Player AAR file (さっき作ったやつ)
│   └── build.gradle.kts              ← App-level configuration
├── settings.gradle.kts               ← Project-level repository settings
└── build.gradle.kts                  ← Root project configuration

リポジトリ設定 (settings.gradle.kts)

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        // Local libs directory for Unity AAR
        flatDir {
            dirs("app/libs")
        }
    }
}

依存関係の設定 (app/build.gradle.kts)

dependencies {
    // Unity Library from AAR file - using flatDir repository
    implementation(group = "", name = "unityLibrary-release", version = "", ext = "aar")

    // Additional Unity dependencies (JARs if any)
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    
    // ...other dependencies
}

じゃあ、結局どちらを使うと良いか

Androidプロジェクト側のNDKとUnity側のNDKバージョンに差異があったときにまともに動く方を採用してください。
という身も蓋もない結論になります。

地獄:Unity側で使っているNative Library重複

libopus.soとかoboe.soとかlibc++_shared.soがUnityとAndroid両方で使っていて、しかもバージョンが違うときに気付きやすいのは従来手法です。

ガチャガチャAndroid側でunityLibrary/build.gradle を編集して、packagingOptions、ndkVersion、abiFilters、consumerProguardFiles、依存関係、リソース衝突回避などを その場で調整できます。(*.soを除外して試行錯誤できます)

aarでまとめている場合はpackagingOptions で pickFirst / exclude を頑張るか、packagingOptions で pickFirst / exclude の芸当をやる必要があります。
AARの中の jni/<abi>/*.so は触れない(触るなら作り直し)のでbuild.gradle に書ける回避策が アプリ側に寄りがちです。

参考事例:FlutterのUaaLサンプルはUnityをaar化してる

最近活発に開発されているFlutter上でUnity as a Librartyを実現するパッケージです。
https://pub.dev/packages/flutter_embed_unity

ドキュメントも(ラフですが)書かれています
https://github.com/learntoflutter/flutter_embed_unity/wiki/Advanced:-AAR-compilation-for-Android

Discussion