Разработка Unity3d-плагина для работы с Facebook

в 20:01, , рубрики: Facebook, Facebook API, mobile development, plugins, unity3d, метки: , , ,

В качестве вступления

Итак, я не буду рассказывать про то, что такое социальные сети и как они используются в игровых (да и неигровых) приложениях. Скажу просто, что однажды поставили передо мной задачу научить нашу игру публиковать всякие разные вещи в Facebook и Twiter.
Игра у нас создается с использованием движка Unity3d. Никаких встроенных возможностей по работе с социальными сетями в нем не предусмотрено. Зато есть возможность писать плагины на c/c++/objective c/… Т.е. на нативном языке платформы. Этим и предстояло мне заняться. Приложение мы разрабатываем под ipad, соответственно платформа iOS и язык Objective-C.
Ниже я расскажу, что и как у меня получилось, поделюсь кодом и задам пару вопросов уважаемому хабрасообществу.
Сразу уточню, что плагин еще не отлажен до конца! Но чем поделиться все равно найдется.

Обязательно ли создавать велосипед?

Существует несколько вариантов, как создать такой плагин:

  1. Первый и, наверное, самый логичный — купить! Да, есть уже готовые варианты, кому интересно могу поделиться ссылкой на наиболее популярный. Единственный минус такого решения — Apple или разработчики из соц. сети легко могут поменять свои API/Frameworks и не факт, что разработчик плагина мгновенно на это отреагирует.
  2. Написать с использованием Facebook SDK for iOS. Товарищ Цукерберг сотоварищи написал свой SDK для нативных приложения с гуем и т.д. Вроде даже неплохо получилось.
  3. Можно использовать WebView и Client-side/Server-side варианты отсюда. Но мне кажется это не очень удобно для мобильных приложений, если только ваше приложение не на HTML5

Но есть и еще один вариант, о котором, как вы уже догадались, я и расскажу. Apple в 6ой версии iOS добавила более тесную интеграцию с Facebook. Настолько тесную, что в настройках девайсов появился одноименный пункт в котором можно было вбить свои логин/пароль от соц. сети. Такая же возможность есть и для Twitterа. При чем Twitter в настройках появился в более ранних версиях iOS.
Для работы с данными аккаунтами Apple предоставляет довольно удобные фреймворки Accounts, Social и Twitter. Первый — Accounts — предоставляет доступ к аккаунтам, заданным пользователем в настройках девайса/ Основная идея такая — указываем один раз логин/пароль от аккаунта в соц. сети и пользуемся им во всех приложениях, без необходимости каждый раз вводить их заново. Второй — Social — помогает формировать запросы к соц. сетям в соответствии с их (сетей) требованиями. Ну а третий — Twitter — не тема данной статьи, т.к. здесь я рассматриваю только работу с Facebook.
Отвечая на заданный в заголовке параграфа вопрос, скажу: лично я считаю, что велосипед изобретать не стоило и проще было купить, но у меня было много свободного времени и решено было попробовать самостоятельно сваять плагин. К тому же дополнительный опыт никому не мешал.

Общие принципы создания плагинов для Unity3d

Все просто — кладем плагин в Assets/Plugins/iOS и он автоматически подцепляется движком при следующей генерации xcode-проекта. В качестве плагина может выступать:

  1. скомпилированные библиотеки — *.a
  2. исходные коды — *.m, *.mm, *.cpp, *.c. Ну и, конечно, не забываем про заголовочные файлы *.h (хотя вот они не очень-то и нужны)

При использовании C++ или Objective-C кода экспортируемые функции должны быть объявлены в С стиле, дабы избежать проблем с различиями в способах вызова функций.
И еще одно замечание — функции из плагинов могут быть вызваны только на конкретных девайсах, поэтому, чтобы избежать проблем при запуске проекта в симуляторах, рекомендуется все импортируемые (в проект из плагина) функции дополнительно заворачивать в С# код, проверяющий на какой платформе запускается проект.
Здесь все.

Настройка приложения Facebook

Необходимо правильно настроить приложение в Facebook прежде, чем начинать с ним работу. Если этого не сделать — можно поймать кучу «мистических» и не очень багов.
Вот мои рекомендации по настройке:

  1. Указываете тип приложения Native iOS App. Делает это на двух страницах: Настройки — Основные, там где спрашивают как ваше приложение встроено в Facebook, и Настройки — Advanced. Здесь нас интересует раздел Authentification, параметр App type -> Native/Desktop.
  2. Обязательно указываем Bundle ID. Естественно он должен совпадать с тем, который указан в настройках проекта.
  3. Настройки — Advanced: параметр App Secret in Client ставим в Нет
  4. Настройки — Основные: Facebook Login — включен

