Unity3d: эксперименты с Social Interface

от автора

Современную мобильную игру трудно представить без социальной интеграции, общих таблиц рекордов (leaderboards) и достижений (achievements). Дабы не отставать от тенденций, решил интегрировать Game Center и Play Services для iOS и Android версий моей игры.

Так как я разрабатываю игру в свободное время в качестве хобби, то мысли о покупке плагинов, например, prime31, были отброшены сразу. Выбор пал на интерфейс Social, который входит в состав Unity. Вокруг этого пакета чувствуется интрига: практическое отсутствие справочной информации наталкивает на две мысли: либо интерфейс очень прост, либо не пригоден к использованию. Итак, пришло время в этом разобраться.

Прежде всего оказалось, что интерфейс этот имеет реализацию только под iOS, а для Android — это, действительно, интерфейс в чистом виде.

Нежелание покупать плагины и желание добавить таблицу рекордов привели меня сюда: https://github.com/playgameservices/play-games-plugin-for-unity. Это бесплатный плагин под Android от Google, который наполняет интерфейс Social живительной реализацией и сохраняет толщину кошелька на прежнем уровне. Плагин имеет пугающую версию 0.9, однако на его работоспособности это не сказывается, но отсутствует часть функционала, о которой речь пойдет дальше.

Полный решимости и веры в успех я начал подготавливать проекты в iTunes Connect и Google Developer Console — на этом этапе никаких проблем не возникает, обе платформы имеют практически идентичные настройки таблиц рекордов и достижений, а обилие справочной информации не дает сбиться с пути.

Есть пара моментов, на которые стоит обратить внимание:

Google Developer Console генерирует идентификаторы достижений и лидербордов сам, а в iTunes Connect их нужно задавать самостоятельно, поэтому для большей совместимости будущего кода удобно начать с Google, а затем по образу и подобию настроить проект под iOS, копируя те же идентификаторы.

При работе с Play Services в Google Developer Console, а также при добавлении альфа/бета версий игры, Google настойчиво предлагает сделать «паблишинг» достижений и лидербордов — на это не стоит соглашаться до самого релиза, т.к. после «паблишинга» вы лишаетесь возможности удалять достижения и таблицы рекордов, а также редактировать такие важные параметры, как кол-во шагов, необходимых для выполнения итеративных достижений.

Я создал лидерборды «High Scores» и минимальный набор достижений (для Google — это пять позиций) так что, даже если вы не собираетесь их использовать — придется из себя что-то выжать. У Apple такого ограничения нет, но раз уж достижения созданы — нет ничего сложного в том, чтобы их скопировать.

Далее устанавливаем плагин для Android. В меню Unity выбираем Assets/Import Package/Custom Package и разворачиваем плагин в свой проект. После успешного импорта в меню появляется пункт Google Play Games, выбираем подпункт Android Setup…, вводим идентификатор приложения, который можно найти в разделе Game Services Google Developer Console и получаем плагин, готовый к использованию.

