Создание традиционных графических интерфейсов пользователя с помощью системы компонентов сущности

Подходит ли Entity Component System для построения традиционных графических интерфейсов?

Оказывается, да! Хотя ECS чаще всего используется при создании игр, его также можно использовать для создания традиционного веб-приложения в стиле «формы», такого как TodoMVC. Однако вам нужно будет радикально переосмыслить организацию моделей, их данных и поведения.

Это, возможно, освежающий, умопомрачительный урок программирования с графическим интерфейсом пользователя! 🤯😉

Живая демка.

ECS - Архитектурный шаблон системы компонентов сущности

Архитектура ECS (Entity Component System) - новая популярность в кругах разработчиков игр. Это способ создания архитектуры вашего программного обеспечения, который отвергает модели как классы в пользу более деконструированного подхода, основанного на данных. Результатом, по крайней мере, в игровых приложениях, является значительное снижение сложности и значительное повышение производительности и удобства обслуживания.

Когда я прочитал о ECS, я задумался - почему бы не использовать его для более традиционного программного обеспечения, а не только для игр?

Вы можете прочитать о Entity Systems в этой статье в Википедии. Основная идея состоит в том, чтобы взять объект (который содержит личность, данные и поведение) и деконструировать его.

  • Сущность = похожа на модель, но не имеет данных и поведения 🤯 Сущности легкие и тупые - просто идентификатор или имя
  • Компонент = данные
  • Система = поведение

Пример - традиционный класс против ECS

Традиционная модель класса Todo элемента будет выглядеть так:

который будет генерировать:

$ node example.js 
Todo item make lunch has id 1 and is completed
Todo item wash dishes has id 2 and is not completed

тогда как подход ECS будет выглядеть так:

Давайте разберем это:

  1. Создание сущности - это всего лишь engine.entity()
  2. Дайте объекту несколько полей данных с entity.setComponent('data', {title, completed, id}). Имя этого компонента оказалось 'data', потому что я выбрал это имя, но я мог бы назвать его примерно как 'todo-data'
  3. Добавьте некоторое поведение с engine.system - код внутри будет запускаться для каждой сущности, имеющей компонент 'data'.

Обратите внимание, что явного цикла нет. Система зацикливается на нас. Все сущности, у которых есть компонент «данные», будут пропущены. Вы можете добавить в список дополнительные компоненты, что будет означать, что система будет перебирать все компоненты, у которых есть все эти компоненты (селектор and).

У вас может быть несколько систем, они будут работать в заявленном порядке. Каждый раз при запуске engine.tick() будут запущены все системы.

Обратите внимание, что нет необходимости хранить главный список из todos - все сущности хранятся в ECS за нас. Если бы мы действительно хотели создать список задач, это было бы немного сложно. См. Следующий раздел Сбор результатов при зацикливании.

Свобода присоединять любой Компонент к любой Сущности

Теперь у вас есть возможность прикрепить любой компонент к любому объекту. Но зачем беспокоиться?

Вы могли бы подумать, что такой Компонент чистых данных, как Position {x:0, y:0}, всегда тесно связан с определенной Сущностью - так зачем же их разделять? Например, Ball имеет позицию, а Person - нет! Вместо этого Person сущности наверняка нужны Name {firstname:'John', surname:'Smith'} и т. Д.

Что ж, оказывается, что часто бывает наоборот - такие сущности, как Ball, Bullet, Car все, нуждаются в Position. А в более бизнес-ориентированных приложениях Person, Employee, Manager всем нужен Name.

Если подумать дальше в этом направлении, еще одним полезным компонентом может быть Address - он понадобится многим Сущностям. Таким образом, наличие единственного объявления Name и Address и т. Д., Которое можно многократно использовать повторно, позволяет избежать дублирования объявлений и связанных с ними ошибок.

А как насчет использования наследования?

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

С языками, поддерживающими множественное наследование (например, Python), вам повезет больше - на самом деле множественное наследование ближе всего к соответствию идее ECS, где вы можете произвольно составить объект модели из любого количества аспектов данных компонента.

А как насчет использования композиции?

Вы также можете решить проблему с помощью композиции - класс может ссылаться на общий экземпляр Address и любые другие общие классы, которые ему нужны. Это также позволит достичь цели объявления Address только один раз и обеспечения возможности комбинирования моделей «смешивать и согласовывать», которые есть в ECS.

