Pull to refresh

Comments 32

Статья написана добротно, читать интересно, примеры понятные. Вы правы, метод очень гибкий и выглядит всё красиво, но есть пара замечаний. Меня сразу смутило огромное количество выделений памяти. Если пойти по порядку, то:

AddComponent не только медленней, чем GetComponent, но и выделяет память на новый объект в куче. Плюс инициализация со стороны Unity (всё это собственно и замедляет выполнение метода). То есть вы решили одну проблему, но забыли о другой, которая в данном контексте более весома.

Передача аргументов в массиве — ещё один объект в куче. Для примера с Vector3 это такой "продвинутый боксинг", когда мы структуру целенаправленно и сознательно превращаем в управляемый объект не получая ничего полезного взамен.

Подписка на колбэк — там аж два новых объекта (на колбэк и на создание нового делегата при передаче метода в качестве параметра)

Если предположить, что подобных команд будет достаточно много (как для примера с прыжком), то оверхед от GC Alloc и его последующие "икания", потраченные на чистку кучи от мусора сведут на нет все преимущества подобного подхода. Если честно, я читал статью с надеждой что под конец будет освещён и этот вопрос, что будут убраны AddComponent и много этих new, но увы.

Что ещё немного вызвало сомнения — передача массива параметров без статической типизации с привязкой к индексам параметров. Мне кажется для этих случаев подошёл бы дженерик метод и пара вспомогательных tuple структур (то есть второй дженерик параметр у метода с описанием входной структуры).

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

Как я уже отметил выше, статья написана живо и интересно, и по крайней мере даёт пищу для размышлений. Так что подобные статьи на описанные вами темы придутся вполне кстати (особенно интересует ваш MVC).
Спасибо за комментарий, приятно слышать.
По поводу AddComponent я действительно забыл, и я пока не думал как решить это. Пока это не вызывало проблем с производительностью, хотя проекты, с которыми я сейчас работаю, целиком построены на использовании команд.
Типизированные аргументы — хороший вариант для рассмотрения, хотя если мне надо передавать больше 2-х параметров, то я просто пишу кастомную структуру привязанную к команде и мне не приходиться запоминать порядок.
И по-поводу callback-ов, я тоже могу только сказать, что это значительно удобнее чем event-ы а памяти на них выделяется не так уж и много. Чаще из-за текстур и моделей по памяти не помещаются и в таких случаях оптимизация callbeck-ов не поможет.
А что за проекты позволите поинтересоваться? Интересен контекст, в котором используется подобное решение.

хотя если мне надо передавать больше 2-х параметров, то я просто пишу кастомную структуру привязанную к команде и мне не приходиться запоминать порядок

Это именно то, о чём я писал про пару вспомогательных tuple структур

памяти на них выделяется не так уж и много

зачастую напрягает не столько объем выделяемой памяти, сколько сам факт её выделения (GC Pressure)
А что за проекты позволите поинтересоваться? Интересен контекст, в котором используется подобное решение.

Сейчас я занимаюсь клоном Crossy Road, перед этим были тоже ранэр и разукраска.
Это именно то, о чём я писал про пару вспомогательных tuple структур

Да, но я не делаю для этого дженерики.
зачастую напрягает не столько объем выделяемой памяти, сколько сам факт её выделения (GC Pressure)

