[Перевод] Связываем две мобильные платформы в едином коде на Unity
Для Unity-разработчиков уже привычно управление игровыми потоками и сервисами на таких платформах, как iOS и Android. Однако после того, как в экосистеме появились мобильные сервисы Huawei, теперь нужно поддерживать и еще одну версию игры, если вы хотите охватить игроков, у которых есть девайсы этой фирмы.
Эта статья — как раз о том, как минимальными усилиями управлять зависимостями между несколькими мобильными сервисами Android и Huawei в одной кодовой базе.

Рисунок 1. Разница между поддержкой разных платформ и мобильных сервисов
Как видно на рисунке выше, мобильные телефоны Huawei используют операционную систему Android, так что и ваш Unity-проект должен использовать ее же при сборке для девайсов этой фирмы. Однако теперь вам нужно разработать 2 разных APK на Android или же отдельный пакет для Huawei AppGallery.
В чем разница между этими APK?
Первое и главное различие — мобильные сервисы. Речь идет о таких сервисах, как внутриигровые покупки, реклама, игровые сервисы, аналитика и т. д. Вы не можете использовать GMS в мобильных устройствах Huawei. Из-за этого возникает необходимость в создании двух APK: для релиза в Huawei AppGallery и в Google Play.
Второе не менее важное отличие — имя пакета. Поскольку обе экосистемы работают на Android, чтобы избежать несогласованности и переопределения, в Huawei App Gallery заведено правило: имя вашего пакета должно заканчиваться на .huawei или .HUAWEI. Такой подход используется для отделения сборок для Huawei от всех остальных девайсов на Android.
Но не волнуйтесь: мы можем справиться с этими различиями в одной кодовой базе.
Расскажем о двух небольших приемах, которые помогут решить эти проблемы.
1. Вы когда-нибудь слышали о #defines?
Благодаря определениям (defines) мы можем управлять нашими потоками во время сборки, а благодаря единой среде разработки — и во время кодирования.
О каких потоках речь?
Представьте, что вам нужно оперировать двумя видами игровых сервисов: Google Play и Huawei. Чтобы создать для них приложение в одном коде, можно разделить его при помощи определений (defines). Рассмотрим небольшой пример:
internal static class GameServiceFactory
{
public static IGameServiceProvider CreateGameServiceProvider()
{
#if HMS_BUILD
return new HMSGameServiceProvider();
#else
return new GooglePlayGameServiceProvider();
#endif
}
}
Если вы добавите ключевое слово «HMS_BUILD» в ваш список определений, игра вызовет HMSGameServiceProvider. Так мы сможем управлять нашими потоками в одном коде.
Для управления определениями перед сборкой можно использовать приведенный ниже скрипт. После изменения и сохранения DefineKeywords интегрированная среда разработки обновит поток кода в соответствии с заданными ключевыми словами.
public class ManageDefines : Editor
{
///
/// Symbols that will be added to the editor
///
public static readonly string [] DefineKeywords = new string[] {
//"TEST_VERSION",
"HMS_BUILD",
//"GMS_BUILD",
};
///
/// Add define symbols as soon as Unity gets done compiling.
///
static AddDefineSymbols ()
{
List allDefines = new List();
allDefines.AddRange ( DefineKeywords.Except ( allDefines ) );
PlayerSettings.SetScriptingDefineSymbolsForGroup (
EditorUserBuildSettings.selectedBuildTargetGroup,
string.Join ( ";", allDefines.ToArray () ) );
}
}
2. Скрипты до и после сборки (pre-build и post-build)
Итак, как уже упоминалось ранее, нам нужно изменить имя пакета нашей игры для версии Huawei AppGallery.
Однако, если вы в то же время используете сервисы Google Play, все конфигурации будут привязаны к вашему существующему имени пакета, и его изменение повлияет на конфигурацию. Кроме того, редактор Unity во всплывающем окне будет из раза в раз предупреждать вас об исправлении имени пакета приложения, ведь теперь имя пакета и конфигурация мобильного сервиса отличаются. Закрывать это всплывающее окно снова и снова очень утомительно.
Чтобы решить эту проблему и управлять двумя разными именами пакетов одновременно, можно использовать обходной путь с помощью pre-build и post-build сценариев с определениями.
Взгляните на этот код:
class MyCustomBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public int callbackOrder { get { return 0; } }
public void OnPostprocessBuild(BuildReport report)
{
PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, "your.package.name");
}
public void OnPreprocessBuild(BuildReport report)
{
#if HMS_BUILD
PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, "your.package.name.huawei");
#elif GMS_BUILD
PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, "your.package.name");
#endif
}
}
Как видите, все просто. С помощью определений мы можем изменить имя пакета во время pre-build только для HMS_BUILD. Затем после сборки можно снова вернуть имя пакета к исходному, и тогда Unity больше не будет предупреждать нас о несовпадении имени пакета.
Вот и все. Теперь мы готовы разрабатывать игру в одном коде для Huawei и Google Play одновременно.
Пример разработки приложения
Создадим приложение в одной кодовой базе для Android и Huawei, включающее в себя игровые сервисы, внутриигровые покупки и рекламу.
Мы не будем реализовывать все функции для каждого сервиса, поскольку это всего лишь демонстрационный пример. Каждую фичу и функциональность вы можете расширить самостоятельно.
Структура сервисных модулей