Решение ECS для компоновки

Вот как мы произвольно прикрепляем Компоненты данных к объектам в ECS:

Мы создали объект person 1 и прикрепили как Name, так и Address Компоненты. Бег

приводит к:

person 1 {"firstname":"John","surname":"Smith"} {"number":12,"street":"Bounty Drive","state":"WA"}

Вы можете прикреплять даже Компоненты, у которых нет фактических данных. Иногда этот метод полезен для отметки объектов, например. флаг dirty Component для целей рендеринга или флаг delete Component и т. д., чтобы системный селектор соответствовал этому условию (наличия этого флага) и запускал эту конкретную систему. Этот метод более подробно обсуждается в разделе Компоненты как флаги ниже.

Вот пример добавления различных компонентов «Флаг» к сущности - конечно, вы бы назвали компоненты лучше, например в TodoMVC-ECS editingmode такой Компонент используется как флаг. Наличие флага Component на объекте запускает конкретную систему. Обратите внимание, что в этом примере данные компонента являются либо пустым объектом {}, либо null. Обычно (хотя ваша ситуация может быть иной) важно наличие компонента, прикрепленного к сущности, а не данные компонента.

Эта свобода отделения данных от сущностей очень важна, особенно когда мы делаем следующий и последний шаг в понимании ECS - Systems.

Системы

В системах происходит большая часть поведения приложения, включая рендеринг / обновление DOM.

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

  • Приведенная выше диаграмма была сгенерирована полуавтоматически из исходного кода Javascript, находящегося на GitHub, с использованием GitUML.
  • Щелкните здесь, чтобы увидеть более подробную информацию о диаграмме в виде .svg и возможность масштабирования.
  • Посмотрите эту актуальную диаграмму 170 на GitUML.

Выбор

Системы - это блоки кода, которые проходят через подмножества объектов, которые имеют все компоненты, перечисленные в объявлении каждой системы, например ['data', 'dirty'] означает, что будут обработаны все сущности, которые обладают обоими этими компонентами.

Вот пример двух систем, которые запускаются одна за другой:

Мне нравится думать о списке компонентов, объявленных в каждой системе, как о селекторе CSS, за исключением того, что он применяется к объектам ECS. Система будет перебирать все сущности, обладающие всеми перечисленными Компонентами (концепция and).

Трубопроводный подход

Системные блоки запускаются в объявленном порядке, один за другим, каждый раз, когда вызывается engine.tick(). Каждая Система будет повторяться по мере необходимости, затем выйдет, и будет запущена следующая Система.

Системы образуют конвейер многоступенчатой ​​обработки. Система часто основывается на работе предыдущей системы - в этом сильная сторона архитектуры ECS, способность разбивать работу на простые для понимания этапы.

См. Системы в этом приложении Todo ниже, чтобы увидеть таблицу Системы, которые я придумал для реализации TodoMVC, и то, что они делают. Удивительно обнаружить, что таким образом можно создать приложение.

Зацикливание

Как вы, наверное, заметили, зацикливание занимает центральное место в ECS. Думаю, это связано с игровым наследием, где нужно обрабатывать множество игровых объектов.

Если у вас есть явный цикл for в вашем коде, вы, вероятно, ошибаетесь в ECS! Вместо этого используйте Систему.

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

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

Подробнее об этой технике читайте ниже, в разделе Уловка с компонентами домашнего хозяйства.

Галочка

Когда в приложении происходят какие-либо изменения, вам нужно снова запустить engine.tick(), чтобы повторно отобразить графический интерфейс. Функция галочки просто повторно запускает все системы снова.

Не забудьте загрузить приложение с помощью единственного вызова engine.tick() в нижней части файла javascript приложения.

Системы ECS на основе игр обычно запускаются tick в цикле со скоростью 60 кадров в секунду, что не подходит для нашего графического интерфейса на основе форм, но обязательно включите функцию автоматического цикла в вашей ECS, если вы чувствуете, что о стратегическом вызове тика слишком много думать!

Вы также можете использовать удобные события 'tick:after' и 'tick:after' для запуска кода до и после каждого тика, запускающего все системы.

ECS - дополнительные полезные приемы

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

