Инструменты самоанализа для лучшей отладки и адаптации

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

Все эти части просты, независимы и могут быть составлены, как кубики LEGO®. Это здорово с точки зрения гибкости, но также пугает, особенно если вы новичок в проекте. Чем больше вы увидите несвязанных частей головоломки, тем больше времени у вас уйдет на объединение их в более крупную картину.

Но не бойтесь, как написал Фред Брукс в книге Мифический человеко-месяц:

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

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

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

Также по моему опыту, количество компонентов меньше, чем количество систем. И всегда полезно начинать с малого.

Как составляются компоненты?

Сильной стороной ECS является отсутствие ограничений на то, какие компоненты могут быть связаны с объектом. Этот факт побуждает нас разбивать компоненты на действительно небольшие типы значений и делает внедрение новых функций приятным занятием. Однако это также означает, что, когда мы не знакомы с проектом, нет единой точки истины. Не существует таких высокоуровневых концепций, как автомобиль, здание, лошадь или единорог. Есть компоненты, которые в совокупности будут представлять собой нечто более сложное. Подумайте об атомах и молекулах. Когда вы смотрите на типы компонентов, вы видите атомы. Мы можем только догадываться, какие молекулы созданы из них. Такие догадки действительно пробуждают творческий потенциал, но могут напугать новичков.

Основы интроспекции ECS

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

Некоторые фреймворки ECS позволяют запустить игру и представить список систем и сущностей, которые в настоящее время активны. Это отличный инструмент для самоанализа, но он дает нам представление о текущем состоянии. Он не записывает, как мы туда попали. В этом инструменте вы видите просто фотографию, а не весь фильм.

Чтобы посмотреть фильм, нам нужно будет записать все соответствующие события, которые произошли с момента запуска приложения (иногда люди называют это Событием Источником).

Какие мероприятия проводятся в ECS?

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

  • willExecuteSystem
  • createEntity
  • destroyEntity
  • addComponent
  • removeComponent
  • replaceComponent
  • didExecuteSystem

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

Какие данные следует хранить вместе с событием?

Во-первых и в большинстве случаев каждое событие должно иметь идентификатор, хотя это может быть просто инкрементное целое число без знака. Кроме того, каждое событие должно иметь какую-то временную метку и номер кадра. Отметка времени дает нам представление о том, сколько времени прошло между событиями. Это особенно интересно в случае событий willExecute и didExecute. Разница между этими двумя событиями отражает время выполнения системы. Номер кадра помогает нам идентифицировать все события, которые произошли в конкретном кадре. В частности, при разработке игр это очень важный ориентир.

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

События создания и уничтожения сущности должны содержать идентификатор сущности и идентификатор мира / контекста (или как вы называете свою сущность, управляющую структурой данных). Это необходимо, потому что у нас может быть несколько сущностей с одинаковым идентификатором, но они размещены в разных мирах / контекстах.

Компоненты, добавляемые, удаляемые и заменяемые, должны содержать идентификатор объекта и идентификатор контекста. Требуется ссылка на имя компонента. Интересный вопрос - хотим ли мы хранить данные?

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

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

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

Слишком много информации!

Как я уже упоминал ранее: для просмотра фильма нам необходимо записать все соответствующие события. Однако это означает, что нам нужно записывать множество событий. Допустим, у нас есть двести систем. События willExecute didExecute уже генерируют 400 событий на кадр. А если у нас 60 кадров в секунду. Мы получаем 24К событий в секунду. И имейте в виду, что мы еще не записывали преобразования данных. Я думаю, можно с уверенностью сказать, что некоторые кадры будут генерировать тысячи событий преобразования данных.

Это означает, что мы сталкиваемся с двумя проблемами:

  1. Технические - как эффективно хранить так много информации?
  2. Пользовательский опыт - как мы можем понять, что на самом деле происходит, когда мы сталкиваемся с миллионами событий?

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

Просмотр фильма…

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

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

  • Компоненты и названия системы
  • Порядок выполнения системы
  • Время выполнения системы, с возможностью увидеть самое медленное и среднее время выполнения
  • Что произошло во время определенного (медленного) выполнения системы
  • Какие системы создают или уничтожают сущности
  • Какие системы добавляют / удаляют / заменяют определенный компонент
  • Сколько сущностей было создано
  • Какие сущности еще живы
  • Полная история определенного лица
  • Какие объекты и связанные компоненты присутствовали в определенный момент времени
  • Какие типы компонентов связаны с определенным компонентом - скажем, у нас есть Car компонент, покажите мне все типы компонентов, которые были добавлены к сущности, в которой присутствовал Car компонент

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

Предоставление инструмента визуализации

Один из способов поднять интересующие точки - предоставить инструмент визуализации, который помогает пользователю просматривать данные. В конце 2016 года я создал прототип такого инструмента и сделал скринкаст, объясняющий некоторые его особенности:

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

Язык запросов событий

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

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

components

Это оно. Если я хочу получить имена систем, я пишу:

systems

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

components where all(system:Move)

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

systems where all(component:Position) any(added replaced)

Я также могу запросить все сущности, у которых был Position компонент:

entities where all(component:Position)

Кроме того, мы могли бы сделать более сложный запрос, в котором я спрашиваю, какие компоненты были добавлены, заменены или удалены в сущности, которая также имела компонент Position (см. Как составляются компоненты? )

components where any(entity:[all(component:Position)])

Объяснение языка запросов

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

Запрос всегда начинается с описания информации, которую мы хотели бы получить:

  • components для названий компонентов
  • systems для имени системы
  • entities для идентификатора объекта и идентификатора контекста
  • events для идентификатора события
  • contexts для имен контекста
  • durations для идентификаторов событий willExecute didExecute событий и разницы времени между этими двумя событиями.

Если мы запишем эти термины как есть, мы получим всю информацию без фильтров. Если мы хотим отфильтровать информацию, нам нужно добавить предложение where.

После where следует составное сопоставление. Я решил использовать знакомую all any none семантику для композиции предикатов.

Итак, если мы хотим сказать, что мы хотим собрать информацию, которая должна передавать предикат A B, мы пишем where all(A B). Если мы хотим, чтобы он прошел A B и C или D, мы пишем where all(A B) any(C D). И, наконец, если мы хотим исключить E, мы пишем where all(A B) any(C D) none(E).

Если мы просто хотим проверить тип события, мы можем написать:

any(created destroyed added removed replaced willExec didExec)

Остальные проверки имеют вид type:value

У нас есть следующие проявления:

  • system где значением является имя системы или -, если вам нужно найти событие, которое было записано вне системы.
  • component где значением является либо имя компонента, либо -
  • context где значением является идентификатор контекста (вы также можете указать удобочитаемые имена)
  • event, за которым следует идентификатор события или диапазон идентификаторов событий, записанный как event:23..45
  • tick, за которым следует номер тика диапазона
  • entity где значением является идентификатор объекта или диапазон, за которым может следовать @ идентификатор контекста

Если мы хотим иметь составной запрос вроде:

components where any(entity:[all(component:Position)])

Значение проверки должно быть в [], что означает, что сначала мы выполним запрос entities where all(component:Postion)

а затем развернуть внешний запрос на основе результатов во что-то вроде:

components where any(entity:5@Core entity:7@Core entity:35@Core)

Заключение

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

Я начал с прототипирования языка запросов событий в EntitasKit и в настоящее время подумываю о переходе со Swift на C #, чтобы опробовать эту идею на Entitas-CSharp или даже ECS на базе Unity3D.

Приветствуются аплодисменты, комментарии приветствуются.