В процессе развития юнити разработчики проходят путь от “god обжектов” до проектирования сложных, гибких, абстрактных систем. Со временем эти системы обрастают своими уникальными особенностями, стандартами и инструментами. Образуя из себя фреймворк или даже движок.
Такие фреймворки зачастую имеют лишнюю функциональность для конкретно взятых задач. Документации становится все больше и больше, да просто тонны документации! Не смотря на это, порог вхождения для вновь прибывшего на проект разраба неуклонно растет. А также есть те кто сразу же отказывается от работы на таких фреймворках, со словами “А зачем мне лезть в это абстрактное болото!?”. И я их прекрасно понимаю.
И что же получается, пытаясь избежать проблем с хард кодом мы обрели другие проблемы, так особо и не повысив скорость разработки!? Проблемы, для устранения которых необходимо ооочень много времени. Т.е. мы прошли путь от крайности к крайности от “хард кода” к фреймворку.
Вот и мы столкнулись с той же проблемой. Но нам пришла идея пойти по другому пути. Так возникла концепция новой модульной архитектуры. Ну ладно, может не такая уж и новая, но она имеет свои уникальные особенности. И это не совсем то, что вы можете найти в интернете на тему модульной архитектуры, речь пойдет немного о другом.
Задачи модульной концепции
Итак к сути! Перед новой концепцией были поставлены следующие задачи:
Разработчику вторичных модулей не требуется вникать во все хитросплетения фреймворка.
Разграничение ответственности между разработчиками должно быть более четким.
Разработчику модуля предоставляется возможность самому выбирать инструменты.
Проект должен быть разделен на слабо связанные модули, в идеале точка связи должна быть одной единственной.
Вторичные модули должны разрабатываться параллельно независимо друг от друга.
Такие задачи мы поставили перед собой изначально. Но в итоге вышло даже лучше!
Описание модульной системы
Что же получилось в результате:
1) Давайте сначала посмотрим на структуру кода. Модульная система состоит из главного модуля и вторичных, в данном случае игровых. В главном модуле находится все основное: инициализация всех сервисов, плагинов, стартовая сцена, сцена меню и т.п. Также там описаны абстрактные классы, от которых должны унаследовать вторичные модули.
Рассмотрим простенький пример, который идеально подходит под модульную концепцию. Представьте проект с набором мини игр (просто в главном меню выбираешь из списка мини игру). Игры никак друг от друга не зависят. И у всех игр разный геймплей. Эти мини игры и являются игровыми(вторичными) модулями.
Каждый игровой модуль имеет в себе класс наследник абстрактного класса, который описан в главном модуле. За счет него главный модуль воспринимает их, как игровые модули. Минимальный набор функций абстрактного класса, от которого наследуется игровой модуль. Это точка входа в модуль, метод который выполняется при старте модуля. И точка выхода, по завершению мини игры. Это минимальный набор функций взаимодействия игрового модуля с главным модулем.
Также в главном модуле есть MiniGamesManager, который запускает мини игры, и еще что-либо с ними делает. Для удобства повсеместно используются конфиги. Для начисления очков или внутриигровой валюты, модули могут отправлять данные в GameData, где затем может выполняться например сохранение этих данных.
2) Теперь посмотрим как это выглядит в окне Project. Все модули находятся в папке _Modules. Здесь мы также видим разделение модулей по папкам. В самом верху главный модуль Main. За него отвечает ведущий разработчик проекта. В нем находится все основное. В папке Arts находятся арты которые относятся к главному модулю, а также к остальным модулям проекта. В папке Scene стартовая сцена и сцена меню.
В папке Scripts все основное по скриптам. Модули также делятся на сборки. В главном модуле _Main их аж несколько. В модульной системе проще делить проект на сборки. Сторонние плагины и ассеты устанавливаются для всего проекта, по дефолту в папку Assets.
В игровых модулях находится только то, что нужно именно для этих модулей. Не нужно копаться во всех папках, выискивать необходимый файл среди сотен других. Модуль может быть реализован в виде сцены или в виде префаба, если мини игры совсем уж маленькие. Можно их прямо в сцене меню запускать. Так бывает выгоднее по оптимизации.
Правда есть пока не полностью решенная проблема. Это дублирование ресурсов. Вторичные модули используют файлы, которые необходимы только для этих модулей. Эти файлы находятся в папке самого модуля. А также могут использовать общие файлы, которые находятся в главном модуле. Но при таком подходе надо быть внимательнее, чтобы не возникало дубликатов файлов. Чаще всего это происходит с файлами для UI. Я это решаю через Addressable/Analyze и AssetStudio. Впрочем дублирование не так страшно.
Если в проекте используется более одного сабмодуля (Git submodule), то придерживаясь концепции, логично вынести все сабмодули в папку Assets/_Modules/_Submodules. В нашем проекте мы были уверены, что будем использовать один единственный сабмодуль, поэтому оставили его в главном модуле.
3) Ресурсы в Addressable тоже можно группировать помодульно. Но тут не все так просто. Зависит от проекта.
4) Поехали далее, в гите тоже легче работать при модульном подходе. У каждого модуля своя ветка. Самое шикарное происходит когда выполняем индексирование файлов для коммита. Просто коммитим те файлы, которые соответствуют директории модуля. Тут сложно ошибиться. Таким образом никогда не возникает конфликтов.
Для лучшего контроля параллельной разработки, требуется периодически мерджить ветки и делить на этапы.
И вот у нас получается такая достаточно четкая линия разделения проекта на модули: в коде, разделение по папкам и в гите.
Стандартизация
Одним из плюсов данной модульной концепции является свобода выбора инструментов для разработчиков вторичных модулей. Но все же необходимы кое-какие стандарты на проекте. Стандартизация подразделяется на 3 типа: обязательно использовать, запрещено использовать, рекомендуется использовать. Все эти стандарты оговариваются перед началом разработки вторичных модулей.
Вот некоторый стандарты, которые были на нашем проекте: всё должно пробиваться по ссылкам! Запрещено использовать Unity Events и компонент Button, вместо него надо использовать нашу Pointer систему. Наследование не должно превышать 5-ти поколений (один из способов уйти от абстрактных болот). Для глобальных событий необходимо использовать (выбранный нами) Messenger. Загрузка ресурсов только через Addressables. Рекомендуется использовать: Zenject, Unitask, для реактивных свойств UniRx или нашу NextVar, DoTween, Unity Input System, события аналитики можно отправлять через главный модуль либо самостоятельно.
Скорость разработки
Для наглядности были построены графики, кривые скорости разработки. Конечно это все условно. Но обратите внимание, насколько круто устремляется вверх график модульной архитектуры после этапа проектирования. И на сколько меньше времени требуется на полишинг. Это достигается в основном за счет параллельности разработки, четкого разграничения ответственности. И благодаря изолированности модулей, разработчики быстрее приступают к выполнению задач не тратя много времени на чтение кода и поиск ресурсов. А также модульная архитектура меньше всего подвержена багам.
Плюсы и минусы
Итак, начнем с плюсов
1) Разработчику вторичных модулей не требуется вникать во все хитросплетения фреймворка/движка. Это снижает нагрузку на всех разработчиков, в т.ч. на ведущего и в целом ускоряет разработку. Низкий порог вхождения - это здорово!
1.1) Над вторичными модулями могут работать разработчики разного уровня.
1.2) Не требуется тратить много времени на написание большого количества документации.
2) В теории возможно распараллелить разработку модулей до 100%. Можно посадить по разработчику на каждый модуль. Так как модули никак не зависят друг от друга.
3) Благодаря модульности (атомизации) проектом проще управлять. Сильно упрощается оценка задач, а это очень важно. Менеджеры и геймдизайнеры могут выдохнуть.
4) Модульность позволяет с легкостью: отключать какой либо модуль, менять их очередность или запускать в какой угодно очередности, хоть с конца списка, хоть через один. Также это помогает в хотфиксах.
5) Проще работать с ресурсами, конфигами и Remote конфигами.
6) Очень удобно пользоваться гитом.
7) Проще тестировать, но не во всех случаях. Но уж точно легко выявить баг, локализовать его и назначить ответственного по устранению. И в целом такая архитектура менее подвержена багам.
8) Есть возможность переиспользовать вторичные модули в следующих проектах клонах. За счет того что они изолированы. В случае с монолитными проектами вам бы пришлось разбираться с кучей зависимостей.
8.1) Проще делить проект на Local и Remote бандлы в Addressebles.
Минусы модульного подхода
1) Сложно разделить проект на модули. Рассмотренный в статье пример был наиболее подходящим для модульной концепции. Многие проекты(жанры) будет не рационально проектировать по такой модульной архитектуре. И это главное разочарование (Вы: -“Надо было с этого начинать статью!!!”).
2) При проектировании модульной системы необходимо продумать все заранее. Кардинальные изменения архитектуры, в процессе разработки вторичных модулей, может привести к длительной переработке модулей.
3) Снижение обмена опытом между разработчиками. Разработчики вторичных модулей в основном развиваются только на специализированных (геймплейных) задачах, особо не разбираясь в архитектуре.
4) Необходимо следить за ресурсами. Могут быть дубликаты.