Некоторое время назад я решил написать простой проект, в котором реализовал бы все свои накопившиеся идеи. В этой статье я хочу рассказать, что из этого получилось.
Всего было написано 6 проектов (1 основной проект и 5 вспомогательных пакетов):
Clean Game Example - пример простого шутера. Пример содержит: главное меню и несколько игровых уровней. В главном меню мы можем выбрать уровень, персонажа или настроить некоторые параметры. В самой же игре мы можем подобрать, выкинуть или поменять оружие, стрелять и убивать врагов, и соответственно пройти или проиграть уровень.
Clean Architecture Game Framework - фреймворк, который задает архитектуру всего проекта и решает некоторые мелкие проблемы.
Addressables Extensions - обертка над Addressables, делающая загрузку и выгрузку ресурсов более удобной.
Addressables Source Generator - инструмент для генерации исходного кода, содержащего все адреса и метки адресуемых ассетов. Благодаря этому инструменту, вы всегда можете быть уверенным, что в вашем проекте используются только правильные адреса и метки.
Colorful Project Window - расширение окна проекта, которое подсвечивает: пакеты, модули, исходники и ассеты. Благодаря этому инструменту вы моментально видите все необходимые пакеты, модули и их исходники и ассеты.
UIToolkit Theme Style Sheet - библиотека стилей для UIToolkit, написанная при помощи Stylus.
Проект Clean Game Example
Проект состоит из нескольких основных модулей: Project, Project.UI, Project.UI.Internal, Project.App, Project.Entities, Project.Entities.Actors, Project.Entities.Things, Project.Entities.Transports, Project.Entities.Worlds (расположены в папке Assets/Project/) и одного дополнительного модуля: Project.Infrastructure (расположен в папке Packages/com.denis535.project-infrastructure/). Основные модули содержат все самое важное и часто изменяемое. А дополнительный модуль содержит всё общие и все, что мне хотелось скрыть с глаз долой. Каждый модуль может содержать папки с исходниками (названия папок отображают названия пространства имен) и папки с ассетами (названия папок отображают адреса ассетов). Причем основные модули устроены так, что все папки с исходниками содержат *.asmref файлы, которые привязывают их к конкретным модулям. А сами модули определены отдельно (в папке Assets/Assemblies/). Такая структура проекта оказалась довольно удобной.
Модуль Project
Корневой модуль Project содержит точку входа, а так же некоторые инструменты.
Модуль Project.UI
Модуль Project.UI содержит аудио тему, экран пользовательского интерфейса, роутер управляющий состоянием приложения и все виджеты.
Модуль Project.UI.Internal
Модуль Project.UI.Internal содержит все вьюшки. Данный модуль не имеет зависимостей на другие модули, таким образом вьюшки так же не имеют лишних зависимостей.
Модуль Project.App
Модуль Project.App содержит все сущности и сервисы уровня приложения. Так же отвечает за запуск и остановку игры.
Модуль Project.Entities
Модуль Project.Entities содержит базовые игровые сущности: игра и игрок.
Модуль Project.Entities.Actors
Модуль Project.Entities.Actors содержит сущности, которые могут действовать под управлением игрока или самостоятельно.
Модуль Project.Entities.Things
Модуль Project.Entities.Things содержит сущности, которыми актеры могут владеть.
Модуль Project.Entities.Worlds
Модуль Project.Entities.Worlds содержит миры и разные объекты окружения.
Модуль Project.Infrastructure
Модуль Project.Infrastructure содержит, в первую очередь, все общие, а так же все, что мне хотелось скрыть от глаз.
Так же стоило бы добавить модуль для разного транспорта, но в моем случае это было не нужно.
Пакет Clean Architecture Game Framework
Фреймворк задает архитектуру проекта, а так же предоставляет некоторые простые утилиты. Я был вдохновлен идеями чистой архитектуры, поэтому название выбрал соответствующие. Сам фреймворк состоит из трех модулей (слоев):
Модуль Clean.Architecture.Game.Framework.Core
Модуль Core содержит все самое важное.
UnityEngine.Framework
ProgramBase - точка входа.
UnityEngine.Framework.UI
UIThemeBase - аудио тема. Для аудио темы я использовал паттерн состояние. Соответственно, в один момент времени, тема может находится в одном состоянии.
UIScreenBase - пользовательский интерфейс. Для пользовательского интерфейса я выбрал state-driven архитектуру (а не view-driven, как обычно). В обычных оконных приложениях все очень просто. Все элементы пользовательского интерфейса расположены в главном окне. Если что-то не умещается в главном окне, то пользователь может открывать или закрывать дополнительные окна. Но в полноэкранных приложениях (мобильные приложения или игры), пользовательский интерфейс обычно представляет из себя множества разных состояний. Это очень похоже на веб сайты. Возможно даже, что пользовательский интерфейс может находится одновременно в нескольких состояниях. Разумеется нужна архитектура, которая будет поддерживать навигацию по этим всем состояниям. Если я правильно понимаю, то фреймворк Uber Ribs как раз реализует такую state-driven архитектуру. В state-driven архитектуре пользовательский интерфейс состоит из иерархического дерева состояний (бизнес юнитов - widget) и иерархического дерева визуальных юнитов (view). Т.е. первичным является именно иерархическое дерево состояний. Каждый виджет может иметь (или не иметь) свою вьюшку. Корневой виджет (точнее его вьюшка) является контейнером для всех остальных вьюшек. Но и другие виджеты так же могут быть контейнерами для некоторых своих вьюшек. Получается, что дерево состояний и визуальное дерево очень сильно отличаются друг от друга. Это может казаться сложным, но это делает навигацию по пользовательскому интерфейсу очень удобной.
UIRouterBase - менеджер состояния приложения. Роутер отвечает за: загрузку главного меню, загрузку игры, перезагрузку игры, выгрузку игры и закрытие всего приложения.
UIWidgetBase - состояние (бизнес юнит) пользовательского интерфейса.
UIViewBase - визуальный юнит пользовательского интерфейса.
UnityEngine.Framework.App
ApplicationBase - приложение. Приложение содержит все сущности и сервисы уровня приложения. А так же приложение содержит сущность игры и отвечает за создание и уничтожение данной сущности.
UnityEngine.Framework.Entities
GameBase - сущность игры. Игра содержит логику и состояние игры, а так же все дочерние сущности.
PlayerBase - сущность игрока. Игрок содержит состояние, а так же камеру и персонажа. А так же предоставляет пользовательский ввод для камеры и персонажа.
EntityBase - сущность, которая существует на сцена. Будь то какой-то персонаж, оружие или пуля. В идеале сущность должна быть обычным POCO классом, но в таком случае нельзя будет обрабатывать "SendMessage" события. Поэтому я был вынужден использовать MonoBehaviour в проекте. на мой взгляд MonoBehaviour это самая большая беда Unity. Невозможность использовать конструктор, readonly поля и свойства, а так же аргументы конструктора превращают простую работу в кошмар. Странно, но в Unreal тоже нельзя передать аргументы в конструктор актера. Хотя часто это просто необходимо.
Модуль Clean.Architecture.Game.Framework
Основной модуль содержит разные дополнения. Самое интересное это:
UIRootWidgetBase и UIRootWidgetViewBase - корневой виджет отвечает за размещение дочерних виджетов на экране.
IDependencyContainer - по своей сути это просто service locator.
Модуль Clean.Architecture.Game.Framework.Internal
Модуль Internal содержит разные утилиты и прочие внутренности. Самое интересное это:
Assert - позволяет делать проверки в более удобной форме, чем стандартный Debug.Assert.
Option - тип который иногда бывает очень полезным.
Пакет Colorful Project Window
Как вы уже заметили мое окно проекта очень удобно разукрашено в разные цвета. Я подсвечиваю: пакеты (синий), модули (красный), исходники (зеленый) и ассеты (желтый). Это значительно упрощает и ускоряет навигацию по проекту и сохраняет нервы).
Итоги
Думаю у меня получилось написать проект с удобной структурой и архитектурой. Но даже такой простой проект очень быстро разросся и стал трудно поддерживаемым. И это даже притом, что в последние годы в Unity появилась поддержка модульности (Assembly Definition) и зависимостей (Package Manager). А не так давно и этого не было.
Уверен, многое можно было бы сделать лучше, если бы Unity это позволял. Далее я хочу отметить некоторые мои претензии к Unity:
Наверное самая основная проблема Unity это MonoBehaviour. В классах наследованных от MonoBehaviour нельзя использовать конструктор, аргументы конструктора и соответственно константные поля и свойства. Это превращает программирование в реальный идиотизм. Кстати, на мое большое удивление, в Unreal тоже нельзя передать аргументы в конструктор актера. Это очень странно и неудобно. Возможно я чего-то не понимаю.
Сторонние объекты не могут слушать события GameObject'а. Только MonoBehaviour может слушать и обрабатывать Unity событие. В теории можно было бы полностью отказаться от MonoBehaviour и использовать обычные POCO классы, но тогда мы не сможем обрабатывать многие события.
Мало возможностей для кастомизации окна проекта. Мне очень хотелось бы самому задавать порядок отображения элементов, изменять их имена и вообще скрывать лишние. Но можно лишь немного дорисовать элементы.
Нет возможности временно выгрузить все лишние из проекта. Например, в Visual Studio можно выгрузить любые проекты из решения. Это позволяет разработчику сфокусироваться на конкретном проекте и выкинуть из головы все остальное. Особенно это полезно, когда решение становится достаточно большим и изменение одного проекта влечет к проблемам в других проектах. К сожалению в Unity это не предусмотрено. И нам приходится постоянно держать в голове весь код и все ассеты. Я не знаю как с этим в других движках, но думаю, что так же.
UIToolkit это ад. Я сильно пожалел, что использовал UIToolkit в данном проекте. Идея с таблицами стилей оказалась очень не удачной. Во первых нужно много времени, чтобы разобраться во всех свойствах стилей. Во вторых нужно еще больше времени, чтобы разобраться как устроены элементы, чтобы написать для них стили. Я потратил крайне много времени и нервов на написание стилей для нескольких элементов. И все это может сломаться в любом следующем обновлении. Хуже того, Unity интегрировала этого монстра в сам редактор. А это значит, что он теперь с нами навсегда. А было бы разумнее максимально облегчить редактор и рантайм.
Просто удивительное отношение компании к качеству своего продукта. Наверное многие знают, что в Unity константы некоторых цветов всегда были немного неправильными. Спустя много-много лет разработчики все таки исправили константу Color.green. Я уже подумал, что наконец-то в Unity взялись за головы, но потом заметил, что Color.yellow до сих пор остается не желтой, а оттенком желтого (Yellow. RGBA is (1, 0.92, 0.016, 1), but the color is nice to look at!). Я помню, как какой-то Unity разработчик писал, что они никогда не будут исправлять Color.green т.к. для них очень важна обратная совместимость. Как видим обратная совместимость для них все же не очень важна. И качество их продукта по всей видимости тоже не очень важно. Вы можете представить, чтобы в каком-то ПЛАТНОМ графическом редакторе был не черный цвет, а почти черный? А вы можете представить, чтобы это продолжалось в течении 20 лет? Весь Unity это сплошное наследие. Конечно же в некоторых случаях обратная совместимость, к большому сожалению, очень важна, но точно не в этом случае. Я недавно открывал баг репорт (достаточно серьёзная проблема в UIToolkit), но Unity его просто закрыли со словами "There are no fixes planned for this Bug". Наверное на то были свои причины, но все же... Как обычно, могло бы быть и лучше, но как говорится, пипл схавает)
Без лишних подробностей, но с картинками, я рассказал вам о своем проекте. Если вам это было интересно, то вы можете более внимательно ознакомится с моим проектом на гитхабе. Если у вас есть идеи как можно сделать что-то лучше, то я буду рад узнать о них!
Ссылки
https://openupm.com/packages/com.denis535.clean-architecture-game-framework
https://openupm.com/packages/com.denis535.addressables-extensions
https://openupm.com/packages/com.denis535.addressables-source-generator
https://openupm.com/packages/com.denis535.colorful-project-window
https://assetstore.unity.com/packages/tools/gui/uitoolkit-theme-style-sheet-273463