Рисунок 2. Схема работы сервисных модулей
Для каждого сервиса у нас будут свои;
- Менеджер (Manager): этот класс включает в себя игровую логику для сервисов и управляет общими функциональностями. Для разных игр может потребоваться изменение этого класса в соответствии со своими требованиями.
- Фабричный класс (Factory): этот класс включает в себя логику выбора провайдера. В нашем подходе мы будем использовать определения, но вы можете изменить механизм выбора провайдера на свой вкус.
- Провайдер (Provider): для каждого сервиса нам нужно создать свой провайдер. Все зависимости между вашим проектом и мобильными сервисами должны оставаться в рамках этого класса.
- Интерфейс провайдера (Provider Interface): для унификации использования различных мобильных сервисов нам нужны общие провайдеры, и с этой целью заводятся интерфейсы провайдеров. В соответствии с требованиями вашей игры вам нужно определить все методы, которые вы будете использовать в своей игре из мобильных сервисов.
При необходимости можно еще включить:
- Общие сущности (entities): для абстрагирования некоторых сервисов от вашей игры вам могут понадобиться общие сущности. Чтобы сохранить зависимость только от классов провайдеров, можно использовать общие сущности в соответствии с вашими требованиями.
- Общих слушателей (listeners): аналогично общим сущностям, если вам нужны общие слушатели, вы можете создать и их.

Рисунок 3. Пример структуры для модуля игрового сервиса
Теперь давайте рассмотрим примеры и попытаемся понять, что мы можем сделать с помощью описанного в статье метода.
Чтобы абстрагировать мобильные сервисы от игровой логики, мы будем использовать менеджеров. Менеджеры связываются с провайдерами через fabrics. Таким образом, мы можем использовать провайдеров как плагины. Им необходимо реализовать общий интерфейс, содержащий методы, к которым мы хотим получить доступ.
public interface IGameServiceProvider
{
void Init();
bool IsAuthenticated();
void SignOut();
void AuthenticateUser(Action callback = null);
void SendScore(int score, string boardId);
void ShowLeaderBoard(string boardId = "");
void ShowAchievements();
void UnlockAchievement(string key);
CommonAuthUser GetUserInfo();
}
Затем нам понадобится хотя бы один провайдер, реализующий интерфейс сервиса. Как уже говорилось ранее, все зависимости между игрой и сторонними мобильными сервисами должны оставаться в пределах этого класса. Создадим два провайдера: для Huawei и Google Play.
Приведем примеры кода. Их вы можете найти в проекте на GitHub в конце статьи.
Huawei Game Service Provider
public class HMSGameServiceProvider : IGameServiceProvider
{
private static string TAG = "HMSGameServiceProvider";
private HuaweiIdAuthService _authService;
private IRankingsClient _rankingClient;
private IAchievementsClient _achievementClient;
public AuthHuaweiId HuaweiId;
public CommonAuthUser commonAuthUser = null;
public void Init()
{
InitHuaweiAuthService();
}
....
}
Google Game Service Provider
public class GooglePlayGameServiceProvider : IGameServiceProvider
{
private static string TAG = "GooglePlayServiceProvider";
private PlayGamesPlatform _platform;
public CommonAuthUser commonAuthUser = null;
public void Init()
{
InitPlayGamesPlatform();
}
....
}
Теперь нам нужно создать менеджер и класс factory. Тогда мы получим провайдера в соответствии с нашим определением.
public class GameServiceManager : Singleton
{
public IGameServiceProvider provider;
protected override void Awake()
{
base.Awake();
provider = GameServiceFactory.CreateGameServiceProvider();
}
.....
}
Так выглядит наш класс factory:
internal static class GameServiceFactory
{
public static IGameServiceProvider CreateGameServiceProvider()
{
#if HMS_BUILD
return new HMSGameServiceProvider();
#else
return new GooglePlayGameServiceProvider();
#endif
}
}
Вот и все: теперь у нас есть класс GameManager, который может управлять функциями игрового сервиса с несколькими провайдерами.
Чтобы инициализировать GameServices, используем приведенные ниже строки кода там, где это нужно:
public class MainSceneManager : MonoBehaviour
{
void Start()
{
GameServiceManager.Instance.Init();
....
}
....
private void OnClickedScoreBoardButton()
{
GameServiceManager.Instance.provider.ShowLeaderBoard();
}
private void OnClickedAchievementButton()
{
GameServiceManager.Instance.provider.ShowAchievements();
}
....
}
Не будем вдаваться в работу каждого сервисного модуля, поскольку все они имеют одинаковую логику и структуру. Посмотреть на них в деле можно, скопировав код из проекта на GitHub.
Кроме того, если вам нужно руководство по настройке плагина Huawei в Unity, это описано в данном посте.
Перейдем к выводам.
С помощью изменения определений между HMS_BUILD и GMS_BUILD в DefineConfig.cs:
- мы можем получить два разных APK или пакета для Huawei AppGallery и Google Play;
- между HMS и GMS меняются функции входа и выхода (Login&Logout), доски лидеров, достижения, внутриигровые покупки.
Ниже приведены короткие видеозаписи обеих сборок.
В случае «HMS_BUILD»:

В случае «GMS_BUILD»:

Демо-проект и подготовленные APK для HMS и GMS можно найти по ссылке на GitHub.
Crazy Taxi: World Tour охватит весь мир — представлена последняя игровая локация
Показали геймплей Worms: Galactic Tactics
Японский композитор призвал завершить серию Dragon Quest
Глава Xbox считает, что будущее игровой индустрии будет похоже на её начало. Microsoft даст разработчикам новые инструменты
GFA Games закроет онлайн‑шутер Pioner спустя менее года после релиза в раннем доступе
Перед выходом GTA VI фанаты устроят необычные проводы GTA V
Издатель Baldur’s Gate 3 оценил раннюю версию Path of Exile 2 выше Diablo IV
Игроки обнаружили издание GTA VI за 2000 долларов