В этой статье я хочу рассказать об использовании игровых сервисов Google в вашем приложении на Unity. На написание данного материала меня сподвигло достаточно большое количество проблем, встретившихся во время разработки нашего приложения, а также отсутствие каких-либо материалов на русском языке по этой теме. Да и собственно, на английском тоже. Описание использующегося плагина на гитхабе очень краткое и не дает ответа на возможные проблемы с работой сервисов. Думаю, здесь не стоит пояснять, что мультиплеер и рейтинги игроков зачастую повышают интерес пользователей, а следовательно и вашу возможную прибыль. А благодаря данной статье начинающие разработчики смогут начать использовать данные преимущества.
Плагин
Мы использовали бесплатный плагин Play Games For Unity. В нем содержатся библиотеки для работы с сервисами Google. Плагин включает в себя авторизацию пользователя в Google+, возможность использования достижений и рейтинга для игроков, облаков Google для хранения данных и организации мультиплеера как в реальном времени, так и в пошаговом режиме. Установка плагина не вызывает никаких трудностей: в Unity нужно выбрать импорт ассета (Assets->ImportPackage->CustomPackage) и в открывшемся окне выбрать ассет, находящийся в папке «current-build».
Далее вам нужно ввести id своего приложения: откройте появившийся выпадающий список «Google Play Games» и выберете пункт «Android Setup». Id приложения вы можете узнать в Google Developer Console. Он выдается после добавления нового приложения в «Игровые сервисы», вы можете видеть его рядом с названием вашей игры. После добавления id можно переходить непосредственно к коду.
Для инициализации плагина используйте следующий код:
// Подключаем необходимые библиотеки
using GooglePlayGames;
using UnityEngine.SocialPlatforms;
// Активирует платформу Google Play Games
PlayGamesPlatform.Activate();
PlayGamesPlatform.Activate(); достаточно вызывать лишь один раз после запуска вашего приложения. После инициализации вы можете получить доступ к платформе при помощи Social.Active.
Чтобы реализовать авторизацию пользователя используйте следующий код:
...
// Аутентификация пользователя:
Social.localUser.Authenticate((bool success) => {
// Код, выполняемый после удачной или неудачной попытки.
});
Переменная success принимает значения true при удачном входе и false, соответственно, при неудачном. В нашем случае при удачном входе вызывается метод загрузки необходимых данный пользователя из облака Google. И сразу же первая проблема, встретившаяся у нас на пути: метод вызывается, но при неудачной попытке не возвращает false, а следовательно авторизация не проходит и нет возможности при (!success) вызвать метод еще раз. Пришлось написать костыль, который вызывает метод с определенным интервалом, пока авторизация не пройдет (при условии, что пользователь подтвердил запрос на авторизацию перед этим).
После авторизации пользователя мы получаем возможность использовать сервисы Google.
Поднимаем мультиплеер
Кое-что из этого описано на гитхабе, кое-что на developers.google. Здесь я собрал полезную выжимку.
В плагине доступно 4 режима работы мультиплеера:
- Создание комнаты со случайными игроками (быстрая игра)
- Создание комнаты с экраном приглашения (позволяет приглашать в игру друзей из кругов Google+)
- Обзор приглашений (позволяет видеть, кто из друзей в Google+ хочет пригласить вас в игру)
- Приглашения по id (его рассматривать не будем, т.к. не использовали данный режим в нашем приложении; интересующиеся могут прочитать о нем по ссылке на Github)
Для того, чтобы облегчить работу со следующими функциями ваш класс должен наследоваться от интерфейса RealTimeMultiplayerListener.
Создать «быструю игру»/соединиться
Создается комната, куда набираются случайные оппоненты, или же происходит автоматическое присоединение к уже созданной комнате.
const int MinOpponents = 1, MaxOpponents = 3;
const int GameVariant = 0;
PlayGamesPlatform.Instance.RealTime.CreateQuickGame(MinOpponents, MaxOpponents, GameVariant, listener);
Очевидно, что минимальное и максимальное количество игроков определяется переменными MinOpponents, MaxOpponents. В нашей игре MaxOpponents = 1, это означает, что в мультиплеере у вас будет только один противник.
Если ваш класс наследуется от RealTimeMultiplayerListener, то вместо listener вам нужно написать this.
Создание комнаты с экраном приглашения
Почти идентично предыдущему. Игрок может пригласить друзей из Google+ или добавить случайных оппонентов.
const int MinOpponents = 1, MaxOpponents = 3;
const int GameVariant = 0;
PlayGamesPlatform.Instance.RealTime.CreateWithInvitationScreen(MinOpponents, MaxOpponents, GameVariant, listener);
Обзор приглашений
PlayGamesPlatform.Instance.RealTime.AcceptFromInbox(listener);
Откроется меню, в котором игрок может видеть свои приглашения из Google+.
Соединение с комнатой
Следующий метод позволяет показать пользователю загрузку во время создания или соединения с комнатой:
public void OnRoomSetupProgress(float progress) {
// (процесс загрузки отображается от 0.0 до 100.0)
}
В нашем случае мы просто выводим переменную progress.
Когда соединение с комнатой произошло удачно (или нет) вызывается следующий метод:
public void OnRoomConnected(bool success) {
if (success) {
// Выполняется при успешном соединении
// …можете начинать игру…
} else {
// Выполняется при неудачном соединении
// …сообщение об ошибке…
}
}
В нашей игре для каждой онлайн гонки препятствия генерируются рандомно, и соответственно их положение должно быть одинаково на обоих телефонах. При успешном подключении из списка участников выбирается хост и на его телефоне генерируются препятствия. После того как они сгенерировались, сразу же начинается передача сообщений с параметрами уровня на другой телефон, и как только принимающий телефон загрузил последнее препятствие, он отправляет сообщение о том что готов и игра начинается. Также передается порядковый номер, используемого космического корабля, чтобы на экране другого игрока отображалась верная модель. Более того передаются различные служебные переменные, необходимые для определения готовности всех необходимых для игры параметров.
id участников
Чтобы узнать id всех участников, включая ваш, вы можете использовать следующий код (может выполняться только после соединения с комнатой).
using System.Collections.Generic;
List<Participant> participants = PlayGamesPlatform.Instance.RealTime.GetConnectedParticipants();
Для всех участников лист будет отсортирован одинаково.
Для того чтобы узнать свой Id воспользуйтесь следующим методом:
Participant myself = PlayGamesPlatform.Instance.RealTime.GetSelf();
Debug.Log("My participant ID is " + myself.ParticipantId);
Отправка сообщений:
Плагин поддерживает 2 типа сообщений, надежное и ненадежное. Надежное сообщение медленнее, но гарантирует доставку, ненадежное быстрее, но проверка доставки не осуществляется.
Для отправки надежного сообщения используется следующий код:
byte[] message = ....; // сообщение в формате byte[]
bool reliable = true;
PlayGamesPlatform.Instance.RealTime.SendMessageToAll(reliable, message);
Соответственно для ненадежного следует изменить переменную reliable на false. Сообщение отправится всем участникам комнаты, кроме вас.
Вы так же можете отправить сообщение конкретному участнику:
byte[] message = ...;
bool reliable = true;
string participantId = ...;
PlayGamesPlatform.Instance.RealTime.SendMessage(reliable, participantId, message);
Этим способом вы можете отправить сообщение сами себе, указав в participantId свой id.
Максимальная длина одного надежного сообщения – 1400 байт, ненадёжного 1168 байт.
Здесь также возникла проблема: даже если отправлять по одному сообщению за фрейм, то они не отправляются. Мы так и не выяснили с чем это связано, вероятно они просто не успевают формироваться (возможно кто-то в комментариях поправит меня). Поэтому был сделан счетчик фреймов и сообщения отправлялись через строго определенный интервал, измеряемый во фреймах. Тогда все стало работать великолепно. Во время игры наше приложение подразумевает постоянную отправку сообщений (координаты и угол поворота космического корабля), поэтому используются ненадежные сообщения, потому что скорость передачи выходит на первый план, а если пара пакетов и потеряется, то ничего страшного, при достаточно частой передаче человек практически не заметит этого. Но при присоединении игроков к комнате все отправляемые сообщения с параметрами участников и их кораблей являются надежными, так как отправляются только один раз и их значения критически важны для начала гонки.
Проверяем получение всех необходимых сообщений для начала игры:
Наконец-то после десятков билдов и тестов все начало работать:
Получение сообщений
Когда вы получаете сообщение, вызывается следующий метод:
public void OnRealTimeMessageReceived(bool isReliable, string senderId, byte[] data) {
// Обработка сообщения
}
Полученное сообщения полностью идентично отправленному, как по длине, так и по содержанию.
Так как в нашей игре используется множество различных сообщений (будь то координаты корабля, либо сообщение о выигрыше одно из участников и т.п.), чтобы понять какое сообщение принято, мы пошли на достаточно простой и очевидный шаг: первым байтом шло число, определяющее, что за сообщение и какой метод вызвать при его получении, а со второго байта начинались передаваемые данные.
События соединения
Если пользователь отключается от комнаты, вызывается следующий метод:
public void OnLeftRoom() {
// Возвращение в меню и показ сообщения об ошибке
// не вызывайте здесь PlayGamesPlatform.Instance.RealTime.LeaveRoom()
// вы уже вышли из комнаты
}
Важно: При сворачивании игры игрок отключается от комнаты. Возможно, для некоторых приложений это будет проблемой. Но в нашем случае сворачивание неминуемо привело бы к поражению, в виду специфики игры. Поэтому при отключении от комнаты одного из участников вызывается метод OnPeersDisconnected(), описанный далее.
Если кто-то подсоединяется или отсоединяется от комнаты, будут вызваны следующие методы.
public void OnPeersConnected(string[] participantIds) {
// реакция на появление нового участника
}
public void OnPeersDisconnected(string[] participantIds) {
// реакция на отсоединение участника
}
Участники могут коннектиться в любое время во время игры, если есть пустые слоты, и тут надо следить, чтобы участник не подсоединился после того, как игра уже началась. Можно сделать ожидание, пока все слоты не заполнятся и только тогда начинать игру.
В нашей игре при досрочном выходе одно из участников отправляется сообщение, определяющее победу соперника и игра заканчивается, а следовательно нужно вызвать нижеследующий метод.
Выход из игры
После того, как ваша игра окончена, вам нужно выйти из комнаты:
PlayGamesPlatform.Instance.RealTime.LeaveRoom();
После этого будет вызван OnLeftRoom().
Заключение
Надеюсь, наша статья будет полезна как для новичков, так и для более опытных Unity-разработчиков, не встречавшихся с организацией мультиплеера в своих проектах. Если будет интерес, напишу продолжение про использование данного плагина для работы с облаком Google, с которым тоже возникли трудности при разработке, а ответов на английском/русском языках, естественно, не было.