Алгоритм работы плагина

Собственно, алгоритм довольно прост — получаем у системы аккаунт, запрашиваем права на публикацию данных в соц. сеть, публикуем. А теперь о каждом из пунктов поподробнее.
Как я писал выше, в iOS SDK есть фреймворк Accounts, который предоставляет доступ к аккаунтам пользователя, заданным в настройках. На самом деле не только к ним, но другие нас не интересуют.
Для начала необходимо создать объект класса ACAccountStore, который представляет собой хранилище аккаунтов. Затем получаем объект класса ACAccountType, который будет содержать информацию обо всех аккаунтах, интересующего нас типа.
Поддерживаются три типа аккаунтов:

  1. ACAccountTypeIdentifierFacebook — аккаунты Facebook
  2. ACAccountTypeIdentifierTwitter — аккаунты Twitter
  3. ACAccountTypeIdentifierSinaWeibo — а это, неинтересующая нас, китайская соц. сеть

Далее мы должны запросить доступ к аккаунтам данного типа с интересующими нас разрешениями. Разрешения специфичны для каждой сети и задаются в виде массива строк.
После этого мы спокойно можем получать аккаунт (при условии, конечно, что такие аккаунты существуют и пользователь разрешил все наши хотелки) и творить с ним все, что захотим.
Казалось бы все просто и примитивно. Но нет, не в случае с Facebook.
Алгоритм немного усложняется. Для того, чтобы iOS SDK совместно с Facebook нас не послала совсем и, чтобы нам не пришлось каждый раз показывать пользователю алерты с просьбами разрешить нам что-то, необходимо выполнить особый алгоритм запроса разрешений:

  1. Сначала необходимо запросить доступ к какой-то совсем уже примитивной личной информации. Например, к email. У меня, кстати, при этом отображалось, что я прошу доступ к личной информации пользователя и списку друзей.
  2. Затем (в случае получения разрешения на предыдущий пункт) надо запросить права на чтение ленты — read_stream ю Фишка в том, что нельзя запрашивать права на публикацию, до прав на чтение (и тем более вообще не спрашивая прав на чтение). Также нельзя запрашивать права на доступ и чтение в одном запросе.
  3. Ну и теперь можно запросить и интересующие нас права — publish_stream

Подробнее про все возможные права/разрешения здесь.
Если не выполнить данный алгоритм — то можно словить вот такие ошибки:

  • The Facebook server could not fulfill this access request: remote_app_id does not match stored id.
  • The Facebook server could not fulfill this access request: application could not be installed.
  • и еще целая пачка других

Кому интересно можете почитать Stackoverflow на эту тему. Там народ много таких ошибок выкладывает!

Итак, у меня получился примерно такой код (восстанавливаю по памяти, сейчас он уже поменялся, а репозитория с историей под рукой нет):

ACAccountStore *store = [[ACAccountStore alloc] init];
ACAccountType *fb_account_type = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
NSDictionary *dict = @{ACFacebookAppIdKey : fb_appid, ACFacebookPermissionsKey : @[@"email"], ACFacebookAudienceKey : ACFacebookAudienceEveryone};
[store requestAccessToAccountsWithType:fb_account_type
                               options:dict
                            completion:^(BOOL granted, NSError *error)
 {
     if (granted && error == nil)
     {
        ACAccountType *fb_account_type = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
        NSDictionary *dict = @{ACFacebookAppIdKey : fb_appid, ACFacebookPermissionsKey : @[@"read_permission"], ACFacebookAudienceKey : ACFacebookAudienceEveryone};
        [store requestAccessToAccountsWithType:fb_account_type
                                       options:dict
                                    completion:^(BOOL granted, NSError *error)
         {
             if (granted && error == nil)
             {
                ACAccountType *fb_account_type = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
                NSDictionary *dict = @{ACFacebookAppIdKey : fb_appid, ACFacebookPermissionsKey : @[@"public_permission"], ACFacebookAudienceKey : ACFacebookAudienceEveryone};
                [store requestAccessToAccountsWithType:fb_account_type
                                               options:dict
                                            completion:^(BOOL granted, NSError *error)
                 {
                     if (granted && error == nil)
                     {
                         // Тут можем уже получать аккаунт и постить в фейсбук
                     }
                 }];
             }
         }];
     }
 }];

Согласен, выглядит жутко!
Сразу пара замечай про постоянно пересоздаваемые dict и fb_account_type:

  • dict. Если присмотреться, то там меняется только ACFacebookPermissionsKey. И в теории можно было бы воспользоваться [dict setObject: forKey:]. НО, еще на стадии написания кода Xcode стал выдавать варнинги о том, что этот метод может и не заработать. В итоге все сбилдилось, запустилось на девайсе и… все-таки упало, именно на этом методе. Краш-дампы, каюсь, не читал, а просто обошел проблему вот таким образом, поставив себе TODO на будущее.
  • fb_account_type. Тут все немного сложнее. Без пересоздания объекта, у меня приложение стабильно крашилось на втором [store requestAccessToAccountsWithType:options:completion]. Очередное TODO.