Сбор результатов во время зацикливания

Создать список задач довольно просто - просто объявите Систему, которая соответствует Компоненту, который есть у всех сущностей задач, и поместите каждую сущность в список.

Таким образом вы можете собрать все, что захотите, например

  • протолкните весь компонент данных с помощью push(data)
  • вставьте только заголовок задачи с push(data.title)
  • подтолкнуть сам объект с помощью push(entity)
  • и т.п.

Единственная проблема заключается в том, что в следующий раз, когда вы отметите (), todos не будет очищен, и ваш список будет удваиваться, утроиться, учетверять и т. Д. Самый простой способ решить эту проблему - использовать событие 'tick:before' и очищать todos перед каждым запуском тика:

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

Расширенный сброс переменной

Если вы хотите сбросить некоторые переменные или выполнить некоторую обработку в середине конвейера, например.

System1
System2
        <-- reset a variable here
System3

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

Чтобы создать Систему, кодовый блок которой запускается один раз, используйте Уловку с компонентами обслуживания, описанную ниже, которая определяет специальную пару Компонент и Сущность, а затем создайте для них сопоставление Системы.

Уловка компонента домашнего хозяйства

Если вам нужно инициализировать переменные в середине запуска системы (помните, что порядок, в котором работают системы, критичен, одна система часто передает результаты в другую систему), то сброс в начале или начале тика не сработает. Нам нужно иметь возможность определить Систему, которая выполняет задачи сброса или обслуживания, которую мы можем разместить в любом месте нашего списка Систем. И мы не хотим, чтобы эта Система зацикливалась, мы хотим, чтобы она запускалась один раз. Это означает, что он должен соответствовать только одному объекту.

Решение состоит в том, чтобы определить специальную пару Компонент и Сущность.

затем, когда вам нужно запустить какой-либо код, вставьте такую ​​систему:

Имя объекта 'single-step' произвольно и может быть любым. Имя Компонента 'housekeeping' также произвольно, но упоминается в Системе, как вы можете видеть в приведенном выше примере.

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

Полный код для сбора списка задач

Таким образом, полный код для сбора списка задач с использованием описанной выше техники системы домашнего хозяйства выглядит следующим образом:

Компоненты как флаги

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

Например, моя 'think-todoitem' Система решает, нужно ли создавать или обновлять объекту задачи GUI DOM <li>, добавляя компонент 'insert' или 'update' к каждой сущности. Обратите внимание, что к компонентам «вставить» и «обновить» прикреплены {} пустые данные, поэтому компонент действует как флаг.

Я широко использую этот трюк в этой реализации Todo.

Когда я спросил эксперта ECS об этом подходе, он сказал:

Присоединение и отсоединение компонентов для активации системы - лучший способ. Фактически, это одно из самых больших преимуществ использования ECS, поскольку, таким образом, вы меняете поведение, используя преимущества композиции.

Инкапсуляция систем в классы

Тот факт, что мы деконструируем классы в соответствии с подходом ECS, не означает, что мы не можем использовать классы, чтобы помочь нам. Системы по-прежнему работают, когда созданы внутри классов - они зарегистрированы в движке и будут работать нормально.

Возможно, вы захотите использовать класс для группировки переменных, которые являются частными для определенных систем вместе, чтобы они не загрязняли внешние пространства имен, например. this.todos_data в примере ниже. Классы также являются удобными местами для определения функций, которые также являются частными для определенных систем, например. метод save() в примере ниже.

Возможно, такое использование систем внутри классов лучше и инкапсулировано.

Единственное, на что следует обратить внимание, это то, что вам необходимо создать экземпляры классов в правильном порядке, чтобы их системы создавались (через их конструкторы) в соответствующей точке «конвейера» систем.

Другие мысли о ECS

События не нужны

Хотя есть события GUI из DOM, обратите внимание, что в этой реализации нет «внутренних» событий. Это очень реальное преимущество, так как за потоком событий может быть сложно следить.

Однако эффективность подхода ECS не так хороша, как подход, основанный на событиях, потому что мы повторно визуализируем больше, чем нам нужно. Внутренние события дают нам более точный контроль над тем, что необходимо обновить в графическом интерфейсе. См. Мою реализацию TodoMVC-OO, чтобы увидеть, насколько эффективно могут работать уведомления о событиях от модели к контроллерам.

Оптимизация обновлений графического интерфейса

Добавление грязного флага к сущностям, которые нуждаются в обновлении, может исправить эту неэффективность повторного рендеринга грубой силы. Я бы порекомендовал компонент под названием 'dirty', который можно прикреплять к объектам. Затем уточните свои системы, чтобы они соответствовали только грязным данным задачи, например.

Обратите внимание на то, что последняя строка этой системы удаляет флаг / компонент загрязнения. Когда вы изменяете данные объекта, вы должны добавить компонент 'dirty', например.

Системы в этом приложении Todo

В системах живет большая часть поведения. Идея состоит в том, что каждая система представляет собой цикл, который «запрашивает» нашу небольшую базу данных сущностей и компонентов и что-то делает с соответствующими сущностями и компонентами. Я придумал следующие системы, которые работают в следующем порядке, сверху вниз, конвейер:

См. Проект TodoMVC-ECS GitHub для текстовой версии приведенной выше таблицы.

Это много систем! Но каждая Система работает за другой, и каждую легко рассуждать. В этом преимущество ECS - я полагаю, он более «плоский».

Помните, что системы будут работать только тогда, когда вы вызовете engine.tick(), поэтому не забудьте это сделать. Если вы хотите, чтобы все системы снова запустились, просто снова вызовите engine.tick().

Выбор фреймворка ECS

Фреймворки Entity Component System на самом деле относительно просты. Они предлагают способы:

  1. определяя Entities,
  2. добавление Components (объектов данных) к этим экземплярам сущностей,
  3. определение Systems - это блоки кода, которые проходят через подмножества совпадающих Компонентов.
  4. tick функция

Для этого проекта я выбрал библиотеку javascript Jecs.

  • Приведенная выше диаграмма была сгенерирована полуавтоматически из исходного кода Javascript, находящегося на GitHub, с использованием GitUML.
  • Щелкните здесь, чтобы увидеть более подробную информацию о диаграмме в виде .svg и возможность масштабирования.
  • Посмотрите эту актуальную диаграмму 168 на GitUML.

Один файл jecs.js можно скопировать в ваш проект, и с обычным <script src="jecs.js"></script> все готово. Или вы можете npm install jecs и потребовать его в своих проектах узлов.

Для своих проектов Python ECS я использую Esper, который представляет собой легкую систему сущностей для Python с упором на производительность.

Заключение

Такой подход к подключению графического интерфейса был очень интересным. Мне нравится подход ECS, и я буду искать способы использования разделенных систем в своих будущих проектах.

Ресурсы

Демо

  • Запуск демо здесь полностью реализует спецификацию TodoMVC.
  • Изучите окончательный код в этом репозитории, а именно app.js.

ECS

  • Проект TodoMVC-ECS GitHub для этой статьи.
  • В этом проекте использована библиотека ECS Jecs. Есть и другие, такие как GGEntities и т. Д.
  • GUI Showdown ECS - еще один пример приложения, реализованного с использованием архитектуры ECS (Javascript, открытый код). Также см. GUI Showdown ECS in Python.

Связанные с TodoMVC

  • TodoMVC-OO GitHub Repo - еще одна моя реализация TodoMVC. Классическое приложение TodoMVC на JavaScript, реализованное без фреймворка с использованием простого объектно-ориентированного программирования.
  • Официальный проект TodoMVC с другими реализациями TodoMVC (например, Vue, Angular, React и т. Д.)

Диаграмма

  • Схема GitUML, использованная в этом проекте
  • Схема Literate Code Mapping, использованная в этом проекте.

Кредит

Создано Энди Булка

Примечание: этот проект не является официально частью проекта TodoMVC - поскольку он соответствует критерию наличия сообщества вокруг него.

Другие ссылки:

  • GitUML - мой самый крупный на сегодняшний день проект Javascript (и Python) со сложным графическим интерфейсом, который я реорганизую, используя методы, описанные в этой статье. GitUML позволяет визуализировать исходный код Github как UML с помощью указателя и щелчка мышью, а также автоматически обновлять диаграммы UML при регистрации кода.
  • Подробнее обо мне, мои шаблоны проектирования MVC и другие статьи.