Для начала несколько комментариев по следам предыдущей статьи. Мы действительно раньше работали в компании Wargaming, где разрабатывали движок, известный как dava.framework или dava.engine. Поэтому многие старые коллеги, с которыми мы по-прежнему в хороших отношениях, активно участвуют в обсуждении.
У ряда людей возникли сомнения: это та же технология или другая? Ответ: это новая технология, написанная с нуля.
Как же мы справились всего за год? Наша команда имеет огромный опыт. Многие занимаются разработкой движков и игр более 15-и лет.
Почему с нуля, если можно было взять наш старый движок, который к тому же лежит в open-source? Ему около 10 лет, и большая часть кода устарела. Даже самые лучшие части движка, которыми мы гордимся, местами содержали куски кода и какие-то рудименты 5-и, 7-и и иногда даже 10-ти летней давности. Многие архитектурные решения были рассчитаны на устройства того времени — начиная с айфона 3G. Сейчас же мы ориентируемся минимум на iPad Air 1 и аналогичные ему по мощности Android-устройства. Соответственно и подходы несколько поменялись.
И самый часто встречающийся вопрос: почему собственный движок? В прошлой статье было несколько доводов разной степени убедительности. Хочу сконцентрироваться на главном: только собственная технология может позволить вам получить максимум из железа, сделать максимальное количество оптимизаций именно для вашего геймплея, визуального стиля. Мы позиционируем себя в том числе как технологическую компанию, не только разработчика игр. Мы считаем, что с нашим уровнем инженеров и нашим опытом можем составить серьезную конкуренцию на рынке высокотехнологичных мобильных продуктов.
А теперь к делу: какие инструменты и техники помогли осуществить нам эту довольно амбициозную задачу в сжатые сроки?
Инфраструктура
Мы выбрали Atlassian Bitbucket Server+Jenkins. В Битбакете лежит главный репозитарий (мастер), к которому подключен Дженкинс. У каждого разработчика есть свой форк. Под каждую задачу создается новый бранч в форке, который назад интегрируется через пулл-реквест. В общем, схема довольно стандартная. Каждый пуллреквест проходит обязательное ревью и автоматические тесты. И, в случае успеха, автоматически вливается в мастер.
Дженкинс
Дженкинс обладает рядом недостатков: он древний, не очень быстрый, прожорливый, веб-морда выглядит как интернет портал из 90-х. Однако его гибкость, огромное количество модулей и бесплатность делают его неплохим выбором даже в 2019м. Пошаманив с модулями и настройкой можно добиться удобоваримого внешнего вида, декларативного описания пайплайнов (лежащих в репозитарии). К слову, пайплайнов сейчас около 40: тесты, редакторы, игра под все платформы; работа с серверной инфраструктурой и метагеймом. Собирают все это 20 билдагентов.
В перспективе, конечно, хочется попробовать и современные хипстерские решения, к примеру GitLab или self-hosted TravisCI. Полностью облачные решения (Nevercode, Bitrise, CircleCI и т.п.) мы не рассматриваем ввиду большого размера нашего репозитария, ассетов, и, соответственно времени сборки и размера артефактов.
Система сборки
Основное требование к системе было следующим: генерация проекта для iOS, MacOS, Android, Windows, Linux одним скриптом. Мы успели попробовать Premake, SCons, Bazel и CMake. По разным причинам остановились на проверенном временем CMake.
В последние годы CMake стал практически стандартом для C++ библиотек. Практически все, начиная от abseil и заканчивая SDL, можно подключить к своему CMake проекту буквально в несколько строк. Есть конечно и исключения, как OpenSSL или V8, с которыми пришлось немного попотеть. Поверх голого Цмейка мы разработали небольшой фреймворк (всего порядка 3000 строк). Основные возможности:
Модульность. Отдельные части движка оформлены в виде модулей. Например, звук, UI, физика, сеть и т.п. Каждый модуль может иметь собственные ассеты (например, шейдеры) и может иметь зависимости от других модулей.
Конечное приложение на движке (игра, редактор, утилиты) подключает только те модули, которые ей необходимы. Немного особняком стоит модуль core, который является зависимостью для большинства других модулей. Core имплементирует точку входа, главный цикл приложения, взаимодействие с операционной системой и другие базовые сущности.
Thirdparty модули. Наш фреймворк позволяет в несколько строк скачать git репозитарий или архив, распаковать, собрать, скопировать библиотеки и/или исходники. На сегодняшний день у нас 66 таких thirdparty модулей: аналитика, сторонние файловые форматы, middleware вроде физики, звуковой библиотеки и т.п.
Процесс разработки
Учитывая предыдущий опыт, мы решили сложить и движок, и игру в один репозитарий. Это позволяет безболезненно вносить изменения в API движка и синхронно адаптировать под него игру. Получился так называемый монорепозитарий с его достоинствами и недостатками. Но, поскольку мы сразу планировали поддержать очень высокий темп разработки, возможность синхронного рефакторинга движка и игры перевесила все остальные недостатки данного решения.
В среднем у нас добавляется более 20 пулл-реквестов в день. Это означает, что мастер может быть потенциально сломан 20 раз в день. К счастью, еще в 1991-м году придумали технику Непрерывной интеграции (Continuous Integration). К чему же мы пришли?
Continuous Integration
Как сказано выше, для каждой задачи в форке разработчика создается бранч. Далее создается пулл-реквест из этого бранча в в основной репозитарий. Этот пулл-реквест проходит ряд автоматических тестов на Дженкинсе:
- Юнит-тесты под все платформы (windows, linux, macos, ios, android). В качестве основы используется googletest, а для проверки процента покрытия — OpenCppCoverage, отчет которого проверяется дополнительным питон-скриптом. Если процент покрытия конкретного файла меньше 75%, тест считается проваленным. Таким образом у нас покрыта тестами большая часть низкоуровневых классов движка.
- Codeformatter. Для C++ кода используем clang-format. Форматирование измененного кода сначала автоматически происходит при коммите на машине разработчика, а потом проверяется в тесте. Для джаваскрипта, который используется у нас в качестве скриптового языка, используется npm linter.
- Тесты ассетов. Довольно большая группа тестов: от валидации форматов файлов до проверки зависимостей (например, проверка, что текстура, используемая в игровом уровне, действительно существует).
- Юнит- и функциональные тесты редактора. Неотъемлемой частью движка является редактор, где создаются и редактируются игровые уровни и прочие ассеты. Кроме юнит-тестов, для тестирования редактора используется froglogic Squish for Qt — утилита для автоматического GUI тестирования. Все это позволяет нам обходиться вообще без ручного тестирования редактора. При этом, по отзывам художников и левелдизайнеров, уровень его качества и стабильности выше, чем в прошлой компании, когда у нас была команда из пяти тестеров. При этом релизы происходят ежедневно, а при ручном тестировании релизы происходили раз в 2 недели.
- Функциональные тесты игры. Понятно, что автоматическое функциональные тесты хочется использовать и для игры. Поэтому мы начали разработку следующей системы:
- тестовое приложение (конкретно — скрипт на питоне) запускает игровые сервер и клиент с определенными параметрами
- запущенные сервер и клиент открывают сетевой порт,
- тестовое приложение подключается к ним и посылает команды: загрузить карту, выбрать персонажа и оружие, переместиться в точку, прицелиться, выстрелить и т.п.
- сам синтаксис тестов — питоновский pytest. Эта система сейчас находится в активной разработке.
Большинство проектов для тестов собираются с включенным флагом «treat warnings as errors», а на платформе MacOS с дополнительно включенным clang AddressSanitizer, что позволяет отлавливать еще больше ошибок на этапе подготовки пулл-реквеста.
Кроме тестов, каждый пулл-реквест проходит ревью как минимум двумя другими разработчиками и, в случае необходимости, отправляется на доработку. Когда все тесты пройдены и у ревьюверов нет замечаний, пуллреквест автоматически вмерживается.
Поскольку некоторые тесты могут занимать значительное время (например, полный GUI тест редактора длится более часа), в пулл-реквестах используется сокращенный сценарий. Полный же набор тестов запускается в мастере каждые 4 часа.
На сегодняшний день так создано и вмержено уже 6600 пуллреквестов.
Continuous Delivery
Мы используем концепцию автоматических ежедневных (а точнее еженочных) релизов. Как именно это происходит:
- создается git tag,
- в нем запускаются полные версии всех тестов,
- в случае успеха собираются артефакты:
- редакторы для MacOS и Windows. Таким образом, каждое утро у всех есть свежая версия инструментов. И, благодаря автоматическим тестам, мы уверены в их определенном качестве и стабильности.
- игровые клиент и сервер для всех платформ. Клиент для iOS заливается в TestFlight, для Android — в Google Play, остальные платформы — в JFrog Artifactory, игровые сервера и прочие сервисы — в облако. То есть каждое утро у нас есть свежая версия игры, готовая для тестирования и плейтестов.
Конечно, не каждый ночной релиз заканчивается успешно. Какие-то тесты могут не пройти, или на запуске приложения возникнет критическая ошибка. В этом случае найденные проблемы чинятся дежурным разработчиком в течение дня и процесс релиза запускается заново.
Дежурных каждый день несколько:
- Дежурный 1го уровня. Следит за стабильностью тестов в главном репозитарии.
- Дежурный 2го уровня по игре. Чинит игровые баги.
- Дежурный 2го уровня по редакторам. Чинит редакторные баги, консультирует пользователей (художников, левелдизайнеров, геймдизайнеров).
Также в день дежурства можно заниматься техническим долгом: дописать недостающие тесты на старую функциональность, дополнить документацию, или сделать рефакторинг, на который не находится времени при обычном планировании релизов.
В следующей статье мы подробнее рассмотрим программную архитектуру самого движка, а также основные модули и подсистемы.
Продолжение следует…
Автор: BlitzTeam