Никому не советую, но мы попробовали, или Интеграция игры в React Native с помощью Unity, Game Engine и Godot
June 10, 2025
preview

Меня зовут Алексей Цуцоев, я разработчик мобильных приложений в KODE, и я хочу поделиться историей того, как мы внедряли игру в уже готовое React-Native приложение. Как выбирали технологию, с какими трудностями столкнулись и к каким выводам пришли. Прежде чем мы начнем, хотелось бы отметить несколько важных моментов:

  • Я не game-developer. Остальная команда также не имела опыта разработки игр. Я — просто мобильный разработчик, которому поставили задачу разработать и интегрировать игру в приложение.
  • У нас была выстроена инфраструктура CI/CD, которая может отличаться от вашей.
  • Я постарался сфокусироваться на процессе от аналитики и дизайна до интеграции и подготовки билда. Для одной библиотеки я отдельно подготовил инструкцию.

Начнем повествование с постановки задачи по внедрению игры, а закончим конкретными реализациями на различных технологиях.

Постановка задачи и преданалитика

Мы разрабатывали финтех-приложение для обмена и инвестиций в криптовалюте. В прошлом году, на Новый год, решили добавить акцию с лотереей: пользователь вращает виртуальный барабан и ему выпадает какой-то приз. Эта акция была признана мега-успешной. И мы решили развивать эту идею. Поскольку второй барабан продукт-овнер не хотела, решили добавить мини-игру. Выбрали клон Flappy Bird — с ограничением по количеству очков и своими ассетами. Исполнителем назначили меня.

Цель игры — поднять вовлеченность и напомнить о приложении. Насколько я знаю, цели мы достигли — продажи были подняты на 10%.

И первые трудности возникли уже на старте. Как упоминалось в предисловии — команда не имела опыта разработки игр. Поэтому, когда дизайнеры закончили макеты, те оказались мало пригодными для использования.

Дизайнер в продуктовой разработке мобильных приложений совсем не равен художнику в геймдеве. Основная проблема в том, что они не умели готовить ассеты, из которы я бы потом мог сделать текстуры, их PNG просто не получалось натянуть на игровые объекты.

Первый важный момент: предоставьте пример ассетов вашим дизайнерам.

Найдите открытый репозиторий кода с игрой, клон которой собираетесь делать, и предоставьте ассеты из этого репозитория вашим дизайнерам. Это серьезно упростит процесс и сэкономит время на старте.

После нескольких итераций подготовки макетов и ассетов мы, наконец, сделали что-то, с чем я бы мог работать, что нравилось дизайнерам и продукт-овнеру. Так что мы приступили к, с позволения сказать, гейм-дизайну.

На этом этапе необходимо максимально продумать, как пользователь будет играть. Склонировать core-gameplay Flappy Bird, или вашего аналога, скорее всего, не составит труда. Проблемы начнутся, когда придётся сделать шаг влево или вправо от изначальной механики: добавить ограничение по очкам, упростить сложность или что-либо ещё, что будет отличать вашу игру от изначальной. И тут проблема не в технической реализации — в целом, большинство вещей будет правиться парой строк кода. Сложность в том, чтобы сделать это весёлым.

Звучит весьма банально, но QA окажется как нельзя кстати — опыт тестировщиков в "а если пользователь сделает вот так?" будет невероятно полезен уже на этапе аналитики. Таким образом вы сможете покрыть максимальное количество кейсов. Забегая вперёд, мы с аналитиком упустили парочку моментов, которые пришлось дорабатывать после тестирования.

Второй момент: если кажется, что продумано всё — подумайте ещё.

Так или иначе, дизайн готов, какой бы то ни было гейм-дизайн — тоже. Пришло время выбрать, как будем делать саму игру.

Поиск технологии

Для начала я взял некоторое время на ресерч и определил круг технологий, которые я смогу применить. Всерьёз рассмотрел три варианта:

  • Сделать всё на React Native, Skia и React Native Reanimated
  • Сделать игру нативно на UIKit/SwiftUI под iOS и Compose под Android
  • Завести один из игровых движков: Unity, Godot, Unreal или React Native Game Engine + matter-js

