ECS - важные изменения

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

Изменения

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // before
    [EcsFilterInclude(typeof(Component1))]
    EcsFilter _filter1;

    [EcsFilterInclude(typeof(Component1), typeof(Component2))]
    [EcsFilterExclude(typeof(Component3))]
    EcsFilter _filter2;

    // after
    EcsFilter<Component1> _filter1;
    EcsFilter<Component1, Component2>.Exclude<Component3> _filter2;

    Фильтры поддерживают до 5 элементов в “компонент должен присутствовать обязательно”-ограничениях и до 2 элементов в “компонент обязательно должен отсутствовать”-ограничениях. Если потребуется больше, то можно запросить такой функционал через github-тикет, но лучше переосмыслить архитектуру проекта - возможно с ней что-то не так.

  • Компоненты автоматически инжектятся в соответствующие типизированные ComponentsX-коллекции фильтра: Components1, Components2, и т.д. Тип коллекций выводится из на типов-ограничений фильтра:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    EcsWorld _world;
    EcsFilter<MyComponent1> _filter1;
    EcsFilter<MyComponent1, MyComponent2> _filter2;

    // EntitiesCount should be used for walk over valid number of enitities in filter.
    for (var i = 0; i < _filter1.EntitiesCount; i++) {
    // int[] Entities array contains entity ID-s.
    int entity = _filter1.Entities[i];

    // You can use standard method for getting component.
    MyComponent1 component1 = _world.GetComponent<Component1> (entity);

    // But better to use new way to do same thing.
    // MyComponent1[] Components1 array contains instances from filtered entities.
    MyComponent1 component11 = _filter1.Components1[i];

    // component1 and component11 are equals - no need to write additional code and waste performance.
    }
    for (var i = 0; i < _filter2.EntitesCount; i++) {
    // We can get first component from filter constraint - Component1 typed.
    MyComponent1 component1 = _filter2.Components1[i];
    // We can get second component from filter constraint - Component2 typed.
    MyComponent2 component2 = _filter2.Components2[i];
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Component1 { }

    [EcsIgnoreInFilter]
    class Component2 { }

    [EcsInject]
    class TestSystem : IEcsSystem {
    EcsFilter<Component1, Component2> _filter;

    public Test() {
    for (var i = 0; i < _filter.EntitiesCount; i++) {
    // its valid code.
    var component1 = _filter.Components1[i];

    // its invalid code due to _filter.Components2 is null for memory / performance reasons.
    var component2 = _filter.Components2[i];
    }
    }
    }
  • Внедрение зависимостей. Опциональное и отключенное по умолчанию поведение. Больше никаких EcsWorld, EcsFilterInclude и EcsFilterExclude атрибутов для разметки. Для активации инжекта используется один EcsInject-атрибут на класс системы:

    1
    2
    3
    4
    5
    6
    [EcsInject]
    class MySystem : IEcsSystem {
    EcsWorld _world;
    EcsFilter<MyComponent1> _filter1;
    EcsFilter<MyComponent2>.Exclude<Component3> _filter2;
    }

    В результате будут инициализированы все поля типов EcsWorld и EcsFilter и все заработает как раньше.

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