TL;DR
Swiftで新規開発を行なっているiOSアプリに、Unityを組み込んだ経験をまとめます。
公式のサンプルではObjective-Cでの実装のみである中で、Swiftでの実装事例の1つとなれば幸いです。
また自分の探し方の問題かもしれませんが、Unity as a Library (UaaL)をSwiftのiOSアプリに組み込む英語の資料があまり見つかりませんでした。何かおすすめがありましたら教えていただけると幸いです!
ポイント
- Unityの起動時にローディングの時間がかかるので、アプリを立ち上げた時点でUnityを起動しバックグラウンドでローディング
- アプリの画面遷移をNavigationControllerで実現したかったため、UnityのViewControllerからViewのみを呼び出す
- 今回のアプリでは実装のメインがSwiftで、背景としてUnityを使うという方針にしたため、iOSアプリ→Unityの呼び出しのみ実装
参考にした記事
[Swift] Unity as a LibraryをSwiftから呼ぶ
主にこちらの記事を参考にしながら、iOSアプリのViewControllerからUnityのViewControllerのViewを呼び出す実装を進めました。
Unity as a LibraryのサンプルプロジェクトをSwiftで書き直した
UaaLのサンプルをSwiftで書き換えてくださっているので、全体像を把握するために大いに活用させていただきました。
(今回、Unityの読み込み方法などがこちらとは異なります)
その他の便利な参考はこちら
実装
前提
アプリのメインはSwiftで実装を行い、背景画像としてUnityの3Dモデルを読み込むようなアプリです。
また、メインのSwiftからUnityの関数を呼び出すことも行いました。
iOSアプリ設計パターン入門
開発に当たっては、こちらの書籍を参考にMVPのアーキテクチャパターンを選択しました。
アプリ起動時にUnityを呼び、バックグラウンドでローディング
Unity Classの作成
様々な場所から呼び出せるよう、Unityをシングルトンオブジェクトとして実装します
import Foundation
class Unity: NSObject, UnityFrameworkListener {
static let shared = Unity()
private let unityFramework: UnityFramework
override init() {
let bundlePath = Bundle.main.bundlePath
let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: frameworkPath)!
if !bundle.isLoaded {
bundle.load()
}
// It needs disable swiftlint rule due to needs for unwrapping before calling super.init()
// swiftlint:disable:next force_cast
let frameworkClass = bundle.principalClass as! UnityFramework.Type
let framework = frameworkClass.getInstance()!
if framework.appController() == nil {
var header = _mh_execute_header
framework.setExecuteHeader(&header)
}
unityFramework = framework
super.init()
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) {
unityFramework.register(self)
unityFramework.setDataBundleId("com.unity3d.framework")
unityFramework.runEmbedded(withArgc: CommandLine.argc,
argv: CommandLine.unsafeArgv, appLaunchOpts: launchOptions)
}
var view: UIView {
unityFramework.appController()!.rootView!
}
func sendMessageToUnity(objectName: String, functionName: String, argument: String) {
unityFramework.sendMessageToGO(withName: objectName, functionName: functionName, message: argument)
}
func applicationWillResignActive(_ application: UIApplication) {
unityFramework.appController()?.applicationWillResignActive(application)
}
func applicationDidEnterBackground(_ application: UIApplication) {
unityFramework.appController()?.applicationDidEnterBackground(application)
}
func applicationWillEnterForeground(_ application: UIApplication) {
unityFramework.appController()?.applicationWillEnterForeground(application)
}
func applicationDidBecomeActive(_ application: UIApplication) {
unityFramework.appController()?.applicationDidBecomeActive(application)
}
func applicationWillTerminate(_ application: UIApplication) {
unityFramework.appController()?.applicationWillTerminate(application)
}
}
AppDelegateから呼び出す
アプリが立ち上がったタイミングでUnityを呼び出します。こうすることで、バックグラウンドでローディングされるため、スプラッシュ画面がユーザーに表示されず、待ち時間も発生しません。
新規のアプリなので、パフォーマンスなどにも今のところは影響ありません。
import Firebase
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
// Unityを呼び出す
Unity.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
// Unityとは関係のない、sign inの画面
let singInViewController = SignInViewController(nibName: nil, bundle: nil)
let navigationController = UINavigationController(rootViewController: singInViewController)
let model = SignInModel()
let presenter = SignInPresenter(view: singInViewController, model: model)
singInViewController.inject(presenter: presenter)
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
return true
}
}
UnityのViewControllerからViewのみを呼び出す
Integrating Unity into native iOS applications
Unity as a Libraryの仕組みとしては、メイン(ホスト)のiOSアプリのUIWIndowとは別に、Unity側でUIWindowを生成しています。そしてホスト側からUnity側のWindowに切り替える際には、showUnityWindow
というUnityFrameworkの関数を呼び出す必要があります。(UIApplecation内に、複数のUIWindowが存在していて、最前面のUIWindowがアプリの画面として表示されているということになります。)
つまりUnityの画面をiOSアプリ内で表示する公式な方法としては、UnityのUIWindowを最前面に出すということになります。
一方で今回は、アプリの画面遷移をNavigationControllerで実現する必要がありました。
その実現に当たって、以下の2つの方法を検討しました。
a. Unity側のWindowからViewだけを呼び出し、アプリの画面を表示しているViewControllerにaddSubViewする
b. Unity側のWindowを後ろに置いたまま、アプリの画面を表示しているViewControllerの背景を透明にすることで、Unity側のWindowを背景にする
結論としては、a.を選択しました。やってみればすぐに分かることですが、b.のやり方だと、NavigatioControllerのアニメーションに対して、背景のUnity側Windowは動きません。つまり、NavigationControllerのアニメーションと連動せず違和感が発生することになるためです。
Unity ClassのViewにアクセスできるようプロパティを実装
先ほどの、Unity.swiftから抜粋。
...
var view: UIView {
unityFramework.appController()!.rootView!
}
...
ホスト側ViewControllerへのaddSubViewと、そのsubViewを背面へ移動
import UIKit
class HostViewController: UIViewController {
// UnityのViewの読み込み
private let unityView = Unity.shared.view
private var presenter: HostPresenterInput!
func inject(presenter: HostPresenterInput) {
self.presenter = presenter
}
init() {
super.init(nibName: nil, bundle: nil)
// addSubView
view.addSubview(unityView)
// 追加したsubViewのサイズをViewControllerのViewのサイズに合わせる
unityView.frame = view.frame
// 追加したsubViewを背面へ(addSubViewは最前面に追加するため、ViewControllerのViewの後ろに設定する必要がある)
view.sendSubviewToBack(unityView)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
}
iOSアプリ→Unityの呼び出し (Calling C# back from native code)
iOSアプリからUnityに対して、データの受け渡しや関数の呼び出しを行います。
メインの開発はSwiftを用いたiOSアプリの開発で、Unityは背景として読み込むという方針を取りました。そのため、Unity→iOSのやりとりは発生しません。
今回は、簡易なUnitySendMessage
を選択しました。
Building plug-ins for iOS (Calling C# back from native code)
補足
今回Unity→iOSアプリの呼び出しが存在しなかったため、次の手順にあるNativeCallProxy.h
のヘッダーファイルは存在しません。
Integrating Unity as a library into standard iOS app (5. Expose NativeCallProxy.h)
UnityFrameworkのメソッドを呼び出す
今回MVPのアーキテクチャパターンを選択したため、Modelからの呼び出しになっています。
import Foundation
protocol HostModelInput {
func sendData(data: [String?])
}
final class HostModel: HostModelInput {
func sendData(data: [String?]) {
// 引数はStringなので、それに合わせる
let dataText =
"""
{\"data0\": \(data[0]!), \"data1\": \(data[1]!), \"data2\": \(data[2]!),
\"data3\": \(data[3]!), \"data4\": \(data[4]!), \"data5\": \(data[5]!)}
"""
// UnityFrameworkdのメソッドを呼び出す
Unity.shared
.sendMessageToUnity(objectName: "SampleData", functionName: "SetData", argument: dataText)
}
}
その他の参考
iOSアプリ設計パターン入門
開発に当たっては、こちらの書籍を参考にMVPのアーキテクチャパターンを選択しました。(選択の過程をまとめた記事はこちら→PoCでiOSアーキテクチャにMVPを採用した話)
iOSアプリ開発に限らず、設計パターンを学びたい方に損得抜きで強くおすすめです。歴史的経緯も含めた設計パターンの丁寧な解説、さらに各設計パターンのサンプルコードで構成されています。
【Unity】MirrativのEmbedding Unityを更新した話: 実践 Unity as a Library
【Unity】Mirrativのアバターがなんで動いているのか誰にもわからないので説明する
実際にサービスを運用されている事例から、実践的な説明がされています。
2本目はUnity as a Library (UaaL)が実装される前に、iOSアプリにembedを行なった方法を説明されています。
(Unityに詳しい先輩曰く、「黒魔術のようなものなので、今はUaaLを使うべき」とのことでした!)
Integrating Unity into native iOS applications
公式のドキュメントで、iOSアプリから呼び出せるメソッドが説明されています。
こぼれ話
Using Unity as a Library in other applications
Unity as a Library is intended for specialist users who use native platform technologies such as Java/Android, Objective C/iOS, or Windows Win32/UWP, and want to include Unity-powered features in their games or applications.
Unity as a Libraryは、Java/Android、Objective C/iOS、Windows Win32/UWPなどのネイティブプラットフォーム技術を使用し、ゲームやアプリケーションにUnity搭載の機能を搭載したいと考えている専門家ユーザーを対象としています。
公式の紹介ページには、上記の脅し文句があります。iOSアプリ開発自体が初めての自分は最初少しひるみましたが、これ以外に方法がないので意を決して取り組みました。確かにUnityがiOSアプリの中でどのように読み込まれるのか、ライフサイクルにどのように組み込まれるべきなのかなど理解に時間がかかった部分もあります。しかしながら、この記事内で紹介している記事を一通り読んでいただければ、全体像は掴めるのではないかと思います。Unityをネイティブアプリに組み込みたくなった方は、不可能なことではないのでぜひ挑戦してみてください!また、不明点などがあれば一緒に学ばせていただければと思いますので、コメントください。