ecsproto
Описание
Entity Component System (ECS) фреймворк для Unity, Godot, MonoGame, .Net Platform
Языки
- C#100%
LeoEcs Proto - Легковесный C# Entity Component System фреймворк
Производительность, нулевые или минимальные аллокации, минимизация использования памяти, отсутствие зависимостей от любого игрового движка - это основные цели данного фреймворка.
ВАЖНО! Требует C#9 (или Unity >=2021.2).
ВАЖНО! Не забывайте использовать
-версии билдов для разработки иDEBUG-версии билдов для релизов: все внутренние проверки/исключения будут работать только вRELEASE-версиях и удалены для увеличения производительности вDEBUG-версиях.RELEASE
ВАЖНО! LeoEcs Proto не потокобезопасен и никогда не будет таким! Если вам нужна многопоточность - вы должны реализовать ее самостоятельно и интегрировать синхронизацию в виде ecs-системы.
ВАЖНО! Проверено на Unity 2021.3 (не зависит от нее) и содержит asmdef-описания для компиляции в виде отдельных сборок и уменьшения времени рекомпиляции основного проекта.
Социальные ресурсы
Официальный блог: https://leopotam.ru
Установка
В виде unity модуля
Поддерживается установка в виде unity-модуля через git-ссылку в PackageManager или прямое редактирование :
"ru.leopotam.ecsproto": "https://gitverse.ru/leopotam/ecsproto.git",
В виде исходников
Код так же может быть склонирован или получен в виде архива со страницы релизов.
Прочие источники
Официальная работоспособная версия размещена по адресу https://gitverse.ru/leopotam/ecsproto, все остальные версии (включая nuget, npm и прочие репозитории) являются неофициальными клонами или сторонним кодом с неизвестным содержимым.
Основные типы
Сущность
Сама по себе ничего не значит и является исключительно идентификатором для для набора компонентов. Реализована как :
ВАЖНО! На сущности может существовать только один экземпляр каждого типа компонента.
ВАЖНО! Сущности не могут существовать без компонентов и будут автоматически уничтожаться при удалении последнего компонента на них.
ВАЖНО! Тип
не является ссылочным, экземпляры этого типа нельзя сохранять за пределами текущего метода без обеспечения целостности. Если требуется сохранение, то следует сохранять паруProtoEntity-сущность и ее поколение, полученное через вызовProtoEntity. ВProtoWorld.EntityGen()-расширении есть готовая реализация в видеEcsProto.QoLилиProtoPackedEntity.ProtoPackedEntityWithWorld
Компонент
Является контейнером для данных пользователя и не должен содержать логику (допускается минимальная вспомогательная обвязка, но не куски основной логики):
Компоненты могут быть добавлены, запрошены или удалены через компонентные пулы.
Система
Является контейнером для основной логики для обработки отфильтрованных сущностей. Существует в виде пользовательского класса, реализующего как минимум один из интерфейсов:
Сервисы
Экземпляр любого пользовательского ссылочного типа (класса) может быть одновременно подключен ко всем системам:
Специальные типы
Аспект
Является контейнером для пулов компонентов, существующих в мире.
ВАЖНО! Пулы можно создавать только внутри инициализатора аспекта для мира.
ВАЖНО! Аспекты могут быть частью других аспектов. В конструктор мира передается главный (корневой) аспект, являющийся композицией всех аспектов / пулов, из данных которых будет состоять мир. Инициализация вложенных аспектов должна выполняться путем вызова методов
иInit().PostInit()
Аспекты могут выступать в качестве группировки уже существующих пулов:
Мир
Является контейнером для всех сущностей, данные каждого экземпляра уникальны и изолированы от других миров.
ВАЖНО! Мир не может существовать хотя бы без одного аспекта.
ВАЖНО! Необходимо вызывать
у экземпляра мира если он больше не нужен.ProtoWorld.Destroy()
Пул
Является контейнером для компонентов, предоставляет апи для добавления / запроса / удаления компонентов на сущности:
ВАЖНО! После удаления компонент будет возвращен в пул для последующего переиспользования. Все поля компонента будут сброшены в значения по умолчанию автоматически.
ВАЖНО! После вызова
иpool.Add()все полученные ранееpool.Del()-ссылки на компоненты из этого пула через вызовыrefиpool.Add()становятся потенциально невалидными, для обращения к компонентам их требуется запрашивать снова через вызовpool.Get().pool.Get()
Итератор
Итератор является способом фильтрации сущностей по наличию или отсутствию на них указанных компонентов:
Если требуется указать отсутствие определенных компонентов, то тип итератора меняется на ,
принимающий 2 параметра (include/exclude списки типов):
ВАЖНО! Итераторы должны создаваться один раз на старте и не предназначены для создания динамически в
-системах.Run()
Итераторы могут быть частью аспекта, в этом случае они должны инициализироваться в методе :
Итераторы поддерживают перебор совместимых сущностей через , который они реализуют:
ВАЖНО! Этот вариант медленнее, чем перебор по конкретным типам итераторов и не рекомендован для большого количества сущностей.
Количество сущностей в итераторе
Итераторы позволяют узнать количество сущностей, подходящих под их условия:
ВАЖНО! Не рекомендуется к использованию если сущностей может быть больше пары десятков - подсчет ведется полным перебором итератора.
Наличие сущностей в итераторе
Если точное количество не требуется, а достаточно просто знать, что итератор не пустой, то можно воспользоваться следующим методом:
ВАЖНО! Этот метод быстрее
, но в худшем случае все-равно выполняется полный перебор итератора.IProtoIt.LenSlow()
Получение первой сущности в итераторе
Если требуется получить только первую сущность из итератора с корректной обработкой ее отсутствия, то можно воспользоваться следующим методом:
ВАЖНО! В худшем случае все-равно выполняется полный перебор итератора.
Группа систем
Является контейнером для систем, определяет порядок выполнения (на примере интеграции в Unity):
ВАЖНО! Необходимо вызывать
у экземпляра группы систем если он больше не нужен.IProtoSystems.Destroy()
Системы можно подключать в одном порядке, а выполнять - в другом, для этого можно указать "вес" системы:
Системы выполнятся в следующем порядке:
System3 > System2 > System1
ВАЖНО! "Веса" являются глобальными отсортированными по возрастанию блоками систем в пределах
, а не уникальными значениями для каждой системы, поэтому можно подключать несколько систем с одинаковым "весом" - в этом случае они будут выполняться в пределах одного "веса" в порядке подключения.IProtoSystems
Если явно не указывать "вес" системы, система будет иметь вес 0:
Системы выполнятся в следующем порядке:
System2 > System4 > System3 > System1
ВАЖНО! "Веса" могут иметь как положительные, так и отрицательные значения - это позволяет выполнять системы до систем с весом по умолчанию.
Модуль
Используется для разделения пользовательского кода на модули:
Аспект модуля так же может быть вынесен отдельно:
Интеграция с движками
Unity
Интегратор выполнен в виде модуля расширения и может быть установлен в дополнение к ядру.
Кастомный движок
Каждая часть примера ниже должна быть корректно интегрирована в правильное место выполнения кода движком:
Расширения
- Улучшение "качества жизни" (Quality of Life) разработчика
- Интеграция в редактор Unity
- Интеграция событий Unity Physics2D
- Интеграция событий Unity Physics3D
- Интеграция событий Unity uGui
- Интеграция многопоточной обработки
- Взаимоотношение родитель-ребенок для сущностей
- Группировка систем в блоки с условным выполнением
- UtilityAI
- Unity-интеграция UtilityAI
Лицензия
Фреймворк выпускается под лицензией MIT-ZARYA, подробности тут.
ЧаВо
В чем отличие от LeoECS Lite?
Я предпочитаю называть их (ecslite) и (ecsproto). Основные отличия следующие:
- Кодовая база фреймворка уменьшилась (на соизмеримом функционале), ее стало проще поддерживать и расширять. Пулы и итераторы теперь могут быть реализованы пользователем.
- Появилась штатная поддержка модулей - пользовательской код теперь проще разделять и подключать в новые проекты.
- Появилась штатная система нелинейного подключения систем - можно явно указывать контрольные точки интеграции.
- Пулы теперь известны на старте мира и не могут быть добавлены в процессе намеренно или случайно.
- Отсутствие фильтров - при большом их количестве (от сотни) и тысячах сущностей, попадающих в них,
серьезно выигрывает по скорости (до х3 раз) при добавлении / удалении компонентов.Прото - Из-за отсутствия фильтров снизилась скорость линейной итерации по сущностям - от 10% с небольшим линейным замедлением в зависимости от количества компонентов в мире.
- Новая лицензия.
Я хочу сохранить ссылку на сущность в компоненте. Как я могу это сделать?
Для этого следует реализовать сохранение Id+Gen сущности самостоятельно, либо воспользоваться реализацией из -расширения.
Я хочу одну систему вызвать в MonoBehaviour.Update(), а другую - в MonoBehaviour.FixedUpdate(). Как я могу это сделать?
Для разделения систем на основе разных методов из необходимо создать под каждый метод отдельную -группу:
Меня не устраивают значения по умолчанию для полей компонентов. Как я могу это настроить?
Компоненты поддерживают установку произвольных значений через реализацию обработчика :
Этот метод будет автоматически вызываться для всех новых компонентов, а так же для всех только что удаленных, до помещения их в пул.
ВАЖНО! В случае применения обработчика
все дополнительные очистки/проверки полей компонента отключаются, что может привести к утечкам памяти. Ответственность лежит на пользователе!SetResetHandler()
Меня не устраивают значения для полей компонентов при их копировании через EcsWorld.CopyEntity() или Pool<>.Copy(). Как я могу это настроить?
Компоненты поддерживают установку произвольных значений при вызове или через реализацию обработчика :
ВАЖНО! В случае применения обработчика
никакого копирования по умолчанию не происходит. Ответственность за корректность заполнения данных и за целостность исходных лежит на пользователе!SetCopyHandler()
Я хочу выполнять сериализацию/десериализацию компонентов без аллокаций. Как я могу это сделать?
Компоненты можно сериализовать своим обработчиком через вызов /:
После этого можно вызывать сериализацию компонента на нужной сущности:
Результат операции будет , если пул не реализует такой функционал, для пула не установлены обработчики / или на указанной сущности нет компонента из указанного пула, иначе .
Я хочу добавить реактивности и обрабатывать события изменений в мире самостоятельно. Как я могу сделать это?
ВАЖНО! Так делать не рекомендуется из-за падения производительности.
Для активации этого функционала следует добавить в список директив комплятора, а затем - добавить слушатель событий:
Я хочу использовать модули в своем коде и у меня возникают проблемы с подключением аспектов из разных модулей - мир требует ручной сборки корневого аспекта. Как я могу это упростить?
Для этого следует воспользоваться -классом из -расширения.
Я хочу отключать системы целыми группами и включать их обратно когда потребуется. Как я могу это сделать?
Для этого следует воспользоваться -системой из -расширения.
У меня будет не более пары десятков тысяч сущностей в мире и несколько тысяч разных компонентов. Как я могу оптимизировать потребление памяти?
Для активации режима экономии памяти на ограниченном количестве сущностей (до 65535 включительно) следует добавить в список директив комплятора.
Мне неудобно создавать новые сущности через пулы. Как я могу создавать пустую сущность и добавлять к ней нужные компоненты?
ВАЖНО! Нет никакого механизма по отслеживанию пустых сущностей, это может привести к утечкам памяти.
Для активации режима создания пустых сущностей следует добавить в список директив комплятора, после этого будет доступен метод .