1
/
5

【TECH BLOG】Unityを組み込んだiOSアプリにおける、UXも考慮した開発

こんにちは、ZOZO NEXTで新規プロダクトの開発を担当している木下です。先日、3Dバーチャル試着に関する実証実験の取り組みが発表されました。3Dバーチャル試着ではユーザーが入力した体型データを基に3Dアバターが作成され、好みのアイテムを選んで着丈やサイズ感を確認できます。


ZOZOSUITで蓄積した体型計測データを活用!次世代のバーチャル試着体験アプリが登場 | 株式会社ZOZO NEXT
〜 ZOZO NEXT、ソフトバンク、MNインターファッションの3社共同で3Dバーチャル試着に関する実証実験を実施 〜 株式会社ZOZO NEXT(本社:千葉市稲毛区、代表取締役:金山 裕樹、以下「ZOZO NEXT」)、ソフトバンク株式会社(本社:東京都港区、代表取締役 社長執行役員 兼 CEO:宮川 ...
https://zozonext.com/news/20220221_virtualfitting


この実証実験のために開発したアプリは、Unity as a Library(UaaL)という技術を利用して実装されています。今回はUaaLをiOSアプリに組み込むにあたって工夫した点を、UX観点も交えながらご紹介します。

Unity as a Libraryとは

Unity as a Library(UaaL)はUnityのARや3D/2Dのリアルタイムレンダリングといった機能をネイティブアプリに組み込むことができる技術です。Unityの2019.3.0a2から導入されたもので、これによってUnityをネイティブアプリの一部として公式に組み込めるようになりました。



画像のキューブや背景と青枠内のボタンがUnityによるもの、赤枠内のボタンがネイティブアプリによるものです(サンプルプロジェクトより)。

背景

3Dシミュレーション技術は、パートナー企業からUnityのSDKとして提供されました。Unityを用いたiOSアプリの開発に当たっては、今回のような(1)UaaLを用いる方法と(2)Unityのみを用いる方法の2つがあります。今回はUXを担保するためにAppleのHuman Interface Guidelinesに則るという方針のもと、(1)の手法を採用しました。

UXを考慮すると、シームレスにUnityを組み込むことが重要になります。今回のバーチャル試着では、お客様ひとりひとりの体型を反映したアバターに、リアルタイムシミュレーションで服を着装します。これはモバイルアプリとしては比較的重い処理であり、負荷によってはUXに大きく関わります。これらの課題に対して、以下のような工夫をしました。

  1. Unityのロードに若干時間がかかる→AppDelegateでUnity呼び出す
  2. Unityとネイティブの画面切り替えが不自然→UnityのWindowからViewだけを利用する
  3. Unityの負荷によってネイティブのアニメーションが不安定になる→Unityを一時停止する
  4. Unityとネイティブでのデータのやりとりが複雑→Unityとのやりとりを一方向にする
  5. UnityのBuild後の設定が複数あって手間になる→Build後の設定を自動化する

UaaLをSwiftで利用するに当たって

UaaLを使うに当たって、Swiftで実装したい方が多いかと思います。しかしながら、公式のサンプルプロジェクトUnity-Technologies/uaal-exampleはObjective-Cで書かれています。幸い先人のおかげで様々な日本語記事が充実しています。私もこれらの記事を大いに参考にさせていただきました。


Unityを組み込んだiOSアプリにおける、UXも考慮した開発 - ZOZO TECH BLOG
こんにちは、ZOZO NEXTで新規プロダクトの開発を担当している木下です。先日、3Dバーチャル試着に関する実証実験の取り組みが発表されました。3Dバーチャル試着ではユーザーが入力した体型データを基に3Dアバターが作成され、好みのアイテムを選んで着丈やサイズ感を確認できます。 この実証実験のために開発したアプリは、 Unity as a Library ...
https://techblog.zozo.com/entry/unity-as-a-library-ux#:~:text=%E3%81%84%E3%81%9F%E3%81%A0%E3%81%8D%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82-,qiita.com,-note.com


Unityを組み込んだiOSアプリにおける、UXも考慮した開発 - ZOZO TECH BLOG
こんにちは、ZOZO NEXTで新規プロダクトの開発を担当している木下です。先日、3Dバーチャル試着に関する実証実験の取り組みが発表されました。3Dバーチャル試着ではユーザーが入力した体型データを基に3Dアバターが作成され、好みのアイテムを選んで着丈やサイズ感を確認できます。 この実証実験のために開発したアプリは、 Unity as a Library ...
https://techblog.zozo.com/entry/unity-as-a-library-ux#:~:text=qiita.com-,note.com,-Unity%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE


Unityクラスの実装

工夫を1つ1つ説明する前に、UaaLをネイティブアプリのプロジェクトから利用する方法について説明します。UaaLはUnityFrameworkというObjective-CのClassから操作することができます。そのクラスを呼び出しやすくするため、以下のようにUnity.swiftというクラスをシングルトンオブジェクトとして実装します。


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)
    }

    // UnityのWindowからViewだけを返す
    var view: UIView {
        unityFramework.appController()!.rootView!
    }

    // ネイティブ側からUnityのメソッドを呼び出す
    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を呼び出す

