DragonECS-Vault

July 26, 2026 · View on GitHub

image

DragonECS-Vault

Данный репозиторий - это сборник заметок и рекомендаций по коду для фреймворка DragonECS. Все, что здесь описано, построено на моем личном опыте и на подходах, которыми я сам пользуюсь в проектах.

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

Местами материал может быть спорным, поэтому спорные темы приглашаю обсудить в Discord

Стиль кода

Пример системы

Обычная версия с интерфейсами инъекции:

class ApplyVelocitySystem : IEcsRun, IEcsInject<EcsDefaultWorld>, IEcsInject<TimeService>
{
    EcsDefaultWorld _world;
    TimeService _time;

    class Aspect : EcsAspect
    {
        public EcsPool<Pose> Poses = Inc;
        public EcsPool<Velocity> Velocities = Inc;
        public EcsTagPool<FreezedTag> FreezedTags = Exc;
    }

    public void Run()
    {
        foreach (var e in _world.Where(out Aspect a))
        {
            a.Poses.Get(e).position += a.Velocities.Get(e).value * _time.DeltaTime;
        }
    }

    public void Inject(EcsDefaultWorld obj) => _world = obj;
    public void Inject(TimeService obj) => _time = obj;
}

Тот же пример, но с Auto-Injections:

class ApplyVelocitySystem : IEcsRun
{
    [DI] EcsDefaultWorld _world;
    [DI] TimeService _time;

    class Aspect : EcsAspect
    {
        public EcsPool<Pose> Poses = Inc;
        public EcsPool<Velocity> Velocities = Inc;
        public EcsTagPool<FreezedTag> FreezedTags = Exc;
    }

    public void Run()
    {
        foreach (var e in _world.Where(out Aspect a))
        {
            a.Poses.Get(e).position += a.Velocities.Get(e).value * _time.DeltaTime;
        }
    }
}

Порядок членов системы

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

Аспект стоит располагать ближе к методу, который с ним работает, обычно прямо перед Run. Так описание выборки остается на виду во время написания логики, и его проще быстро редактировать рядом с местом использования.

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

Аспекты

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

Многие системы работают только с одним аспектом, поэтому аспект можно называть просто Aspect. Если аспектов несколько, основной также можно назвать Aspect, а второстепенные - с префиксом, например EventAspect.

Поля для кэша пулов стоит называть по названию компонента во множественном числе и с заглавной буквы, например EcsPool<Health> Healths. Для этого пулы формально реализуют IEnumerable<T>, чтобы автодополнение IDE предлагало такое имя.

Локальные имена

Возвращаемый запросом Where экземпляр аспекта можно называть просто a, а сущность внутри foreach - просто e, например: foreach (var e in _world.Where(out Aspect a)).

Если система работает с несколькими аспектами, к a и e добавляется префикс, например: foreach (var eventE in _world.Where(out EventAspect eventA)).

Разделение на фичи

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

Структура папки

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

.../
+-- SomeFeature/
    +-- Components/
    |   +-- SomeComponent.cs
    |   +-- IsTagged.cs
    |   +-- SomeRequest.cs
    |   +-- SomeAnswer.cs
    |   +-- SomeEvent.cs
    |   ...
    +-- _SomeFeatureModule.cs
    +-- SomeSystem1.cs
    +-- SomeSystem2.cs
    ...
  • Components/ - папка для обычных компонентов, тегов и сообщений фичи.
  • SomeComponent.cs - обычный компонент.
  • IsTagged.cs - компонент-тег, который используется как bool-флаг. Такие компоненты рекомендуется называть по аналогии с bool-полями, то есть с префиксом Is.
  • SomeRequest.cs, SomeAnswer.cs, SomeEvent.cs - сообщения фичи.
  • _SomeFeatureModule.cs - класс, реализующий интерфейс IEcsModule и добавляющий системы фичи в пайплайн.
  • SomeSystem1.cs, SomeSystem2.cs - системы фичи, лежащие в корне папки фичи.

Модули-агрегаторы

Фичи удобно группировать через модуль-агрегатор: создавать модуль, который в методе Import просто добавляет модули других фич. Например, фичи, которые могут работать независимо от Unity, можно объединить в модуль ProjectCoreModule, а Unity-зависимые - в ProjectUnityModule. После такого разделения в EcsRoot достаточно добавить эти два модуля.

Мета-атрибуты

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

// Суффикс Module из _SomeFeatureModule будет автоматически удален, останется _SomeFeature
[MetaGroup(nameof(_SomeFeatureModule))]
public struct SomeComponent : IEcsComponent
{
   //...
}

Следующей подгруппой можно указать, чем является тип: компонентом или системой. Для этого в EcsConsts есть готовые константы.

[MetaGroup(nameof(_SomeFeatureModule), EcsConsts.COMPONENTS_GROUP)]
public struct SomeComponent : IEcsComponent { /* ... */ }

[MetaGroup(nameof(_SomeFeatureModule), EcsConsts.SYSTEMS_GROUP)]
public class SomeSystem : IEcsRun { /* ... */ }

Сообщения

Сообщения фичи лежат в Components/, потому что технически это такие же компоненты. По назначению они делятся на Request, Answer и Event.

Request

Request описывает намерение получить действие или данные. Такой компонент может порождаться несколькими системами, но обрабатывается одной системой-владельцем запроса. Ответственность за очистку Request лежит на обрабатывающей системе либо на общей системе очистки в конце Update.

Answer

Answer - это ответ на Request: результат обработки запроса, найденные данные или причина отказа. По механике Answer похож на Event, но предназначен для обработки системой, породившей Request, либо другой связанной системой, например внутри одной фичи.

Event

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

image

Диаграмма взята со страницы codewriter-packages/Morpeh.Events, так как она очень качественно демонстрирует эту идею.

Self и Target

В названиях сообщений перед Request, Answer или Event может добавляться маркер Self. Он используется, когда целью сообщения является сущность, на которой висит сам компонент. Если цель указывается явно через поле вида entlong Target, Self не добавляется.

public struct DamageSelfEvent : IEcsComponent
{
    public float Points;
}

public struct DamageEvent : IEcsComponent
{
    public entlong Target;
    public float Points;
}

Note


Идея нейминга Request, Answer и Event была взята из этой статьи, проверена на практике и подтвердила себя как эффективный нейминг.

Мульти-миры

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

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

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

Unchecked

В фреймворке некоторые методы имеют две версии: основную и с суффиксом Unchecked. Unchecked-методы опускают все проверки, поэтому неправильное использование может привести к нестабильному состоянию компонентов фреймворка или всего проекта.

К Unchecked-методам стоит относиться как к unsafe: применять их только в узких местах, где критична производительность. Если вы не уверены, что делаете, лучше их не использовать.