Определившись с шорт-листом номинантов, я пошёл методом исключения.

Почему не React Native?

Причины просты: все мы любим React Native за удобство и скорость разработки, но ещё ни один разработчик мобильных приложений в этой вселенной не сказал: "я люблю React Native за его производительность". У меня были серьёзные опасения, что перфоманс на не флагманских девайсах будет неудовлетворительным. Кроме того, пришлось бы самостоятельно реализовывать механизмы спавна-удаления игровых объектов и коллизии. Посовещавшись с командой, мы решили, что делать на React Native нам не подходит.

Почему не нативно?

Это решение 100% работало бы быстрее, чем RN + Skia + RNR, даже на слабых девайсах, но проблема реализации спавна-удаления объектов и коллизии никуда не девается. Кроме того, при выборе нативной реализации астрологи объявляют месяц дублирования кода — количество игр увеличивается вдвое. Да, выбирая нативную реализацию, мы обрекаем себя на разработку и поддержку сразу двух игр: одну для iOS, вторую для Android. А это означает, что любую правку от менеджмента или баг сразу умножаем на два.

Остаются игровые движки

Unreal мы сразу отмели, поскольку экспертизы в С++ в команде не было. Почти сразу за ним отправился React Native Game Engine из-за недоверия к этой технологии — последний коммит по существу был оставлен летом 2020-го.

А вот дальше интереснее: остаются Unity и Godot.

Плюсы Unity:

  • Популярная и понятная технология с миллионами туториалов
  • Есть несколько библиотек для интеграции в React Native
  • Условно-бесплатное распространение (рекомендую внимательно ознакомиться с правилами лицензирования перед релизом)

Плюсы Godot:

  • Полностью бесплатный
  • Легковеснее Unity (что нужно для простой игры в духе клон Flappy Bird)
  • Можно вести разработку на gdScript – питонообразный язык, который с нуля будет проще и быстрее освоить, чем C#, который необходим для Unity.

Но имелся один большой минус, перечёркивающий всё — для него существует единственная библиотека интеграции игры в React Native, и она не поддерживает Android. А нас это категорически не устраивает. Таким образом, выбор сам собой сузился до Unity. Наконец-то мы можем приступить к разработке.

Внедрение Unity