Заметно будет только в очень исключительных ситуациях. Мобильные игры не на столько большие, чтобы одновременно исполнялось больше 100-а команд, и запускаться в таких количествах они тоже вряд ли будут.
Почему бы не использовать ключевое слово params для передачи аргументов? Намного удобней выглядит и код не пострадает
Это просто дело привычки, ничего больше
Передача параметров через params — это просто сахар, внутри каждый раз создается массив объектов, куда пихаются параметры и обратная распаковка внутри метода. Как результат — гарантированный GC allocation.
Странное ощущение… Как будто вернулся во времена, когда не было ООП и методы работали над данными без жесткой связи...
Вы правы, здесь чувствуется функциональный стиль.
А зачем команду наследовать от MonoBehaviour? Если команду сделать обычным классом и использовать конструктор, то все проблемы с аргументами отпадут, да и производительность вырастет (монобех очень тяжелый). Вроде этот подход называется композиция и самая приятная на мой взгляд реализация у Entitas — https://github.com/sschmid/Entitas-CSharp 
Конечно команду можно сделать и не MonoBehaviour, но удобнее держать команду на объекте из-за одтладки (в любой момент видно что исполняется на контролере). Плюс для отслеживания коллизий или триггеров команда на объекте не будет нуждаться в дополнительных event-ах от контроллера.
Кстати по поводу отладки, Wooga (в Entitas) сделали достаточно хорошую работу — их non-monobeh компоненты можно смотреть в редакторе почти как в обычном инспекторе (какие компоненты висят на каки объектах), плюс там свой дебаговый график для проверки производительности и пиков нагрузки. Что ещё более интересно — в их подходе решена проблема выделений памяти и скорости работы AddComponent — их компоненты (non-monobeh) берутся из пула. То есть два одинаковых прыжка на одном объекте в разное время будут использовать один компонент без необходимости каждый раз создавать новый объект. В принципе там как раз решается как раз описанная в статье проблема.
Но в таком случае, если нужно будет чтобы несколько готовых юнитов, которые по разному прыгали, стали прыгать одинаково, то вам придется или чуть подправить иерархию классов, или просто скопипастить подходящий кусок кода во все нужные классы (что есть ужасающе).
А что за странный кейс вообще? Просто по статье остается ощущение, что вы придумали проблему и решение, но проблема — выдуманная.
Суть статьи — показать подход, который позволяет динамически изменять поведение при этом остается легким для понимания и изменения. Особенно хорошо ощущается легкость изменения при работе над проектом в команде — для внесения нового функционала достаточно будет написать команду и не перелопачивать класс вникая в зависимости оставленные там кем-либо. Это касается чего-угодно в Вашей игре, будь то юнит (каст заклинания), окружение (изменение погоды) либо сам игровой процесс (показать обучение).
Это очень спорное решение. Увеличение точек входа в действие конечно упрощает его настройку на каждый чих, зато анализ и исправление — усложняет.
Вы сейчас говорите о том что команды могут вызываться из многих мест или я не понял про увеличение точек входа? В таком случае можно и публичный метод юнита вызывать из разных мест, но обычно это делает менеджер сцены, например.
Был простой прыжок в юните. Вы добавляете Actor -> Теперь есть прыжок и в юните, и в Actor-е, можно писать код и там и там.
Извените, но вы не поняли принцып. Таким же аргументом было бы писать код прыжка в менеджере сцены. Actor-ы и Command-ы — это способ инкапсулировать от юнита код прыжка. Тоесть в самом юните кода прыжка быть не должно. В случае с Actor-ами менеджер вызывает метод на юните, а юнит вызывает метод в Actor-е и он уже проделывает всю работу. В случае с Command менеджер цеплчет команду на юнита и она все делает. В любом случае сам юнит не содержит в себе кода прыжка и не знает как он прыгнет. И это дает легкий способ заставить гнома порхать как бабочка не меняя код в самом юните а просто прицепив на него подшодящую компоненту или запустив правильную команду.
При использовании SendMessage возникает сильная привязка к названию метода и при этом пропадает легкий способ отследить что и откуда вызывается. К примеру Вы случайно опечатались при названии метода, эта ошибка перешла в Ваши SendMessage, и если кто-нибудь исправит ошибку в названии метода, то сломает логику программы и долго никто не будет понимать почему.
сломает логику программы и долго никто не будет понимать почему.

Это не так, гарантированной доставкой можно управлять путем указания требования на наличие получателя последним параметром SendMessage.
Помимо вышесказанного SendMessage медленный.
Зависит от способа использования. Если нужно вызывать больше пары раз за фрейм — да, можно подумать о чем-то другом, иначе не сильно критично.
Он сильно тормозит если ты только что создал объект и потом делаешь SendMessage. Так же он может не правильно работать если объект был уничтожен через Destroy() (надо через DestroyImmediate()).

Возмжно сча это пофиксили, на 4-ой юнити было так.
Он как раз не тормозит, а делает отложенный вызов в следующем апдейте, как и коротина, например. Еще никто не сказал об основном плюсе SendMessage — отсутствие жесткой связки с инстансами компонентов, что обеспечивает безболезненное их удаление и отсутствие утечек памяти если некорректно отпишешься (забудешь отписаться)
Пока читал статью, возникло 3 замечания:
1) Уже сказали выше, что AddComponent тормозной. Будет у вас сотня юнитов прыгать, лазить, плавать и прочие команды выполнять — будут тормоза. На медленных девайсах точно. Хотя для каких-нибудь паззлов/хогов/квестов будет нормально.
2) Про массив параметров — тоже уже сказали, тоже сомнительный велосипед.
3) Start выполняется с некоторой задержкой (Awake мгновенно). Поэтому может быть некоторая инертность при выполнении.
А вообще поход интересный, хоть и не понятно чем он лучше оверрайда методов.
Композиция в любом виде предпочтительней оверрайда методов, так как не привязывает функционал к определённой иерархии классов.
Awake мгновенно

Это не так для компонентов, висящих на задизейбленных GameObject-ах, например, если они содержаться в инстанцирующемся префабе и GO были помечены как неактивные.
После того как енаблишь их вызовется, по моему так. А Start несколько после (может даже на следующий кадр).
Я про то, что не мгновенно после создания и если есть зависимости других компонентов от данных, которые должны быть проиничены в компонентах на задизейбленном ГО — будет фейл, об этом нужно помнить. Из-за этого пришлось городить костыль ленивой инициализации здесь.
Sign up to leave a comment.

Articles