В Unity3d существует возможность подключения нативных плагинов к приложению. На официальном сайте Unity3d есть документация по взаимодействию с нативным кодом на iOS. Данная документация ограничивается описанием того, как вызвать ту или иную функцию из плагина, который вы собрали сами и как сделать обратный вызов. В документации описано каким правилам должна соответствовать описываемая функция и немного о том, как передать в нее параметры. В конце статьи с документацией есть ссылка, по которой можно скачать пример под названием Bonjour.

К сожалению в документации не описано, что делать в случае если вашему плагину необходимо перехватить событие о том, что пользователь подписался на Push Notification или приложение перешло в бэкграунд (AppDidEnterBackgound), тогда как доступ к этим событием бывает необходим для некоторых плагинов, которые вы возможно захотите написать.

Начнем с того, что в документации описано, что можно передавать параметры из C# или js кода в нативный и обратно. Однако явным образом эти правила не описаны, и возможно имеет смысл описать их подробней. Правила о передаче параметров, сформулированные ниже были получены при изучении уже существующих плагинов: Fb Unity SDK и GoogleAnalytics.

Типы параметров, которые можно передавать между нативным кодом и C# (и наоборот):

string = const char* //Из Unity в натив 
char* = string  //Из натива в Unity 
bool //(конвертация не нужна) 
int //(конвертация не нужна) 
float //(конвертация не нужна) 

При работе в нативном коде const char* можно просто конвертировать в более удобный для iOS NSString таким образом:

NSString* CreateNSString (const char* string)
{
     if (string)
          return [NSString stringWithUTF8String: string]; 
     else
          return [NSString stringWithUTF8String: ""];
}

Строковые значения, возвращаемые из нативного метода должны быть в кодировке UTF–8, кодироваться и выделяться в куче. Сортировочные вызовы Моно бесплатны для строк. Выделение в куче необходимо, потому что Mono компилятор для возвращаемых С строк создает .NET string и вызывает для возвращаемого значения free.

Для того, чтобы избежать проблем можно вызывать метод, который создает копию строки в куче (из пример Bonjour):

char* MakeStringCopy (const char* string)
{
      if (string == NULL)
           return NULL;

      char* res = (char*)malloc(strlen(string) + 1);
      strcpy(res, string);
      return res;
}

Функционал, связанный с изменением состояния приложения или его view не документирован в Unity, для получения информации был собран проект, в который был интегрирован официальный Fb SDK для Unity3d. В проекте использовалась версия Unity3d 5.1.2f1 и Fb Unity SKD версии 6.2 (Можно найти здесь), iOS SDK 8.4. Проект был собран под iOS, в Player Settings в качестве Scripting Backend был выбран IL2CPP.

Для чего может понадобится данная информация? Для разработки нативного iOS плагина, использующего нативные функции и слушающая сообщения об изменениях состояния приложения (The App Life Cycle — Apple Developer), изменениях состояния главного View приложения и рендерера. К примеру данный функционал может потребоваться для интеграции нативных плагинов социальных сетей или плагинов различных аналитик, у которых есть поддерживаемые нативные iOS плагины, но нет версий плагинов для Unity3d.

В таком случае есть 2 выхода:
  • Сгенерировать XCode проект. Найти исходный код делегата приложения, изменить его и интегрировать плагин. Минус такого подхода в том, что если версия вашего приложения далека от финала и в дальнейшем будут вноситься правки, то XCode проект придется генерировать заново и интегрировать плагин с нуля. Я использовал такой подход только однажды. Издательство настаивало на интеграции собственного трекера инсталлов и покупок, а проекте использовался плагин Unibill, соответственно трекинг платежки был завраплен этим плагином и код, вызывающий трекинг платежки от издателя я вызывал из нативной части исходников Unibill. Это решение использовалось только потому, что времени на то, чтобы написать Unity плагин на основе нативного издатель не давал и сама интеграция заняла полчаса.
  • Использовать нативный функционал Unity3d, реализованный в проекте iOS. Минус в том, что этот функционал не документирован. Однако он используется плагинами Google Analytics и Fb SKD для Unity.

Так как в текущем проекте использовался FB SDK, а времени было достаточно, было принято решение разобраться в том, что происходит под капотом XCode проекта, который генерирует Unity.

В состав FB плагина для Unity входят два файла, которые интересуют больше всего FbUnityInterface.mm и .h, в которых содержится нативный код для iOS.

В .mm файле бросается в глаза такой участок кода:

