Хочу поделиться опытом портирования нашей игры The Unexpected Quest на Андроид. В качестве движка использовали Unreal Engine 4. Родная версия 4.23, об особенностях портирования игры на этой версии и пойдет разговор ниже.

Картинка для привлечения внимания
Картинка для привлечения внимания

Бредисловие

Хочу сразу отметить, что при разработке игры мы вообще не ориентировались на мобильную аудиторию. Поэтому никаких KPI, MAU, ARPU, RGB, LGBT и прочего не будет. Игра вышла в SteamNintendo Switch и казуальных порталах. Вот это наша аудитория и по другому мы пока продавать не умеем.

Почему решили выпустить? А потому что можем: аккаунт разработчика есть (тестировали как-то Unity), анрил с андроидом дружит, ну и пощупать что-нибудь новенькое хотелось. Независимо от результатов продаж, я все равно в выигрыше: буду знать тонкости работы с этой мобильной платформой.

Портирование я начал 4 апреля, а закончил 16 июня. Итого два с половиной месяца ненапряжной работы. В этот момент в приоритете был новый DLC, поэтому срок можно смело сократить до 2-х месяцев. А еще очень много времени заняло оформление странички: подготовка скриншотов, тексты и иконки.

В качестве основы использовалась версия движка 4.23. Это не самый лучший вариант, чтобы избежать множества проблем нужно использовать самую свежую версию движка. Но, у меня была "уважительная причина": все новые версии выдавали на моем телефоне Redmi 5 Plus на 10 fps меньше. И хотя на планшете Galaxy Tab A все было нормально, я решил не рисковать. Попытки с наскока разобраться с этой ситуацией не увенчались успехом, а тратить время и зарываться в нее с головой не хотелось.

Подготовка окружения

Операционная система Windows 10. В качестве IDE использовалась Visual Studio 2019 (на момент финального билда игры версии 16.10.2). Как написал выше, движок Unreal Engine 4, версия 4.23. Установлен через лаунчер (т.е. я не собирал движок из сырцов), все галочки сняты, кроме Engine Source и Android. А вот дальше начинаются интересности.

Дело в том, что современные версии движка, начиная с 4.25, работают через Android Studio. А вот старые версии, в том числе и моя, через NVIDIA CodeWorks (совместимость можно проверить в Release Notes). И самое поганое, что NVIDIA закончила поддержку этого набора SDK.

С этого момента начинаются танцы с бубном. Во-первых, этот самый NVIDIA CodeWorks нужно скачать, причем определенной версии 1R7: заходим в архивы с инсталляторами. И видим, что без присоединения к программе NVIDIA Developer Program, скачать его не дадут. Нужно завести аккаунт, присоедниться к программе и повторно зайти в архив. Только после этого появится заветная ссылка.

Далее можно прочитать старую статью об установке этого пакета. Если сделать все как в ней написано, то игру то вы соберете. И даже зальете на свой телефон. Но, google ее не примет: минимальный уровень API в Google Play равен 28, а установится только 26-й. Поэтому вносим свои коррективы. И так, устанавливаем NVIDIA CodeWorks и в какой-то момент установки появится такое окошко:

Component Manager в NVIDIA CodeWorks
Component Manager в NVIDIA CodeWorks

Менять можно только столбец Action, по нему и тыкаем. Если места много, то можно установить все. Если нет, то все дерево Developer Tools срубаем, как и всю документацию. Устанавливаем весь предложенный Android Toolchain, базовый Android SDK и дерево Android 8.0.0 (API 26). Все остально отключаем.

Теперь нужно избавиться от API 26 и поставить что-нибудь новее. Идем в папку, в которую установили NVIDIA CodeWorks и ищем в ней Android SDK Manager: android-sdk-windows\tools\android.bat.

Android SDK Manager
Android SDK Manager

Все что он предлагает удалить — удаляем. Заодно сносим и ранее установленный API 26. Я работаю с Android 10 (API 29), можно накатить все, но достаточно будет и SDK Platform. Все что утилита предлагает обновить — обновляем.

Ребут, молельный танец и можно программить.

Подготовка проекта игры

