proton

0

Описание

Легковесный Entity Component System фреймворк на Nimlang.

Языки

  • Nim100%
README.md

Logo

LeECS ProtoN - Легковесный Nimlang Entity Component System фреймворк

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

ВАЖНО! Требует Nimlang >= 2.2.8.

ВАЖНО! Не забывайте использовать

DEBUG
-версии билдов для разработки и
RELEASE
-версии билдов для релизов: все внутренние проверки/исключения будут работать только в
DEBUG
-версиях и удалены для увеличения производительности в
RELEASE
-версиях.

ВАЖНО! LeoECS ProtoN не потокобезопасен и никогда не будет таким! Если вам нужна многопоточность - вы должны реализовать ее самостоятельно и интегрировать синхронизацию в виде ECS-системы.

Социальные ресурсы

Официальный блог: https://leopotam.ru

Лицензия

Фреймворк выпускается под лицензией MIT-ZARYA, подробности тут.

Установка / обновление

При установке

proton
необходимо выполнить следующие операции (в папке проекта):

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

    deps/proton
    .

  2. Выполнить настройку путей для

    proton
    (путь до
    proton.nim
    должен учитывать локальную папку с
    proton
    , расширение
    .nim
    можно опустить):

  3. Рестарт LSP (рестарт сессии VSCode в случае использования)

Использование в коде

Подключение пакета в коде происходит следующим образом:

Основные типы

Далее идут основные типы и поддерживаемые ими действия.

Сущность

Сама по себе ничего не значит и является исключительно идентификатором для набора компонентов. Реализована как

ProtonEntity
.

Создание сущности

Создание возможно только через пул, полученный ранее из мира:

Удаление сущности

Любая сущность может быть удалена, при этом сначала все ее компоненты будут автоматически удалены, затем сущность будет считаться уничтоженной:

Копирование сущности

Компоненты одной сущности могут быть скопированы на другую сущность. Если копируемые компоненты отсутствовали на целевой сущности, то они будут добавлены:

Клонирование сущности

Любая сущность может быть склонирована:

ВАЖНО! На сущности может существовать только один экземпляр каждого типа компонента.

ВАЖНО! Сущности не могут существовать без компонентов и будут автоматически уничтожаться при удалении последнего компонента на них.

Упаковка сущностей

Тип

ProtonEntity
не является безопасным, его экземпляры нельзя сохранять за пределами текущего метода без обеспечения целостности. Если требуется сохранение, то следует использовать
ProtonPackedEntity
:

Компонент

Является контейнером для данных пользователя и не должен содержать логику (допускается минимальная вспомогательная обвязка, но не куски основной логики):

Компоненты могут быть добавлены, запрошены или удалены через компонентные пулы.

Система

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

Определение зависимостей систем

  • definePool(T)
    - требует наличия пула для компонента
    T
    в мире. Если пул отсутствует - будет создан автоматически. Это единственное место, где можно запросить создание пула, в рантайме они не могут быть запрошены или добавлены.

  • defineIt([I1, I2, ...])
    - требует создания итератора на базе компонентов
    I1
    ,
    I2
    ... (будут считаться
    Include
    -частью итератора). Пулы для этих компонентов так же будут запрошены и созданы в случае отсутствия. Это единственное место, где можно запросить создание итератора, в рантайме они не могут быть запрошены или добавлены.

  • defineIt([I1, I2, ...], [E1, E2, ...])
    - требует создания итератора на базе компонентов
    I1
    ,
    I2
    ... (будут считаться
    Include
    -частью итератора) и
    E1
    ,
    E2
    ... (будут считаться
    Exclude
    -частью итератора). Пулы для этих компонентов так же будут запрошены и созданы в случае отсутствия.

  • defineService(T)
    - требует наличия сервиса типа
    T
    .

Подключение систем

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

Допускается добавление систем цепочкой:

Если требуется поменять порядок добавляемых систем в одну группу, то можно явно указать "вес" системы дополнительным параметром - он будет использоваться для сортировки систем по возрастанию:

Запуск групп систем

Запуск группы систем выполняется следующим образом:

Допускается выполнение групп систем цепочкой:

ВАЖНО! При вызове

world.init()
и
world.destroy()
нет никаких скрытых вызовов групп систем, если есть необходимость вызывать системы сразу после создания или перед уничтожением мира - их следует вызывать самостоятельно:

Сервис

Экземпляр любого пользовательского типа может быть подключен ко всем системам мира:

Есть возможность установки обработчика дополнительной инициализации сервиса при инициализации мира:

В обработчике доступно все апи инициализации мира до вызова

world.init()
, кроме добавления сервисов.

Мир

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

ВАЖНО! Необходимо вызывать

world.destroy()
у экземпляра мира если он больше не нужен.

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

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

Пул

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

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

add()
добавляет компонент на сущность. Если компонент уже существует - будет брошено исключение в
DEBUG
-версии:

Проверка компонента

has()
проверяет наличие компонента на сущности и возвращает результат:

Запрос компонента

get()
возвращает существующий на сущности компонент. Если компонент не существовал - будет брошено исключение в
DEBUG
-версии:

Удаление компонента

del()
удаляет компонент с сущности. Если компонент не существовал - будет брошено исключение в
DEBUG
-версии. Если это был последний компонент, то сущность будет удалена автоматически:

ВАЖНО! После удаления компонент будет возвращен в пул для последующего переиспользования. Все поля компонента будут сброшены в значения по умолчанию автоматически. Если были присвоенные значения - будут использованы они.

ВАЖНО! После вызова

pool.add()
и
pool.del()
все полученные ранее
ptr
-ссылки на компоненты из этого пула через вызовы
pool.add()
и
pool.get()
становятся потенциально невалидными, для обращения к компонентам их требуется запрашивать снова через вызов
pool.get()
.

Кастомная инициализация компонента

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

Кастомное копирование компонента

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

Автоматизация кастомного поведения

Назначение обработчиков можно автоматизировать, реализовав следующую функцию рядом с типом компонента:

setHandlers()
будет вызываться автоматически при первой регистрации пула данного типа в мире.

Итератор

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

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

world.defineIt()
:

Количество сущностей в итераторе

ВАЖНО! Не рекомендуется к использованию если сущностей может быть больше пары сотен - подсчет ведется полным перебором итератора.

Есть ли сущности в итераторе

Если точное количество не требуется, а достаточно просто знать, что итератор не пустой, то можно воспользоваться следующим методом:

ВАЖНО! Этот метод быстрее

lenSlow()
, но в худшем случае все-равно выполняется полный перебор итератора.

Получение первой сущности в итераторе

Если требуется получить только первую сущность из итератора с корректной обработкой ее отсутствия, то можно воспользоваться следующим методом:

ВАЖНО! В худшем случае все-равно выполняется полный перебор итератора.

Событие

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

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

ВАЖНО! Каждый читатель события обязан сделать запрос на чтение, иначе события будут копиться.

События могут быть отменены:

Модуль

Логика может быть разделена на модули, содержащие свои системы и сервисы. Модули регистрируются отдельно в мире, для них доступно все апи инициализации мира до вызова

world.init()
:

Если требуется передать параметры в модуль - достаточно обернуть их в вызов функции. Например, передача имени группы для систем:

Интеграция с движками

Кастомный движок

Каждая часть примера ниже должна быть корректно интегрирована в правильное место выполнения кода движком: