В комментариях к переводу доклада с HaxeUp Sessions 2020 Hamburg — Зимний отчет о состоянии Haxe — был задан вопрос о том, зачем нужен Haxe. На него, конечно же, ответили там же, в комментариях. Предлагаемый вашему вниманию перевод еще одного доклада с прошедшего HaxeUp должен, по моему мнению, стать дополнительным аргументом в защиту Haxe, т.к. данный доклад посвящен игре, заработавшей более 500 млн. евро.


Автор доклада Ненад — один из программистов InnoGames, работавших над конвертацией Forge of Empires из ActionScript в Haxe. 2 года назад в Амстердаме InnoGames рассказывали о своей работе в данном направлении. Сегодня же можно сказать, что им это удалось в полной мере — теперь обе версии веб-клиента (Flash и html5) собираются из единой кодовой базы на Haxe. И данный доклад является по сути обзором проделанной работы и принятых решений, как удачных, так и не очень.


image


Начнем с представления — кто такие InnoGames?


InnoGames — один из крупнейших разработчиков игр в Германии (г. Гамбург) со штатом более 400 человек. В настоящее время компания в основном занимается мобильными играми, но также продолжает поддержку и развитие браузерных игр, созданных ранее (у большинства их браузерных игр также есть и мобильные клиенты). В этом году доход компании за все время ее существования (lifetime revenue) достиг отметки в 1 млрд. евро.


image


Forge of Empires — это:


  • один из браузерных проектов InnoGames (и довольно успешный)
  • одна из крупнейших браузерных игр на рынке
  • градостроительная игра со стратегическим компонентом: игроку предоставляется ограниченная территория, где он может строить здания (при этом игрок может управлять несколькими городами). Игроки могут участвовать в стратегических битвах в режимах PvE, PvP и гильдия против гильдии. Развитие городов происходит на протяжении различных исторических эпох
  • за время жизни проект принес 500 млн. евро
  • довольно старый проект, выпущенный в 2012 году
  • кроме браузерной версии у игры есть клиенты для Android и iOS.

image


Как можно понять, Forge of Empires приносит существенную долю прибыли, и InnoGames не могли позволить ей умереть вместе с Flash. Таким образом, возникла необходимость создания html5-версии игры, которая работала бы не хуже Flash-версии.


Давайте рассмотрим удачные, по мнению компании, решения, принятые в ходе работы по созданию html5-клиента:


image


1. Для оценки возможностей и ограничений существующих технологий, а также для минимизации рисков в InnoGames приступили к прототипированию — созданию небольшой демонстрации, показывающей игровой город с анимированными зданиями, работающими tween-анимациями, работающим пользовательским интерфейсом и взаимодействием с бэкендом игры.


image


В рамках прототипирования было испробовано множество технологий, при этом не все из них позволили создать работающий прототип (например, CreateJS не подошел по причине слабой поддержки Flash API и проблем конвертации ActionScript-кода в JavaScript). До стадии работающего прототипа дошли только Egret и OpenFL — обе технологии соответствовали предъявляемым требованиям. Но в случае с Egret дополнительным минусом было то, что это проприетарная технология. Имеющийся в Egret конвертер кода не всегда выдает оптимальный код — в тех частях кода, которые конвертер не смог "понять", избыточно использовалась рефлексия. А так как код самого конвертера закрыт, то на его работу невозможно повлиять.
Случай Haxe и OpenFL — это совершенно другая история, т.к. это технологии с открытым кодом. Кроме того, на создание работающего прототипа на OpenFL потребовалось значительно меньше времени.


image


2. Управление проектом:
С самого начала перед нами стояли ясные цели:


  • в процессе портирования нельзя было останавливать развитие проекта. Мы не могли просто взять слепок существующего ActionScript-кода и портировать его, в то время как ActionScript-код продолжал бы параллельное развитие. Это означало бы, что в процессе портирования нам пришлось бы постоянно "догонять" ActionScript-версию, а из нашего опыта мы знаем, что такой подход плохо работает
  • мы не хотели заниматься переписыванием кода игры
  • процесс портирования должен быть автоматизирован настолько, насколько это возможно
  • производительность html5-клиента должна быть не хуже производительности flash-клиента. У нас был тест производительности (спасибо нашей команде QA), который мы прогоняли во всех браузерах перед каждым релизом. Таким образом, мы могли выявить существующие узкие места и исправить их в дальнейшем. При этом в процессе конвертации проекта мы параллельно занимались оптимизацией flash-клиента, таким образом требования к производительности html5-клиента также росли.

