📜 ⬆️ ⬇️

We write the plugin for Unity correctly. Part 1: iOS



When you make games on Unity for mobile platforms, sooner or later you will have to write some of the functionality in the native language of the platform, be it iOS (Objective C or Swift) or Android (Java, Kotlin). This may be your own code or integration of a third-party library, the installation itself may consist in copying files or decompressing the unitypackage, not the essence. The result of this integration is always the same: add libraries with native code (.jar, .aar, .framework, .a, .mm), scripts in C # (for the facade to the native code) and Game Object with a specific MonoBehavior to catch engine events and interactions with a scene. And it is often required to include libraries of dependencies that are needed for the native part to work.

This whole integration mechanism usually does not cause problems on a pure project, in which there is no (or little) integration of such third-party libraries. But when the project grows, many problems arise that complicate this process, and often give the need for additional modifications and adaptations to the plug-in project, which then results in an increase in the complexity of subsequent support and updating.
')
Here are the main ones:

  1. The Game Object should normally be loaded with the first scene, and be DontDestroyOnLoad. We have to create a special scene with a bunch of such non-paged objects, and then also see them in the editor during the testing process.
  2. All of these files are often added to Assets / Plugins / iOS and Assets / Plugins / Android, with all dependencies. Then it is difficult to figure out where and for what is the library file, and dependencies often conflict with those already installed for other plugins.
  3. If the libraries are in special subfolders, there is no conflict during the import, but during the assembly there may be an error of duplicate classes, if in the end there are still somewhere the same dependencies of different versions.
  4. Sometimes it is too late to initialize the native part in Awake, and the MonoBehavior event may not be enough.
  5. Unity Send Message for interaction between native and C # code is inconvenient, since it is asynchronous and with one string argument, without variants.
  6. I want to use C # delegates as callbacks.
  7. Some plug-ins require on iOS to launch the implementation of their UIApplicationDelegate, the successor of UnityAppController, and on Android their Activity, the successor of UnityPlayerActivity, or their Application class. Since there can be only one UIApplicationDelegate on iOS, and on Android one main Activity (for games) and one Application, several plug-ins become difficult to get along in one project.

But these problems can be avoided if you are guided by certain recipes when writing plugins. In this article we will look at tips for iOS, in the second part - for Android.

The main principle when writing plugins is: do not use the Game Object unless you need to draw something on the stage (use graphics api). Unity and Cocoa Touch already have all the major events required by an ordinary plugin: start, resume, pause, notification event. And the interaction between C # and ObjectiveC (Swift) can be accomplished via AOT.MonoPInvokeCallback . The essence of this method is that we register a static C # function of some class as a C function, and store a reference to it in C (ObjectiveC) code.

I will give an example of my class that implements a functional similar to 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); } 

In this class, there is both a declaration of the signature of the exported method via the delegate, and its implementation OnMessage, and automatic transmission of the reference to this implementation when the game starts.

Consider the implementation of this mechanism in the native code:

 /* 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); } }); } 

As an example, I wrote a native implementation in the form of a global static variable and function. If you wish, you can wrap all this in some class. It is important to make a call to MonoPMessageDelegate in the main thread, because on iOS this is the Unity stream, and on the C # side you cannot transfer to the right stream without having a Game Object on the stage.

We implemented the interaction between Unity and native code without using a Game Object! Of course, we just repeated the functionality of UnitySendMessage, but here we control the signature, and we can create as many such methods with the necessary arguments. And if you want to call something before Unity is initialized, you can create a message queue if MonoPMessageDelegate is still null.

But transferring primitive types is not enough. Often you need to transfer a calback to the native C # function, which will then need to pass the result. Of course, you can save a colbek in any Dictionary, and transfer the unique key to it to the native function. But in C # there is a ready solution, using the capabilities of the GC, to fix the object in memory and get a pointer to it. This pointer is passed to the native function, it, after performing the operation and generating the result, passes the pointer along with this result back to Unity, where we receive a callback object (for example, 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); } 

And on the side of the native code:

 /* 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]); }); } 

In this example, a fairly universal approach was used to transfer the result as a json string. By passing the pointer, the Action is retrieved with unlocking in the GC (that is, the callback is called once, then the pointer becomes invalid, and the Action can be deleted by the GC), the type of the required argument is checked (one!), And through Json.Net the data is de-serialized this type. All these actions are optional, you can create a signature MonoPCallbackDelegate another, specific to your particular case. But this approach allows us not to produce many methods of the same type, but to reduce the use itself to the definition of the simplest class specifying the data format and setting this format through generic arguments:

 /* 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 }); } 

With the interaction between Unity and native code sorted out. It is worth adding that the native code in the form of .mm files, or compiled .a or .framework is not necessary to put in Assets / Plugins / iOS. If you are writing not for yourself, but any package for export to other projects, put everything in a subfolder inside your specific folder with the code - then it will be easier to connect the ends with the ends and remove unwanted packages. If the plugin requires you to add some standard iOS dependencies (frameworks) to the project, use the import settings in the Unity editor for .mm, .a and .framework files. Only use the PostProcessBuild function as a last resort. By the way, if the required framework is not in the inspector list, you can write it directly in the meta file through a text editor, following the general syntax.



Now we will consider how to catch the events of UIApplicationDelegate and the application life cycle in particular. Here we are already assisted by messages already transmitted to Unity via NotificationCenter. Consider a way to execute the native plugin script even before Unity is loaded and subscribe to these events.

 /* 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 

So you can catch most of the events of the application life cycle. Not all methods, of course, are available. For example, from the latter, there is no application: performActionForShortcutItem: completionHandler : to respond to the launch by a shortcut from the 3d touch context menu. But since this method does not exist in the basic UnityAppController, it can be expanded using the category in any plug-in file and, for example, throw a new event in the Notification Center:

 /* 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 

On iOS, there is another problem when you need to add third-party libraries from CocoaPods, the package manager for Xcode. This is rare, there is often an alternative to introducing the library directly. But in this case, too, there is a solution . Its essence is that instead of the Podfile (file - dependency manifest), dependencies are published in the xml file, and when exporting the XCode project, support for CocoaPods is automatically added and xcworkspace is created with the dependencies already included. There may be several Xml files, they may be located in Assets in a sub-folder with a specific plugin, Unity Jar Resolver will scan all these files and find dependencies. The tool got its name, because initially it was created to do the same with Android dependencies, and there the problem of including third-party native libraries is more acute, so you can’t do without such a tool. But about this - in the next part of the article.

Source: https://habr.com/ru/post/353088/


All Articles