Я этот код немного упростил:

void _fb_request_access(NSArray *permissions, FBAccessGrantedHandler handler)
{
    ACAccountType *fb_account_type = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
    NSDictionary *dict = @{ACFacebookAppIdKey : fb_appid, ACFacebookPermissionsKey : permissions, ACFacebookAudienceKey : ACFacebookAudienceEveryone};
    [store requestAccessToAccountsWithType:fb_account_type
                                   options:dict
                                completion:^(BOOL granted, NSError *error)
     {
         if (granted && error == nil)
         {
             handler();
         }
     }];
}
_fb_request_access(@[@"email"], ^()
                   {
                     _fb_request_access(@[@"read_stream"], ^()
                                        {
                                          _fb_request_access(@[@"publish_stream"], ^()
                                                             {
                                                                 _fb_post_impl(post);
                                                             });
                                        });
                   });

К сожалению, данный код протестить не успел — ipad забрали (а с 6ой версией он один у нас). Но проект собрался без ошибок.

Постинг на стену

Теперь о, собственно, постинге на стену. У меня этим занимается отдельная функция — _fb_post_impl.

void _fb_post_impl(NSString *text)
{ 
  ACAccountType *fb_account_type = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
  NSArray *accounts = [store accountsWithAccountType:fb_account_type];
  fb_account = [accounts objectAtIndex:0];
  SLRequest *fb_request = [SLRequest requestForServiceType:SLServiceTypeFacebook
                                             requestMethod:SLRequestMethodPOST
                                                       URL:[NSURL URLWithString:@"https://graph.facebook.com/me/feed"]
                                                parameters:[NSDictionary dictionaryWithObject:text forKey:@"message"]];
  [fb_request setAccount:fb_account];
  [fb_request performRequestWithHandler:^(NSData* responseData, NSHTTPURLResponse* urlResponse, NSError* error)
  {
    NSLog([[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding]);
  }];
}

Алгоритм прост:

  • Получаем ACAccountType. Подразумевается, что все необходимые разрешения получены ранее.
  • Затем получаем все аккаунты с этим типом и необходимыми нам разрешениями.
  • Из полученного массива, берем самый первый элемент. Нигде ничего конкретного на эту тему я не нашел, но вроде как это и есть тот аккаунт из настроек.
  • Ну и постим.

У меня сейчас постится только текст. Для того, чтобы добавить картинку/ссылку/заголовок/т.д. необходимо дополнить словарь parameters. Список возможных полей смотреть здесь. Еще советую почитать вот это, если вам нужен не только постинг на стену.

Вызов функций плагина в C#

Тут на самом деле все просто и примитивно.

	[DllImport ("__Internal")]
	private static extern void _fb_init(String appid);		
	[DllImport ("__Internal")]
	private static extern void _fb_post(String text);		
		
	public static void fb_init() {
		_fb_init("тут application id");
	}
	public static void fb_post(String text) {
		_fb_post(text);
	}

Маленькое замечание — типу String из С#, соответствует const char* в плагине. Я, например, пытался сначала использовать NSString.

Вместо заключения

В итоге запостить текст (и не только) к себе на стену у меня получилось. Времени, правда, потратил немерено из-за вот тех мелких косяков, про которые я писал — использование плагина только на девайсе, хитрый алгоритм получения разрешений.
Код выложил на bitbucket — bitbucket.org/mnazarov/fb_plugin.
Кстати, с Objective-C столкнулся в первый раз, так что прошу сильно не пинать, а отнестись с пониманием. Рад буду получить советы как улучшить код.
Также рад буду если кто-то сможет попробовать у себя на девайсах и отпишется как работает.
Ну и очень рад буду если кто-то решит помочь с разработкой!
Ну и теперь пара вопросов:

  1. Что я не так делал со словарем, что у меня не работал метод [dict:setObject:forKey]. При том, что Xcode об этом методе знал, да и на том же Stackoverflow народ его использует в таком же случае как у меня.
  2. Второй вопрос больше по отладке под iPadом — как отлаживаетесь, как ловите «мистические» баги. В частности при отладке этого плагина наблюдал ситуацию когда приложение падало просто на вызове NSLog!
    Пытался читать краш-дампы, но ничего не понял.

P.S. Надесь данной статьей сэкономил кому-нибудь пару нервных клеток.

Автор: Greezlee

Поделиться

* - обязательные к заполнению поля