Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Flutterを活用してUnity製アプリの表現をさらに広げよう!

Unity製のiOSアプリにFlutterを組み込む

本連載は、iOS/Android向けのアプリでUIの表現力を高めることを目標に、Unity製アプリにFlutterを導入した例を具体的な実装方法を交えながら紹介する記事の3回目となります。

前回の記事ではUnityからExportしたAndroidプロエジェクトに対してFlutterを組み込む方法を解説しました。

3回目となる今回はUnity製のiOSプロジェクトにFlutterを組み込む解説となります。

前回までのおさらい

まず今回の本題であるUnityから出力されたiOSプロジェクトにFlutterを入れる方法を解説する前に前回までの話しをおさらいします。

前回までの記事で、

  1. なぜUnity製アプリにFlutterを入れる必要があるのか
  2. アプリ、Unity、Flutterの関係性
  3. 今回の記事の内容を実行している環境の紹介
  4. FlutterのModuleプロジェクトの作成
  5. UnityからExportされたAndroidプロエジェクトにFlutterを追加する

以上のことを解説しています。

今記事は前回までの記事を読んでいることを前提に解説を進めますので、まだ読まれていないかたはぜひそちらを読んでください。

Unity製のAndroidアプリにFlutterを組み込む

Unity製iOSアプリにFlutterを組み込む

では具体的な実装の解説に入っていきます。

簡略的にはなりますが、手順としては以下となります。⁠※Flutterプロジェクトは前回の記事で作成している前提で進めます)

  1. UnityプロジェクトからXcodeプロジェクトをBuild
  2. XcodeプロジェクトにPodfileを追加しxcworkspaceを作成
  3. iOS起動時にFlutterEngineを初期化しViewを作成

前回の記事でも説明しましたが、ここから以下のようなフォルダ構成で進めていきます。

CocoaPods実行時にパスでFlutterプロジェクトを参照しているのでフォルダ構成が違うとうまくビルドできないので気をつけてください。

root(任意のworkフォルダ)/
 ├ unity/
 ├ module/
 └ builds
    └ ios/

unity:  Unityプロジェクトフォルダ。プロジェクト名は任意で問題なし。
module: Flutterプロジェクトフォルダ。CocoaPods実行時にパス参照されているので名前は設定と一致させる必要がある。
builds: UnityからのBuildしたプロジェクトを配置。

前回のAndroidの記事でも紹介しましたが、基本的にはFlutterの公式に書かれていることをUnityプロジェクト向けにカスタマイズして実装を進めて行きます。

1. UnityプロジェクトからXcodeプロジェクトをBuild

Unity側は基本的にデフォルトの設定で問題ありませんが、Identificationの設定は各自でお願いします。

iOS設定
図

BuildSettingsでPlatformをiOSに変更しBuildを実行してください。

Build時に出力先を指定する必要があるため上述したroot/builds/ios を指定してください。

Build
図

2. XcodeプロジェクトにPodfileを追加しxcworkspaceを作成

XcodeプロジェクトにFlutterの依存を追加するためにCocoaPodsを使用し xcworkspace を作成します。

podコマンドが必要なためインストールしていなければ CocoaPods のインストールを行ってください。

この記事で使用しているversionです。

$ pod --version
1.15.2

Flutterの公式ページを参考にPodfileを用意します
https://docs.flutter.dev/add-to-app/ios/project-setup

以下の内容を Podfile という名前のファイルとして保存しroot/builds/ios/Podfile に配置し pod コマンドを実行します。

source 'https://cdn.cocoapods.org/'

platform :ios, '12.0'

flutter_application_path = '../../module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Unity-iPhone' do
  use_frameworks!
  use_modular_headers!
  install_all_flutter_pods(flutter_application_path)
end

post_install do |installer|
  flutter_post_install(installer) if defined?(flutter_post_install)
end
$ cd root/builds/ios
$ pod install

podコマンドを実行すると root/builds/ios/Unity-iPhone.xcworkspace が作成されるのでxcworkspaceをXcodeで開きます。

