Pull to refresh

Comments 51

Перезалейте, пожалуйста, картинки на habrastorage.org. Vk у части читателей может быть заблокированным.

Похоже на сборник советов того, как не нужно делать.

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

Как текущий подход помог избавиться от этого гемора?
За то добавилось много нового:
  • компилятор больше не помощник, он не скажет где вы ошиблись, где использовали не тот
  • ide больше не помощник, он вам просто так уже не переименует переменные
  • производительность?
  • порядок OnStart должен быть определен строго, если вдруг кто-то уже захочет взять данные из другого компонента в своем OnStart
  • создаем новый объект, добавили в него FirstComponent. А он не работает, потому что ожидает «count». Куда идти, где искать?


А вообще, если появилась такая необходимость завязать много компонентов друг на друга, может что-то не так с архитектурой?

Порядок OnStart определяется цепочкой наследования в данном случае. Да компилятор не помошник, но это цена за удобство, а на производительности это даже лучше сказывается, так как избегается несколько случаев, когда приходится мониторить что-то в каждом фрейме.
Компонент будет работать, просто он не будет получать значение и ошибки не будет, если вы пробовали запускать решение, то убедились бы в этом.

Такая архитектура из коробки предоставляется. И дальше, кто во что горазд. Я смотрел в сторону ECS, и у меня есть своя реализация, что я опишу в следующих статьях.

Так а в чем удобство? Вы одним махом обрубаете кучу прелестей современных IDE, при этом связность кода понижается совсем слегка.
Когда одному из компонентов понадобится изменить функционал так, что некая его «переменная» приобретёт совершенно иной смысл, то вы оставите ей прежнее имя (которое более не соответствует действительности) или будете все же переименовывать? Разумеется под именем я подразумеваю ключ переменной в вашем словаре.
И раз уж вы тут производительность упомянули: доступ по строке в словаре отнюдь не бесплатный, в вашем решении имеет место быть боксинг (возможен даже в очень больших количествах, если например в апдейтах обращаться к общему состоянию), зачем-то определён Update в базовом классе (кстати зачем?).
Для уменьшения связности кода я бы в первую очередь предложил пересмотреть архитектуру. Ну а далее ECS и/или DI.

Про боксинг согласен, я подумаю как использовать дженерик. ECS это то к чему я собираюсь прийти, просто я не могу сурию туторов в одну статью вместить, а DI контейнер я тоже заимплементил и он работает на уровне всего приложения, а не для разрешения зависимостей между компонентами в геймобджекте, для этого тут и RequiredComponent удачно вписывается. А про собственную реализацию контейнера я позжеопубликую.

В общем случае для разных компонентов порядок вызова Start не определен, а значит и OnStart.
Производительность: если дорого проверять в каждом апдейте, то можно сделать явную подписку на событие, а без вот этих вот словарей/боксинга/кастов.
Что если два компонента добавят элемент с одним и тем же именем?

Компонент будет работать, просто он не будет получать значение и ошибки не будет
А так ли это хорошо? Может лучше, если будет ошибка, и разработчики узнают о проблеме?

На мой взгляд это writeonly подход, написал — проверил что работает — закрыл навсегда. При каком-либо усложнении логики чтение и даже отладка превратиться в сущий ад.

А какие плюсы дает система, я так и не получил ответа.
Производительность: если дорого проверять в каждом апдейте, то можно сделать явную подписку на событие


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

а без вот этих вот словарей/боксинга/кастов

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

И да и нет. Я не предлагаю это как единственное правильное решение, я описываю свой опыт и свое видение, и лично мне проще и намного быстрее использовать словари, нежели поля.
При каком-либо усложнении логики чтение и даже отладка превратиться в сущий ад

Дело в привычке, на самом деле и во внимательности. Опять же если вы находите такое решение не приемлемым для себя, то я ж не настаиваю.
А какие плюсы дает система, я так и не получил ответа.

Да, я в этой статье не продемонстрировал реальное использование данного подхода в проекте и сделаю это в следующей.
Дружище, как быть если нужно «обменяться» с объектом в другой сцене или прифабе?
Я напишу об этом в следующих статьях, просто пока первую опубликовали. Если интересно, напиши я скину ссылку на группу вк где публикую
Так не надо делать — особенно не в тему наследование, а если нужно наследовать от чего-то другого или уже наследуется от другого?
Реализация «состояний» тоже странная.

Тут не нужны весопеды
Для решения проблемы связности используем DI-контейнеры ( см. Zenject )