Тут все дико зависит от проекта, поэтому рассказ только про мой опыт. Без большого тестового стенда устройств, гарантировать запуск игры и приемлемый FPS очень сложно. Поэтому платная версия отпадает, надо дать игроку «пощупать» игру преждем чем определяться с покупкой. Значит будет внутренняя покупка и нужен плагин OnlineSubsystemGooglePlay.

Теперь интересное. Без изменений он не подойдет, получите от гугла варнинг в духе: «We’ve detected that this app is using an old version of Google Play Billing». Вроде как до ноября работать будет, но потом все отвалиться. Создаем папку Plugins в своем проекте (если ее нет) и копируем в нее плагин из движка. Путь к плагину в движке такой: Engine\Plugins\Online\Android\OnlineSubsystemGooglePlay. У себя в проекте городить сложные пути не надо, просто в Plugins закинуть и все.

В скопированном плагине открываем файл: Plugins\OnlineSubsystemGooglePlay\Source\OnlineSubsystemGooglePlay_UPL.xml и вставлем в тег root такой код:

<buildGradleAdditions>
	<insert>
		dependencies {
			implementation 'com.android.billingclient:billing:3.0.0'
		}
	</insert>
</buildGradleAdditions>

Например, после закрытия тега </init>. Все, google задобрили. С покупкой будет еще один маленький нюансик, о котором я расскажу позже. Еще можно удалить папку Intermediate в плагине, для успокоения совести и гарантированной пересборки при следующей компиляции.

На этом подготовка закончена. Можно заново генерировать sln-файл для проекта игры, и открывать его в студии. По идее компиляция не должна добавить новых проблем, так как редактор собирается для x64 платформы майкрософтовским cl. А вот когда собираешь андроид сборку, можно огрести проблем от компилятора, ведь сборка идет в gcc. Но, у меня их небыло: все ошибки я отловил когда готовил сборку для macOS.

Project Settings…

Читаем документацию, куда же без нее? Мне этого не хватило… И остальной документации тоже. Я не буду расписывать настройки для отладки, поговорим только о Shipping версии. Поэтому в разделе Project -> Packaging выставляем Build Configuration в Shipping и ставим галки на Full Rebuild и For Distribution:

Далее идем в Platforms -> Android и тут начинается самое веселье. Очевидные вещи я пропущу, все можно найти на просторах интернета, ну или спросить тут. А вот на нехороших моментах остановлюсь подробнее.

Первое — это Store Version. Она должна быть уникальной, для каждой сборки которую мы готовим. В Google Play их должно быть минимум две: 32- и 64-битные. Эпики заботливо дали дополнительные смещения для этих сборок:

Store Version offset
Store Version offset

Вот только они не работают. Точнее у меня работало так, что 64-битная сборка крашилась на старте быстро, тихо и не оставляя следов. Причина простая: для каждой сборки генерируется также свой OBB файл ресурсов, у которого имя содержит этот самый Store Version + offset. И, "естественно", при поиске этот offset игнорируется. У меня offset в 1 был у 64-битной версии, obb файл содержал номер 116 (например), а игра искала 115-й файл, не находила и завершалась. А со смещением ноль 32-битная версия работала отлично.

Выход? Все ручками! Выставляем Store Version и собираем только 32-битную версию. Затем меняем Store Version и собираем только 64-битную версию. Коряво? Зато дешево, надежно и практично!

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