Теперь все готово к тому, чтобы написать пару строк кода (C#) в Unity. Прежде всего нужно сделать предварительные настройки для iOS и Android, а также авторизироваться:

#if UNITY_ANDROID // активируем плагин Google Play Games, если приложение собирается под Android, // таким образом интерфейс Social получает его реализацию GooglePlayGames.PlayGamesPlatform.Activate(); #endif  #if UNITY_IPHONE // по умолчании при получении достижения под iOS ничего не происходит, чтобы игрок видел стандартное сообщение о получении достижения нужно вызвать эту функцию UnityEngine.SocialPlatforms.GameCenter.GameCenterPlatform.ShowDefaultAchievementCompletionBanner(true); #endif  Social.localUser.Authenticate(onProcessAuthentication); // функция вызывается, когда завершается авторизация // если операция проходит успешно, Social.localUser будет содержать данные сервера private void onProcessAuthentication(bool success) { 	Debug.Log("onProcessAuthentication: " + success); } 

После успешной авторизации мы можем работать с лидербордами и достижениями.

При работе с лидербордами я решил, что мне нужно прежде всего получить текущий рекорд игрока — это нужно, чтобы можно было сравнивать старый рекорд с новым и если игрок достигает нового топа, выводить об этом сообщение «Congratulations! New Top: XXX». Для этого я написал следующий код, который создает таблицу, устанавливает фильтр игроков, по которым нам нужны данные (только наш игрок), и получает текущий рекорд игрока в случае успеха:

string[] userIds = new string[] { Social.localUser.id }; highScoresBoard = Social.CreateLeaderboard(); highScoresBoard.id = "LEADERBOARD_ID"; highScoresBoard.SetUserFilter(userIds); highScoresBoard.LoadScores(onLeaderboardLoadComplete);  private void onLeaderboardLoadComplete(bool success) { 	Debug.Log("onLeaderboardLoadComplete: " + success); 	if (success) 	{ 		long score = highScoresBoard.localUserScore.value; 	} } 

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

public void reportScore(long score) { 	if (Social.localUser.authenticated) 	{ 		Social.ReportScore(score, "LEADERBOARD_ID", onReportScore); 	} } private void onReportScore(bool result) { 	Debug.Log("onReportScore: " + success); } 

После тестирования этого кода появилась проблема — он не работает под Android, т.к. в плагине нет реализации этой функции — вот она, прелесть версии 0.9.

Но это не повод для расстройств, поразмыслив, я пришел к выводу, что нет необходимости получать текущий рекорд игрока, достаточно хранить его локально. Дело в том, что если игрок поменяет устройство, или просто удалит вашу игру и вернется в нее спустя какое-то время, ему будет приятнее заново раз за разом бить свой собственный локальный рекорд. Рекорд же в лидербордах будет всегда содержать максимальный прогресс игрока, и чтобы его побить у игрока может уйти много времени, что в конечном счете может снизить мотивацию игрока. Так что я решил отказаться от глобального рекорда, и дописал следующий код к авторизации:

public long highScore = 0;  private void onProcessAuthentication(bool success) { 	Debug.Log("onProcessAuthentication: " + success);  	if (success) 	{ 		if (PlayerPrefs.HasKey("high_score")) 			highScore = (long)PlayerPrefs.GetInt("high_score"); 	} } 

Отправка прогресса на сервер приняла вид:

public void reportScore(long score) { 	if (Social.localUser.authenticated) 	{ 		if (score > highScore) 		{ 			highScore = score; 			PlayerPrefs.SetInt("high_score", (int)score);  			Social.ReportScore(score, "LEADERBOARD_ID", onReportScore); 		} 	} } 

Таким образом мы запоминаем локальный рекорд игрока, который затем можно использовать для проверки достижения нового рекорда.

Осталось вывести стандартный диалог лидербордов, что можно сделать с помощью функции Social.ShowLeaderboardUI(). По умолчанию для Android отображается список всех лидербордов, даже если он у вас один (таблица «High Scores»), это не очень красиво и требует лишнего выбора от игрока, поэтому пришлось дописать такой код:

#if UNITY_ANDROID 	(Social.Active as GooglePlayGames.PlayGamesPlatform).SetDefaultLeaderboardForUI("LEADERBOARD_ID"); #endif Social.ShowLeaderboardUI(); 

Разобравшись с таблицами рекордов и довольный результатом я приступил к реализации достижений, и тут меня ждал большой и неприятный сюрприз, но давайте по порядку.

Достижения есть двух типов: «одноходовые» (achievement) и инкрементируемые (incremental achievement). Первые подразумевают достижение с одного раза, например «запустить ракету» — как только игрок нашел и запустил одну ракету, мы считаем, что достижение выполнено на 100% и открываем его игроку. Инкрементируемые достижения подразумевают пошаговое выполнение в несколько этапов, например, достижение «Охотник за вишенками» подразумевает сбор 15 вишенок, в процессе чего игроку будет постепенно открываться достижение, а после сбора всех 15 вишенок он получит его полностью. Такие достижения мне показались более уместными в моей игре; для начала, я добавил 5 достижений:

Приступив к реализации инкрементируемых достижений я столкнулся с двумя проблемами:
— Разница во взаимодействии с Android и iOS серверами;
— Нужно хранить текущий прогресс по достижению, чтобы каждый раз слать увеличенное значение, иначе достижение не будет расти.

Разница во взаимодействии состоит в том, что Google Play рассчитывает процентное приращение достижения сам, указав в Google Developer Console кол-во шагов 15, мы можем каждый раз отправлять на сервер значение 1, и серверная логика будет складывать единицы до тех пор, пока не наберется 15 и достижение не будет открыто.

Apple Game Center перекладывает заботу о приращении прогресса по достижению на логику клиента, и ждет от нас постепенного увеличение прогресса в пределах от 0 до 100 единиц (процентов). Поэтому если мы будем слать ему постоянно 1, то прогресс постоянно будет 0,01%.

Итак, в случае с iOS нам нужно получать текущий прогресс достижений и сохранять его, чтобы можно было в будущем слать увеличенное значение. А также нам нужно хранить на клиенте количество шагов (итераций) для того, чтобы отправлять правильное приращение прогресса. Для этих целей я создал вспомогательный класс:

public class AchievementData  { 	public string id; 	public int steps; 	public AchievementData(string id, int steps) 	{ 		this.id = id; 		this.steps = steps; 	} } 

И подготовил данные по своим достижениям (фактически это копия данных, которые я ввел в Google Developer Console для Android):

// описываем все возможные достижения - их идентификаторы и кол-во итераций для достижения public static readonly AchievementData cherryHunter = new AchievementData("ACHIEVEMENT_ID",  15); public static readonly AchievementData bananaHunter = new AchievementData("ACHIEVEMENT_ID",  25); public static readonly AchievementData strawberryHunter = new AchievementData("ACHIEVEMENT_ID",  50); public static readonly AchievementData rocketRider = new AchievementData("ACHIEVEMENT_ID",  15); public static readonly AchievementData climberHero = new AchievementData("ACHIEVEMENT_ID", 250);  // массив всех возможных достижений private readonly AchievementData[] _achievements = { 	cherryHunter, 	bananaHunter, 	strawberryHunter, 	rocketRider, 	climberHero };  // таблица достижений игрока, заполняется основываясь на результатах от сервера private Dictionary<string, IAchievement> _achievementDict = new Dictionary<string, IAchievement>(); Следующий код нужен только для iOS:  if (Application.platform == RuntimePlatform.IPhonePlayer) { 	Social.LoadAchievements(onAchievementsLoadComplete); }  private void onAchievementsLoadComplete(IAchievement[] achievements) { 	// заносим в таблицу достижения, по которым у игрока уже есть прогресс 	foreach (IAchievement achievement in achievements) 	{ 		_achievementDict.Add(achievement.id, achievement); 	} 	 	// создаем остальные достижения, по которым у игрока еще нет прогресса 	for (int i = 0; i < _achievements.Length; i++) 	{ 		AchievementData achievementData = _achievements[i];  		if (_achievementDict.ContainsKey(achievementData.id) == false) 		{ 			IAchievement achievement = Social.CreateAchievement(); 			achievement.id = achievementData.id;  			_achievementDict.Add(achievement.id, achievement); 		} 	} } 

Важно обратить внимание, что пока по достижениям нет прогресса, будет приходить пустой список — это не баг, в этом массиве приходят только достижения, по которым у игрока уже есть прогресс больше 0, поэтому после получения списка имеющихся достижений «заполняем пробелы» по остальным достижениям (с прогрессом 0), чтобы в дальнейшем работать со всеми достижениями по одному принципу.

Отправка прогресса по достижению отличается для обоих платформ:

public void reportProgress(string id) { 	if (Social.localUser.authenticated) 	{ #if UNITY_ANDROID 		(Social.Active as GooglePlayGames.PlayGamesPlatform).IncrementAchievement(id, 1, onReportProgressComplete); #elif UNITY_IPHONE 		IAchievement achievement = getAchievement(id); 		 		// нормализуем значение в рамках 0 - 100 		achievement.percentCompleted += 100.0 / getAchievementData(id).steps; 		 		achievement.ReportProgress(onReportProgressComplete); #endif 	} } 

В нем используются две вспомогательные функции:

// возможность получить данные по достижению за пределами класса public IAchievement getAchievement(string id) { 	return _achievementDict[id]; } // возможность получить вспомогательные данные по достижению, которые нам нужны при расчете прогресса для iOS и которые мы специально храним на клиенте (массив всех возможных достижений) public AchievementData getAchievementData(string id) { 	for (int i = 0; i < _achievements.Length; i++) 	{ 		AchievementData achievementData = _achievements[i]; 		if (achievementData.id == id) 			return achievementData; 	}  	return null; } 

Чтобы отобразить стандартный диалог достижений, воспользуемся функцией:

#if UNITY_ANDROID || UNITY_IPHONE 	Social.ShowAchievementsUI(); #endif

Подводя итог, работа с достижениями под Android проще. В случае с iOS нужно больше всего контролировать на стороне клиента. В этом есть только один плюс — большая гибкость под iOS, за что приходится платить временными затратами.

Так как под Android пришлось использовать сторонний плагин, то я начал проверять написанную логику именно с него. Убедившись, что все работает окей, я решил быстренько проверить логику на iPad и подготовить релизы игры. И тут меня ждал тот самый неприятный сюрприз, который всплыл, когда его меньше всего ожидаешь: функция отправки прогресса для iOS постоянно возвращала false и загадочную строку:

Looking for «ACHIEVEMENT_ID», cache count is 1.

Почитав форумы и вдоволь наэкспериментировавшись, я понял, что достижения под iOS мне не светят, и что это какой-то баг Unity или Game Center. Следующим утром, пребывая в прескверном настроении, я запустил игру на iPad и с удивлением обнаружил, что достижения корректно обрабатываются. Вечером же ситуация повторилась снова. Поразмыслив, я пришел к выводу, что проблема может быть связана с этим: транзакции песочницы имеют намного меньший приоритет, чем игр в сторе, поэтому в «час пик», когда в Америке день, практически ни один прогресс по достижению не выполняется, но если попробовать обновить прогресс достижения, когда в Америке глубокая ночь, и сервера Apple «отдыхают» в ночной прохладе калифорнийской ночи, то практически все достижения обрабатываются. А сообщение «Looking for „ACHIEVEMENT_ID“, cache count is 1.» означает, что в настоящее время отправить прогресс не удается, и Unity кэширует прогресс по достижению локально. Этот прогресс не будет потерян, и отправится на сервер, когда будет возможность установить с ним связь.

Против этой теории выступает тот факт, что разработчики, использующие prime31-плагин для этих целей таких «задержек» не испытывают, и что вероятнее всего проблема именно в Unity. Я решил рискнуть и выдать игру с достижениями в таком «подвешенном» состояли, чтобы проверить свою теорию.

Спустя полторы недели ожиданий игра появилась в сторе. Протестировав лидерборды и достижения я обнаружил, что на продакшене они работают так же загадочно, как и в песочнице. Такое ощущение, что Unity кэширует прогресс по достижениям до какой-то «критической массы», а потом в один момент их синхронизирует. Такой результат работы нельзя назвать удовлетворительным.

Подводя итог: для интеграции достижений и лидербордов в Unity без собственного или стороннего плагина не обойтись, а интеграция занимает определенное время, львиная часть которого уходит на «борьбу с ветряными мельницами» и не является такой простой, как хотелось бы.

ссылка на оригинал статьи http://habrahabr.ru/post/256951/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *