Ранее я уже писал о своем опыте разработки мобильной словесной игры на Android и iOS, которая пользуется определенной популярностью, и я решил прикрутить к ней режим мультиплеера, когда два игрока соревнуются между собой, составляя слова по очереди, как заключительном раунде телепередачи Сергея Супонева «Звездный час».

На изучение и реализацию мультиплеера у меня ушло полтора месяца, в статье я постараюсь описать концпецию без примеров исходного кода, сделав выжимку из объема проделанной работы.
Приложение было написано на языке С++ c использованием Marmalade SDK. С тех пор вендор прекратил поддержку этой платформы, продав сорцы японцам, и будущее этой среды разработки стало очень туманным.

Встал вопрос о том, на что же портировать текущие проекты для их дальнейшей поддержки.

Cocos2d-x — один из самых распространенных движков разработки кроссплатформенных мобильных игр на C++. Видимо, засчет своей бесплатности и открытого исходного кода. Движок плохо документирован. Описание покрывает скудную часть движка и большая часть материала давно устарела.
По результатам какого-то периода мне все-таки удалось создать прототип своего приложения. Но впечатления были очень нехорошие: такое ощущение, что cocos2d-x собран на коленке. Уровни абстракции Scene, Sprite, Application Delegate показались мне очень неудобными, а необходимость искать ответы на вопросы на кокосовом форуме все чаще приводят тебя к мысли, что ты занимаешься чем-то не тем. Наверное руки у меня не из того места растут.

SDL, так же как и Marmalade SDL, это не движок, это платформа. Он предоставляет низкоуровневое API, из которого я потом выстраиваю удобные мне уровни абстракции. Написано все это на языке C, исходный код открыт.
Если в двух словах, то SDL — это свободная кроссплатформенная библиотека для работы с графикой, звуком, и обработки сообщений от операционной системы. Очень удобно делать win32-сборку и отлаживать логику игры на винде, оставляя для мобильных эмуляторов и физических устройств только отладку специфичного для ОС функционала.
К счастью или сожалению, в SDL не предусмотрены инструменты, для такой узкой задачи, как разработка мультиплеера для iOS и Android, поэтому мне пришлось интегрироваться с соответствующими сервисами самому.
Логика приложения и вся работа с графикой реализована в основном потоке, который представляет из себя цикл обработки сообщений и стартует в функции main. Назовем этот поток SDL Thread. В свою о��ередь другие потоки, бросают ему события (SDL_PushEvent) для обработки в очередь, а тот читает их из нее с помощью SDL_WaitEvent и SDL_PollEvent. Это либо системные события, бросаемые системой и поддержка которых уже реализована в SDL, либо вызовы Callback-ов и Listener-ов, которые мы реализуем уже сверх функционала SDL.

Вся логика игры написана на C++. Каталог проекта содержит набор *.cpp файлов, которые можно разделить на три группы:
Соответственно есть три отдельных каталога под проект каждой платформы:
Теперь нам нужно приклеить отдельный слой, который будет отвечать за такой функционал как:
Обе платформы iOS и Android поддерживают Real-time Multiplayer (RTMP). В случае с Android мы интегрируемся с Google Play Services (GPS), в случае с iOS — Game Center. Ранее Google поддерживал и интеграцию с iOS, но в этом году решил от нее отказаться.
В данной статье я не буду описывать те действия, которые нужно выполнить в Google Play Console и AppStoreConnect для настройки мультиплеера, не буду описывать спецификацию классов и методов интеграции — все это описано на сайтах вендоров.
Далее я кратко опишу, какие изменения нужно сделать в проекте для каждой из платформ.
Как? Я об этом еще не сказал? Для компиляции C++ кода используется Android NDK. Хотя, если Вы Android-разработчик, то и так знаете.
Общая инструкция для интеграции Google Play Services в Android-проект описана на сайте для Android-разработчиков. В своем проекте я использую следующие зависимости:
Изначально была мысль заюзать C++ api, которое поставляется в виде скомпилированных статических библиотек без исходников. Из-за того, что в списке библиотек отсутствует сборка под платформу x86_64, я решил, что ребята из Google не очень-то и следят за актуальностью этого SDK и решилизобрести свой велосипед написать этот слой на Java, обернув его JNI-врапперами. Да и потом, зачем мне лишняя зависимость в виде либ без исходников, которые внутри себя все равно дергают Java? Помимо актуальности Java-классов нужно будет следить еще и за актуальностью этих либ.
В качестве пособия использовал наглядный пример из Google Samples. Спасибо Google за это. Apple, бери пример с Google!
Для интеграции с Game Center необходимо подключить GameKit framework. Весь слой интеграции с Game Center опишем в одном *.m-файле и интерфейс к нему предоставим через отдельный *.h файл. Так как C++ является подмножеством языка objective-C, то и со сборкой *.cpp и *.m файлов в одном проекте проблем не возникнет.
Помимо официальной документации руководствовался этим проектом: GameCenterManager. Правда некоторые вещи из примера уже устарели, XCode 10 Вам на это укажет и Вы замените устаревший функционал новым.
Изучив особенности работы с мультиплеерам на обоих платформах, для своего приложения я создал единую C++ асбстракцию и в момент компиляции под нее «подкладывается» соответствующая реализация в зависимости от конкретной платформы. То есть, мое приложение не знает ни о каких Google Play Services, Game Center и их особенностях. Оно знает только предоставленное ей C++ api, где например, есть такие методы, как:
Игрок может пригласить друга из списка своих контактов, либо начать игру со случайным соперником. Игрок, получивший приглашение, может его принять, либо отклони��ь. Для всех этих сценариев я использую стандартный интерфейс используемого сервиса. Хочется отметить, что гугловые мордочки смотрятся гораздо приятнее iOS-овских. Может быть когда-нибудь руки доберутся и я напишу свой интерфейс с домино и барышнями.
Когда два игрока подключились к виртуальной игровой комнате, они получают соответствующие Callback-и. Теперь надо выбрать, кто будет хостом.
Среди игроков нужно выбрать хоста, чтобы именно он определил начальное состояние игры.
Рассмотрим возможные способы маршрутизации сообщений между игроками в общем случае. Обратите внимание, что во втором варианте хосту еще отводится и роль маршрутизатора.

Поскольку у меня всегда в игре только два игрока, то, выходит, у меня частный случай peer-to-peer соединения. И поэтому на роль хоста выпадает только определение начального состояния, а именно — выбор слова, из которого будут составляться слова.
Итак, после того, как произошло подключение игроков к игровой комнате, каждому из игроков известен список идентификаторов участников начавшейся игры. Будем называть его список of participantID. participantID — это некий уникальный строковый идентификатор участника игры, который присваивается сервисом. Нужно выбрать, кто из них будет хостом, донести это до самого хоста и сообщить другому, что в качестве хоста выбран его соперник. Как это сделать?
В гугловой доке я не нашел советов по выбору хоста. Молчат, партизаны. Но добрые люди на stackoverflow.com кинули ссылку на видео, где подробно объясняется следующий принцип:
Для iOS есть метод chooseBestHostPlayerWithCompletionHandler, который значительно упрощает сценарий выбора хоста по сравнению с тем, что я описал для Android. Но, судя по заметным задержкам во время вызова этого метода, он оценивает параметры отклика сети, меряет ping-и и на основе этой статистики решает, кому быть хостом. Это скорее подходит для приведенной выше клиент-серверной архитектуры, где хост выполняет роль маршрутизатора. В моем же варианте частного peer-to-peer соединения это не имеет смысла и в целях экономии времени я юзаю принцип, аналогичный тому, что я сделал для Android.
Что представляет собой сообщение? Сообщение — это массив байт.
Существует 2 типа отправки сообщений:
Как правило Unreliable доставляется быстрее, чем Reliable. Подробнее можно прочитать на сайте вендоров:
Как мы будем использовать это массив? Очень просто:
Итак, определяем enum с типами сообщений, которыми игроки будут обмениваться между собой в процессе игры:
Когда приложение получает входящее сообщение от соперника, вызывается соответствующий Callback, который в свою очередь передает его основному потоку SDL Thread для обработки.
Игровые сервисы (что у Google, что у Apple) имеют функционал listener-ов, которые в том или ином виде призваны оповещать нас о разрыве связи с соперником. Но, я заметил, что если одного из игрока отключить от интернета, то второй далеко не сразу узнает, что первый отключился и играть-то не с кем. Callback-и в таких случаях не вызываются, либо вызываются по прошествии достаточно долгого времени. Чтобы в этом случае второму игроку не ждать, когда рак на горе свистнет, мне пришлось сделать собственный мониторинг соединения, работающий по принципу:
В результате проделанной работы я получил игру, в которую играю сам со своими друзьями и семьей. Играю как на iOS, так и на Android-е.
Правда на iOS есть нюанс — почему-то не фиксируются очки в Leaderboards, о чем в настоящий момент я веду переписку со службой поддержки Apple.
Надеюсь, эта статья будем полезна как и участникам моей команды, так и тем, кто интересуется разработкой мобильных приложений. Спасибо за внимание.

На изучение и реализацию мультиплеера у меня ушло полтора месяца, в статье я постараюсь описать концпецию без примеров исходного кода, сделав выжимку из объема проделанной работы.
Немного истории
Приложение было написано на языке С++ c использованием Marmalade SDK. С тех пор вендор прекратил поддержку этой платформы, продав сорцы японцам, и будущее этой среды разработки стало очень туманным.

Встал вопрос о том, на что же портировать текущие проекты для их дальнейшей поддержки.
Почему не cocos2d-x

Cocos2d-x — один из самых распространенных движков разработки кроссплатформенных мобильных игр на C++. Видимо, засчет своей бесплатности и открытого исходного кода. Движок плохо документирован. Описание покрывает скудную часть движка и большая часть материала давно устарела.
По результатам какого-то периода мне все-таки удалось создать прототип своего приложения. Но впечатления были очень нехорошие: такое ощущение, что cocos2d-x собран на коленке. Уровни абстракции Scene, Sprite, Application Delegate показались мне очень неудобными, а необходимость искать ответы на вопросы на кокосовом форуме все чаще приводят тебя к мысли, что ты занимаешься чем-то не тем. Наверное руки у меня не из того места растут.
Мой выбор пал на SDL2

SDL, так же как и Marmalade SDL, это не движок, это платформа. Он предоставляет низкоуровневое API, из которого я потом выстраиваю удобные мне уровни абстракции. Написано все это на языке C, исходный код открыт.
Если в двух словах, то SDL — это свободная кроссплатформенная библиотека для работы с графикой, звуком, и обработки сообщений от операционной системы. Очень удобно делать win32-сборку и отлаживать логику игры на винде, оставляя для мобильных эмуляторов и физических устройств только отладку специфичного для ОС функционала.
К счастью или сожалению, в SDL не предусмотрены инструменты, для такой узкой задачи, как разработка мультиплеера для iOS и Android, поэтому мне пришлось интегрироваться с соответствующими сервисами самому.
Архитектура многопоточного приложения
Логика приложения и вся работа с графикой реализована в основном потоке, который представляет из себя цикл обработки сообщений и стартует в функции main. Назовем этот поток SDL Thread. В свою о��ередь другие потоки, бросают ему события (SDL_PushEvent) для обработки в очередь, а тот читает их из нее с помощью SDL_WaitEvent и SDL_PollEvent. Это либо системные события, бросаемые системой и поддержка которых уже реализована в SDL, либо вызовы Callback-ов и Listener-ов, которые мы реализуем уже сверх функционала SDL.

Вся логика игры написана на C++. Каталог проекта содержит набор *.cpp файлов, которые можно разделить на три группы:
- кроссплатформенные — те файлы, которые включаются в сборки всех платформ (логика игры);
- моноплатформенные, т.е. включаются в приложение какой-то одной платформы для реализации ее особенностей.
Соответственно есть три отдельных каталога под проект каждой платформы:
- proj.win32 — проект VS2017 Community Edition;
- proj.android — проект Android с использованием Gradle;
- proj.ios — XCode проект для iOS.
Интеграция с сервисами мультиплеера
Теперь нам нужно приклеить отдельный слой, который будет отвечать за такой функционал как:
- поиск соперника, подключение к игре;
- обмен сообщениями между соперниками;
- выход из игровой комнаты;
- фиксация набранных очков игрока в Leaderboards.
Обе платформы iOS и Android поддерживают Real-time Multiplayer (RTMP). В случае с Android мы интегрируемся с Google Play Services (GPS), в случае с iOS — Game Center. Ранее Google поддерживал и интеграцию с iOS, но в этом году решил от нее отказаться.
В данной статье я не буду описывать те действия, которые нужно выполнить в Google Play Console и AppStoreConnect для настройки мультиплеера, не буду описывать спецификацию классов и методов интеграции — все это описано на сайтах вендоров.
Далее я кратко опишу, какие изменения нужно сделать в проекте для каждой из платформ.
Android
Как? Я об этом еще не сказал? Для компиляции C++ кода используется Android NDK. Хотя, если Вы Android-разработчик, то и так знаете.
Общая инструкция для интеграции Google Play Services в Android-проект описана на сайте для Android-разработчиков. В своем проекте я использую следующие зависимости:
implementation 'com.google.android.gms:play-services-games:16.0.0'
implementation 'com.google.android.gms:play-services-nearby:16.0.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'Изначально была мысль заюзать C++ api, которое поставляется в виде скомпилированных статических библиотек без исходников. Из-за того, что в списке библиотек отсутствует сборка под платформу x86_64, я решил, что ребята из Google не очень-то и следят за актуальностью этого SDK и решил
В качестве пособия использовал наглядный пример из Google Samples. Спасибо Google за это. Apple, бери пример с Google!
iOS
Для интеграции с Game Center необходимо подключить GameKit framework. Весь слой интеграции с Game Center опишем в одном *.m-файле и интерфейс к нему предоставим через отдельный *.h файл. Так как C++ является подмножеством языка objective-C, то и со сборкой *.cpp и *.m файлов в одном проекте проблем не возникнет.
Помимо официальной документации руководствовался этим проектом: GameCenterManager. Правда некоторые вещи из примера уже устарели, XCode 10 Вам на это укажет и Вы замените устаревший функционал новым.
Принцип работы с Multiplayer-слоем
Единая точка входа
Изучив особенности работы с мультиплеерам на обоих платформах, для своего приложения я создал единую C++ асбстракцию и в момент компиляции под нее «подкладывается» соответствующая реализация в зависимости от конкретной платформы. То есть, мое приложение не знает ни о каких Google Play Services, Game Center и их особенностях. Оно знает только предоставленное ей C++ api, где например, есть такие методы, как:
SignIn() // войти в игровой сервис
SignOut() // выйти из игрового сервиса
LeaveRoom() // покинуть комнату игры
SendMessage(...) // отправить сообщение оппоненту
ShowLeaderboards() // показать доску лидеров
SubmitScore(...) // зафиксировать набранные очки
...
Поиск соперника
Игрок может пригласить друга из списка своих контактов, либо начать игру со случайным соперником. Игрок, получивший приглашение, может его принять, либо отклони��ь. Для всех этих сценариев я использую стандартный интерфейс используемого сервиса. Хочется отметить, что гугловые мордочки смотрятся гораздо приятнее iOS-овских. Может быть когда-нибудь руки доберутся и я напишу свой интерфейс с домино и барышнями.
Подключение к игровой комнате
Когда два игрока подключились к виртуальной игровой комнате, они получают соответствующие Callback-и. Теперь надо выбрать, кто будет хостом.
Выбора хоста
Среди игроков нужно выбрать хоста, чтобы именно он определил начальное состояние игры.
Рассмотрим возможные способы маршрутизации сообщений между игроками в общем случае. Обратите внимание, что во втором варианте хосту еще отводится и роль маршрутизатора.