У нас был хороший план:


  • у нас был четкий план действий, в котором были определены основные этапы разработки. Поэтому мы всегда знали наши текущие цели
  • мы старались сократить риски к минимуму, поэтому каждое принятое решение было взвешенным
  • у нас был хороший график релизов, и мы могли позволить себе не торопиться сильно с релизами

image


В феврале 2018 была выпущена бета html5-клиента, в которую по своему желанию могли поиграть наши пользователи. Два месяца мы занимались сбором обратной связи и исправлением выявленных проблем.


В апреле html5-версия стала версией беты по-умолчанию.


Еще два месяца спустя мы добавили возможность выбора версии клиента пользователям, играющим в стабильную версию игры.


Затем в июле было проведено A/B тестирование, в ходе которого половине новых пользователей выдавался html5-клиент, а другой половине — flash-клиент. Тестирование показало хорошие результаты и в ноябре 2018 года html5-версия клиента стала использоваться по-умолчанию. И сейчас в html5-версию играет 85-86% всех пользователей.


image


3. Решение использовать технологии с открытым исходным кодом было одним из важнейших:


  • мы не зависели от какой-то определенной компании-разработчика. Именно по этой причине мы отказались от Egret, т.к. не могли напрямую повлиять на работу их конвертера кода. Еще одна интересная история, связанная с этим и стоящая упоминания: на момент начала работы по конвертации Forge of Empires в Гамбурге уже была одна компания, успешно перешедшая с ActionScript на Haxe, поэтому в InnoGames рассматривали вариант покупки конвертера у этой компании. Однако в данной компании не были заинтересованы в продаже конвертера, вместо этого InnoGames предложили услугу конвертации ActionScript->Haxe. И хотя для InnoGames это был приемлемый вариант, в итоге оказалось, что имеющийся конвертер не был универсальным и выдавал неоптимальный код, с которым было бы тяжело работать в дальнейшем (а в InnoGames как раз планировали переход на Haxe). Поэтому от этого варианта также пришлось отказаться
  • т.к. у выбранных технологий исходный код был открыт, то в InnoGames могли вносить любые требуемые им изменения
  • Haxe и OpenFL распространяются бесплатно
  • сообщество Haxe оказалось очень отзывчивым и активно помогало в исправлении найденных ошибок.

image


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


image


Рассмотрим подробнее, что такое mocked-компиляция:
Исходный ActionScript-код подготавливается с помощью препарсера — из кода удаляется реализация методов, в нем остаются только публичные свойства и методы, таким образом получается mocked-код на ActionScript. Этот код затем конвертируется в Haxe-код (также без реализации методов). Полученный код компилируется и проверяется таким образом на наличие ошибок компиляции.


Затем то же самое делается с "нормальным" кодом (кодом, из которого не вырезана реализация методов). Для каждого из полученных Haxe-классов с реализацией методов выполняем следующее:


  • берем mocked-код на Haxe
  • подменяем в нем mocked-класс на выбранный класс с реализацией
  • компилируем полученную комбинацию кода
  • сохраняем информацию о полученных ошибках компиляции.

Такой подход дает очень хорошее представление об имеющихся ошибках (их количестве и важности) и помогает расставить приоритеты в составлении планов по исправлению найденных ошибок.


image


5. Аннотации — инструмент, который использовался в ActionScript-коде для того, чтобы "помочь" конвертеру в принятии решений:


  • выбор типов переменных (для типизации массивов, словарей и т.д.)
  • дать команду конвертеру игнорировать определенный фрагмент кода (данный фрагмент не попадет в Haxe-код)
  • или же внедрить определенный фрагмент кода в получаемый на выходе Haxe-код

Т.к. аннотации — это просто комментарии в исходном коде, то они никак не влияли на работу ActionScript-кода.


image


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


Для flash-версии использовались ассет-бандлы в формате swf, а также ATF-текстуры. Ни те, ни другие не подходили для использования в html5-версии. Поэтому возник вопрос: либо использовать отдельный набор ассетов для html5-версии, или же внести изменения во flash-клиент и использовать один набор ассетов для обеих версий клиента.


В итоге было принято решение в пользу второго варианта: все swf-бандлы были заменены на текстурные атласы, а в OpenFL была добавлена поддержка сжатых текстур.


image


7. Одно из неправильных решений (о котором чуть позже) привело в итоге к принятию правильного решения — использованию непрерывной конвертации, когда каждый коммит в репозиторий клиента автоматически запускал конвертацию проекта. Каждый Pull Request также компилировался под Flash и JavaScript.


В дополнение к этому, оба клиента проверялись с помощью UI-тестов.


Данные меры позволяли оперативно сравнивать качество работы html5-клиента и Flash-клиента.


image


8. Для внедрения зависимостей (dependency injection) в проекте используется фреймворк Robotlegs, который в свою очередь базируется на библиотеке swiftsuspenders. Для работы инжектора в swiftsuspenders необходима информация об используемых типах. Для получения данной информации в Haxe-версии изначально использовался метатег @:rtti — данный метатег сохраняет информацию о типе в xml-формате, который затем необходимо распарсить. Т.к. в коде проекта очень активно используется механизм внедрения зависимостей, то для html5-клиента данный подход работал недостаточно быстро (приходилось обрабатывать слишком много xml-строк уже во время исполнения программы). Для решения этой проблемы Даниил Коростелев создал инжектор, основанный на макросах: вместо метатега @:rtti используется интерфейс ITypeDescriptionAware — макрос, встретив класс, помеченный таким интерфейсом, сгенерирует для него метод, возвращающий описание типа (которое в свою очередь будет использоваться инжектором зависимостей). Таким образом, часть работы удалось перенести с этапа выполнения программы на этап ее компиляции.


image


9. Использование Canvas-элементов было сведено к минимуму, т.к. они сильно влияли на скорость работы клиента, особенно в случае Microsoft Edge — по этой причине все дальнейшие тесты скорости работы клиента теперь основывались на этом браузере (все остальные браузеры показывали лучшую производительность).


image


10. Было ли этого достаточно для обеспечения быстрой работы клиента? Нет, нужно было сделать еще что-то для его ускорения.
InnoGames пришлось добавить в OpenFL поддержку батчинга, когда несколько объектов, использующих одно и то же состояние WebGL, можно отрисовать в один drawcall.


image


11. Чтобы эффективнее использовать батчинг в форк OpenFL была добавлена поддержка текстурных атласов. Для этого был добавлен класс SubBitmapData — объект, представляющий собой только часть атласа (до этого нововведения атласы использовались совершенно неэффективно с точки зрения потребления памяти и оптимизации отрисовки — для каждого игрового объекта создавалась новая текстура!). Теперь, используя батчинг, стало возможным отрисовать весь игровой интерфейс в один drawcall.


image


12. Было ли этого достаточно? Конечно, нет!
html5-клиент продолжал работать недостаточно быстро. Для дальнейшего улучшения производительности было необходимо улучшить батчинг — следующей его итерацией стал мультитекстурный батчинг, способный отрисовать в один drawcall объекты с разными текстурами (количество текстур, которые можно использовать в рамках drawcall’а, зависит от видеокарты; получить его можно с помощью вызова метода gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)). Данная оптимизация значительно снизила количество drawcall’ов. Подробнее о ней вы можете узнать из доклада Даниила Коростелева.


image


13. Было ли этого достаточно? И ответ — снова нет!
Наконец-то html5-клиент начал выдавать стабильные 60 кадров в секунду, но в Internet Explorer он работал не стабильно — часто происходила потеря webgl-контекста, после которой клиент не мог восстановиться. При этом, что важно, пользователи Internet Explorer приносят около 10% прибыли, поэтому "бросать" их нельзя!


Было принято решение выдавать пользователям Internet Explorer Flash-версию клиента.


image


Но используя текущий рабочий процесс, InnoGames не могли перейти на Haxe, т.к. Flash-клиент все еще собирался из ActionScript-кода, а html5-клиент — из Haxe-кода, полученного с помощью конвертера. При этом конвертер кода был оптимизирован только для генерации кода html5-клиента, форк OpenFL тоже был оптимизирован только для html5.