簡易に計測したところ、Unity起動時のロードには0.2-0.3秒かかります。これを任意のタイミングで呼び出すと、ロードしている間は真っ暗な画面が表示されます。軽微であるとは言え、UXに関わる部分です。そこで、AppDelegateの application(_:didFinishLaunchingWithOptions:) の中で呼び出すこととしました。こうすることで、ネイティブアプリのスプラッシュ画面が表示されているタイミングでUnityをロードでき、不要な画面遷移を減らすことができます。


import Firebase
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Unityを呼び出す
        Unity.shared.application(application, didFinishLaunchingWithOptions: launchOptions)

        // 最初に表示する画面を呼び出す
        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のロードを検討しました。しかしその方法では、スプラッシュ画面が表示されたあと、Unityをロードする真っ暗な画面が表示されました。

結果的に、起動時間そのものは変わらないものの、不要な画面遷移を減らしスプラッシュ画面1つにまとめるという方法に落ち着きました。

UnityのWindowからViewだけを利用する

UaaLの仕組みとしては、ネイティブ(ホスト)側のiOSアプリのUIWindowとは別に、Unity側でUIWindowを生成しています。ホスト側からUnity側のWindowに切り替える際には、前述したUnityFrameworkの showUnityWindow という関数を呼び出す必要があります。この関数はアニメーションもなく、単にUnityのUIWindowをアプリの最前面に表示する仕様となっています。

一方で今回のアプリでは、NavigationControllerによるプッシュ遷移に組み込む必要がありました。そのため、Unity側のWindowからViewだけを呼び出し、アプリの画面を表示しているViewControllerにaddSubViewするという方法を取りました。

UnityのWindowのViewにアクセスできるようプロパティを実装しました。先ほどの、Unity.swiftから抜粋しています。


var view: UIView {
    unityFramework.appController()!.rootView!
}


ホスト側ViewController(HostViewController)への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
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // addSubView
        view.addSubview(unityView)
        // 追加したsubViewのサイズをViewControllerのViewのサイズに合わせる
        unityView.frame = view.bounds
        // 追加したsubViewを背面へ(addSubViewは最前面に追加するため、ViewControllerのViewの後ろに設定する必要がある)
        view.sendSubviewToBack(unityView)
    }
    ...
}


実際の画面は画像のようになり、アバターと背景からなるUnityの画面の前に、ネイティブ側で実装したボタンやリストなど(赤枠で囲った部分)を配置しています。

続きはこちら

株式会社ZOZOからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社ZOZOでは一緒に働く仲間を募集しています
2 いいね!
2 いいね!

同じタグの記事

今週のランキング

株式会社 ZOZOさんにいいねを伝えよう
株式会社 ZOZOさんや会社があなたに興味を持つかも