Поскольку у меня всегда в игре только два игрока, то, выходит, у меня частный случай peer-to-peer соединения. И поэтому на роль хоста выпадает только определение начального состояния, а именно — выбор слова, из которого будут составляться слова.
Итак, после того, как произошло подключение игроков к игровой комнате, каждому из игроков известен список идентификаторов участников начавшейся игры. Будем называть его список of participantID. participantID — это некий уникальный строковый идентификатор участника игры, который присваивается сервисом. Нужно выбрать, кто из них будет хостом, донести это до самого хоста и сообщить другому, что в качестве хоста выбран его соперник. Как это сделать?
Выбора хоста на Android
В гугловой доке я не нашел советов по выбору хоста. Молчат, партизаны. Но добрые люди на stackoverflow.com кинули ссылку на видео, где подробно объясняется следующий принцип:
- каждый из участников сортирует список participantID (по возрастанию или по убыванию — не важно, главное, чтобы все делали в одном порядке);
- каждый из участников сравнивает свой participantID с первым participantID из списка;
- если они совпадают, значит текущему игроку дано право выбора, кто будет хостом. Он
бросает монеткудергает random(), выбирая тем самым хоста из существующих участников, и сообщает всем, кто есть хост.
Выбора хоста на iOS
Для iOS есть метод chooseBestHostPlayerWithCompletionHandler, который значительно упрощает сценарий выбора хоста по сравнению с тем, что я описал для Android. Но, судя по заметным задержкам во время вызова этого метода, он оценивает параметры отклика сети, меряет ping-и и на основе этой статистики решает, кому быть хостом. Это скорее подходит для приведенной выше клиент-серверной архитектуры, где хост выполняет роль маршрутизатора. В моем же варианте частного peer-to-peer соединения это не имеет смысла и в целях экономии времени я юзаю принцип, аналогичный тому, что я сделал для Android.
Обмен сообщениями между игроками
Что представляет собой сообщение? Сообщение — это массив байт.
- в языке java это тип:
byte[] - в objective-C это:
NSData * - в C++ я маплю все вышеуказанное на
std::vector<Uint8>
Существует 2 типа отправки сообщений:
- Reliable — гарантированная доставка через очередь. Используется для доставки критичных сообщений.
- Unreliable — негарантированная доставка. Используется сообщений, успешностью доставки которых можно пренебречь.
Как правило Unreliable доставляется быстрее, чем Reliable. Подробнее можно прочитать на сайте вендоров:
Как мы будем использовать это массив? Очень просто:
- в первый байт будем писать тип сообщения.
- если сообщение будет иметь какие-то параметры, то будем класть их в следующие байты. Для каждого типа сообщения, имеющего доп. параметры, реализуем свою функцию сериализации и десериализации.
- в конец сообщения для проверки целостности будем класть контрольную сумму.
Итак, определяем enum с типами сообщений, которыми игроки будут обмениваться между собой в процессе игры:
- Я выбран хостом. Передаю начальное состояние. Сейчас мой ход (параметры: номер версии протокола обмена сообщениями, исходное слово);
- Ты выбран хостом. Жду от тебя начальное состояние;
- Я открываю (называю) слово. Теперь твоя очередь (параметр: названное слово);
- Я сдаюсь. Ты победил;
- Я не смог составить слово за время хода. Ты победил;
- Я согласен на реванш;
- Я выхожу из игры;
- Ошибка разбора сообщения. Отключаюсь;
- Твоя версия протокола обмена сообщениями устарела. Проверь обновление приложения. Отключаюсь;
- Моя версия протокола обмена сообщениями устарела. Нужно проверить обновление. Отключаюсь;
- Пинг (системное сообщение);
Когда приложение получает входящее сообщение от соперника, вызывается соответствующий Callback, который в свою очередь передает его основному потоку SDL Thread для обработки.
Мониторинг соединения
Игровые сервисы (что у Google, что у Apple) имеют функционал listener-ов, которые в том или ином виде призваны оповещать нас о разрыве связи с соперником. Но, я заметил, что если одного из игрока отключить от интернета, то второй далеко не сразу узнает, что первый отключился и играть-то не с кем. Callback-и в таких случаях не вызываются, либо вызываются по прошествии достаточно долгого времени. Чтобы в этом случае второму игроку не ждать, когда рак на горе свистнет, мне пришлось сделать собственный мониторинг соединения, работающий по принципу:
- Каждый из игроков каждую секунду отправляет сопернику пинг-сообщение;
- Каждый игрок проверяет: если от соперника не было сообщения больше 5 секунд, значит связь потеряна, выходим из игры.
Результат
В результате проделанной работы я получил игру, в которую играю сам со своими друзьями и семьей. Играю как на iOS, так и на Android-е.
Правда на iOS есть нюанс — почему-то не фиксируются очки в Leaderboards, о чем в настоящий момент я веду переписку со службой поддержки Apple.
Надеюсь, эта статья будем полезна как и участникам моей команды, так и тем, кто интересуется разработкой мобильных приложений. Спасибо за внимание.