-(void)didBecomeActive: (NSNotification *)notification {
  [FBAppCall handleDidBecomeActiveWithSession:self.session];
}
-(void)willTerminate:(NSNotification *)notification {
  [self.session close];
} 
-(void)didFinishLaunching:(NSNotification *)notification {

Выглядит так, будто FB перехватывает вызовы стандартных методов делегата приложения. В заголовочном файле мы видим следующие стоки:

#if UNITY_VERSION >= 430
#import "AppDelegateListener.h"
@interface FbUnityInterface : NSObject <AppDelegateListener>

AppDelegateListener.h находится в директории Classes/PluginBase/ Xcode проекта. Для того, чтобы перехватывать события делегата Unity описывает 4 протокола. Для тех, кто не знаком с понятием протокола Objective C — Английская версия и Русская версия документации.

Основным протоколом является LifeCycleListener. Именно его расширяют два из трех оставшихся протокола. Он позволяет слушать события:

- (void)didFinishLaunching:(NSNotification*)notification;
- (void)didBecomeActive:(NSNotification*)notification;
- (void)willResignActive:(NSNotification*)notification;
- (void)didEnterBackground:(NSNotification*)notification;
- (void)willEnterForeground:(NSNotification*)notification;
- (void)willTerminate:(NSNotification*)notification;

А также объявляет 2 метода:

void UnityRegisterLifeCycleListener(id<LifeCycleListener> obj);
void UnityUnregisterLifeCycleListener(id<LifeCycleListener> obj);

Первый метод подписывает объект на получение перечисленных событий, второй отписывает. Следующим рассмотрим протокол AppDelegateListener:

@protocol AppDelegateListener<LifeCycleListener>

Он дает доступ к сообщениям о Push Notifications, открытии ULR приложениям и некоторым другим.Все сообщения, которые доступны классу, реализующему AppDelegateListener можно посмотреть в AppDelegateListener.mm.

Протокол RenderPluginDelegate дает доступ к событиям рендера Unity и также расширяет LifeCycleListener. Исходник содержит хорошие комментарии и информацию обо всех методах можно получить оттуда.

@protocol RenderPluginDelegate<LifeCycleListener, NSObject>

Последний в очереди протокол UnityViewControllerLIstener. Он позволяет получить доступ к основным событиям главного view приложения. Этот протокол не имеет зависимости от LifeCycleListener. Список событий:

- (void)viewDidDisappear:
- (void)viewWillDisappear:
- (void)viewDidAppear:
- (void)viewWillAppear:
 - (void)interfaceWillChangeOrientation:
- (void)interfaceDidChangeOrientation:

Из исходного кода Fb SDK видно, что данные возможности появились в Unity3d только начиная с версии 4.3.

#if HAS_UNITY_VERSION_DEF
  #include "UnityTrampolineConfigure.h"
#endif
#if UNITY_VERSION >= 430
#import "AppDelegateListener.h"
@interface FbUnityInterface : NSObject <AppDelegateListener>
#else

Константа HAS_UNITY_VERSION_DEF, хранящая информацию о том, что в проекте есть информация о версии Unity3d, использованной для сборки проекта объявляется в заголовочном файле RegisterMonoModules.h:

void RegisterMonoModules();
#define HAS_UNITY_VERSION_DEF 1

Это все содержимое RigisterMonoModules.h. Если HAS_UNITY_VERSION_DEF объявлена в проекте, тогда за значением версии Unity3d можно обратиться к константе UNITY_VERSION. Таким образом, если вы используете Unity3d версии ниже 4.3, то не стоит расчитывать на наличие четырех описанных выше протоколов.

Для некоторых нативных плагинов может потребоваться наличие того или иного ключа в файле .plist нативного приложения. По факту plist можно принять эквивалентным xml. В состав Fb SDK входят исходники FbPlistParser и PlistDic, которые можно использовать для добавления ключа, который потребуется вашему плагину. К примеру можно добавить в проект скрипт с PostProcessBuildAttribute для того, чтобы по окончании сборки проекта найти в XCode проекте plist файл и добавить к нему необходимые ключи со значениями.

Надеюсь, что данная информация оказалась полезной и помогла кому-нибудь в борьбе с нативными плагинами.

Комментарии (2)


  1. loreglean
    15.09.2015 15:31

    Еще, если нужен более полный контроль над AppController-ом (извиняюсь за каламбур), можно просто отнаследоваться от classes/UnityAppController и переопределить нужные методы (не забывая добавлять [super ...]). Для того, чтобы использовался новый AppController вместо дефолтного, нужно дополнительно поправить main.mm (а конкретно AppControllerClassName поле)


  1. TheRabbitFlash
    16.09.2015 02:32

    Спасибо, полезно. Применимо и к Adobe AIR.