Pull to refresh

Comments 26

не пробовали https://www.assetstore.unity3d.com/en/#!/content/17276?
Пробовали, это более высокий уровень нежели то, что описано в статье и помимо прочего UniRx он про асинхронное программирование, а там надо несколько все по другому думать в плане архитектуры и т.п.
Я стараюсь в своих проектах уйти от концепции событий в сторону обработки состояний объектов. Для понимания, можно посмотреть решение от Wooga – Entitas.

При таком подходе вся логика хранится в системах, что срабатывают для N-объектов при Х-состоянии объектов. Например, при возникновении объектов с компонентом DamageComponent и HealthComponent, система DamageDealSystem отнимет у HealthComponent необходимое количество жизней и отправит запрос на уничтожение состояния DamageComponent. В свою очередь DamagePopupUISystem система выведет в интерфейс значение из модели Damage (до его уничтожения). Думаю, что не нужно объяснять, что количество систем может быть неограниченно, например, DamageToScoreSystem, DamageReflectSystem, DamageNetworkSyncSystem и т.д.
Но самое шикарное, это возможность быстро и безболезненно создавать сохранения, записи игр (реплеи). Так как вся игра на любой момент времени — это набор объектов с их состояниями (плоскими данными без логики)
Да, есть системы разные, для одних проектов подходят одни для других другие. Но вот в данном случае, DamageDealSystem как-то должно узнать что появился объект с компонентом DamageComponent и HealthComponent.
В Entitas эти события «появлений» сделаны очень хитро, с помощью так называемых Group Observer. Поскольку каждое изменение значений жизней (например) делается через ReplaceHealth (грубо), то есть как бы меняя один компонент на другой, эти Group Observer имеют возможность сразу понять, что в этот момент нужно что-то делать. Там (в Entitas) на основе этого принципа работают Reactive Systems. Вне систем эти группы можно использовать в любом коде, то есть просто описываем интересующие нас сущности (сущность должна иметь такие-то и такие-то компоненты), подписываемся на событие обновления (грубо говоря) и обрабатываем полученные данные. Таким образом, например, можно сделать чистую вьюшку в интерфейсе, которая мониторит этот Health и просто выводит его на экран.
Вообще Entitas — это крайне интересная вещь, сейчас как раз делаю мелкий тестовый проект чтобы разобраться. До конца в самой нутрянке работы ещё не копался, но внешний «API» вдохновляет, а общая архитектура приложения получается крайне гибкой и расширяемой, очень советую посмотреть
В целом понятно, это из разряда UniRx, без «ста грамм» не разобраться и придумать адекватное применение еще сложнее, надо разбираться и долго. Спасибо за наводку, посмотрим этот Entitas.
Почему бы не использовать DI контейнер для слабосвязанного кода? Например вот очень легковесный пример: github.com/intentor/adic
DI и IoC это паттерны, которые я считаю применять можно, но не шибко удобно в объектно-компонентном подходе, который я стараюсь использовать при разработке игр на Unity3D.
В чем заключаются неудобства?
Ну во-первых, у меня объекты игровой логики не инициализируются в конструкторах, практически никогда. Во вторых, иньекции в конструктор или в свойство так или иначе требуют ссылки на интерфейс, что в моем понятии может и менее жесткая связь, чем прямая ссылка на экземпляр класса, но и не мягкая связь тоже. Я предпочитаю, чтобы компоненты вообще ничего не знали, ни через интерфейсы ни каким либо другим способом друг о друге, т.е. абсолютная независимость. Может конечно я заблуждаюсь, но мой линчый опыт говорит об обратном. К сожалению, я больших проектов мобильных аля Clash of Clans не делал, поэтому не могу сказать как описанные в статье системы поведут себя там, равно как я не знаю применяется ли там DI, в том же смысле, что и система уведомлений.

В целом, если бы я например писал не игровую логику (реализация игрового процесса), а некую большую обособленную систему, которая использовалась бы в ней, и в этой системе нужно было бы использовать внешние какие-то вещи, тогда да, DI и сам принцип IoC имел бы смысл. В некотором смысле в тех проектах, в которых я участвовал, эти принципы применялись при подключении тех же платежных API для мобильный проектов.
К недостаткам описанного способа можно так же отнести и сложность отладки. Как правило во время дебага обработчика события стэк вызовов содержит слишком большое количество элементов. А если любите корутины то понять кто же выбросил эвент часто бывает невозможным.
Тут больше тогда проблема к Coroutine, которые в силу своей асинхронности всегда неудобно отлаживать. В случае синхронного вызова — не такая большая проблема со стеком, если учесть гибкость которую дают подобные системы.
Согласен, тем более, что описанные системы в своей сути синхронны до мозга костей, это надо было уточнить наверное. Если нужны ассинхронные сообщения, тогда надо использовать несколько другой подход.
Я лично считаю этот способ менее удобным, чем второй, поскольку требует лишних телодвижений.
Создание обертки над классом MonoBehaviour:


Он (метод), конечно удобнее, пока не появится необходимость в наследовании от другого класса. Впрочем за удобства почти всегда приходится расплачиваться гибкостью.
Другой класс может наследоваться от CustomBehaviour. Поскольку в игровой логике, компоненты так или иначе MonoBehaviour, ничто не мешает сделать
public class ChildClass: MyClass {}
public class MyClass: CustomBehaviour {}
Чем в проекте меньше подписок тем лучше(с). На самом деле, подобное решение крайне сложно поддается дебагу, в большой команде это вообще похоже на ад и целое кладбище костылей. Приведу пример. Встречал программиста, который злоупотреблял методами SendMessage(). Логика такова — «кому нужно тот получит». И вот он ушел с той команды. И… Как дебаг ерланга (что за мессадж, от кого пришел, кому должен, зачем должен и тд.) В итоге систему частично выпилили. Я бы порекомендовал использовать евентовую систему в виде классов. MVC одним словом. Тогда даже если есть подписка, всегда можно поймать откуда ушел класс, куда и зачем (например юзать поле Sender или что-то типа него).
На самом деле, при синхронном вызове, узнать откуда пришло событие достаточно просто. Поскольку описанные выше системы построены на вызове функций сохраненных в хеше через рефлексию, то фактически происходит синхронный вызов методов у других классов через прослойку скрывающую от нас реализацию этих классов. Точно также можно ставить точки остановки, получать стэк вызовов (а если пошаманить в Unity, можно получить полный стэк).

Самая большая проблема это ассинхронные сообщения, но это совершенно отдельная история.

SendMessage это вообще рудимент, использовать его для системы сообщений мягко сказать плохо. Единственный его плюс, в возможности вызвать метод у компонента, без получения экземпляра класса, причем любой.
А что насчет сигналов и комманд? Я вот последнее время использую фреймворк StrangeIoC для DI и плюс сигналы-комманды. Можно создавать цепочки команд, можно подписаться в любом месте на сигнал.

Интересно, но есть вопросы. А как это система относится к изменениям в цепочках команд? И что если в цепочке у нас может быть 8-10 событий? Или например в каких-то командах после тестов гейм-дизайнерами необходимо поменять передаваемые данные? Или нам надо протестировать в этой цепочки источник и конечного элемент, а у нас еще не готов код промежуточных команд?
А как это система относится к изменениям в цепочках команд?
В разделе Mapping command написано, что процесс биндинга команд к сигналам можно выполнять где и когда угодно. Если вы об этом.
И что если в цепочке у нас может быть 8-10 событий?
Событий или команд? Если команд, то системе все равно, какое кол-во. Так же, как и синхронная команда или асинхронная. Для этого нужно лишь правильно реализовать команду.
Или например в каких-то командах после тестов гейм-дизайнерами необходимо поменять передаваемые данные?
Первый способ — вызывать сигнал с параметрами, тогда эти параметры будут передаваться в команду. Второй — ничего не мешает в самой команде обратиться к сервису или модели. Например:
class StartGameCommand : EventCommand
	{
		[Inject]
		public ITimer gameTimer{get;set;}

		override public void Execute()
		{
			gameTimer.start();
		}
	}

Или нам надо протестировать в этой цепочки источник и конечного элемент, а у нас еще не готов код промежуточных команд?
Этим и крут данный подход, что можно дополнять цепочку команд по мере готовности, можно изменять порядок команд, заменять одну на другую, можно ставить заглушки для команд(как раз Ваш случай).

Если данная тема интересна кому-нибудь еще кроме Вас, то мог бы написать статью. Но на первых порах достаточно прочитать документацию.
Мне интересна, буду рад если напишите.
При включении поддержки .NET 4.5 и использовании C# 6.0 (что уже повсеместно в проектах), можно немного видоизменить код.
До:
if (StartEvent != null)
{
    StartEvent();
}

После:
StartEvent?.Invoke();
Да, это пришло с 2018 версию (2017 это еще было экспериментальным), а статья писалась когда еще Unity 5 был актуален
Само использование рефлексии крайне ресурсоёмко. Выполнение одноразово при инициализации экземпляра немногочисленного набора объектов — допустимо, но рефлексия для каждого подписчика при наступлении каждого события в игре — ужасная практика. Вы сами описали эти события: выстрел, попадание пули, отнимание здоровья и т.д. И это только примитивный набор функционала для тестовой игры. Даже в малом проекте в продакшене сотни связей и вызовов методов других классов, если всё это реализовывать на рефлексии — всё процессорное время будет тратиться на её обработку. (Извиняюсь за тавтологию) Рефелексия — bad practice и используется только в самых крайних случаях, а не тогда, когда Вам лень проверять Event на null. Многое из проделанного — излишне. В интернете множество графиков сравнений скорости работы различных методов вызова функций. Например:
habr.com/post/353780
Здесь ясно описана разница различных методов. Ваш подход уступает в производительности в 55-90 раз. Вы считаете это хорошим результатом?
В этой статье описаны методы, но не конечные реализации, прочитайте про способы ускорения вызова методов, полученных через рефлексию в обход MethodInvoke (который и является самым тормозным способом). Рефлексия bad practiсe, для тех кто не понимает когда, как и зачем.
А в приведенной статье, про сравнение сокростей, как раз таки рефлексия даже не используется.
Sign up to leave a comment.

Articles