Интеграция Unity кода в React Native

Время прочтения — 16 минут
Содержание
Всем привет! На связи снова команда dev.family с весьма необычной темой. В этот раз поговорим об играх. А именно, как интегрировать Unity в React Native.
На самом деле, это очевидно, что на React Native игру не напишешь. Оно и не надо. Движков, позволяющих разрабатывать игры под разные платформы и операционных системы, будь то iOS или Android, macOS или Windows, — огромное множество. Есть среди Unity и Unreal Engine. Сегодня мы посмотрим, как использовать первый из них в кросс-платформенных мобильных приложениях.
Думаю, не стоит говорить, что сделать полноценно игру на Unity удобнее. Зачем тогда вообще нужна эта странная и непонятная интеграция?
Начнем с того, что игры могут иметь обширное меню и различный функционал. Допустим, встроенный мессенджер, большие настройки, — те же гильдии внутри игры и тд. Речь не идет об AAA играх по типу Genshin, а, скорее, о маленьких инди или подобных, где всю игру можно сделать отдельным приложением, а сами механики и сцены скинуть на юнити. Также юнити хорош не только в создании игр, но и использовании таких технологий как VR или AR, где можно использовать сцены, различные модели, коллизии этих моделей и другое. Тут схожая ситуация с той, где у нас есть отдельное приложение и мы делаем переход на экран с Unity проектом.

Инициализация проекта на React Native

Для инициализации проекта на React Native вводим команду в директории, где хотим создать проект: npx react-native init unityapp (Данный проект использует версию react-native 0.73)
Получаем стандартное начальное приложение на react-native
После чего добавим пару библиотек для навигации, они нам нужны только чтобы переходить на другой экран. В нашем случае, — экран с юнити.

yarn add @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context
Затем cd ios && pod install && cd .. для iOS и Android, следуя документации react-navigation, добавляем этот код в MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(null)
  }
И сверху добавляем импорт:

import android.os.Bundle;
После добавим два экрана, чтобы переходить с одного на другой, и сам навигатор (данный код лишь пример работы библиотек). Таким образом получаем два экрана: один — самого приложения и второй, — где будет наш юнити проект. Получается следующий результат:
Собственно, с подготовкой нашего приложения мы закончили. Приступим ко второй част. Создадим проект на юнити.

Инициализация Unity проекта

Для начала создадим сам проект на Unity, если у вас нет готового. Перед этим установите Unity Hub на компьютер. Инструкцию можно найти здесь. Там есть установщики для всех платформ. После этого выберите и установите LTS версию, так как это поддерживаемая версия. Но вы и так, скорее всего, это поняли.
Теперь создадим сам Unity проект:
В данной статье мы будем рассматривать все на примере 2D игры, — клона Flappy Bird. То есть сейчас создадим 2D проект и сразу на мобильные платформы (по факту это ни на что не влияет):
Таким образом, получаем проект и сразу открываем его.
Здесь вы видите уже готовый проект. Но вы ничего не пропустили. Мы решили не тратить время на описание всего, что было сделано в самом юнити, т.к. это не относится к нашей теме.
Весь код вы можете сами посмотреть, скачать или раскритиковать, если перейдете по ссылке (ссылки находятся внизу) и скачаете проект себе. Или если захотите повторить следующие этапы самостоятельно.
Собственно вот наша мини игра на Unity. Сейчас необходимо переместить ее в мобильное приложение, а по окончании игры перенести наш результат, записать его в приложении и вывести в списке с результатами. Приступим

Интеграция Unity в React Native

React Native part I

Для начала давайте поставим библиотеку, которая позволит нашему приложению взаимодействовать с игрой:

yarn add @azesmway/react-native-unity
После выполняем команду для установки подов под iOS:

cd ios && pod install
ВАЖНО: перед тем, как дальше работать с андроидом, выполнить yarn run android, чтобы появился файл local.properties (он хранит в себе путь к android sdk). Это понадобится в дальнейшем для запуска прилаги, когда будет установлен проект на Unity.

Unity Android

Начну с плохой новости: Hot Reload с Unity проектом мы поддерживать не сможем. Объясню почему: чтобы интегрировать игру в React Native проект, нужно собрать билды самой игры и потом перенести к нам. Как вы поняли, билды и есть та причина, которая лишает нас всякой возможности Hot Reload’а. Перейдем к сборке.
Начнем с Android платформы.
Для начал откроем саму настройку сборки (жаль, что нельзя все сделать одной командой, как в привычном JS, но имеем что имеем).
Переходим в File > Build Settings
Дальше, как видно на картинке, в пункте 4 переходим в Player Settings.
Раскрываем раздел Other Settings и убираем галочку с Auto Graphics API. После этого под Graphics API добавляем OpenGLES3 & OpenGLES2 (не смотрим на то, что оно пишет deprecated), нажимая на значок Плюса. Настройки Color Gamut можете сделать под себя (или оставить как есть, потому что лень разбираться ;-) ).
После этого спускаемся еще ниже до таба Configuration. Там значение поля Scripting Backend меняем с Mono на IL2CPP. Это нужно, чтобы ниже появились новые варианты Target Architectures. Там выбираем все значения (для чего? Да на всякий случай! Шутка) arm нам нужны для мобильных платформ. А x86_64 оставим, поскольку раньше мобильные смартфоны был с процессорами этой архитектуры).
На этом настройка билда для андроида закончена. Можем перейти к сборке и интеграции:
Для начале в корне нашего проекта создаем директорию unity/builds/android . В этой папке будут хранится наши сборки для iOS и Android приложений.
После нажимаем на кнопку Export и выбираем только что созданную папку, экспортируем билд для андроида.
Затем переходим в папку Android нашего приложения и делаем следующее:
В android/settings.gradle добавляем