Про интеграцию Unity в React Native уже написано немало — и статей, и туториалов. Плюс библиотека, которую мы будем использовать (@azesmway/react-native-unity, имеет вполне приличную документацию. Но, как всегда, дьявол в деталях. Поэтому ниже — пошаговое описание процесса с указанием нюансов.

Установка Unity и создание проекта

Заходим на официальный сайт Unity и скачиваем Unity Hub.

Через него устанавливаем нужную версию движка. В моём случае это была Unity 6 (6000.0.36f1).

  1. В Unity Hub создаём новый проект:
  • Жмём New Project
  • Выбираем шаблон 2D (Mobile)
  • Даём имя, указываем папку, запускаем

Игру писать с нуля мы не стали — взяли готовый туториал, адаптировали под свои нужды и заменили ассеты. Сам код геймплея сюда не включаю — это отдельная тема. Уверен, вы легко найдёте десятки руководств по Flappy Bird под Unity.

После окончания разработки игры необходимо экспортировать исходники под каждую платформу в React-Native. Начнем с подготовки самого проекта для этой интеграции.

Теперь нам понадобится библиотека @azesmway/react-native-unity. Она поможет интегрировать игру в наше React-Native приложение.

  1. Устанавливаем зависимость по инструкции.

yarn add @azesmway/react-native-unity && npx pod-nstall

Могут возникнуть проблемы с кешами. После выгрузки новых iOS-исходников имеет смысл очищать поды, но об этом позже. После установки зависимости необходимо экспортировать исходники под каждую платформу. Начнем с iOS, так как там меньше нюансов.

iOS

Важный нюанс: пока в проекте присутствует Unity, сборка под iOS Simulator работать не будет. Только физическое устройство. Это означает, что вы теряете привычный цикл быстрой отладки на симуляторе. К этому стоит подготовиться заранее.

В первую очередь в unity-проект необходимо добавить код, чтобы ios могла общаться с unity. Нам потребуется два файла: NativeCallProxy.h и NativeCallProxy.mm. Им можно найти в репозитории самой библиотеки.

Вот что нужно сделать:

  1. В Unity откройте File → Build Settings и выберите платформу iOS. Нажмите Switch Platform.
  2. Затем откройте Player Settings. В разделе Other Settings укажите: Bundle Identifier, версию, IL2CPP как Scripting Backend
  3. Перейдите в раздел Identification и настройте Team ID и Provisioning Profile, если необходимо.
  4. В проекте добавьте файлы NativeCallProxy.h и NativeCallProxy.mm в Assets/Plugins/iOS.
  5. Вернитесь в Build Settings, нажмите Build, выберите папку (не внутрь RN-проекта!) — Unity сгенерирует Xcode-проект.
  6. Откройте его и выполните ручные настройки:
  • У Data и NativeCallProxy.h измените Target Membership на UnityFramework
  • Установите public для NativeCallProxy.h, чтобы он был доступен
  1. В Xcode выберите таргет UnityFramework, схему Any iOS Device (arm64) и соберите проект (Cmd + B).
  2. Найдите полученный UnityFramework.framework в Xcode → Products, щёлкните правой кнопкой, выберите Show in Finder, и скопируйте его в папку unity/builds/ios внутри вашего RN-проекта.

Android

Теперь нужно повторить то же упражнение, но уже для Android. В этот раз буду краток.

Сборка Android-проекта в Unity

  • В Unity выбираем Android как платформу.
  • По необходимости настраиваем Player Settings. Экспортируем исходники для Android (как делали для iOS).

Первая волна правок

Далее следуем инструкции из библиотеки:

  • В AndroidManifest.xml (ROOT_OF_YOUR_ANDROID_UNITY/unityLibrary/src/main/AndroidManifest.xml) находим и удаляем тег <intent-filter> вместе с его содержимым.

  • Вносим настройки в: build.gradle (корневой), gradle.properties, settings.gradle, strings.xml.

Вторая волна правок (эксклюзив!)

На этом библиотечная инструкция заканчивается, но я раскопал еще пару мест, которые нуждаются во внимании:

  • Глобальным поиском по проекту ищем mobilenotifications.androidlib и удаляем в конфигаруционных файлах все что с ним связано.
  • После этого делаем Sync Gradle.
  • В структуре проекта появится одноимённый модуль — его можно удалить (необязательно, он и так не участвует в сборке, но зачем держать лишний мусор?).

Фокус с enableOnBackInvokedCallback Возвращаемся к AndroidManifest.xml, где мы уже удаляли интенты. Там есть тег <application> и в нём параметр: android:enableOnBackInvokedCallback="false"

Нужно убедиться, что это значение совпадает с тем, что указано в основном проекте. Эту настройку я не встречал ни в одном туториале или статье. Пришел к ней методом проб и ошибок. Без нее проект отказывался собираться. Дело в том, что она конфликтовала с нашим манифестом. Так что пришлось перевести значение в true.

Финал: копируем проект

После всех танцев с бубном — копируем весь игровой Android-проект целиком и вставляем его в: ROOT_YOUR_RN_PROJECT/unity/builds/android

Unity 6 не поддерживается (но мы хитрые)

Текущая версия библиотеки официально не поддерживает Unity 6, но мы можем это быстро починить.

  1. Открываем файл: android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java
  2. Находим метод: public FrameLayout requestFrame()
  3. И правим catch-блок следующим образом:

diff --git a/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java b/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java

index b944779adbf2392ad8fed91bfd0f5a93c7fff28d..f9668b64827fe161717676bf1b562630e68b4d3b 100644

Вот тебе и «патч с любовью». Я оформил его с помощью yarn patch, если что.

Интеграция

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

Этот код позволит нам отправлять ивенты в React Native.

Теперь нужно создать GameObject с этим компонентом в сцене и вызывать нужные методы в нужных местах. Примеры можно найти в документации библиотеки или в Unity-проекте.

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

Теперь двигаемся в обратную сторону — React Native шлёт команды в Unity. Всё тоже довольно просто. В RN у нас будет доступен ref на unityView, у которого можно вызывать метод postMessage. Он принимает три строки:

  1. gameObject — важно! Это имя объекта, а не класса. Старайтесь, чтобы имя объекта совпадало с названием класса, обрабатывающего сообщение. Так будет меньше путаницы.
  2. methodName — метод, который вы хотите вызвать на стороне Unity.
  3. message — строка, которую передаём. Это может быть обычный текст, а может быть и JSON.stringify(object).

В моем коде это выглядело примерно так:

Теперь интеграция готова в обе стороны. Нам остается просто протестировать логику и проверить, что все работает хотя бы локально.

Пара советов

UI: Unity или React Native

Можно, конечно, сверстать интерфейс прямо внутри Unity — у него для этого есть вполне приличные инструменты. Но есть нюанс: забудьте о вашей теме. Все шрифты, цвета и отступы придётся прописывать вручную, без поддержки системных стилей.

Поэтому мой совет: делайте интерфейс в React Native и размещайте его поверх Unity-вьюхи с помощью абсолютного позиционирования. Так вы сохраните визуальную консистентность с остальным приложением, плюс получите бесплатную поддержку светлой/тёмной темы.

Разделяйте логику: геймплей vs бизнес

Старайтесь отделить игровую логику от бизнес-логики. Пусть Unity полностью отвечает за геймплей, физику, анимации и прочее веселье, а React Native — за всё, что связано с данными, аналитикой, навигацией и прочими взрослыми делами.

Идеально, если Unity просто отправляет события, а RN уже решает, что с ними делать.

Минимизируйте связь Unity ↔ RN

Каждое взаимодействие между Unity и RN — потенциальный источник багов, особенно если вы ещё не полностью уверены в стабильности геймплея. Постарайтесь сократить такие вызовы до минимума. Чем проще канал связи, тем спокойнее жизнь.

Тестируйте игру в изоляции Перед тем как интегрировать игру в приложение, тщательно тестируйте её отдельно. Я, например, собирал отдельные билды под macOS, чтобы дать возможность всем заинтересованным пощупать игру без обёртки.

Это важно. Изменения в игровом процессе — штука неизбежная. Кто-то захочет изменить музыку, кто-то — сложность, кто-то — скорость появления врагов. А теперь вспомните, у нас нет hot reload'а. И будет совсем обидно, если вы закончите интеграцию, и тут вам скажут: «А давай сделаем другую музыку?».

Тогда вам придется идти обратно в Unity Hub, вносить правки, тестировать, а потом… заново проходить через экспорт и подготовку исходников под платформы. То есть все, что я описывал в пунктах iOS и Android, придется делать заново. Это не страшно, но порядком утомительно.

Проблемы только начинаются

А сейчас я расскажу о реальных проблемах, с которыми мы столкнулись при развёртывании приложения. Я не нашел информации на эту тему. Ни в одной статье. Именно они и стали толчком к написанию этой статьи — хочется вас предупредить, а в идеале — вообще отговорить от использования Unity в вашем проекте. И начнём мы с самого очевидного.

iOS Simulator? Забудьте

Библиотека честно предупреждает: Unity не запускается на iOS Simulator. И это значит ровно то, что написано. Пока у вас в проекте есть Unity-игра — о симуляторе можно забыть. И это, мягко говоря, неудобно. Все React Native-разработчики, которых я знаю (и я в их числе), используют iOS-симулятор как основное средство разработки, прибегая к Android-эмулятору лишь для сверки. Так что подключая Unity, вы фактически лишаетесь привычного способа разработки.

Решения? Увы:

  • использовать реальное iOS-устройство;
  • или переключиться на Android-эмулятор.

Вес Unity-файлов

Во всех статьях и туториалах говорится, что экспортированный промежуточный код занимает не так уж и много места. Но мои сборки занимали очень много места на диске.

Я даже консультировался с Unity-разработчиками — вдруг я что-то делаю не так? Но серьёзных косяков никто не нашёл. Похоже, дело в версии Unity: чем новее, тем прожорливее.

Пример:

  • Flappy Bird под iOS — ~250 MB,
  • Flappy Bird под Android — ~800 MB (!). И это только исходники. Финальный билд, к счастью, выходит не таким огромным.

Но вся эта масса кода должна попасть в репозиторий. Git не обрадуется файлу на 250 MB. А вы не обрадуетесь, когда git push просто откажется это проглатывать.

Варианты решений:

  1. Удалять ненужные файлы вручную — звучит просто, но без опыта работы с Unity понять, что можно удалить, а что нет, практически невозможно.
  2. Оптимизировать экспорт в PlayerSettings: сжать текстуры, поиграться с Graphic API, Scripting Backend и т.д. Я пробовал — толку мало. Может, у вас получится лучше. Если да — расскажите в комментах, я серьёзно. В моем случае разница составила менее 1%.
  3. Git LFS — наше финальное решение. Устанавливаете, подключаете, трекаете большие файлы, пушите. Хорошее ли это решение? Трудно сказать, но на момент написания статьи другого я не нашел.

CI/CD: добро пожаловать в ад

Ошибки в CI/CD — это худшее, что может случиться. Почему? Потому что у вас всё работает, а на билд-машине — нет. Нужно каждый раз фиксить проблему наугад, заливать, ставить сборку, ждать 20-40 минут и надеяться, что проблема решена.

И именно тут у нас возник целый каскад трудностей.

Сначала у нас падали iOS-сборки, потому что на билд-машине не было unityFramework. Проблема оказалась в Git LFS — он не подтягивался по умолчанию на нашу билд-машину. Но это мы решили с третьей попытки.

Потом мы увидели проблему в Android. Она заключается в том, что по умолчанию Unity экспортирует проект с захардкоженным ndkPath. Выглядит он примерно так:

Как вы можете догадаться, на вашей билд-машине не будет никакой папки Applications, и уж тем более — Unity/Hub, откуда можно было бы подтянуть нужную версию NDK. Поэтому нам придётся ещё пошаманить и заменить путь к NDK на что-то вроде:

  • Мы будем прокидывать путь к нашему NDK из наших CI-скриптов. Для GitHub Actions есть специальный action, который это делает. Если у вас GitLab — можно настроить вручную. Тут в нагрузку добавляются проблемы с версионированием этих NDK, потому что где-то указана полная версия (как на скринах выше), а в репозитории NDK они указаны с буквенными обозначениями, так что придётся ещё поискать нужную. 🙂

И в этот момент мы начали ловить ошибки в CMAKE_C_COMPILER и CMAKE_CXX_COMPILER.

И… это стало концом. Я понял, что уже неделю пытаюсь наладить сборки, озадачил своей проблемой примерно половину техлидов компании и всех знакомых, кто мог хоть как-то помочь. И всё это не дало результатов. После ошибок CMAKE будет ещё сотня похожих. А релиз всё ближе.

Именно в этот момент я отказался от реализации Unity в нашем проекте и сделал быстрое решение на React Native Game Engine. О котором речь пойдёт дальше. Там будет сильно короче — обещаю.

Промежуточный итог

Вся секция про Unity может показаться бесполезной — ведь мы так и не довели игру до продакшена. Но я так не считаю.

Во-первых, мы подсветили множество нюансов, о которых до этого никто не писал. Во-вторых, эта часть может оказаться полезной тем, кто просто хочет добавить игру в свой пет-проект, без необходимости заморачиваться с полноценным развёртыванием и поддержкой. А может, кто-то решится пройти весь этот путь от начала до конца. В таком случае он потратит меньше времени на уже изученные и описанные здесь грабли.

Если кто-то из читателей всё-таки доведёт интеграцию Unity до production, и поделитесь опытом в комментариях Тогда у нас — как у сообщества React Native-разработчиков — наконец появится готовая инструкция по решению не самой тривиальной задачи.

Пара слов о Unity в отрыве от интеграции в RN

Мне очень понравился dev experience при разработке игры на Unity: редактор интуитивно понятный, с кучей полезных функций, много готовых шаблонов, всё работает быстро, и туториалов — хоть залейся. А ещё — отличная производительность.

Я тестировал игру в нашем приложении на Samsung Galaxy A11, и всё работало как часы, без тормозов. Более того, я бы даже сказал, что игра в приложении работала лучше самого приложения.

React Native Game Engine

Тут все будет невероятно просто:

  1. Переходим на репозиторий библиотеки.
  2. Читаем документацию
  3. Находим туториал по созданию нужной вам игры
  4. Готово

Напомню, что мы здесь больше рассуждаем о разработке игр в составе RN-приложения, а не составляем пошаговое руководство по разработке конкретной игры. За такими подробностями можно обратиться к статьям из серии «Как сделать GAME NAME с помощью React Native Game Engine». Я же постараюсь разобрать плюсы и минусы этого подхода.

Начнем с плюсов:

  • Разработка на JS/TS, никаких новых ЯП.
  • Понятное API.
  • В отличии от Unity доступен Hot Reload.

Но на каждое преимущество технологии найдётся свой недостаток

  • "Game Engine" в названии — лукавство. Инструментария для отладки практически нет. Хотите проверить уничтожение или спавн объектов? Делайте это вручную. Изолированного тестирования игрового процесса — нет. Проверка коллизий? Только визуально, с помощью бордеров. Создание и удаление объектов — через console.log. Да, серьёзно.

  • Дополнительные зависимости. Придётся подключить:

— matter.js — для физики, — ещё одну библиотеку — для воспроизведения музыки.

Но, будем честны: для JS-разработчика пара новых зависимостей — не повод для паники.

Производительность страдает

Было сразу понятно, что RNGE не сравнится с Unity по производительности. Но реальность оказалась хуже, чем я ожидал. На флагманах (Realme 14 Pro, Samsung Galaxy S20FE, S22, iPhone 14 Pro) всё выглядело хорошо. А вот на бюджетных устройствах — производительность была на грани допустимого.

Возможные утечки памяти

В какой-то момент я заметил, что приложение с запущенной игрой начинает постепенно увеличивать потребление оперативной памяти. Я запустил игру с пустым полем — без объектов, без физики — но потребление всё равно росло. Более того, каждый новый запуск игры потреблял ещё больше памяти, несмотря на то, что я строго следовал инструкциям по корректной остановке движка.

Трудно сказать, была ли проблема в самом RNGE или в моём коде — без полноценного инструментария это проверить невозможно. Поэтому я не выношу этот пункт в отдельный "минус", но посчитал важным его упомянуть.

Компромисс, который заработал

Как и ожидалось, React Native Game Engine — крайне компромиссное решение: производительность и инструментарий явно оставляют желать лучшего.

Но! Все заработало. И, как вы могли заметить по супер-короткой инструкции, заработало буквально сразу — без миллиона подводных камней, которые утянули на дно нашу интеграцию с Unity.

В итоге главной проблемой RNGE стала производительность. Но поскольку релиз был буквально «вчера», мы признали это решение «good enough» и выпустили игру.

На этом история разработки и интеграции заканчивается. Я остался не до конца доволен результатом и хотел, уже без спешки, попробовать переехать на Godot после релиза.

Если вы помните, мы изначально отказались от Godot потому, что библиотека для интеграции с React Native не поддерживала Android, а писать свою интеграцию тогда было некогда. Сейчас я как раз этим и занимаюсь. Godot, с Android-поддержкой, выглядит как идеальный вариант для внедрения игр в RN-приложения:

  • значительно проще Unity,
  • заметно легче — исходники занимают меньше места,
  • производительность на хорошем уровне.

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

Вместо заключения

Внедрение игры в уже готовое, живое и не самое новое React Native-приложение — это действительно интересный и ценный опыт. И хотя идеального решения я пока не нашёл, кажется, что я на пути к нему.

Если у вас есть подобный опыт или идеи, как могу бы упросить путь – жду вас в комментариях.

The site uses cookies, which allows you to receive information about you. This is necessary to improve the site. By continuing to use the site, you agree to the use of cookies - more details in our Policy on the processing of personal data