Unity as a LibraryでAndroidに組み込む時にaarを使う
概要
Unityで全部の画面を完結するわけではなく、AndroidやiOSなどの通常のアプリの一部だけでUnityの画面を使う、という試みは広く使われています。
- アプリ起動速度の高速化
- ネイティブUIを使った文字入力を使う
- Unityでリッチなグラフィックスを分担
などのメリットがあることから(アプリの複雑さを検討したうえで価値があるなら)Unity as a Libraryを採用することもあります。
参考:Unity as a Libraryをあなたのプロジェクトにおいて採用すべきか
この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サンプルを見てみましょう。
ここにあるように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を実現するパッケージです。
ドキュメントも(ラフですが)書かれています
Discussion