include ':unityLibrary'
project(':unityLibrary').projectDir=new File('..\\unity\\builds\\android\\unityLibrary')
В android/build.gradle

allprojects {
  repositories {
    // this
    flatDir {
        dirs "${project(':unityLibrary').projectDir}/libs"
    }
В android/gradle.properties

unityStreamingAssets=.unity3d
В android/app/src/main/res/values/strings.xml

<string name="game_view_content_description">Game view</string>
Дальше переходим в нашу созданную папку unity/builds/android, после в unityLibrary/src/main/AndroidManifest.xml и в этом файле удаляем тег <intent-filters> и то что в нем. Это может вызвать проблемы с запуском Android.
Таким образом приготовления закончены, можем добавить UnityView в наше приложение.
Заходим в App.tsx и заменяем View на UnityView

const UnityScreen = () => {
  return ;
};
И не забываем про импорт самого UnityView, а то мало ли ;).
На выходе получаем следующее:

Проблемы, с которыми мы столкнулись (Android)

При интеграции Unity проекта в React Native, хоть все было по инструкции, мы столкнулись со следующими проблемами/ошибками:
• У нас был установлен пакет mobilenotifications.androidlib и он вызывал ошибку при запуске: unityLibrary при сборке не мог его найти. Это можно исправить, закомментировав строку implementation project('mobilenotifications.androidlib') в unityLibrary/build.gradle. На самом деле, его можно вообще выпилить, но было принято решение : работает - не трогай. Поэтому его пока что оставили.

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// implementation project('mobilenotifications.androidlib') - вот эту
}
• Отсутствие compileSdkVersion в build.gradle. Это мы поправили так: заменили compileSdkVersion на ту же что и android/build.gradle в корне проекта и добавили compileSdkVersion version в unityLibrary/build.gradle в defaultConfig.

android {
ndkPath "/Applications/Unity/Hub/Editor/2022.3.16f1/PlaybackEngines/AndroidPlayer/NDK"

compileSdkVersion 34
buildToolsVersion '34.0.0'

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

defaultConfig {
compileSdkVersion 34 // вот здесь
minSdkVersion 22
targetSdkVersion 33
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
versionCode 1
versionName '1.0.0'
consumerProguardFiles 'proguard-unity.txt'
}
}
• И не забудьте проверить, чтобы targetSdkVersion совпадал с тем что в android/build.gradle ;)
В итоге, у нас есть рабочее приложение, написанное на React Native, которое имеет в себе игру написанную на Unity. Но это пока что только на Android. Теперь давайте перейдем к iOS.

Unity iOS

Честно говоря, для нас это была самая сложная часть, хоть и самая короткая. Далее объясним почему. Итак, приступим.
Наверное, больше всего времени ушло на часть c Libraries/Plugins/iOS, так как, если следовать инструкции самой библиотеки, нужно поменять таргет UnityFramework. Но во время сборки при переходе в папку Libraries никакой папки Plugins найдено не было. На github issues мы нашли ответ, что папку нужно добавить самим и поместить туда два файла NativeCallProxy.m & NativeCallProxy.h (их можно взять из репозитория с кодом Unity). Теперь добавляем в папку Assets > Plugins/iOS с этими двумя файлами
После этого нужно собрать билд. Переходим снова в Unity > File > Build Settings. Тут мы выбираем iOS и нажимаем на кнопку Switch Platform (по сути, как и в случае с Android).
Далее переходим в окно Player Settings. Тут указываем Bundle Identifier как у нашего приложения, затем Signing Team ID (если делаем sign, это не обязательно). И посмотреть чтобы Scripting Backend стоял IL2CPP.
Далее в Build Settings нажимаем на кнопку Build и выбираем местоположение нашего билда, для удобства мы собрали его в [root_project]/ios/unityBuild, чтобы его не потерять ;), но это не обязательно так как весь билд в кодовой базе нам не понадобится.
После того как мы собрали билд, открываем в Xcode Unity-iPhone.xcodeproj и выполняем следующие действия:
• Переходим в Libraries/Plugins/iOS и выбираем NativeCallProxy.h. В Target Membership у UnityFramework меняем с Project на Public.
• Выбираем папку Data и меняем Target Membership на UnityFramework.
• Далее, если требуется, выбираем команду разработки и собираем UnityFramework (сборка обязательна!!!).
• После того, как билд был собран, копируем наш UnityFramework в [root_project]/unity/builds/ios
• В заключении выполняем команду
rm -rf ios/Pods && rm -f ios/Podfile.lock && npx pod-install
После всего вышеперечисленного собираем наше приложение на iOS. Обращаем внимание, что корректно работать UnityView будет именно на реальном iOS устройстве, в отличие от Android.

Конечный результат

Заключение

Вот мы и разобрали, как устроена интеграция Unity-проекта в мобильное приложение на React Native. На самом деле, мы понимаем, что задача весьма нетривиальна, и далеко не все проекты могут в этом нуждаться. Однако, как мы и говорили в вводной части, такие примеры встречаются и имеют право на жизнь.
В статье мы пошагово рассмотрели, как устроена данная интеграция. Если тема окажется актуальной и наберет большой охват, напишем следующую, в которой мы улучшим нашу интеграцию, чтобы передавать данные с react-native в Unity и наоборот. Или вы можете попробовать сделать это самостоятельно, следуя инструкции из документации к библиотеке.
С вами была команда dev.family и до новых встреч ;)

Ссылки

Репозиторий, где можно посмотреть код мобильного приложения
Репозиторий, где можно взять код игры