Как и почему мы написали свой ECS

Автор: admin от 13-06-2018, 10:25, посмотрело: 29

В прошлой статье я описал технологии и подходы, которые мы используем при разработке нового мобильного fast paced шутера. Т.к. это была обзорная и даже поверхностная статья — сегодня я копну глубже и подробно расскажу, почему мы решили написать собственный ECS-фреймворк и не стали использовать существующие. Будут примеры кода и небольшой бонус в конце.

Как и почему мы написали свой ECS
Data locality).
  • Проще тестировать, когда данные отделены от логики. Особенно если учесть, что логика — это небольшая система в несколько строк кода.

  • Просмотр и редактирование состояния мира в реальном времени. Т.к. состояние мира это просто данные, мы написали тулзу, которая на веб-странице отображает все состояние мира в матче на сервере (а также сцену матча в 3D). Любой компонент любой сущности можно просмотреть, изменить, удалить. То же самое можно сделать в редакторе Unity для клиента.



  • Как и почему мы написали свой ECS


    А теперь минусы:




    • Нужно учиться думать, проектировать и писать код по-другому. Думать в рамках сущностей, компонент и систем. Многие паттерны проектирования в ECS реализуются совсем по-другому (см. пример реализация паттерна Состояние (State) в одной из обзорных статей в конце).

    • Больше кода. Спорно. С одной стороны, из-за того, что мы разбиваем логику на мелкие системы, вместо того, чтобы описать всю функциональность в одном классе, классов становится больше, но именно кода не намного больше.

    • Порядок вызова систем влияет на работу всей игры. Обычно, системы зависимы друг от друга, порядок их выполнения задается списком и они выполняются в этом порядке. Например, сначала DamageSystem считает урон, затем RemoveDamageSystem удаляет компонент Damage. Если случайно поменять порядок, то все станет работать по-другому. В целом, это актуально и для обычного ООП-случая, если поменять порядок вызова методов, но в ECS ошибиться проще. Например, если часть логики работает на клиенте для prediction, то порядок должен быть такой же, как на сервере.

    • Нужно как-то связывать данные и события логики с представлением. В случае с Unity у нас MVP:



      — Model — GameState из ECS;

      — View — у нас это исключительно стандартные MonoBehaviour-классы Unity (Renderer, Text и т.д.) и префабы;

      — Presenter использует GameState для определения событий появления/исчезания сущностей, компонент и т.д., создает объекты Unity из префабов и меняет их в соответствии с изменением состояния мира.



    А знаете ли вы, что:




    • ECS — это не только про data locality. Для меня это больше парадигма программирования, паттерн, еще один способ проектирования игрового мира — назовите как угодно. Data locality — это лишь оптимизация.

    • В Unity нет ECS! Часто на собеседовании в команду спрашиваешь кандидатов — а что вы знаете про ECS? Если не слышали, рассказываешь им, а они в ответ: «А, так это ж как в Unity, тогда знаю!». Но нет, это не как в движке Unity. Там данные и логика объединены в компоненте MonoBehaviour, а GameObject (если сравнивать с сущностью в ECS) обладает дополнительными данными — имя, место в иерархии и др. Разработчики Unity сейчас работают над нормальной реализацией ECS в движке и пока видится, что она будет хороша. Они наняли специалистов в этой области — надеюсь, получится круто.



    Наши критерии выбора ECS-фреймворка



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




    • Entitas

    • Artemis C#

    • Ash.NET

    • ECS — наше собственное решение на момент, когда мы его задумывали. Т.е. наши предположения и хотелки, что мы можем сделать сами.



    Мы составили таблицу для сравнения, куда я также включил наше текущее решение (обозначил его как ECS(now)):



    Как и почему мы написали свой ECS
    Красный цвет — решение не поддерживает наше требование, оранжевый — поддерживает частично, зеленый — поддерживает полностью.



    Для нас аналогией операций доступа к компонентам, поиска сущностей в ECS были операции в sql-базе данных. Поэтому мы использовали понятия типа table (таблица), join (операция соединения), indices (индексы) и т.д.



    Распишем наши требования и насколько сторонние библиотеки и фреймворки им соответствовали:




    • separate data sets (history, current, visual, static) — возможность отдельно получить и хранить состояния мира (например, текущее состояние для обработки, для отрисовки, история состояний и т.д.). Все из рассматриваемых решений поддерживали это требование.

    • entity ID as integer — поддержка представления сущности ее идентификатором-числом. Нужно для передачи по сети и возможности связывать сущности в истории состояний. Ни в одном из рассматриваемых решений поддержки не было. Например, в Entitas сущность представлена полноценным объектом (как GameObject в Unity).

    • join by ID O(N+M) — поддержка относительно быстрой выборки по компонентам двух типов. Например, когда нужно получить все сущности с компонентами типа Damage (допустим, их N штук) и Health (M штук) для расчета и нанесения урона. Была полная поддержка в Artemis; в Entitas и Ash.NET она быстрее O(N?), но медленнее O(N+M). Точнее оценку сейчас уже не помню.

    • join by ID reference O(N+M) — то же самое, что выше, только когда в компоненте одной сущности есть ссылка на другую, и у последний нужно получить другой компонент (в нашем примере компонент Damage на вспомогательной сущности ссылается на сущность игрока Victim и оттуда нужно получить компонент Health). Не поддерживалось ни одним из рассмотренных решений.

    • no query alloc — нет лишних аллокаций памяти при запросе компонентов и сущностей из состояния мира. В Entitas в определенных кейсах она была, но незначительная для нас.

    • pool tables — хранение данных мира в пулах, возможность повторного использования памяти, аллокации только когда пул пустой. Была «какая-то» поддержка в Entitas и Artemis, полное отсутствие в Ash.NET.

    • compare by ID (add, del) — встроенная поддержка событий создания/уничтожения сущностей и компонент по ID. Нужно для уровня отображения (View), чтобы показывать/скрывать объекты, проиграть анимации, эффекты. Не поддерживалось ни одним из рассмотренных решений.

    • ? serialisation (quantisation, skip) — встроенная дельта-компрессия при сериализации состояния мира (например, для уменьшения размера пересылаемых по сети данных). «Из коробки» не поддерживалась ни в одном из решений.

    • Interpolation — встроенный механизм интерполяции между состояниями мира. Ни одно из решений не поддерживало.

    • reuse component type — возможность использовать один раз написанный тип компонента в разных типах сущностей. Поддерживал только Entitas.

    • explicit order of systems — возможность задать свой порядок вызова систем. Все решения поддерживали.

    • editor (unity/server) — поддержка просмотра и редактирования сущностей в реальном времени, как для клиента, так и для сервера. Entitas поддерживал возможность просматривать и редактировать сущности и компоненты только в редакторе Unity.

    • fast copy/replace — возможность дешевого копирования/замены данных. Ни одно из решений не поддерживало.

    • component as value type (struct) — компоненты как типы-значения. В принципе, хотелось на основе этого добиться хорошей производительности. Не поддерживалось ни одной из систем, везде были компоненты-классы.



    Необязательные требования (ни одно из решений на тот момент их не поддерживало):




    • indices — индексирование данных как в БД.

    • composite keys — сложные ключи для быстрого доступа к данным (как в БД).

    • integrity check — возможность проверки целостности данных в состоянии мира. Полезно при отладке.

    • content-aware compression — лучшее сжатие данных на основе знания о природе данных. Например, если мы знаем, максимальный размер карты или максимальное кол-во объектов мира.

    • types/systems limit — ограничение на кол-во типов компонент или систем. В Artemis на тот момент нельзя было создать больше 32 или 64 типов компонент и систем.



    Как видно по таблице, самостоятельно мы хотели реализовать все требования, кроме необязательных. На деле на текущий момент мы не сделали:




    • join by ID O(N+M) и join by ID reference O(N+M) — выборка по двум разным компонентам у нас до сих пор занимает O(N?) (фактически, вложенный цикл for). С другой стороны, сущностей и компонент на матч не так уж и много.

    • compare by ID (add, del) — не понадобилось на уровне фреймворка. Это мы реализовали на уровне выше, в MVP.

    • fast copy/replace и component as value type (struct) — в какой-то момент мы поняли, что работать с структурами будет не так удобно, как с классами, и остановились на классах — предпочли удобство разработки вместо лучшей производительности. Кстати говоря, разработчики Entitas поступили в итоге так же.



    При этом мы все таки реализовали одно из изначально необязательных на наш взгляд требований:




    • content-aware compression — за счет него нам удалось значительно (в десятки раз) уменьшить размер передаваемого по сети пакета. Для мобильных сетей передачи данных очень важно уместить размер пакета в MTU, чтобы «по дороге» его не разбивали на мелкие части, которые могут потеряться, дойти в другом порядке, и которые потом нужно будет собирать по частям. Например, в Photon, если размер данных не умещается в заданный в библиотеке MTU, он разбивает данные на пакеты и посылает их как reliable (с гарантированной доставкой), даже если вы сами «сверху» посылаете их как unreliable. Проверено с болью на собственном опыте.



    Особенности нашей разработки на ECS




    • Мы в ECS пишем исключительно бизнес-логику. Никакой работы с ресурсами, представлением и т.д. Так как код ECS-логики одновременно работает на клиенте в Unity и на сервере — он должен быть максимально независим от других уровней и модулей.

    • Стараемся минимизировать компоненты и системы. Обычно на каждую новую задачу мы заводим новые компоненты и системы. Но иногда бывает, что модифицируем старые, добавляем в компоненты новые данные, а системы «раздуваем».

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

    • Иногда, представлению недостаточно той информации, которая есть в GameState. Тогда приходится добавлять специальные компоненты или дополнительные данные, которые в логике не участвуют, но нужны представлению. Например, на сервере выстрел моментальный, живет один тик, а на клиенте визуально он дольше. Поэтому для клиента выстрелу добавляется параметр «время жизни выстрела».

    • События/запросы мы реализуем за счет создания специальных компонент. Например, если игрок умер, мы вешаем на него компонент без данных Dead, что является событием для других систем и View-уровня о том, что игрок умер. Или если нам нужно заново возродить игрока на точке, мы создаем отдельную сущность с компонентом Respawn с дополнительной информацией кого возродить. Отдельная система RespawnSystem в самом начале игрового цикла проходится по этим компонентам и уже создает сущность игрока. Т.е. фактически первая сущность является запросом на создание второй.

    • У нас есть специальные «singleton»-компоненты/сущности. Например, у нас есть сущность с ID=1, на которой висят специальные компоненты — настройки игры.



    Бонус



    В процессе решения — а нужна ли Хабру статья про ECS — я провел небольшое исследование. Как и обещал в начале, вот мой небольшой обзор статей по этой теме, а вы решайте, читать или нет:





    Источник: Хабр / Интересные публикации

    Категория: Веб-разработка, Game Development, Android, iOS

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

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *