[Из песочницы] Создание общей библиотеки кода в геймдеве и не только
На днях я пообщалась с Артёмом Воробьёвым, техлидом игровой студии zGames, входящей в группу компаний Softeq, который, ничтоже сумняшеся, поделился опытом своей команды (а это более 5 лет разработки мобильных игр для мобилок, консолей и других модных девайсов). Представляем вашему вниманию элегантную инструкцию с конкретными практическими советами.1. Мотивация: зачем оно надоМы любим копировать хорошие решения. Программисты называют это «повторным использованием кода». В этой статье речь пойдёт о том, как наладить повторное использование библиотеки кода и эффективно её расширять.Задача создания библиотеки кода обычно усложняется тем, что: а) Библиотеку используют и расширяют несколько человекб) Библиотека задействована одновременно на нескольких проектах
Наша библиотека общего кода существует на протяжении уже четырёх лет. Начиналось всё с пары классов на Objective-C. Затем мы перешли на C++ и в несколько раз увеличили библиотеку. Сейчас работаем в Unity3d, и библиотека общего кода насчитывает уже около 400 классов.
2. Для чего создавать библиотеку, если есть игровой движок? Игровой движок содержит очень много полезных компонентов, которые повторно используются на разных проектах. Но движок, как правило, предоставляет решения лишь для самых общих задач: физика, рендеринг, звук, сеть, общая архитектура объектов и т.д. Однако часто бывает нужно расширить круг этих решений либо создать какую-то новую подсистему.Рассмотрим, например, реализацию логов (англ. logs) в движке Unity3d. Есть метод Debug.Log, который отображает сообщения в консоли Unity. Но движок не умеет записывать эти сообщения в заданный файл. Также реализация лога в движке не способна фильтровать сообщения. Эти и многие другие расширения логов можно вынести в общую библиотеку.
3. Название: КСТ — всегда збс? Начать стоит с названия. Можно, конечно, именовать библиотеку как-то вроде «CommonCode», но лучше бы придумать более «собственное» имя для проекта. Это поможет вашей команде проще общаться на тему библиотеки общего кода.Наша библиотека называется zLib — сокращённый вариант «zGames Library». Звучит красиво, хотя слегка совпадает с названием небезызвестной библиотеки для сжатия данных. Мы в своё время как-то проигнорировали этот факт, и, по-хорошему, стоило бы придумать что-нибудь более уникальное. Поэтому, подбирая имя для своей библиотеки, убедитесь, что оно не будет пересекаться с названиями других библиотек, с которыми вам доведётся работать.
4. Схема интеграции в проект. Репозитории, externals, SVN vs Mercurial Следующий вопрос — как библиотека будет попадать в проект. Есть простой способ, которым часто всё начинается, — копирование кода из проекта в проект. Тут возникает несколько проблем: фиксы багов и новые компоненты достаточно сложно обменивать между проектами.Способ более продвинутый — интеграция библиотеки через сторонний репозиторий. В SVN такая схема называется «внешние включения» (англ. externals), в Mercurial — «субрепозитории» (англ. subrepository, subrepo), в Git — «подмодули» (англ. submodule). Использование данной техники позволит вам сделать репозиторий проекта составным, и в результате он сможет включать не только свой код, но и код других проектов (например, общей библиотеки).
Библиотека будет представлять собой самостоятельную папку где-то внутри вашего проекта, обновляющуюся из отдельного репозитория. Указать можно не только адрес стороннего репозитория, но и конкретную ревизию (не обязательно HEAD), с которой должен работать ваш проект. У нас все сторонние репозитории размещаются в одной папке «Externals» внутри проекта (Externals/zLib, Externals/NGUI и т.д.)
5. Модерация кода. Какой код помещать в библиотеку Один из самых важных вопросов, возникающих при создании общей библиотеки кода — какой код туда помещать. Мне довелось видеть несколько неуспешных попыток организовать общую библиотеку кода. Практически всегда причина неудач в том, что в общий код размещают то, что было нужно только в рамках одного проекта, и больше никому не пригодится. Чтобы библиотека заработала, добавлять в неё необходимо лишь код, который можно реально использовать повторно.Стоит отметить, что в играх обычно присутствует много специфичного для данной конкретной игры кода. Поэтому не торопитесь выносить в библиотеку вашу первую реализацию match-3/раннера/шутера, если вы не планируете разрабатывать клоны именно этого геймплея. Подойдите к задаче более системно: старайтесь выделять в решениях общие части.
Мы следуем простой схеме: а) Любая задача, решаемая в первый раз, выполняется в рамках данного проекта и «затачивается» под его целиб) Если мы сталкиваемся с этой задачей повторно, она, по возможности, обобщается и выносится в библиотекув) До тех пор, пока подход не удаётся обобщить, код решения копируется из проекта в проект, продолжая адаптироваться под конкретные нужды
В нашей практике некоторые архитектурные решения проходили до 6 итераций изменений на разных проектах, пока мы не смогли проследить закономерности и вынести решение в библиотеку.
6. Модерация структуры файлов: не кодом единым Немаловажной задачей также является определение структуры файлов внутри общей библиотеки. Подход можно выбрать любой, важно, чтобы он был конкретным, и все члены команды его понимали. У нас корневая папка библиотеки выглядит следующим образом: zLib/_ExternalszLib/_ResourceszLib/ZGLibzLib/ZGLib.Collectionsи т.д. Папка _Externals содержит сторонние библиотеки, которые мы интегрируем в нашу. Папка _Resources содержит файлы, относящиеся к библиотеке, но не являющиеся кодом, — например, текстовые файлы или картинки.Папки ZGLib, ZGLib.Collections и прочие обозначают пространства имён (англ. namespace) в рамках библиотеки и содержат классы, относящиеся к данному пространству имён. Внутри пространства имён структура папок может быть произвольной.
7. Ограничения архитектуры: когда не в фокусе Одно из главных возражений, которые мне доводилось слышать по поводу общей библиотеки кода, касалось ограничений архитектуры. Действительно, со временем в общей библиотеке появляются компоненты, которые формируют каркас приложения так, что вам приходится придерживаться его рамок при написании своего проекта.Фокус в том, что при правильной организации каркас нисколько не будет ограничивать ваши возможности. Ключ к правильной реализации каркаса — модульность. Делайте его таким, чтобы он жёстко определял интерфейсы, но нисколько не ограничивал функциональность.Приведу пример. У нас есть каркас глобальных объектов — подсистема загрузки всех синглтонов (англ. singleton). Все создаваемые нами синглтоны должны реализовывать определенный интерфейс (ZGlob) и помещаться в специальное место в игровой сцене (естественно, это не касается синглтонов, поставляемых вместе со сторонними фреймворками). Таким образом, мы создаём одну точку загрузки и инициализации приложения. Единый интерфейс позволяет нам гибко управлять очерёдностью загрузки глобальных объектов, никоим образом не ограничивая их функциональность.
8. Единый стиль: жесточайше важно Необходимым условием при создании библиотеки кода является формирование общего стиля написания кода у команды. Вопрос единого стиля, в принципе, актуален для команды и при отсутствии библиотеки общего кода: как минимум ребятам проще будет переключаться между проектами. А при создании общей библиотеки вопрос стиля прямо-таки жизненно важен.Общий код должен быть написан в одном стиле, точка.Если у вас ещё нет общего стиля — начните его формировать. Создайте документ в Google Docs или любом другом онлайн-сервисе документов и внесите туда по пунктам общие правила, которым вы уже сейчас следуете при написании кода. Затем постепенно договаривайтесь о создании новых правил и расширяйте документ. Для успешного формирования общего стиля нужно назначить в команде человека, который будет принимать решения, если команда не сможет прийти к единому мнению.
9. Владение подсистемами: «Очень приятно, царь» Если делать всё правильно, библиотека общего кода будет разделена на подсистемы. У каждой подсистемы должен быть владелец — человек, отвечающий за всё, что с ней происходит. Изменения в подсистеме должны обсуждаться с ним, хотя вносить их могут и другие люди. Владелец подсистемы несёт её философию, знает историю её создания и лучше всех понимает её цели.10. Зависимость от сторонних фреймворков: Facebook В какой-то момент вы столкнётесь с необходимостью вынесения в общий код решений, завязанных на другие сторонние библиотеки. Это характерно, например, для общего кода по работе с Facebook, работающего с классами Facebook SDK непосредственно.Для себя мы решили эту проблему разделением общей библиотеки на части, каждая из которых лежит в отдельном репозитории.
У нас есть ядро zLib, в котором содержатся основные классы, не связанные ни с каким специфичным сторонним сервисом. Есть zLibFacebook, где лежит Facebook SDK и наши общие классы для работы с ним. Есть zLibTwitter, zLibChartboost и т.д. — уже около 10 разных модулей zLib’а. В наши проекты всегда добавляется ядро zLib, и, по необходимости — дополнительные модули отдельными субрепозиториями.
11. Системы регистрации доп. поведений Помимо модульности самой общей библиотеки (zLib, zLibFacebook и т.д.) иногда также требуется создать модульную архитектуру отдельной подсистемы внутри библиотеки общего кода. Пример модульной архитектуры одной из подсистем приведён выше — это каркас глобальных объектов.Другим, ещё более мощным примером, может послужить система реализации игровой аналитики. Представьте себе, что вы хотите создать общую систему аналитики так, чтобы регистраторы некоторых событий могли быть общими и использоваться повторно на разных проектах (в нашем случае, помещены в zLib). Некоторые события могут быть также общими, но привязанными к стороннему сервису (у нас они находятся в zLibFacebook). Остальные же события должны быть специфичными для данного приложения (размещены в его коде).
Чтобы решить такую проблему, мы создаём каркас подсистемы в общей библиотеке с управляющим синглтоном (в нашем случае, это ZAnalyticsManager). Дальше мы создаём базовый класс для регистратора события (ZAnalyticsTracker). Регистраторы могут быть динамически добавлены в ZAnalyticsManager. В Unity3d это легко делать через композицию игровых объектов (англ. GameObject). Для других технологий можно выбрать подход, общепринятый для данных технологий.
Для поддержки разных сторонних сервисов аналитики (Flurry, Google Analytics, Localytics и т.д.) мы создаём интерфейс отправителя событий (ZAnalyticsSender). Данный интерфейс реализуется для каждого из сервисов аналитики, и мы добавляем в ZAnalyticsManager те объекты отправителей, которые требуются в данном проекте.
Таким образом, для поднятия системы аналитики мы должны создать объект управляющего синглтона и надобавлять в него регистраторов и отправителей — они могут быть реализованы как внутри общей библиотеки кода, так и внутри специфичного кода вашего проекта. Главное — поддержать общий интерфейс.
12. Бранчевание и стабильность проектов: призраки бага При разделении одного и того же кода между проектами есть огромный плюс в том, что фикс бага в общем коде одновременно фиксит этот баг во всех проектах. Но есть и негативная сторона: при внесении бага в общий код он появляется во всех проектах. Чтобы предотвратить неконтролируемое внесение багов, можно делать бранчи на каждый проект в репозитории общего кода. Тогда команда, работающая с данным проектом, будет сама решать, когда мержить изменения из основной ветки.
13. Изменения Deprecate: плавно и безболезненно Иногда какие-то части общего кода приходится менять — придумывается более удачное название для интерфейса или рефакторится общая структура подсистемы. В этом случае лучше всего отмечать старые интерфейсы как запрещённые (англ. deprecate), но на некоторое время оставлять их в общем коде, чтобы позволить разработчикам безболезненно пересесть на новые рельсы. В C# это делается с помощью атрибута Obsolette, в других технологиях есть аналогичные механизмы. При внесении серьёзных изменений стоит подготавливать письменную инструкцию по переходу на новую реализацию.Бывают случаи, когда требуется ну очень кардинальный рефакторинг, и сохранить старые интерфейсы не получается. Здесь придётся вручную контролировать стабилизацию нового подхода в каждом из текущих проектов. Интеграцию серьёзных изменений лучше не осуществлять на конечных этапах разработки (перед релизами, майлстоунами, дедлайнами), а откладывать на более спокойные периоды. Здесь поможет система бранчевания и версионности общей библиотеки.
14. Покрытие тестами: «дайте два» Все мы знаем о пользе написания автоматизированных тестов. А ещё мы знаем, как редко они пишутся. Причём, если задуматься, не всегда оно надо. В рамках небольших проектов стоимость ручного тестирования и стабилизации может оказаться ниже стоимости написания автоматизированных тестов. Однако для библиотеки общего кода автоматизированные тесты почти наверняка себя окупят.Поэтому, если вы планируете вводить автоматизированное тестирование, но всё никак не получается, — начните с создания общей библиотеки и пишите тесты для неё. Хорошие менеджеры поймут пользу и выделят вам на это время.
Для игр автоматизированные тесты — отдельный больной вопрос. По сравнению с бизнес-приложениями, игровые объекты имеют больше взаимосвязей, и их сложнее протестировать по отдельности. Мы на данный момент покрываем Unit-тестами утилитные классы и планируем покрыть интеграционными тестами те объекты общей игровой логики, которые могут работать по отдельности.
15. Библиотека общего кода: не время курить бамбук Интересным бонусом создания общей библиотеки для нас стало избавление от простоев. Безусловно, всегда есть категория разработчиков, которым в радость «ничегонеделанье» на работе, — им, конечно, придётся туго. Зато для людей, которые без работы киснут — это универсальное решение. Если человек не занят в проекте, он может приниматься за развитие общей библиотеки.Чтобы наладить процесс эффективно, у общей библиотеки должен быть менеджер, лучше всего — техлид. Как и у любого проекта, у общей библиотеки есть баги, есть запросы на изменение и на фичи. Сейчас наш пул задач по общей библиотеке состоит из 60 пунктов и постепенно увеличивается. Там есть задачи как на пару часов, так и на несколько недель, что позволяет утилизировать даже короткие свободные промежутки в работе над основными проектами.
16. zLib в разрезе: какие подсистемы есть у нас В заключение приведём список некоторых подсистем, которые мы вынесли в библиотеку общего кода: a) Логи и ассертыb) Обёртка над асинхронными задачамиc) Работа с JSON’амиd) Загрузчик глобальных объектовe) Система внутриигрового интерфейса разработчика (cheat UI)f) Система распознавания жестовg) Система аналитикиh) Оконная системаi) Система конфигурирования приложенияj) Система событийk) Обёртки над сторонними фреймворкамиНам было бы очень интересно услышать обратную связь от читателей и коллег по цеху. Полезен ли наш опыт, какие вы можете дать рекомендации, как организована библиотека общего кода у вас? Будем рады комментариям!