Unityのプラグインを正しく作成しています。 パート1:iOS





Unityでモバイルプラットフォーム用のゲームを作成する場合、遅かれ早かれ、iOS(Objective CまたはSwift)でもAndroid(Java、Kotlin)でもプラットフォームのネイティブ言語で機能の一部を記述する必要があります。 独自のコードまたはサードパーティライブラリの統合が可能です。インストール自体は、ポイントではなく、ファイルのコピーまたはunitypackageのアンパックで構成されます。 この統合の結果は常に同じです。ネイティブコード(.jar、.aar、.framework、.a、.mm)のライブラリ、C#(ネイティブコードのファサード用)のスクリプト、およびエンジンイベントと相互作用をキャッチする特定のMonoBehaviorのゲームオブジェクトが追加されます。シーンで。 そして、多くの場合、ネイティブパーツが機能するために必要な依存関係ライブラリを含める必要があります。



この統合メカニズム全体は、通常、このようなサードパーティライブラリの統合がまったくない(またはほとんどない)クリーンなプロジェクトでは問題を引き起こしません。 しかし、プロジェクトが大きくなると、このプロセスを複雑にする多くの問題が発生し、多くの場合、プラグインプロジェクトに追加の修正と適応が必要になり、その後のサポートと更新の複雑さが増します。



主なものは次のとおりです。



  1. 通常、ゲームオブジェクトは最初のシーンで読み込まれ、DontDestroyOnLoadである必要があります。 このようなアップロードできないオブジェクトの束で特別なシーンを作成し、テストプロセス中にエディターでそれらを確認する必要があります。
  2. これらのすべてのファイルは、多くの場合、すべての依存関係とともにAssets / Plugins / iOSおよびAssets / Plugins / Androidに追加されます。 それから、どこで、どのライブラリファイル、および依存関係が他のプラグイン用に既にインストールされているものと競合するかを理解するのは困難です。
  3. ライブラリが特別なサブフォルダーにある場合、インポート中に競合はありませんが、最終的に異なるバージョンの同じ依存関係がある場合、アセンブリ中に重複クラスのエラーが発生する可能性があります。
  4. Awakeでネイティブ部分の初期化を呼び出すのが遅すぎる場合があり、MonoBehaviorイベントでは不十分な場合があります。
  5. ネイティブコードとC#コード間のやり取りのためのUnity Send Messageは、非同期であり、オプションなしの1つの文字列引数があるため、不便です。
  6. C#デリゲートをコールバックとして使用したいと思います。
  7. 一部のプラグインでは、iOSでUnityAppControllerの子孫であるUIApplicationDelegateの実装を実行し、AndroidでUnityPlayerActivityの子孫であるアクティビティ、またはApplicationクラスの実装を実行する必要があります。 iOSにはUIApplicationDelegateが1つしかなく、Androidには1つのメインアクティビティ(ゲーム用)と1つのアプリケーションしか存在できないため、1つのプロジェクトで複数のプラグインを取得することが難しくなります。


ただし、プラグインを作成するときに特定のレシピがガイドされていれば、これらの問題を回避できます。 この記事では、第2部-iOSのiOSのヒントを検討します。



プラグインを作成する際の主な原則:シーンに何かを描画する必要がない場合は、Game Objectを使用しないでください(グラフィックAPIを使用)。 UnityおよびCocoa Touchには、通常のプラグインに必要なすべての主要なイベント(開始、再開、一時停止、通知イベント)が既にあります。 そして、C#とObjectiveC(Swift)間の相互作用は、 AOT.MonoPInvokeCallbackを介して実行できます。 このメソッドの本質は、あるクラスの静的なC#関数をC関数として登録し、そのリンクをC(ObjectiveC)コードに保存することです。



UnitySendMessageに類似した機能を実装するクラスの例を次に示します。