3. iOS起動時にFlutterEngineを初期化しViewを作成

Androidと同じようにUnityEngineの初期化コードを編集してFlutterEngineを初期化したいところではありますが、iOS側は少し複雑な手順を踏む必要があります。

まず注意すべきなのはPodfileで Unity-iPhone ターゲットにFlutterの依存を入れているところです。

UnityのXcodeプロジェクトには Unity-iPhoneUnityFramework という2つのターゲットが存在しており、Unityの処理は基本的にUnityFramework側に集中しています。

こう説明するとPodfileでUnityFramework側にFlutterの依存を追加すればよいのでは?と思う方もいるとは思います。

その場合はUnity-iPhone、UnityFramework両方にFlutterの依存を追加することでFlutterを動作させることが可能となります、シンプルなFlutterプロジェクトであれば恐らく問題は無いと思うのですがFlutter側にFirebaseなどのpackageを入れた場合に両方のターゲットにFirebaseが含まれている構成になってしまいFirebaseの初期化に失敗し正しく動作しないという問題が発生してしまいます。

そこでFlutterの依存をUnity-iPhone側だけに集中させ、Unity-iPhone側でFlutterEngineの初期化を行い、UnityFramework側から必要に応じてイベントをUnity-iPhone側に送りFlutterの処理を実行するという形で実装します。

余談となりますがAndroidプロジェクトでも同じ構成になっており launcher というプロジェクトに対して unityLibrary というライブラリを追加する構成になっています。

本体となるアプリ(android=launcher、ios=Unity-iPhone)に対してUnityライブラリ(android=unityLibrary、ios=UnityFramework)を追加し、本体側はアプリ起動時にUnityライブラリを呼び出すだけであとの処理はUnityライブラリ内部で行うことでアプリを動作させています。

この構成になっているからこそ既存のAndroid/iOSアプリに対してUnityをライブラリとして挿入する機能である Unity as a Library を実現できているのだと思われます。

UnityFrameworkからアプリイベントを通知

まずはUnityFramework側に存在するアプリイベントを取得する処理を追加します。

今回Targetをまたいでイベントをやり取りするために UNUserNotificationCenter を使用します。

Unity側のアプリ初期化処理は ios/Classes/UnityAppController.mm に記述されているのでこのファイルを編集してきます。

UNUserNotificationCenterを使用するためHeaderを追加

+#import <UserNotifications/UserNotifications.h>

アプリの起動完了時のイベントを通知

(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    ~~~

    [self initUnityWithApplication: application];
    
+    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+    [nc postNotificationName:@"unity2flutter" object:nil  userInfo:@{@"unity_initialized":@""}];

    return YES;
}

アプリの主要イベントに通知を追加

(void)applicationDidEnterBackground:(UIApplication*)application
{
    ::printf("-> applicationDidEnterBackground()\n");
    // 末尾に追加
+    [[NSNotificationCenter defaultCenter] postNotificationName:@"unity2flutter" object:nil userInfo:@{@"applicationDidEnterBackground":@""}];
}

(void)applicationWillEnterForeground:(UIApplication*)application
{
    // 省略

+    [[NSNotificationCenter defaultCenter] postNotificationName:@"unity2flutter" object:nil userInfo:@{@"applicationWillEnterForeground":@""}];
}

(void)applicationDidBecomeActive:(UIApplication*)application
{
    // 省略

+    [[NSNotificationCenter defaultCenter] postNotificationName:@"unity2flutter" object:nil userInfo:@{@"applicationDidBecomeActive":@""}];
}

(void)applicationWillResignActive:(UIApplication*)application
{
    // 省略

+    [[NSNotificationCenter defaultCenter] postNotificationName:@"unity2flutter" object:nil userInfo:@{@"applicationWillResignActive":@""}];
}

(void)applicationWillTerminate:(UIApplication*)application
{
    // 省略

+    [[NSNotificationCenter defaultCenter] postNotificationName:@"unity2flutter" object:nil userInfo:@{@"applicationWillTerminate":@""}];
}