Во-первых, работает эта галка только в Shipping версии! Во-вторых, без нее в папке пользователя будет создан каталог UE4Game, куда и будут скидываться все настройки и сохранения. А с ней, все должно ложиться в секретную папочку /data/*, или куда там складывают настройки всех приложений. И по идее, на этом все проблемы решены.

Но, не на Android 10! Не везде, но в интернете ходят легенды (и я этот баг поймал на Galaxy Tab), что сэйвы не работают. Именно поэтому пришлось установить свежий SDK. Также нужно указать в Target SDK Version минимум 29 (Android 10), а лучше 30 (Android 11). И добавить дополнительные разрешения:

Дополнительные разрешения
Дополнительные разрешения

И продублирую кодом для DefaultEngine.ini:

[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
...
+ExtraApplicationNodeTags=android:requestLegacyExternalStorage="true"
+ExtraPermissions=android.permission.READ_EXTERNAL_STORAGE
+ExtraPermissions=android.permission.WRITE_EXTERNAL_STORAGE
+ExtraPermissions=com.android.vending.BILLING

Были еще небольшие заморочки с подписью приложения, но это из-за моей невнимательности. Просто на будущее: НЕ ТЕРЯЙТЕ ФАЙЛ КЛЮЧЕЙ!!! Для его замены придется общаться с тех. поддержкой гугла, а это долго и нудно.

Изменения в коде и ассетах

Для меня самой крупной модификацией было новое управление в игре. Пришлось полностью переписать метод APlayerController::InputTouch и всем акторам добавить реакцию на касания. Естественно, по мелочи было очень много правок во всем коде, но глобальных изменений они не повлекли.

По рендеру. Mobile HDR оставил включенным. Все материалы были принудительно переведены в режим Fully Rough. Пришлось полностью отказаться от Refraction и от Pixel Depth Offset. Я боялся, что будут тормозить Masked материалы, но они особо на FPS не повлияли и я оставил их в покое. Освещение как было полностью динамическим, так и осталось. Тени включены на высоких и эпик настройках и полностью отключил на низких и средних.

Кстати, если вы собираетесь управлять графонием через Scalability Settings, то не забудьте перелопатить все Window -> Developer Tools -> Device Profiles. Они имеют повышенный приоритет. Я долго соображал, почему у меня никак не отключаются тени на планшете, пока внимательно не прочитал лог.

По мешам правок было немного. Полигонаж у нас невысокий, правда объектов на поле многовато, но мои устройства справлялись нормально. Пришлось порезать крупные меши на небольшие кусочки. Дело в том, что на меш может влиять только 4 источника света (три если есть один направленный, типа солнца). И на некоторых крупных объектах пропадало освещение от факелов. В остальном, никаких проблем не заметил.

Крупные исправления были в UI. Пришлось поднять в настройках проекта Application Scale до 1.2 и из-за этого часть диалоговых окон «поплыла». А еще я покупку реализовал именно внутри UI и полностью на Blueprints. И с ней возник один неприятный момент, о котором хочу рассказать.

Для работы in-app purchase есть три метода: Read In App Purchase Information (общие сведения о покупке)Make an In-App Purchase (сделать покупку) и Restore In-App Purchases (восстановить покупку на другом девайсе). Из особенностей хочу отметить только то, что у make и restore лучше всего объединять ветки On Success и On Failure в одну, а проверять именно Completion Status:

Проверка успешности восстановления покупки
Проверка успешности восстановления покупки

Но не будем отвлекаться. Рассказываю о неприятности. Покупка была реализована просто: автоматом делаю read и проверяю результат, если все хорошо, то пытаюсь восстановить (restore) покупку, если все плохо — то интырнета нет и рыпаться не стоит. Если игрок жмякнул make, то просто проверяю успешность операции. И вылезла неприятная особенность: делаем покупку на одном из устройств. На другом отключаем инет и заходим в игру. И в этот момент или плагин или андроид делают гадость: они у себя кэшируют пустой список покупок для restore. Как итог при последующем входе с интернетом, покупка не восстанавливается, а купить ее нельзя, потому что make будет писать, что покупка есть и возвращать Cancelled. Выход из ситуации такой, в случае если make возвращает ошибку, заново делать restore, кэш очищается и все работает хорошо. Причем, это я увидел в исходниках другой игры и, пока сам не напоролся на баг, долго ломал голову, зачем они так сделали.

Итог

Do You Guys Not Have Phones?
Do You Guys Not Have Phones?

Как пойдет игра я не знаю, но потыкать в мобильный рынок палочкой точно стоит. Попробуем порекламировать ее или поищем издателя... Надеюсь, что результат будет поводом для еще одной статьи.

Сейчас у нас проходит открытое тестирование, вы можете присоединиться по ссылкам ниже. По этой ссылке можно присоединиться к тестированию через сайт. А по этой ссылке можно присоединиться к тестированию в Google Play на устройстве Android. Ну и добавить нас в список желаемого можно тут.

Я буду крайне признателен за любой фидбэк и тесты! И спасибо за то, что дочитали до этого места! ?