diff --git a/README.md b/README.md index c67a35f..e76efc1 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Unity's default approach to game development relies on component-oriented progra In this approach, an OOP class (inheriting from MonoBehaviour) consists of a set of data and methods to modify and display it. Encapsulation requires that this data be hidden from outside access and changed only through the provided methods. As a result, classes often become quite large, even when broken down into individual components. While inheritance can address some of these issues, it may complicate readability. Additionally, interactions between components are managed through events or specific classes. Coordinating these components isn’t straightforward; managing synchronization can lead to callback complexities, making it harder to understand over time.![](docs/Images~/MvcMvp.svg) -The **MVC/MVP** approach changes this principle. Data is put into **model or models** (a model is a class for storing data). Usually, a model has two interfaces: one for reading only and the other for changing data. This is done to easily separate who reads data from who changes it. In some projects, models contain APIs and methods to change data, while sometimes they contain only data. +The **MVC/MVP** approach changes this principle. Data is put into **model or models** (a model is a class for storing data). Usually, a model has two interfaces: one for reading only and the other for changing data. This is done to easily separate who reads data from who changes it. In some projects, models contain APIs and methods to change data **(Rich Model)**, while sometimes they contain only data **(Anemic Model)**. Next is the **view** – in Unity, it's a MonoBehaviour, with objects for display/input. There is no logic in them, just output and input data. @@ -453,4 +453,89 @@ public enum ControllerBehaviour } substituteControllerFactory.SetBehaviourFor(ControllerBehaviour.CompleteOnFlowAsync); -``` \ No newline at end of file +``` + +### Controllers Hierarchy Utility: + +For convenient viewing of the controller tree, there is the Controllers Hierarchy utility. + +![](images/Menu.png) + +You can open it via the menu: Tools → Controllers → Controllers Hierarchy. + +The following window will appear: +![](images/ControllerHierarchy1.png) + +This window displays the controller tree, starting from the root controller. + +**Section descriptions:** + +1 **Name** – the controller’s name and its short description. + +2 **State** – the controller’s current state. + +3 **Scope** – the scope from which the controller was created. + +4 **Type** – the controller type (ControllerBase, ControllerWithResultBase, as well as custom types you can configure). + +Additional features: + +5 **Edit** – (appears when a controller is selected) opens the file with the selected controller for editing. + +6 **Pin** – creates an additional tab starting from the selected controller. + ![](images/ControllerHierarchy2.png) + +7 **Infos** – opens an extra section to view the controller’s fields and their values. + ![](images/ControllerHierarchy3.png) + +8 **Methods** – opens a section with the controller’s methods. Methods without arguments can be invoked directly. Methods marked with the [DebugMethod] attribute are displayed at the top of the list — useful during development for triggering various actions, such as cheat codes. + ![](images/ControllerHierarchy4.png) + +**If necessary, you can implement the IControllerDebugInfo interface in any controller** — it allows customizing how the controller is displayed in the utility. The IControllerDebugInfo interface is only used in the Unity Editor. + + +### Controllers Profiler: + +![](images/Menu.png) + +When the CONTROLLERS_PROFILER define is enabled, a new Controllers section appears in the Unity Profiler. +In the profiler, you can see which controllers were started and completed in each frame. + +![](images/ControllersProfiler.mov) + +The data is collected at the end of every frame, and three graphs are displayed: + +![](images/ControllersProfiler1.png) + +Active Controllers graph – the number of controllers currently in the tree. + +Total Controllers graph – the total number of controllers over the entire runtime of the application. + +Controllers Started graph – the number of controllers that started in the selected frame. + +![](images/ControllersProfiler2.png) + +On the left side, you’ll see summary information about controllers that started or finished in the selected frame: + +1 Controllers that started in the selected frame. + +2 Execution time of OnStart (in ms). If this number is greater than 0, it’s worth checking – it means OnStart contains a long synchronous method. + +3 Controllers that completed in the selected frame. + +4 Controller lifetime (in ms). + +On the right side, you can see a dump of the controller tree relevant to the current frame. If a controller was started and immediately completed, it will not be shown in this tree. + +5 The controller’s name in the tree. + +6 The scope from which the controller was spawned. + +7 The controller’s OnStart (same as point 2). + +8 Controller lifetime at the selected frame (same as point 4). + +9 A filter to quickly find a controller by substring in its name or scope. + +### Video about Controllers Tree: +Alexey Merzlikin on Digital Dragons - Architecture Behind Our Most Popular Unity Games : https://www.youtube.com/watch?v=-TlQAm8IZp4 \ No newline at end of file diff --git a/README_ru.md b/README_ru.md index 85a51f9..89a71da 100644 --- a/README_ru.md +++ b/README_ru.md @@ -14,21 +14,21 @@ Обычно в Unity для разработки игр используется самый простой способ, который предлагает сама Unity - это компонентно-ориентированное программирование (пока не рассматриваем ECS). Подход выглядит примерно так: каждый компонент это ООП класс унаследованный от MonoBehaviour - набор данных, и методов для их изменения (какой-то бизнес логики), и отображения. -Причем инкапсуляцию требует чтобы эти данные были максимально скрыты извне, и изменялись только предоставленными методами. +Причем инкапсуляция требует чтобы эти данные были максимально скрыты извне, и изменялись только предоставленными методами. За счет этого, во-первых, сами классы становятся достаточно большими даже с учетом разбивки на отдельные компоненты (наследованные отчасти решает эту проблему, ценой усложнения читаемости). -А так же для взаимодействия между собой используются эвенты либо какие-то классы которые управляют всем этим. Заставить всё это взаимодействовать между собой иногда нелегкая задача, заводятся какие-то классы которые все это синхронизируют, иногда бывает настоящий колбек хол, и порой в этом бывает трудно разобраться даже авторам, спустя какое-то время. +А также для взаимодействия между собой используются события либо какие-то классы которые управляют всем этим. Заставить всё это взаимодействовать между собой иногда нелегкая задача, заводятся какие-то классы которые все это синхронизируют, иногда бывает настоящий колбек хол, и порой в этом бывает трудно разобраться даже авторам, спустя какое-то время. -![](docs/Images~/MvcMvp.svg) +![](images/MvcMvp.svg) В подходе **MVC/MVP** этот принцип меняется. Данные выносятся в **модель или модели** (модель это класс для хранения данных). -Обычно у модели 2 интерфейса - один только на чтение, другой на изменение данных. Делается это для того, чтобы легко отделить тех кто читает данные, от тех кто их меняет. -На некоторых проектах модели содержат API и методы для изменения данных, иногда - только данные. +Обычно у модели 2 интерфейса - один только на чтение, другой на изменение данных. Делается это для того, чтобы легко отделить тех, кто читает данные, от тех, кто их меняет. +На некоторых проектах модели содержат API и методы для изменения данных **(Rich Model)**, иногда - только данные **(Anemic Model)**. Далее **представление** (вью) - в Unity это MonoBehaviour, с объектами для отображения/ввода. никакой логики в них нет, только вывод и ввод данных. И наконец **контроллеры**, по сути это классы отвечающие за бизнес логику. Тут есть важный момент - если в ООП единица - это класс (набор данных и методов их изменения и отображения), то в **MVC/MVP контроллер - это единица какой-то выполнимой работы, операция (паттерн "Команда")**. -Сам контроллер не имеет публичных методов и не содержит в себе каких то данных, результат его работы обычно какие-то изменения в моделях, либо во вьюхах, либо обращение к каким-то сервисам, либо запуск других контроллеров. +Сам контроллер не имеет публичных методов и не содержит в себе каких-то данных, результат его работы обычно какие-то изменения в моделях, либо во вьюхах, либо обращение к каким-то сервисам, либо запуск других контроллеров. Контроллер может быть запущен только из другого контроллера (кроме рута). Этим обеспечивается **code-first подход**. @@ -36,15 +36,15 @@ Итак, в **MVC/MVP каждый контроллер - это по сути паттерн «команда» (для контроллера с результатом)**. Если надо сохранить - то запускается контроллер сохранения, он инжектит модели которые надо сохранить в файл (интерфейсы только для чтения), возможно какой-то сервис для работы с файлами, и сохраняет. И всё, в этом контроллере больше нет никакой другой логики, к примеру за чтение будет отвечать уже совсем другой контроллер. За счет этого контроллеры остаются маленькими классами (обычно прялдка 200-300 строк), которые отвечают только за свою операцию. Так же так как контроллер инжектит несколько моделей - не надо уведомлять разные классы скажем о сохранении, такой подход позволяет уменьшить число эвентов. У каждого контроллера 1 зона ответственности - SRP (single responcibility) из коробки, любители солида останутся довольны. -![](docs/Images~/ControllersTree.svg) +![](images/ControllersTree.svg) H в **HMVP/HMVC означает hierarchical**. То есть контроллеры выстроены иерархически, в дерево. Есть единая точка входа, RootController, и остальные контроллеры можно породить только внутри родительского контроллера. Code-first подход, код намного легче читается и более предсказуемо работает. Те, кто сталкивался с логикой которая стартует в MonoBehaviour в Awake/Start понимают о чем я и какой хаос может быть, когда при загрузке сцены стартует несколько скриптов и надо учитывать кто за кем, заглядывая в экзекюшен ордер. -**Жизненный цикл контроллера строго стандатизован (паттерн "стратегия")**, более подробно расскажу ниже. За счет этого во-первых все участники проекта пишут в одном стиле, так как стандартизация заставляет следовать определенным правилам, и код на проекте консистентный, при таком подходе намного проще разобраться в фиче в которую ты первый раз заглядываешь. Во-вторых, флоу сделан безопасным, есть некоторая гарантия что даже в случае какой-то ошибки контроллер остановится корректно и ошибка будет обработана. +**Жизненный цикл контроллера строго стандартизован (паттерн "стратегия")**, более подробно расскажу ниже. За счет этого во-первых все участники проекта пишут в одном стиле, так как стандартизация заставляет следовать определенным правилам, и код на проекте консистентный, при таком подходе намного проще разобраться в фиче в которую ты первый раз заглядываешь. Во-вторых, флоу сделан безопасным, есть некоторая гарантия что даже в случае какой-то ошибки контроллер остановится корректно и ошибка будет обработана. Так как идет разделение логики и данных (и отображения), проще писать тесты на такой контроллер. Хотя первый раз надо мокать вьюхи, но в целом намного легче. -Один ньюанс с нашими контроллерами: рельно если смотреть на схему, то видно что в если вьюха не общатеся напрямую с моделью и все взаимодействия и с моделью и с вьюхой делает контроллер, то такой контроллер называется "презентер", а подход MVP. Но так уж сложилось, что технология внутри компании имеет устоявшееся название "Дерево контроллеров", **пусть наши контроллеры которые на самом деле презентеры называются контроллерами**. Но в нашей релизации вьюха не знает ни про кого, модель не знает ни про кого, контроллер знает про всех. Так же, как упоминалось выше, модель - это только данные, там нет логики для их обработки (в некоторых других релизациях MVXXX там бывает и логика). +Один нюанс с нашими контроллерами: если смотреть на схему, то видно, что в если вьюха не общается напрямую с моделью и все взаимодействия и с моделью и с вьюхой делает контроллер, то такой контроллер называется "презентер", а подход MVP. Но так уж сложилось, что технология внутри компании имеет устоявшееся название "Дерево контроллеров", **пусть наши контроллеры которые на самом деле презентеры называются контроллерами**. Но в нашей реализации вьюха не знает ни про кого, модель не знает ни про кого, контроллер знает про всех. Так же, как упоминалось выше, модель - это только данные, там нет логики для их обработки (в некоторых других релизациях MVXXX там бывает и логика). ### Итак, подведу кратки итог, плюсы HMVC/HMVP: - **Единая точка входа и код центрик подход**. @@ -54,7 +54,7 @@ H в **HMVP/HMVC означает hierarchical**. То есть контролл - **Высокая поддерживаемость кода**, человеку знакомому с архитектурой достаточно легко разобраться. - **Хорошо масштабируется**, фичи выходят достаточно изолированные. - **SRP** - single responsibility principe. -- **Меньше рисков поломать** - за счет SRP, в целом точно знаешь за что отвечает контроллер, есть конечно неявные зависимости, но в целом шансы сломать меньше +- **Меньше рисков поломать** - за счет SRP, в целом точно знаешь за что отвечает контроллер, есть, конечно, неявные зависимости, но в целом шансы сломать меньше - **Проще тесты** (хотя есть некоторый бойлерплейт). - **Легче работать в большой команде** (у нас клиентских разработчиков примерно порядка 40 человек на каждом проекте, все активно меняют код). - **Относительно безопасный флоу, try/catch**. @@ -193,11 +193,11 @@ var result = await ExecuteAndWaitResultAsync(new Exception(" ```csharp public enum ControllerBehaviour -{ +{[README_ru.md](README_ru.md) CompleteOnStart, CompleteOnFlowAsync, NeverComplete, @@ -428,3 +428,83 @@ public enum ControllerBehaviour substituteControllerFactory.SetBehaviourFor(ControllerBehaviour.CompleteOnFlowAsync); ``` + +### Утилита Controllers Hierarchy: + +Для удобства просмотра дерева контроллеров есть утилита Controllers Hierarchy. Утилита открывается через пункт меню **Tools → Controllers → Controllers Hierarchy**. +![](images/Menu.png) + +Появляется следующее окно: +![](images/ControllerHierarchy1.png) + +В окне отображается дерево контроллеров, начиная от корневого контроллера. + +**Описание секций:** + +1. **Name** - имя контроллера и его краткое описание. + +2. **State** - текущее состояние контроллера. + +3. **Scope** - скоуп (область видимости), из которого был создан контроллер. + +4. **Type** - тип контроллера (ControllerBase, ControllerWithResultBase, а также можно настроить кастомные типы). + +**Дополнительные функции:** + +5. **Edit** - (появляется при выделении контроллера) открывает файл с выбранным контроллером для редактирования. + +6. **Pin** - позволяет создать дополнительную вкладку, начинающуюся с выделенного контроллера. + ![](images/ControllerHierarchy2.png) + +7. **Infos** - открывает дополнительную секцию для просмотра полей контроллера и их значений. + ![](images/ControllerHierarchy3.png) + +8. **Methods** - открывает секцию с методами контроллера. Методы без аргументов можно вызвать напрямую. Методы, помеченные атрибутом **[DebugMethod]**, отображаются в верхней части списка — это удобно при разработке для запуска различных действий, например, чит-кодов. + ![](images/ControllerHierarchy4.png) + +**При необходимости можно реализовать интерфейс IControllerDebugInfo у любого контроллера** — он позволяет кастомизировать отображение контроллера в утилите. Интерфейс IControllerDebugInfo используется только в Unity Editor. + + +### Controllers Profiler: +![](images/Menu.png) +При включении дефейна CONTROLLERS_PROFILER в профайлере Unity появляется дополнительная секция **Controllers**. +В профайлере можно посмотреть, какие контроллеры запускались и комплитились в каждый кадр. + +![](images/ControllersProfiler.mov) + +Данные снимаются в конце каждого фрейма, и рисуется 3 графика: +![](images/ControllersProfiler1.png) + +График активных контроллеров - число контроллеров в дереве на данный момент. + +Общее число контроллеров - общее число контроллеров за всё время работы приложения. + +Число контроллеров, запущенных в выбранный фрейм. + +![](images/ControllersProfiler2.png) + +С левой стороны краткая информация о контроллерах запущенных и закончивших свою работу в выбранный фрейм. + +1 - Контроллеры которые стартовали в выбранный фрейм. + +2 - Сколько времени заняло выполнение OnStart (в мсек), если это число больше 0 то стоит обратить внимание, значит в контроллере OnStart это долгий синхронный метод. + +3 - Контроллеры которые закомплитились в выбранный фрейм. + +4 - Время жизни контроллера (в мсек) + + +С правой стороны виден дамп дерева контроллеров, актуальное для данного фрейма. Если контроллер был запущен и сразу закомпличен - он не будет отображен в этом дереве. + +5 - Имя контроллера в дереве. + +6 - Скоуп из которого был порожден контроллер. + +7 - OnStart контроллера (то же самое что и 2). + +8 - Lifetime контроллера на выбранный фрейм (то же самое что и 4). + +9 - Фильтр чтобы быстро найти контроллер по подстроке в имени или в скоупе. + +### Дополнительные материалы о Controllers Tree: +Доклад Алексея Мерзликина на Digital Dragons (Eng) : https://www.youtube.com/watch?v=-TlQAm8IZp4 \ No newline at end of file diff --git a/images/ControllerHierarchy1.png b/images/ControllerHierarchy1.png new file mode 100644 index 0000000..66f9c00 Binary files /dev/null and b/images/ControllerHierarchy1.png differ diff --git a/images/ControllerHierarchy2.png b/images/ControllerHierarchy2.png new file mode 100644 index 0000000..46ecc42 Binary files /dev/null and b/images/ControllerHierarchy2.png differ diff --git a/images/ControllerHierarchy3.png b/images/ControllerHierarchy3.png new file mode 100644 index 0000000..f80e4e2 Binary files /dev/null and b/images/ControllerHierarchy3.png differ diff --git a/images/ControllerHierarchy4.png b/images/ControllerHierarchy4.png new file mode 100644 index 0000000..3a00f42 Binary files /dev/null and b/images/ControllerHierarchy4.png differ diff --git a/images/ControllersProfiler.mov b/images/ControllersProfiler.mov new file mode 100644 index 0000000..1e82454 Binary files /dev/null and b/images/ControllersProfiler.mov differ diff --git a/images/ControllersProfiler1.png b/images/ControllersProfiler1.png new file mode 100644 index 0000000..0e1a7c7 Binary files /dev/null and b/images/ControllersProfiler1.png differ diff --git a/images/ControllersProfiler2.png b/images/ControllersProfiler2.png new file mode 100644 index 0000000..fc67a90 Binary files /dev/null and b/images/ControllersProfiler2.png differ diff --git a/images/Menu.png b/images/Menu.png new file mode 100644 index 0000000..9093d4f Binary files /dev/null and b/images/Menu.png differ diff --git a/images/MvcMvp.svg b/images/MvcMvp.svg new file mode 100644 index 0000000..f17bf66 --- /dev/null +++ b/images/MvcMvp.svg @@ -0,0 +1,4 @@ + + + +ControllerModelViewPresenterModelView
MVC
MVC
MVP
MVP
Text is not SVG - cannot display
\ No newline at end of file