Я знаю, что при создании приложений (собственных или веб-приложений), таких как приложения в Apple AppStore или магазине приложений Google Play, очень часто используется архитектура Model-View-Controller.
Однако разумно ли также создавать приложения, использующие архитектуру Component-Entity-System, общую для игровых движков?
design-patterns
architecture
mvc
game-development
applications
Эндрю Де Андраде
источник
источник
Ответы:
Для меня абсолютно. Я работаю в Visual FX и изучал широкий спектр систем в этой области, их архитектуры (включая CAD / CAM), жаждал SDK и любых работ, которые позволили бы мне понять плюсы и минусы кажущихся бесконечными архитектурных решений, которые могут быть сделаны, даже самые тонкие из них не всегда оказывают тонкое воздействие.
VFX очень похож на игры в том, что есть одна центральная концепция «сцены» с окнами просмотра, которые отображают визуализированные результаты. Также обычно происходит много цикличной обработки, постоянно вращающейся вокруг этой сцены в анимационных контекстах, где может происходить физика, порождающие частицы частицы, порождающие анимацию и рендеринг сетки, анимации движения и т. Д., И, в конечном счете, для их рендеринга. все пользователю в конце.
Другой концепцией, подобной, по крайней мере, очень сложным игровым движкам, была необходимость в аспекте «дизайнера», в котором дизайнеры могли бы гибко проектировать сцены, в том числе возможность самостоятельно создавать легковесные программы (скрипты и узлы).
За эти годы я обнаружил, что ECS идеально подходит. Конечно, это никогда не будет полностью отделено от субъективности, но я бы сказал, что это, по-видимому, создает меньше проблем. Это решило намного больше серьезных проблем, с которыми мы всегда боролись, и в то же время дало нам лишь несколько новых мелких проблем.
Традиционный ООП
Более традиционные ООП-подходы могут быть действительно эффективными, когда у вас есть твердое понимание требований к проектированию, но не требований к реализации. Будь то с помощью более плоского многоинтерфейсного подхода или более вложенного иерархического подхода ABC, он имеет тенденцию цементировать проект и усложнять его изменение, делая реализацию проще и безопаснее для изменения. Всегда существует потребность в нестабильности в любом продукте, который выходит за рамки одной версии, поэтому подходы ООП, как правило, отклоняют стабильность (трудность изменения и отсутствие причин для изменения) к уровню разработки и нестабильность (простота изменения и причины изменения) до уровня реализации.
Однако, несмотря на изменяющиеся требования конечных пользователей, как дизайн, так и реализация могут нуждаться в частых изменениях. Вы можете найти что-то странное, например, сильную потребность конечного пользователя в аналогичном существе, которое должно быть одновременно и растительным, и животным, полностью аннулируя всю концептуальную модель, которую вы создали. Обычные объектно-ориентированные подходы не защищают вас здесь, и иногда могут сделать такие неожиданные, принципиальные изменения еще сложнее. Когда речь идет об областях, очень критичных к производительности, причины изменений в проекте еще больше умножаются.
Объединение нескольких гранулярных интерфейсов для формирования соответствующего интерфейса объекта может очень помочь в стабилизации клиентского кода, но это не помогает в стабилизации подтипов, которые иногда могут привести к уменьшению количества клиентских зависимостей. Например, у вас может быть один интерфейс, который используется только частью вашей системы, но с тысячами различных подтипов, реализующих этот интерфейс. В этом случае поддержание сложных подтипов (сложных, потому что у них так много разнородных интерфейсных обязанностей), может стать кошмаром, а не кодом, использующим их через интерфейс. ООП, как правило, переносит сложность на уровень объекта, в то время как ECS передает ее на уровень клиента («системы»), и это может быть идеальным, когда существует очень мало систем, но целая куча соответствующих «объектов» («сущностей»).
Класс также владеет своими данными в частном порядке и, таким образом, может поддерживать инварианты самостоятельно. Тем не менее, существуют «грубые» инварианты, которые на самом деле все еще трудно поддерживать, когда объекты взаимодействуют друг с другом. Чтобы сложная система в целом находилась в допустимом состоянии, часто необходимо учитывать сложный граф объектов, даже если их индивидуальные инварианты должным образом поддерживаются. Традиционные подходы в стиле ООП могут помочь в поддержании гранулярных инвариантов, но на самом деле могут затруднить поддержание широких, грубых инвариантов, если объекты сосредоточены на крошечных гранях системы.
Вот где такие подходы или варианты ECS для построения лего-блоков могут быть очень полезны. Также, когда системы имеют более грубый дизайн, чем обычный объект, становится проще поддерживать такие грубые инварианты с высоты птичьего полета системы. Множество маленьких объектных взаимодействий превращаются в одну большую систему, фокусирующуюся на одной широкой задаче, а не на маленьких маленьких предметах, фокусирующихся на маленьких маленьких задачах с графом зависимостей, который будет покрывать километр бумаги.
И все же мне пришлось смотреть за пределы своей области, игровой индустрии, чтобы узнать об ECS, хотя я всегда был ориентирован на данные. Кроме того, как ни странно, я почти самостоятельно пробился к ECS, просто перебирая и пытаясь придумать лучший дизайн. Хотя я не прошел весь путь и упустил очень важную деталь, которая заключается в формализации «системной» части и сжатии компонентов вплоть до необработанных данных.
Я попытаюсь объяснить, как я остановился на ECS и как он решил все проблемы с предыдущими итерациями проекта. Я думаю, что это поможет подчеркнуть, почему ответом может быть очень сильное «да», что ECS потенциально применим далеко за пределами игровой индустрии.
Архитектура грубой силы 1980-х годов
Первая архитектура, над которой я работал в индустрии VFX, имела давнее наследие, которое прошло уже более десяти лет с тех пор, как я присоединился к компании. Это была грубая грубая C-кодировка (не на C, как я люблю C, но способ ее использования здесь был действительно грубым). Миниатюрный и слишком упрощенный фрагмент напоминал такие зависимости:
И это чрезвычайно упрощенная схема одного крошечного кусочка системы. Каждый из этих клиентов на диаграмме («Рендеринг», «Физика», «Движение») получит некоторый «универсальный» объект, через который они будут проверять поле типа, например:
Конечно, со значительно более уродливым и сложным кодом, чем этот. Часто из этих случаев переключения будут вызываться дополнительные функции, которые будут рекурсивно выполнять переключение снова и снова и снова. Эта схема и код может почти выглядеть ECS-лайт, но не было никакого сильного сущность-компонент различия ( « является этот объект камера?», А не „не этот объект обеспечит движение?“), И никакой формализации „системы“ ( просто куча вложенных функций, идущих повсюду и смешивающих обязанности). В этом случае, почти все было сложно, любая функция была потенциальной возможностью для катастрофы, ожидающей, чтобы случиться.
Наша процедура тестирования здесь часто должна была проверять такие вещи, как сетки, отдельно от других типов элементов, даже если идентичные вещи происходили с обоими, так как грубая сила характера кодирования здесь (часто сопровождаемого большим количеством копирования и вставки) часто выполнялась Весьма вероятно, что в противном случае та же самая логика может потерпеть неудачу от одного типа элемента к другому. Попытка расширить систему для обработки новых типов элементов была довольно безнадежной, даже несмотря на то, что существовала явно выраженная потребность конечного пользователя, поскольку это было слишком сложно, когда мы изо всех сил пытались справиться с существующими типами элементов.
Некоторые плюсы:
Некоторые минусы:
Архитектура СОМ 1990-х годов
Большая часть индустрии VFX использует этот стиль архитектуры из того, что я собрал, читая документы об их дизайнерских решениях и просматривая их комплекты для разработки программного обеспечения.
Это может быть не совсем COM на уровне ABI (некоторые из этих архитектур могут иметь плагины, написанные только с использованием одного и того же компилятора), но имеют много сходных характеристик с интерфейсными запросами к объектам, чтобы увидеть, какие интерфейсы поддерживают их компоненты.
При таком подходе аналогичная
transform
функция, описанная выше, стала напоминать эту форму:Это подход, на котором остановилась новая команда этой старой кодовой базы, чтобы в конечном итоге провести рефакторинг. И это было значительное улучшение по сравнению с оригиналом с точки зрения гибкости и удобства обслуживания, но были некоторые проблемы, о которых я расскажу в следующем разделе.
Некоторые плюсы:
Некоторые минусы:
IMotion
всегда будут иметь одинаковое состояние и одинаковую реализацию для всех функций. Чтобы смягчить это, мы начали бы централизовать базовые классы и вспомогательные функции по всей системе для вещей, которые, как правило, избыточно реализуются одинаковым образом для одного и того же интерфейса, и, возможно, с множественным наследованием, происходящим за укрытием, но это было довольно грязный под капотом, хотя в коде клиента все было просто.QueryInterface
функцию, почти всегда проявляющуюся как точка доступа от середины до верхней точки, а иногда даже точка доступа № 1. Чтобы смягчить это, мы бы сделали такие вещи, как рендеринг частей кеша кодовой базы списка объектов, о которых уже известно, что они поддерживаютIRenderable
, но это значительно увеличило сложность и затраты на обслуживание. Аналогично, это было сложнее измерить, но мы заметили некоторые определенные замедления по сравнению с кодированием в стиле C, которое мы делали раньше, когда каждый отдельный интерфейс требовал динамической диспетчеризации. Такие вещи, как неправильные прогнозы ветвей и барьеры оптимизации, трудно измерить за пределами небольшого аспекта кода, но пользователи просто замечали, что отзывчивость пользовательского интерфейса и подобные вещи ухудшаются, сравнивая предыдущие и более новые версии программного обеспечения рядом друг с другом. сторона для областей, где алгоритмическая сложность не изменилась, только константы.Прагматичный ответ: композиция
Одна из вещей, которую мы замечали раньше (или, по крайней мере, я так делал), вызывала проблемы, заключалась в том, что она
IMotion
могла быть реализована 100 различными классами, но с точно такой же реализацией и связанным состоянием. Кроме того, он будет использоваться только несколькими системами, такими как рендеринг, движение по ключевым кадрам и физика.Таким образом, в таком случае мы могли бы иметь отношение 3 к 1 между системами, использующими интерфейс к интерфейсу, и отношение 100 к 1 между подтипами, реализующими интерфейс к интерфейсу.
В этом случае сложность и обслуживание будут резко искажены для внедрения и обслуживания 100 подтипов вместо 3 клиентских систем, от которых зависит
IMotion
. Это переместило все наши трудности с обслуживанием на обслуживание этих 100 подтипов, а не на 3 места, использующих интерфейс. Обновление 3 мест в коде с небольшим количеством или без «косвенных эфферентных связей» (как в зависимости от него, но косвенно через интерфейс, а не в виде прямой зависимости), нет ничего сложного: обновление 100 мест подтипа с загрузкой «косвенных эфферентных связей» , довольно большое дело *.Поэтому мне пришлось приложить немало усилий, но я предложил, чтобы мы постарались стать немного более прагматичными и ослабить идею «чистого интерфейса». Для меня не имело смысла делать что-то наподобие
IMotion
абстрактного и не имеющего состояния, если мы не увидели преимущества для него, имеющего богатое разнообразие реализаций. В нашем случае,IMotion
если иметь богатое разнообразие реализаций, это на самом деле превратилось бы в настоящий кошмар обслуживания, поскольку мы не хотели разнообразия. Вместо этого мы пытались создать единую реализацию движения, которая действительно хороша против изменения требований клиента, и часто работали над идеей чистого интерфейса, пытаясь заставить каждого разработчикаIMotion
использовать одну и ту же реализацию и связанное состояние, чтобы мы не Т дублирующие цели.Таким образом, интерфейсы стали больше похожи на широкие,
Behaviors
связанные с сущностью.IMotion
просто стал быMotion
«компонентом» (я изменил способ, которым мы определили «компонент», с COM на тот, где ближе к обычному определению части, составляющей «завершенную» сущность).Вместо этого:
Мы развили это до чего-то большего:
Это является вопиющим нарушением принципа инверсии зависимости, чтобы начать переход от абстрактного к конкретному, но для меня такой уровень абстракции полезен, только если мы можем предвидеть подлинную потребность в каком-то будущем, вне разумных сомнений и не использование смешных сценариев «что если», полностью оторванных от пользовательского опыта (что, вероятно, в любом случае потребовало бы изменения дизайна), для такой гибкости.
Таким образом, мы начали развиваться до этого дизайна.
QueryInterface
стало больше похожеQueryBehavior
. Кроме того, стало казаться бессмысленным использовать наследование здесь. Вместо этого мы использовали композицию. Объекты превратились в набор компонентов, доступность которых можно запрашивать и внедрять во время выполнения.Некоторые плюсы:
Motion
реализации, например, и не распределены по сотне подтипов.Некоторые минусы:
Одним из явлений, которое произошло, было то, что, поскольку мы потеряли абстракцию этих поведенческих компонентов, у нас их стало больше. Например, вместо абстрактного
IRenderable
компонента мы бы прикрепили объект к конкретномуMesh
илиPointSprites
компоненту. Система рендеринга будет знать, как визуализироватьMesh
иPointSprites
компоненты, и будет находить объекты, которые предоставляют такие компоненты, и рисовать их. В других случаях у нас были разные визуализируемые объекты, подобные тому,SceneLabel
что мы обнаружили в ретроспективе, поэтомуSceneLabel
в этих случаях мы добавляли a к соответствующим объектам (возможно, в дополнение к aMesh
). Реализация системы рендеринга будет затем обновлена, чтобы знать, как рендерить сущности, которые ее предоставили, и это было довольно легко изменить.В этом случае объект, состоящий из компонентов, также может затем использоваться в качестве компонента для другого объекта. Мы построили бы все таким образом, подключив блоки lego.
ECS: системы и компоненты необработанных данных
Эта последняя система была настолько, насколько я сделал это самостоятельно, и мы все еще убивали ее с помощью COM. Мне казалось, что он хочет стать системой сущностей-компонентов, но я не был знаком с ней в то время. Я смотрел на примеры в стиле COM, которые насыщали мое поле, когда я должен был искать игровые движки ААА для архитектурного вдохновения. Я наконец начал делать это.
Чего мне не хватало, так это нескольких ключевых идей:
Я, наконец, покинул эту компанию и начал работать над ECS как инди (все еще работаю над этим, истощая мои сбережения), и это была самая простая система для управления на сегодняшний день.
Что я заметил в подходе ECS, так это в том, что он решил проблемы, с которыми я до сих пор боролся. Для меня самое главное, я чувствовал, что мы управляли «городами» здорового размера, а не маленькими деревушками со сложными взаимодействиями. Его было не так сложно поддерживать, как монолитный «мегаполис», слишком большой по численности населения, чтобы эффективно управлять им, но он не был таким хаотичным, как мир, заполненный крошечными деревушками, взаимодействующими друг с другом, где просто думать о торговых путях в между ними образовался кошмарный граф. ECS перевела всю сложность на громоздкие «системы», такие как система рендеринга, «город» здорового размера, а не «перенаселенный мегаполис».
Компоненты, превращающиеся в необработанные данные, сначала показались мне странными , поскольку они нарушают даже базовый принцип сокрытия информации ООП. Это было своего рода вызов одной из самых больших ценностей ООП, которой я дорожил, а именно ее способности поддерживать инварианты, которые требовали инкапсуляции и сокрытия информации. Но это стало не беспокоить, поскольку быстро стало очевидно, что происходит с дюжиной или около того широких систем, преобразующих эти данные, вместо того, чтобы такая логика была распределена по сотням и тысячам подтипов, реализующих комбинацию интерфейсов. Я склонен думать об этом как в стиле ООП, за исключением случаев, когда системы предоставляют функциональность и реализацию для доступа к данным, компоненты предоставляют данные, а объекты предоставляют компоненты.
Стало еще проще , нелогично, рассуждать о побочных эффектах, вызванных системой, когда было всего несколько громоздких систем, преобразующих данные в широкие проходы. Система стала намного более «плоской», мои стеки вызовов стали меньше, чем когда-либо прежде, для каждого потока. Я мог думать о системе на этом уровне наблюдателя и не наталкиваться на странные сюрпризы.
Точно так же он упростил даже критичные для производительности области в отношении устранения этих запросов. Поскольку идея «Системы» стала очень формализованной, система могла подписаться на интересующие ее компоненты и просто получить кэшированный список объектов, которые удовлетворяют этим критериям. Каждому человеку не нужно было управлять этой оптимизацией кэширования, она стала централизованной в одном месте.
Некоторые плюсы:
Некоторые минусы:
Так или иначе, я бы сказал абсолютно «да», с моим личным примером VFX, являющимся сильным кандидатом. Но это все еще довольно похоже на потребности игр.
Я не практиковал это в более отдаленных районах, полностью оторванных от проблем игровых движков (VFX очень похож), но мне кажется, что гораздо больше областей являются хорошими кандидатами для подхода ECS. Может быть, даже система с графическим интерфейсом подойдет для одного, но я все еще использую более ООП подход (но без глубокого наследования в отличие от Qt, например).
Это широко неисследованная территория, но мне кажется подходящим всякий раз, когда ваши сущности могут состоять из богатой комбинации «черт» (и точно, какую комбинацию черт они предоставляют, когда-либо подверженных изменениям), и где у вас есть несколько обобщенных системы, которые обрабатывают объекты, которые имеют необходимые черты.
В таких случаях это становится очень практичной альтернативой любому сценарию, где у вас может возникнуть соблазн использовать что-то вроде множественного наследования или эмуляции концепции (например, миксины) только для создания сотен или более комбинаций в иерархии глубокого наследования или сотен комбинаций. классов в плоской иерархии, реализующих определенную комбинацию интерфейсов, но там, где число ваших систем немного (десятки, например).
В этих случаях сложность кодовой базы начинает ощущаться более пропорциональной количеству систем, а не количеству комбинаций типов, поскольку каждый тип теперь является просто сущностью, составляющей компоненты, которые являются не более чем необработанными данными. Системы с графическим интерфейсом естественным образом соответствуют этим видам спецификаций, где они могут иметь сотни возможных типов виджетов, комбинированных с другими базовыми типами или интерфейсами, но только несколько систем для их обработки (система макетов, система рендеринга и т. Д.). Если бы система с графическим интерфейсом использовала ECS, вероятно, было бы намного проще рассуждать о правильности системы, когда все функции обеспечивались горсткой этих систем вместо сотен различных типов объектов с унаследованными интерфейсами или базовыми классами. Если бы система с графическим интерфейсом использовала ECS, виджеты не имели бы функциональности, только данные. Только несколько систем, которые обрабатывают объекты виджетов, будут иметь функциональность. То, как будут обрабатываться переопределенные события для виджета, мне неизвестно, но, основываясь на моем ограниченном опыте, я не нашел случая, когда этот тип логики не мог бы передаваться централизованно в данную систему таким образом, чтобы в Оглядываясь назад, мы получили гораздо более элегантное решение, которое я когда-либо ожидал.
Я хотел бы видеть это занятым в большем количестве областей, поскольку это было спасателем в моей. Конечно, он не подходит, если ваш дизайн не ломается таким образом, от сущностей, объединяющих компоненты, до грубых систем, которые обрабатывают эти компоненты, но если они естественным образом соответствуют такой модели, это самая замечательная вещь, с которой я когда-либо сталкивался ,
источник
Архитектура Component-Entity-System для игровых движков работает для игр из-за природы игрового программного обеспечения, его уникальных характеристик и требований к качеству. Например, сущности предоставляют единообразные средства адресации и работы с вещами в игре, которые могут кардинально отличаться по своему назначению и использованию, но должны быть единообразно воспроизведены, обновлены или сериализованы / десериализованы системой. Включая модель компонентов в эту архитектуру, вы позволяете им сохранять простую структуру ядра, добавляя при этом дополнительные функции и возможности с низким уровнем связывания кода. Существует ряд различных программных систем, которые могут извлечь выгоду из характеристик этого проекта, таких как приложения САПР, A / V-кодеки,
TL; DR - Шаблоны проектирования работают хорошо только тогда, когда проблемная область в достаточной степени соответствует функциям и недостаткам, которые они накладывают на дизайн.
источник
Если проблемная область хорошо подходит для этого, конечно.
Моя текущая работа включает в себя приложение, которое должно поддерживать различные возможности в зависимости от множества факторов времени выполнения. Использование компонентов на основе компонентов для разделения всех этих возможностей и обеспечения возможности расширения и тестирования в изоляции было для нас идиллическим.
редактировать: моя работа заключается в обеспечении подключения к проприетарному оборудованию (в C #). В зависимости от того, какой форм-фактор у оборудования, какая прошивка установлена на нем, какой уровень обслуживания приобрел клиент и т. Д., И т. Д., Мы должны обеспечить различные уровни функциональности устройства. Даже некоторые функции, имеющие одинаковый интерфейс, имеют разные реализации в зависимости от версии устройства.
Предыдущие кодовые базы здесь имели очень широкие интерфейсы, многие из которых не реализованы. У некоторых было много тонких интерфейсов, которые затем были статически скомпонованы в одном классном классе. Некоторые просто использовали строковые -> строковые словари для моделирования. (у нас есть много отделов, которые думают, что могут сделать это лучше)
Все они имеют свои недостатки. Широкие интерфейсы - это боль и половина, чтобы эффективно макетировать / тестировать. Добавление новых функций означает изменение открытого интерфейса (и всех существующих реализаций). Многие тонкие интерфейсы привели к очень уродливому потреблению кода, но, поскольку мы в конечном итоге обошли большой жирный объект, тестирование все еще страдало. Плюс тонкие интерфейсы плохо справлялись со своими зависимостями. Строковые словари имеют обычные проблемы с синтаксическим анализом и существованием, а также с дырами в производительности, удобочитаемости и удобстве обслуживания.
Теперь мы используем очень тонкий объект, компоненты которого обнаружены и составлены на основе данных времени выполнения. Зависимости выполняются декларативно и автоматически разрешаются структурой ядра компонента. Сами компоненты можно тестировать изолированно, так как они работают напрямую со своими зависимостями, а проблемы с отсутствующими зависимостями обнаруживаются раньше - и в одном месте, а не при первом использовании зависимости. Новые (или тестовые) компоненты могут быть добавлены, и на них не повлияет ни один существующий код. Потребители запрашивают у сущности интерфейс с компонентом, поэтому мы можем свободно относиться к различным реализациям (и как реализации сопоставляются с данными времени выполнения) с относительной свободой.
Для такой ситуации, когда состав объекта и его интерфейсы могут включать в себя некоторое (сильно различающееся) подмножество общих компонентов, это работает очень хорошо.
источник