UnityFrameworkからのイベントを受け取りFlutterEngineを管理するクラス追加

今回も全文記述すると長くなってしまうのでサンプルとなるファイルを添付します。

この2つのファイルを ios/MainApp/ にコピーしてください。

Xcodeのプロジェクトビューで右クリックのメニューから Add Files to "Unity-iPhone"... を選択しこの2つのファイルを選択し追加してください。

AddFiles
図

このとき追加するTargetが Unity-iPhone にチェックがついていることを確認してください。

SelectTaget
図

Unityにネイティブコードを追加する方法はUnityのAssets/Pluginsにファイルを追加する方法が一般的ですが、この方法で追加するとXcodeプロジェクトのUnityFrameworkターゲットにファイル参照が追加されることになります。

先ほど、Unity-iPhoneターゲットとUnityFrameworkターゲットについて少し説明しましたが、CocoaPodsによりUnity-iPhone側にFlutterの依存が追加されているためUnityFramework側にFlutterのコードを書いても参照できずエラーになってしまいます。

Flutterへの参照を必要とするFlutterControllerのコードはUnity-iPhoneターゲットから参照できるようにプロジェクトに追加する必要があります。

ポイントとなる処理を解説していきます。

NSNotificationCenterのイベントを購読

クラスの初期化時にNSNotificationCenterからのイベントを購読しています。

UnityAppController.mm で unity2flutter というキーを設定しイベントをpostしているので同じキーを使用して購読しています。

受け取ったイベントは unityNotification のメソッドに渡され処理されています。

// UnityAppConttoler.mmで "unity2flutter" というキーに対して通知をポストしているので受け取る
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unityNotification:) name:@"unity2flutter" object:nil];
FlutterEngine初期化

unity_initialized というイベントを受け取ったタイミングでFlutterEngineの初期化を行い FlutterViewController の表示を行っています。

- (void)initializeFlutter {
    // FlutterEngineのインスタンス作成
    flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
    
    // Flutter初期化時にエントリポイントとなるメソッドを指定
    [flutterEngine runWithEntrypoint:@"main" initialRoute:@"/"];

    ~~~
}
FlutterPluginのための設定
// FlutterのPlugin管理のためにEngineをセット
[GeneratedPluginRegistrant registerWithRegistry:flutterEngine];
FlutterViewControllerを作成
// FlutterEngineのインスタンを使いFlutterViewControllerを作成
flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
flutterViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
    
// UnityのRootViewに対してFlutterViewを表示
[rootViewController presentViewController:flutterViewController animated:NO completion:nil];
アプリのエントリポイントでFlutterControllerを初期化

先ほど追加したFlutterControolerを実際に初期化しFlutterを呼び出します。

ios/MainApp/main.mm にアプリ本体のエントリポイントとなる処理があるのでこのファイルを編集します。

+#include "FlutterController.h"

// 省略

int main(int argc, char* argv[])
{
    @autoreleasepool
    {
        id ufw = UnityFrameworkLoad();
+        // FlutterControllerの初期化
+        id _ = [[FlutterController alloc] init:ufw];
        [ufw runUIApplicationMainWithArgc: argc argv: argv];
        return 0;
    }
}

この状態でアプリをビルドするとUnityプロジェクトにFlutterを追加された状態でビルドされ、初期化時にFlutter表示を行っているので起動するとFlutterの画面が表示されるはずです!

UnityアプリでFlutterが起動している
図

ビルド自動化

前回のAndroidと同じく、UnityからiOSプロジェクトをExportしているのでUnity側を変更しビルドし直すたびにプロジェクトに変更がかかってしまいます。

なのでUnityプロジェクトにFlutterを組み込むにはビルドの自動化が非常に重要になってきますので自動化の知見も少し紹介します。

Unityで pod install を実行

上述したPodfileを別のパスに用意しておきビルドされたXcodeプロジェクトへのコピーとpod installの処理をUnityのPostProcessで実行します。