Для реализации паттерна наблюдателя используем реактивные расширения, которые намного элегантней и функциональнкй (см. UniRx)
Для решения проблемы связности используем DI-контейнеры ( см. Zenject )/blockquote>
Я бы с удовольствием почитал статью на Хабре от человека, который имеет такой опыт. Сам пользуюсь Zenject, но кажется, что в некоторых местах делаю криво.

Вы написали что плохо, по поводу наследований, но не написали почему. Я думаю многим было бы интереснее, и мне в том числе, почему вы так считаете.
По поводу DI котейнера, я использую его на другом уровне, так как считаю что на этом от него мало толка.
И я заимплементил свой, опять же.
Потповоду реактивных расширений. Мне оч интересно, я посмотрю, спасибо.

каждый кадр каждый компонент делает чек в Dictionary?
Сейчас да, но это исключительно для демонстрации именно этого функционала, в следующей я пишу о уведомлении об изменении состояния и там я не использую проверку в каждом фрейме.
На днях была похожая задача, решил с помощью Unidux, который использует UniRx.

Так же C# позволяет использовать статические методы, в не статических классах.
Соответсвенно вы делаете базовую реализацию Singleton, который возвращает ссылку на объект в сцене, а если его нет создает и сохраняет. Далее в компонентах просто используете метод по имена класса «Class.Instance.staticMethod()». И у вас появляется возможность использовать статические методы, без использования ссылок.
Да, в UniRx есть компоненты со схожим смыслом, но мне показалось он излишне перегружен и слишком универсален. И у меня не совсем синглтон, так как для каждого геймобджекта будет использоваться свой экземпляр
В zenject для этого есть gameobject context. В вашем случае либо надо переходить на другую модель(модель акторов), либо решать зависимости, как это принято (di). Я так считаю
Чем модель акторов лучше, для данной задачи? И зависимость разрешается за счет RequireComponent, а использование контейнера у меня на другом уровне, в данной статье на раскрыто
Модель акторов лучше тем, что вы по сути пытаетесь сделать ее своими сообщениями. Только это отдельная парадигма, а не троллейбус из буханки хлеба :)

Ну или не ее, а шину сообщений в контексте GameObject
Я не пытаюсь сделать систему собщений через модель акторов, а всего лишь заимплементил паттерн Pub/Sub
В zenject для этого есть gameobject context.
О! Вы тоже разбираетесь! Как на счет того, чтобы написать статью? Лично я — очень хочу почитать про Zenject от рядового пользователя.
Та мануал я давно прочитал и уже много времени пользуюсь им.

Интересен не сухой мануал, а личный опыт, который зависит от всяких нюансов. Ну вот, к примеру, я играю в игру, в контейнере лежат всякие зависимости, потом выхожу в меню и загружаю другую игру. Что происходит с зависимостями? Старые выгружаются и загружаются новые? И сколько вообще завимостей должны зависеть от стейта? Я сейчас стараюсь сделать побольше Stateless классов, которые получают данные извне, а сам стейт делать без логики — таким образом вьюшка в основном запрашивает Stateless классы, а уже они зависят от стейта. Но я не уверен, что правильно решаю эту задачу

Я вообще даже задавал вопрос на тостере, но не получил на него ответов.
Зависит от типа контекста. В SceneContext все выгружается при переходе между сценами. В ProjectContext все хранится постоянно. Ну и байндить можно как синглтон, получая постоянно одну и ту же сущность, или каждый раз по запросу создавать новую сущность. Не думаю что здесь прям какие-то практики есть.
Я все делаю в пределах одной сцены, то есть при загрузке-сохранении сцены не переключаются.

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

Тема настолько обширно, что непонятно, что вы хотите услышать?
Если вам нечего сказать — не пишите.
А в примерах, если я не ошибаюсь, никакого сейв-лоада нету.
Zenject активно использует рефлексию, а это значит, что не оч положительно сказывается на произовдительности. И не вижу выгоды в использовании его, против своего решения «под себя».
Связь между рефлексией и просадкой производительности не очевидна. Вложенные циклы, тоже просаживают производительность, может откажемся от их использования?
Так если бы она мне была остро необходима, я бы согласился, но я смог и без лишней траты ресурсов обойтись, так зачем тогда мне прикручивать крупный фреймворк?
Я вам указал, на то что не стоит обобщать Рефлексия = просадка перфоманса.