/* MessageHandler.cs */ using UnityEngine; using System.Runtime.InteropServices; public static class MessageHandler { //        private delegate void MonoPMessageDelegate(string message, string data); //        , //      [AOT.MonoPInvokeCallback(typeof(MonoPMessageDelegate))] private static void OnMessage(string message, string data) { //      MessageRouter.RouteMessage(message, data); } //        Unity Engine   [RuntimeInitializeOnLoadMethod] private static void Initialize() { //          RegisterMessageHandler(OnMessage); } //  ,        [DllImport("__Internal")] private static extern void RegisterMessageHandler(MonoPMessageDelegate messageDelegate); }
      
      





このクラスでは、デリゲートを介してエクスポートされたメソッドのシグネチャの宣言、OnMessageの実装、およびゲームの開始時にこの実装へのリンクの自動転送が行われます。



ネイティブコードでのこのメカニズムの実装を検討してください。



 /* MessageHandler.mm */ #import <Foundation/Foundation.h> //     ,    Unity typedef void (*MonoPMessageDelegate)(const char* message, const char* data); //     . //         -  static MonoPMessageDelegate _messageDelegate = NULL; //   ,    Unity FOUNDATION_EXPORT void RegisterMessageHandler(MonoPMessageDelegate delegate) { _messageDelegate = delegate; } //  - ,      Unity, //    void SendMessageToUnity(const char* message, const char* data) { dispatch_async(dispatch_get_main_queue(), ^{ if(_messageDelegate != NULL) { _messageDelegate(message, data); } }); }
      
      





例として、グローバルな静的変数と関数としてネイティブ実装を作成しました。 必要に応じて、これをすべてのクラスでラップできます。 メインスレッドでMonoPMessageDelegateを呼び出すことが重要です。iOSではこれはUnityストリームであり、C#側ではステージにゲームオブジェクトがなければ目的のストリームに転送できないためです。



Game Objectを使用せずにUnityとネイティブコード間の相互作用を実装しました! もちろん、UnitySendMessageの機能を繰り返しただけですが、ここでは署名を制御し、必要な引数を持つメソッドを好きなだけ作成できます。 また、Unityが初期化される前に何かを呼び出したい場合、MonoPMessageDelegateがまだnullであればメッセージをキューに入れることができます。



しかし、プリミティブ型を渡すだけでは十分ではありません。 多くの場合、ネイティブC#関数にコールバックを渡す必要があり、その後、結果を渡す必要があります。 もちろん、コールバックをいくつかの辞書に保存し、固有のキーをネイティブ関数に渡すことができます。 しかし、C#には、GCの機能を使用して、メモリ内のオブジェクトを修正し、それへのポインターを取得する既製のソリューションがあります。 このポインターをネイティブ関数に渡し、操作を実行して結果を生成した後、ポインターをこの結果とともにUnityに戻し、そこでコールバックオブジェクト(Actionなど)を取得します。



 /* MonoPCallback.cs */ using System; using System.Runtime.InteropServices; using UnityEngine; public static class MonoPCallback { //   ,     Action //     private delegate void MonoPCallbackDelegate(IntPtr actionPtr, string data); [AOT.MonoPInvokeCallback(typeof(MonoPCallbackDelegate))] private static void MonoPCallbackInvoke(IntPtr actionPtr, string data) { if(IntPtr.Zero.Equals(actionPtr)) { return; } //      Action var action = IntPtrToObject(actionPtr, true); if(action == null) { Debug.LogError("Callaback not found"); return; } try { // ,       Action var paramTypes = action.GetType().GetGenericArguments(); //        var arg = paramTypes.Length == 0 ? null : ConvertObject(data, paramTypes[0]); //  Action     , //     var invokeMethod = action.GetType().GetMethod("Invoke", paramTypes.Length == 0 ? new Type[0] : new []{ paramTypes[0] }); if(invokeMethod != null) { invokeMethod.Invoke(action, paramTypes.Length == 0 ? new object[] { } : new[] { arg }); } else { Debug.LogError("Failed to invoke callback " + action + " with arg " + arg + ": invoke method not found"); } } catch(Exception e) { Debug.LogError("Failed to invoke callback " + action + " with arg " + data + ": " + e.Message); } } //       public static object IntPtrToObject(IntPtr handle, bool unpinHandle) { if(IntPtr.Zero.Equals(handle)) { return null; } var gcHandle = GCHandle.FromIntPtr(handle); var result = gcHandle.Target; if(unpinHandle) { gcHandle.Free(); } return result; } //       public static IntPtr ObjectToIntPtr(object obj) { if(obj == null) { return IntPtr.Zero; } var handle = GCHandle.Alloc(obj); return GCHandle.ToIntPtr(handle); } //  ,    public static IntPtr ActionToIntPtr<T>(Action<T> action) { return ObjectToIntPtr(action); } private static object ConvertObject(string value, Type objectType) { if(value == null || objectType == typeof(string)) { return value; } return Newtonsoft.Json.JsonConvert.DeserializeObject(value, objectType); } //    [RuntimeInitializeOnLoadMethod] private static void Initialize() { RegisterCallbackDelegate(MonoPCallbackInvoke); } [DllImport("__Internal")] private static extern void RegisterCallbackDelegate(MonoPCallbackDelegate callbackDelegate); }
      
      





そして、ネイティブコードの側で:



 /* MonoPCallback.h */ //       Unity  typedef const void* UnityAction; //     ,     void SendCallbackDataToUnity(UnityAction callback, NSDictionary* data); /* MonoPCallback.mm */ #import <Foundation/Foundation.h> #import "MonoPCallback.h" //     Objective C typedef void (*MonoPCallbackDelegate)(UnityAction action, const char* data); //    , //          static MonoPCallbackDelegate _monoPCallbackDelegate = NULL; FOUNDATION_EXPORT void RegisterCallbackDelegate(MonoPCallbackDelegate callbackDelegate) { _monoPCallbackDelegate = callbackDelegate; } //      -  void SendCallbackDataToUnity(UnityAction callback, NSDictionary* data) { if(callback == NULL) return; NSString* dataStr = nil; if(data != nil) { //    json NSError* parsingError = nil; NSData* dataJson = [NSJSONSerialization dataWithJSONObject:data options:0 error:&parsingError]; if (parsingError == nil) { dataStr = [[NSString alloc] initWithData:dataJson encoding:NSUTF8StringEncoding]; } else { NSLog(@"SendCallbackDataToUnity json parsing error: %@", parsingError); } } //    Unity ()  dispatch_async(dispatch_get_main_queue(), ^{ if(_monoPCallbackDelegate != NULL) _monoPCallbackDelegate(callback, [dataStr cStringUsingEncoding:NSUTF8StringEncoding]); }); }
      
      





この例では、結果をjson文字列として渡すために、かなり普遍的なアプローチが使用されました。 渡されたポインターは、GCのコミットのリリースでアクションを取得するために使用されます(つまり、コールバックが1回呼び出された後、ポインターが無効になり、GCによってアクションを削除できます)。必要な引数のタイプがチェックされ(1!)このタイプに。 これらのアクションはすべてオプションです。特定のケースに固有の別のMonoPCallbackDelegate署名を作成できます。 しかし、このアプローチにより、同じタイプの多くのメソッドを生成することはできませんが、データ形式を定義する最も単純なクラスの定義と、汎用引数を介したこの形式の仕様への使用を減らすことができます。



 /* Example.cs */ public class Example { public class ResultData { public bool Success; public string ValueStr; public int ValueInt; } [DllImport("__Internal", CharSet = CharSet.Ansi)] private static extern void GetSomeDataWithCallback(string key, IntPtr callback); public static void GetSomeData(string key, Action<ResultData> completionHandler) { GetSomeDataWithCallback(key, MonoPCallback.ActionToIntPtr<ResultData>(completionHandler); } }
      
      







 /* Example.mm */ #import <Foundation/Foundation.h> #import "MonoPCallback.h" FOUNDATION_EXPORT void GetSomeDataWithCallback(const char* key, UnityAction callback) { DoSomeStuffWithKey(key); SendCallbackDataToUnity(callback, @{ @"Success" : @YES, @"ValueStr" : someResult, @"ValueInt" : @42 }); }
      
      





Unityとネイティブコードの相互作用を理解しました。 .mmファイルの形式のネイティブコード、またはコンパイルされた.aまたは.frameworkをAssets / Plugins / iOSに配置する必要がないことを追加する価値があります。 自分用ではなく、他のプロジェクトにエクスポートするための何らかのパッケージを作成する場合は、コードを使用して特定のフォルダー内のサブフォルダーにすべてを入れてください。そうすれば、エンドとエンドをリンクして不要なパッケージを削除するのが簡単になります プラグインで標準のiOS依存関係(フレームワーク)をプロジェクトに追加する必要がある場合は、Unityエディターの.mm、.a、および.frameworkファイルのインポート設定を使用します。 PostProcessBuild関数は、最後の手段としてのみ使用してください。 ところで、必要なフレームワークがインスペクターリストにない場合は、一般的な構文に従って、テキストエディターを使用してメタファイルに直接書き込むことができます。







次に、UIApplicationDelegateイベントをキャッチする方法と、特にアプリケーションのライフサイクルを見てみましょう。 ここでは、NotificationCenterを介して既にUnityに送信されたメッセージが役立ちます。 Unityをロードする前にネイティブプラグインスクリプトを実行し、これらのイベントをサブスクライブする方法を検討してください。



 /* ApplicationStateListener.mm */ #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import "AppDelegateListener.h" @interface ApplicationStateListener : NSObject <AppDelegateListener> + (instancetype)sharedInstance; @end @implementation ApplicationStateListener //      , //    Unity Player static ApplicationStateListener* _applicationStateListenerInstance = [[ApplicationStateListener alloc] init]; + (instancetype)sharedInstance { return _applicationStateListenerInstance; } - (instancetype)init { self = [super init]; if (self) { //    -    //   Notification Center    UIApplicationDelegate, //    Unity    UnityRegisterAppDelegateListener(self); } return self; } - (void)dealloc { //    . -,     [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark AppDelegateListener - (void)applicationDidFinishLaunching:(NSNotification *)notification { NSDictionary *launchOptions = notification.userInfo; //    -   launchOptions, //    sdk } - (void)applicationDidEnterBackground:(NSNotification *)notification { //    } - (void)applicationDidBecomeActive:(NSNotification *)notification { //     } - (void)onOpenURL:(NSNotification*)notification { NSDictionary* openUrlData = notification.userInfo; //     } @end
      
      





これにより、アプリケーションのライフサイクルイベントのほとんどをキャッチできます。 もちろん、すべてのメソッドが利用できるわけではありません。 たとえば、後者からは、3Dタッチコンテキストメニューのショートカットを使用して起動に応答するperformActionForShortcutItem:completionHandlerのアプリケーションはありません。 ただし、このメソッドはベースのUnityAppControllerにもないため、プラグインファイルのカテゴリを使用して拡張でき、たとえば、通知センターで新しいイベントをスローできます。



 /* ApplicationExtension.m */ #import "UnityAppController.h" @implementation UnityAppController (ShortcutItems) - (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler { [[NSNotificationCenter defaultCenter] postNotificationName:@"UIApplicationPerformActionForShortcutItem" object:nil userInfo:@{ UIApplicationLaunchOptionsShortcutItemKey : shortcutItem }]; completionHandler(YES); } @end
      
      





iOSでは、XcodeのパッケージマネージャーであるCocoaPodsからサードパーティライブラリを追加する必要がある場合、別の問題があります。 これはまれであり、多くの場合、ライブラリを直接実装する代わりになります。 しかし、この場合の解決策もあります 。 その本質は、Podfile(依存関係マニフェストファイル)の代わりにxmlファイルで依存関係が公開され、Xcodeプロジェクトをエクスポートすると、CocoaPodsサポートが自動的に追加され、既に含まれている依存関係でxcworkspaceが作成されることです。 複数のXmlファイルが存在する可能性があり、それらは特定のプラグインを持つサブフォルダーのAssetsにあります。UnityJar Resolver自体はこれらすべてのファイルをスキャンし、依存関係を見つけます。 ツールの名前は、もともとAndroidの依存関係で同じことを行うために作成されたものであり、サードパーティのネイティブライブラリを含めるという問題がより深刻であるため、このようなツールなしでは実行できません。 しかし、それについては記事の次の部分で詳しく説明します。



All Articles