private const string PodfilePath = "{Podfileのパス}";
private const string PodPath = "{podコマンドのインストールパス}/pod";

[PostProcessBuild(50)]
public static void OnPostProcessPodInstall(BuildTarget buildTarget, string path)
{
    File.Copy(PodfilePath, $"{path}/Podfile", true);

    var info = new ProcessStartInfo
    {
        FileName = "bash",
        Arguments = $"-l -c '{PodPath} install'",
        UseShellExecute = false,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        CreateNoWindow = true,
        // コマンドを実行するディレクトリを指定
        WorkingDirectory = path,
        // コマンド実行時の環境変数を指定
        EnvironmentVariables =
        {
            ["LANG"] = "en_US.UTF-8",
        },
    };

    using var process = new Process();
    process.StartInfo = info;
    process.Start();

    process.WaitForExit();

    Debug.Log(process.StandardOutput.ReadToEnd());
}

Unityのコードを編集

Unityがビルドして作成したXcodeプロジェクトのコードを編集する必要があるのですが編集をうまく自動化する手が思いつかなかったのでC#で追加したい上の行の文字列を検索しその下にコードを入れるという地道なファイル編集しています(何かいい手があれば教えてほしいです……⁠⁠。

  • ios/Classes/UnityAppController.mm にFlutter側へ伝える通知処理を追加
  • ios/MainApp/main.mm にFlutterControllerの初期化処理を追加

XcodeプロジェクトにFlutterControllerクラスを追加

Unity側にXcodeプロジェクトの設定を変更するためにPBXProjectというクラスが存在しますがこのクラスではファイルの追加はできないようだったのでruby gemのxcodeprojを使用します。

FlutterController.hとFlutterController.mmをios/MainAppのフォルダに配置した状態で以下のコードをファイルに保存しrubyコマンドで実行すればXcodeにファイル参照が追加されます。

require 'xcodeproj'

def add_file(project, file_path)
  # すでに同じファイルが存在するかチェック
  absolute_path = File.expand_path(file_path)
  file_reference = project.reference_for_path(absolute_path)

  if file_reference.nil?
    file_ref = project.new_file(file_path)

    project.targets.each do |target|

      # Unity-iPhoneのターゲットにファイルを追加
      next unless target.name == 'Unity-iPhone'
      target.add_file_references([file_ref])
      puts "File reference for '#{file_path}' added to project."
      break
    end

  else
    puts "File reference for '#{file_path}' already exist in the Xcode project."
  end
end

# xcodeプロジェクトの取得
project_path = './Unity-iPhone.xcodeproj'
project = Xcodeproj::Project.open(project_path)

# xcodeプロジェクトに対してFlutter制御用のコード追加
add_file(project, './MainApp/FlutterController.h')
add_file(project, './MainApp/FlutterController.mm')

project.save

まとめ

UnityがビルドしたXcodeプロジェクトに対してFlutterModuleを組み込む実装方法を紹介しました。

前回のAndroidと同じく基本的にはFlutter公式の Flutter Add-to-app に書かれている既存アプリにFlutterを入れる方法をUnityからビルドされたプロジェクトに対して実行しているだけとなります。

私自身がメインスキルはUnityでAndroid/iOS側にそこまで詳しい訳では無いのでもっと良い実装方法があるとは思いますが一例として読んでいただけると幸いです。

UnityアプリにFlutterを組み込むと聞くとすごく難しそうに聞こえるかもしれませんが、前回と今回で紹介した方法をお読みいただいたとおり、基本の考え方はUnityがビルドしたAndroid/iOSプロジェクトに対してFlutterModuleを組み込むものになります。

そのため、どうしてもAndroid/iOSの知識が必要にはなってしまいますが、その他はそこまで難しいことはやっていません。

今回までで単純にプロジェクトにFlutterを組み込むところまで解説しました。

次回はUnityEngineとFlutterEngineでデータのやり取りを行い実際にアプリケーションとしての動作を実装する方法の解説を行います。

おすすめ記事