Неразумное использование рефлексии ведёт к просадке перфоманса. Как и неразумное использование вложенных циклов ведёт туда же.
То есть вы хотите сказать, что поиск и инициализация инстанса через рефлексию равна по скорости путем прямого создания?
Я хочу сказать, что оптимизировать рефлексию в проекте, это тоже самое, что экономить на спичках. Нормально оптимизированные графические ассеты дадут прироста больше, чем отказ от рефлексии.
Опять же, если бы в ней был бы смысл, в моем примере, я бы согласился. Но у меня небыло в этом необходимости и прикручивать фреймворк, который пусть и немного но отъест ресурсов, при этом не давая мне ничего, от чего бы я не смог реализовать, мне не нужно
если проект размером с тетрис — можете и обойтись. Стоит проекту стать крупнее — вот там вы вполне ощутите его пользу. Если, конечно, вы умеете его готовить.
А что касается рефлексии — так она не в каждом фрейме используется.
У меня проект не оч большой, но и не тетрис. И для данного проекта, как и для многих, этого более чем достаточно. Я не скажу что я не умею готовить именно zenject, я с разными ioc фреймворками работал, с ним нет, помотрел только исходники. И обязательно попробую как нибудь на тестовом проекта
Ееее топ система я вписал её в свою игру и у меня сразу появились инвесторы, команда и девушка классная, спасибо автор!!!
Автор изобрёл шаблон проектирования mediator и скрестил его с observer?
Я не изобретал шаблоны, я их применил в собственной имплементации. Разберитесь, что такое шаблон и какую роль он играет в разработке программного обеспечения.
Так указали бы в статье, что вы применили эти шаблоны. И людям которые пытаются их понять будет польза, в виде примера. И новичкам будет понятно, куда копать если захотят расширить эту систему. А то выглядит, как изобретение шаблона заново.
Так применение шаблона это не суть статьи. Какая разница какой шаблон применен, главное что в целом решение решает какую-то проблему. И статья именно ориентирована на новичков, и мне бы не хотелось чтобы у них развивались «паттерны головного мозга». Иногда шаблоны можно не использовать или менять их, главное чтобы это решало задачу и не создавало трудностей в развитии проекта.
Но указать, откуда вы взяли подход можно, же было?
К вопросу про Zenject и разрыв связей между классами.

Если проектировать полностью гибкую систему. То мы можем построить всю систему на сигналах Zenject`a.

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

А Менеджеры и Контроллеры будут обрабатывать эти сигналы.
Конечно можно, только зачем, если шину данных я сам заимплементировал, достаточно быстро и мне для этого не потребовался сторонний фреймворк. Это не настолько сложная задача, чтобы прикручивать, опять же, целый фреймворк
Я вам не говорю, что вам нужно брость всё и использовать фреймворк. Фреймворк нужно использовать для решения комплексных проблем. А не ради одно фичи.

И тут я пояснил, как ещё одним способом можно решать проблему высокой свзяности кода в проекте
Ну а я показал, что в нем нет необходимости. Еще раз, статья для начинающих и для них это было бы сложно. Я много раз пытался объяснять людям почему не стоит создавать мегакомпоненты, которые включают все, или реализация всей логики в методе апдейт, и подобное мракобесие. Я пытался объяснять зачем применять шаблоны и для чего это нужно. Зачастую это бесполезно, а вы говорить вкрячить фреймворк с другим подходом в разработке, нежели то что предлагают стандартные средства юнити.
Используя готовый фремворк вы, как минимум, сэкономите кучу времени(и денег) на разработку архитектуры и своих инструментов. А также время на адаптацию и введение в курс дела, нового члена команды разработки. Также, фреймворк задаёт рамки использования компонентнов и.т.д.

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

Можно же сделать класс с данными и его брать через интерфейс и кастить.


public interface ISharedData {
    public object SharedData { get; }
}
public class Class1 : Monobehaviour : ISharedData {
    public class SharedConfig {
        public int intValue;
        public float floatValue;
        public string stringValue;
    }
    private SharedConfig config;
    public object ISharedData.SharedData() {
        return config;
    }

}

public class Class2 : Monobehaviour {
    private Class1.SharedConfig config;

    private IEnumerator Start() {
        while (GetComponent<ISharedData>() == null || GetComponent<ISharedData>().SharedData == null)
            yield return null;
        config = (Class1.SharedConfig) GetComponent<ISharedData>().SharedData;
    }
}
Sign up to leave a comment.

Articles