Да, в InnoGames могли продолжать использовать построенный ранее рабочий процесс (см. п. 4), но это, по их мнению, был бы тупиковый путь, разработка так и продолжалась бы на ActionScript. А полный переход на Haxe позволил бы значительно упростить процесс. В дополнение к этому следует заметить, что старый конвертер генерировал слишком "неудобоваримый" код.
Поэтому было принято решение о необходимости создания нового конвертера.


Новый конвертер позволил получить Haxe-код, который можно скомпилировать как во Flash, так и в JavaScript. Кроме того, полученный Haxe-код меньше использовал рефлексию и был лучше типизирован.


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


image


Интересный момент: OpenFL используется только для компиляции html5-клиента, Flash-клиент собирается напрямую, без использования данной библиотеки.


Подробнее о новом конвертере можно узнать из еще одного доклада Даниила Коростелева с октябрьского HaxeUp.


image


14. И последний (но не по значению) пункт — полный переход на Haxe, произошедший примерно 2 месяца назад. Хотя кажется, что это должно быть очень просто (просто возьми и закоммить Haxe-код в репозиторий, да поменяй несколько расширений в IDE), но данный процесс занял некоторое время:


  • во-первых, потребовалась реструктуризация каталогов в проекте. Желательно, чтобы части проекта находились в очевидных местах. Но, например, каталог src уже был занят ActionScript-кодом, и сконвертированный Haxe-код сначала находился в другом каталоге. Естественно, что на существующую структуру каталогов уже были "завязаны" конфигурации CI, IDE, и прочих инструментов. Поэтому "поменяв местами" каталоги с кодом (ActionScript-код при этом оставался основным), также было необходимо внести соответствующие изменения в конфигурациях используемых инструментов, убедиться, что сделанные изменения ничего не поломали, и только после этого можно было сделать "финальный коммит", убрав игнорирование исходников на Haxe и закоммитив их в git, а также запретив изменение исходников в каталоге с ActionScript-кодом
  • во-вторых, необходимо было выполнить миграцию всех веток в репозитории проекта. Так как переход на Haxe осуществлялся без остановки процесса разработки, то у разработчиков были незамерженные ветки с ActionScript-кодом. Для автоматической конвертации этих веток в Haxe и отката сделанных изменений в ActionScript-коде использовался Ant-скрипт. В результате данные ветки можно было смержить с основной веткой без конфликтов.

Это были удачные решения, а теперь давайте рассмотрим не самые удачные:


image


1. Был построен довольно сложный рабочий процесс (workflow) — использовалось 2 инструмента: препарсер и конвертер. При этом одни правила конвертации применялись в препарсере, а другие — в конвертере. Причина этого заключается в том, что изначально команда работала над препарсером и mocked-компиляцией, и только значительно позже приступила к работе над конвертером и его улучшениями.


Если команде пришлось бы заново приступить к конвертации проекта с ActionScript в Haxe, то все правила конвертации кода применялись бы только в одном инструменте (новый конвертер как раз реализует данный подход).


image


2. Второй момент — был выбран довольно сложный процесс управления изменениями в проекте. Для Haxe-кода был заведен отдельный репозиторий:


  • из кода в master-ветке автоматически собиралась "стабильная" версия клиента
  • в conversion-ветку попадал автоматически сконвертированный Haxe-код
  • develop-ветка использовалась для работы над исправлениями рантайм-ошибок.

При этом часто возникали merge-конфликты, когда одновременно вносились изменения в код какого-либо класса на ActionScript и в develop-ветке Haxe-репозитория. Приходилось постоянно разрешать их вручную, кодовая база начинала "расходиться". Поэтому довольно скоро от такого подхода отказались и перешли к непрерывной автоматической конвертации кода.


image


3. И третий момент — команда изначально сфокусировалась на html5-версии клиента, поэтому созданный конвертер кода подходил только для нее. Но когда оказалось, что для поддержки Internet Explorer необходима Flash-версия, возникла необходимость разработки нового конвертера, иначе переход на Haxe оказался бы невозможным.


Поэтому если команде пришлось бы заново приступить к разработке конвертера, то он был бы как можно более кросс-платформенным.


image


Но теперь обе версии браузерного клиента Forge of Empires используют код на Haxe. Спасибо за внимание!