Некоторое время назад я решил написать простой проект, в котором реализовал бы все свои накопившиеся идеи. В этой статье я хочу рассказать, что из этого получилось.

Всего было написано 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

Проект Clean Game Example
Проект 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

Корневой модуль Project содержит точку входа, а так же некоторые инструменты.

Модуль Project.UI

Project.UI
Модуль Project.UI

Модуль Project.UI содержит аудио тему, экран пользовательского интерфейса, роутер управляющий состоянием приложения и все виджеты.

Модуль Project.UI.Internal

Project.UI.Internal
Модуль Project.UI.Internal

Модуль Project.UI.Internal содержит все вьюшки. Данный модуль не имеет зависимостей на другие модули, таким образом вьюшки так же не имеют лишних зависимостей.

Модуль Project.App

Project.App
Модуль Project.App

Модуль Project.App содержит все сущности и сервисы уровня приложения. Так же отвечает за запуск и остановку игры.

Модуль Project.Entities

Project.Entities
Модуль Project.Entities

Модуль Project.Entities содержит базовые игровые сущности: игра и игрок.

Модуль Project.Entities.Actors

Project.Entities.Actors
Модуль Project.Entities.Actors

Модуль Project.Entities.Actors содержит сущности, которые могут действовать под управлением игрока или самостоятельно.

Модуль Project.Entities.Things

Project.Entities.Things
Модуль Project.Entities.Things

Модуль Project.Entities.Things содержит сущности, которыми актеры могут владеть.

Модуль Project.Entities.Worlds

Project.Entities.Worlds
Модуль Project.Entities.Worlds

Модуль Project.Entities.Worlds содержит миры и разные объекты окружения.

Модуль Project.Infrastructure

Project.Infrastructure
Модуль Project.Infrastructure

Модуль Project.Infrastructure содержит, в первую очередь, все общие, а так же все, что мне хотелось скрыть от глаз.

Так же стоило бы добавить модуль для разного транспорта, но в моем случае это было не нужно.

Пакет Clean Architecture Game Framework

Пакет Clean.Architecture.Game.Framework
Пакет Clean.Architecture.Game.Framework

Фреймворк задает архитектуру проекта, а так же предоставляет некоторые простые утилиты. Я был вдохновлен идеями чистой архитектуры, поэтому название выбрал соответствующие. Сам фреймворк состоит из трех модулей (слоев):

Модуль Clean.Architecture.Game.Framework.Core

Модуль Core содержит все самое важное.

  • UnityEngine.Framework

    • ProgramBase - точка входа.

  • UnityEngine.Framework.UI

    • UIThemeBase - аудио тема.

    • UIScreenBase - пользовательский интерфейс. Пользовательский интерфейс состоит из иерархии бизнес юнитов (UIWidgetBase) и иерархии визуальных юнитов (UIViewBase). Т.е. к экрану крепится иерархия виджетов, каждый виджет может содержать (или не содержать) вьюшку. За добавление вьюшки в иерархию визуальных юнитов, обычно отвечает корневой виджет, но могут и другие виджеты-контейнеры добавлять дочерние вьюшки в себя. По идее получается как в Uber Ribs.

    • 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

Пакет Colorful Project Window
Пакет 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". Наверное на то были свои причины, но все же... Как обычно многое могло бы быть лучше, но как говорится, пипл схавает.

Без лишних подробностей, но с картинками, я рассказал вам о своем проекте. Если вам это было интересно, то вы можете более внимательно ознакомится с моим проектом на гитхабе. Если у вас есть идеи как можно сделать что-то лучше, то я буду рад узнать о них!

Ссылки

