Частые ошибки

Список частых ошибок при использовании StaticEcs. Полезен как разработчикам, так и AI-ассистентам.


Ошибки жизненного цикла

Забыли зарегистрировать типы

ВСЕ типы компонентов, тегов, событий, связей и мультикомпонентов ДОЛЖНЫ быть зарегистрированы между W.Create() и W.Initialize(). Использование незарегистрированного типа вызывает ошибку.

// НЕПРАВИЛЬНО: компонент не зарегистрирован
W.Create(WorldConfig.Default());
W.Initialize();
var e = W.NewEntity<Position>(0); // RuntimeError!

// ПРАВИЛЬНО — ручная регистрация
W.Create(WorldConfig.Default());
W.Types().Component<Position>();
W.Initialize();
var e = W.NewEntity<Position>(0); // OK

// ПРАВИЛЬНО — авторегистрация всех типов из сборки
W.Create(WorldConfig.Default());
W.Types().RegisterAll();
W.Initialize();

Операции с сущностями до Initialize

NewEntity, запросы и все операции с сущностями работают только после W.Initialize(). Вызов их во время фазы Created (между Create и Initialize) приведёт к ошибке.

Повторный вызов Create

Вызов W.Create() без предварительного W.Destroy() — ошибка. Мир должен быть уничтожен перед повторным созданием.


Ошибки работы с Entity

Использование Entity после Destroy

Entity — 4-байтовый uint-хендл без счётчика поколений. После Destroy() слот немедленно доступен для переиспользования. Старый хендл теперь молча указывает на совершенно другую сущность.

var entity = W.NewEntity<Position>(0);
entity.Destroy();
// entity теперь НЕВАЛИДЕН — любое использование — неопределённое поведение
entity.Ref<Position>(); // ОПАСНО: может обратиться к данным другой сущности

Хранение Entity между кадрами

Поскольку Entity не имеет счётчика поколений, он не может обнаружить устаревание. Никогда не храните Entity в полях, списках или других постоянных структурах. Используйте EntityGID.

// НЕПРАВИЛЬНО
class MySystem { Entity targetEntity; } // Устареет после уничтожения цели

// ПРАВИЛЬНО
class MySystem { EntityGID targetGid; } // Безопасно — проверка версии обнаружит устаревание
// Использование:
if (targetGid.TryUnpack<WT>(out var entity)) {
    // entity валиден и жив
}

Сравнение Entity для идентификации

Равенство Entity — только по IdWithOffset (uint). Две сущности, созданные в разное время в одном слоте, имеют одинаковое значение Entity. Используйте EntityGID для сравнения идентичности.


Ошибки работы с компонентами

Семантика Add и Set

Add<T>() без значения — идемпотентен: если компонент уже существует, возвращает ref на существующие данные, хуки НЕ вызываются. Это НЕ перезапись.

Set(value) всегда перезаписывает: вызывает OnDelete на старом значении, перезаписывает данные, вызывает OnAdd на новом.

entity.Set(new Position { Value = Vector3.Zero }); // Устанавливает позицию
entity.Add<Position>(); // Ничего НЕ делает — возвращает ref на существующий {0,0,0}
entity.Set(new Position { Value = Vector3.One }); // Перезаписывает: OnDelete(old) → set → OnAdd(new)

Реализация пустых хуков

ComponentTypeInfo<T> использует рефлексию при старте для обнаружения реализованных хуков. Если хоть один хук имеет непустое тело, диспетчеризация хуков включается для ВСЕХ экземпляров данного типа компонента.

// НЕПРАВИЛЬНО: пустое тело хука всё равно вызывает оверхед диспетчеризации
public struct Foo : IComponent {
    public void OnAdd<TW>(World<TW>.Entity self) where TW : struct, IWorldType { }
}

// ПРАВИЛЬНО: не реализуйте хуки, которые вам не нужны
public struct Foo : IComponent { }

HasOnDelete vs DataLifecycle

Хук OnDelete и DataLifecycle (сброс к DefaultValue) — взаимоисключающие пути очистки. Если у компонента есть хук OnDelete, хук отвечает за очистку — данные НЕ сбрасываются. Сброс DataLifecycle применяется только к компонентам без OnDelete. При noDataLifecycle: true в конфиге фреймворк не выполняет ни инициализацию, ни очистку.


Ошибки запросов

Нарушение Strict режима

В стандартном режиме Strict модификация фильтруемых типов компонентов/тегов на ДРУГИХ сущностях во время итерации запрещена.

// НЕПРАВИЛЬНО в Strict режиме:
foreach (var e in W.Query<All<Position>>().Entities()) {
    otherEntity.Delete<Position>(); // Модифицирует фильтруемый тип у другой сущности!
}

// ПРАВИЛЬНО: используйте Flexible режим
foreach (var e in W.Query<All<Position>>().EntitiesFlexible()) {
    otherEntity.Delete<Position>(); // OK в Flexible режиме
}

Ограничения параллельной итерации

Во время ForParallel модифицируйте только данные ТЕКУЩЕЙ сущности. Не создавайте/уничтожайте сущности, не модифицируйте другие сущности.

Ненужный Flexible режим

Flexible режим перепроверяет битовые маски на каждой сущности, что значительно медленнее Strict. Используйте Flexible только когда действительно нужно модифицировать фильтруемые типы у других сущностей.


Ошибки регистрации

MultiComponent без обёртки Multi

Типы IMultiComponent должны регистрироваться через W.Types().Multi<Item>(), а не как обычные компоненты.

Отсутствие настройки сериализации

Сериализация требует:

  1. Зависимость FFS.StaticPack
  2. Регистрация Guid для всех сериализуемых типов: W.Types().Component<T>(new ComponentTypeConfig<T> { Guid = ... })
  3. Для не-unmanaged компонентов нужны реализации хуков Write/Read
  4. Для unmanaged компонентов можно использовать UnmanagedPackArrayStrategy<T>

Ошибки ресурсов

Проблема кэширования NamedResource

NamedResource<T> кэширует внутреннюю box-ссылку при первом доступе. Если хранится как readonly или передаётся по значению после первого использования, копия кэша становится устаревшей.

// НЕПРАВИЛЬНО
readonly NamedResource<Config> config = new("main"); // readonly ломает кэш

// ПРАВИЛЬНО
NamedResource<Config> config = new("main"); // мутабельный — кэш работает

This site uses Just the Docs, a documentation theme for Jekyll.