Современную мобильную игру трудно представить без социальной интеграции, общих таблиц рекордов (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, а также авторизироваться:
После успешной авторизации мы можем работать с лидербордами и достижениями.
При работе с лидербордами я решил, что мне нужно прежде всего получить текущий рекорд игрока — это нужно, чтобы можно было сравнивать старый рекорд с новым и если игрок достигает нового топа, выводить об этом сообщение «Congratulations! New Top: XXX». Для этого я написал следующий код, который создает таблицу, устанавливает фильтр игроков, по которым нам нужны данные (только наш игрок), и получает текущий рекорд игрока в случае успеха:
Отправка текущего прогресса выглядит следующим образом (при чем, нам не обязательно заботится о том, что новый результат может быть меньше старого — в этом случае данные будут отброшены сервером):
После тестирования этого кода появилась проблема — он не работает под Android, т.к. в плагине нет реализации этой функции — вот она, прелесть версии 0.9.
Но это не повод для расстройств, поразмыслив, я пришел к выводу, что нет необходимости получать текущий рекорд игрока, достаточно хранить его локально. Дело в том, что если игрок поменяет устройство, или просто удалит вашу игру и вернется в нее спустя какое-то время, ему будет приятнее заново раз за разом бить свой собственный локальный рекорд. Рекорд же в лидербордах будет всегда содержать максимальный прогресс игрока, и чтобы его побить у игрока может уйти много времени, что в конечном счете может снизить мотивацию игрока. Так что я решил отказаться от глобального рекорда, и дописал следующий код к авторизации:
Отправка прогресса на сервер приняла вид:
Таким образом мы запоминаем локальный рекорд игрока, который затем можно использовать для проверки достижения нового рекорда.
Осталось вывести стандартный диалог лидербордов, что можно сделать с помощью функции Social.ShowLeaderboardUI(). По умолчанию для Android отображается список всех лидербордов, даже если он у вас один (таблица «High Scores»), это не очень красиво и требует лишнего выбора от игрока, поэтому пришлось дописать такой код:
Разобравшись с таблицами рекордов и довольный результатом я приступил к реализации достижений, и тут меня ждал большой и неприятный сюрприз, но давайте по порядку.
Достижения есть двух типов: «одноходовые» (achievement) и инкрементируемые (incremental achievement). Первые подразумевают достижение с одного раза, например «запустить ракету» — как только игрок нашел и запустил одну ракету, мы считаем, что достижение выполнено на 100% и открываем его игроку. Инкрементируемые достижения подразумевают пошаговое выполнение в несколько этапов, например, достижение «Охотник за вишенками» подразумевает сбор 15 вишенок, в процессе чего игроку будет постепенно открываться достижение, а после сбора всех 15 вишенок он получит его полностью. Такие достижения мне показались более уместными в моей игре; для начала, я добавил 5 достижений:
Приступив к реализации инкрементируемых достижений я столкнулся с двумя проблемами:
— Разница во взаимодействии с Android и iOS серверами;
— Нужно хранить текущий прогресс по достижению, чтобы каждый раз слать увеличенное значение, иначе достижение не будет расти.
Разница во взаимодействии состоит в том, что Google Play рассчитывает процентное приращение достижения сам, указав в Google Developer Console кол-во шагов 15, мы можем каждый раз отправлять на сервер значение 1, и серверная логика будет складывать единицы до тех пор, пока не наберется 15 и достижение не будет открыто.
Apple Game Center перекладывает заботу о приращении прогресса по достижению на логику клиента, и ждет от нас постепенного увеличение прогресса в пределах от 0 до 100 единиц (процентов). Поэтому если мы будем слать ему постоянно 1, то прогресс постоянно будет 1%.
Итак, в случае с iOS нам нужно получать текущий прогресс достижений и сохранять его, чтобы можно было в будущем слать увеличенное значение. А также нам нужно хранить на клиенте количество шагов (итераций) для того, чтобы отправлять правильное приращение прогресса. Для этих целей я создал вспомогательный класс:
И подготовил данные по своим достижениям (фактически это копия данных, которые я ввел в Google Developer Console для Android):
Важно обратить внимание, что пока по достижениям нет прогресса, будет приходить пустой список — это не баг, в этом массиве приходят только достижения, по которым у игрока уже есть прогресс больше 0, поэтому после получения списка имеющихся достижений «заполняем пробелы» по остальным достижениям (с прогрессом 0), чтобы в дальнейшем работать со всеми достижениями по одному принципу.
Отправка прогресса по достижению отличается для обоих платформ:
В нем используются две вспомогательные функции:
Чтобы отобразить стандартный диалог достижений, воспользуемся функцией:
Вспомогательная функция, которая может пригодится во время тестирования достижений для iOS, возвращает весь список достижений (полученные от сервера + созданные на клиенте):
Подводя итог, работа с достижениями под 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 без собственного или стороннего плагина не обойтись, а интеграция занимает определенное время, львиная часть которого уходит на «борьбу с ветряными мельницами» и не является такой простой, как хотелось бы.
Так как я разрабатываю игру в свободное время в качестве хобби, то мысли о покупке плагинов, например, 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, то прогресс постоянно будет 1%.
Итак, в случае с 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
Вспомогательная функция, которая может пригодится во время тестирования достижений для iOS, возвращает весь список достижений (полученные от сервера + созданные на клиенте):
override public string ToString()
{
string result = "";
foreach (KeyValuePair<string, IAchievement> pair in _achievementDict)
{
IAchievement achievement = pair.Value;
result += achievement.id + " " +
achievement.percentCompleted + " " +
achievement.completed + " " +
achievement.lastReportedDate + "\n";
}
return result;
}
Подводя итог, работа с достижениями под 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 без собственного или стороннего плагина не обойтись, а интеграция занимает определенное время, львиная часть которого уходит на «борьбу с ветряными мельницами» и не является такой простой, как хотелось бы.