Комментарии (10)


  1. LinarMast
    03.08.2024 08:17
    +6

    Не совсем понятна цель статьи, больше похоже на поток мыслей и с претензиями к движку.

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

    > Но даже такой простой проект очень быстро разросся и стал трудно поддерживаемым

    Мне ближе по душе, то, как предлагает делать сама Unity. Она простая и очень понятная, поэтому другим людям будем проще вкатываться в проект.

    Противоречивое сложилось ощущение после прочтения статьи, как будто вы боролись с ветреными мельницами (и не очевидно, кто выиграл).


    1. Denis535 Автор
      03.08.2024 08:17

      Я просто хотел поделится с миром своими идеями. Статья действительно весьма поверхностно описывает мой проект, но на мой взгляд все самое важное я рассказал.


  1. Lekret
    03.08.2024 08:17
    +6

    Если опираться на название, то я не согласен, что это лучшие практики. Советы крайней субъективные и вряд-ли похожи на распространенные "лучшие практики", так ещё и проверены на крайней меленьком проекте, что выглядит как минимум не убедительно. Рассказывать про лучшие практики надо, когда есть проект в релизе, вам нравится как он написан и работает, и тогда уже есть смысл чем-то поделиться.
    В остальном, похоже на попытку перенести вебовские практики в игры и юнити, где это не очень уместно. Есть множество более удобных и простых вариантов построить архитектуру для малых, средних и большие проектов.

    Раздел с претензиям к статье не вяжется, непонятно зачем он здесь. Выглядит как "я программировал не игры, и когда имея опыт другой разработки пришёл в юнити, то мне многие вещи непонятны или не нравятся". Многие претензии уверен отпадут, если чуть больше поработать с игровым движком и вникнуть в технические ограничения, которые как вы сами заметили не только к юнити относятся.


    1. Denis535 Автор
      03.08.2024 08:17
      +1

      • Если опираться на название, то я не согласен, что это лучшие практики. Советы крайней субъективные и вряд-ли похожи на распространенные "лучшие практики", так ещё и проверены на крайней меленьком проекте, что выглядит как минимум не убедительно.
        У разных людей разные привычки и предпочтения. Если вы привыкли к другим парадигмам, то это не значит, что мои идеи плохие. И я не думаю, что есть что-то лучше, чем идеи чистой архитектуры, которые я использовал.

      • Рассказывать про лучшие практики надо, когда есть проект в релизе, вам нравится как он написан и работает, и тогда уже есть смысл чем-то поделиться.

        Сделав много проб и ошибок, и потратив много времени, я нашел достаточно хорошие решения (по-моему субъективному мнению). Было бы лучше, если бы никто о них не узнал?

      • В остальном, похоже на попытку перенести вебовские практики в игры и юнити, где это не очень уместно. Есть множество более удобных и простых вариантов построить архитектуру для малых, средних и большие проектов.

        Почему в Unity это не уместно? Чем Unity отличается от других областей разработки?

      • Раздел с претензиям к статье не вяжется, непонятно зачем он здесь. Выглядит как "я программировал не игры, и когда имея опыт другой разработки пришёл в юнити, то мне многие вещи непонятны или не нравятся".

        Именно так! Многие вещи мне не нравятся. Что-то можно было бы сделать лучше. Что-то в Unity могло бы быть лучше. И это тоже полезно знать.

      • Многие претензии уверен отпадут, если чуть больше поработать с игровым движком и вникнуть в технические ограничения, которые как вы сами заметили не только к юнити относятся.

        Каким образом они отпадут? Нет, не отпадут. Конечно же везде есть свои проблемы. И это всегда будет отнимать время и нервы.


      1. GoRoX98
        03.08.2024 08:17

        Я хоть с часть претензий и согласен, тем не менее писать статьи это сложно. Так что как бы то ни было - молодец, продолжай, старайся:3


      1. Darell_Ldark
        03.08.2024 08:17

        Почему в Unity это не уместно? Чем Unity отличается от других областей разработки?
        Потому что для Unity, как и других движков, существуют некоторые рекомендации, которые применяются на существенно большем числе проектов. И этот вариант, который используется в индустрии, становится своего рода "стандартом". Отсюда возникает резонный вопрос - для чего делать не стандартными способами?
        Касательно MonoBehaviour и AActor - это механизм, предовращающий прямой контроль над временем жизни объектов. Поэтому конструкторы вынесены на сторону движка, а наружу торчат только несколько методов, которые можно определить для инициализации. Насколько мне известно, этот способ актуален как минимум для UE и Unity, но лично видел еще в двух проприетарных движках.


  1. dducode
    03.08.2024 08:17

    Насчет MonoBehaviour конструкторов и невозможности их использования. Все C# компоненты, по сути, являются лишь обертками над нативными объектами Unity. Соответственно, их жизненный цикл возможно контролировать только через методы, которые предоставляет сам движок (Instantiate, Destroy, AddComponent). К тому же, насколько я понимаю, это необходимо для сериализации компонентов в редакторе.


  1. RandomVertex
    03.08.2024 08:17

    Лучшая практика для Юнити = не использовать Юнити)


    1. Denis535 Автор
      03.08.2024 08:17

      Как бы там ни было, на Unity были созданы: Need for Speed World, Call of Duty: Mobile, Genshin Impact и наверное еще много сложных проектов.


  1. GoRoX98
    03.08.2024 08:17

    Устал листать статью из-за большего количества практически идентичных скриншотов (без них читалось бы лучше, ИМХО (не все естественно убрать, а которые не несут по сути полезно информативности).

    Что касается сути статьи, как отметили выше - это субъективщина и называть лучшими ее конечно громко)

    Далее про итог:
    Монобех - поэтому используют методы инициализации, не совсем понятно (если логически думать) зачем тебе прям конструктор монобеха, ведь монобех это компонент объекта, а не сам игровой объект. Так что либо не используй монобех, либо используй метод инициализации /другие практики для создания объекта без конструктора.

    Слушать сообщения Unity - делаешь управляющий класс, который содержит композицию сторонних объектов не монобехов и выполняешь исходя из нужной логики все это дело. Либо делаешь статичное событие и не монобехи на него привязываются. Да и в целом много вариантов.

    Много возможностей кастомизировать окна/создавать свои (все ли не знаю, но в инспекторе достаточно возможностей). Для упрощения этого дела существует например ODIN.

    Про выгрузку - так создай отдельную сцену под фичу и работай там с ней. Если не нагружать ее и не использовать для перехода в другие сцены - то по идее она отдельно билдится. Но тут могу ошибаться.

    UIToolkit - днище если откровенны будем))) Надеюсь его доработают

    Ну а последний пункт (да и к предпоследнему тоже относится) - Classic by Unity Team