Частые ошибки
Список частых ошибок при использовании 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>(), а не как обычные компоненты.
Отсутствие настройки сериализации
Сериализация требует:
- Зависимость FFS.StaticPack
- Регистрация Guid для всех сериализуемых типов:
W.Types().Component<T>(new ComponentTypeConfig<T> { Guid = ... }) - Для не-unmanaged компонентов нужны реализации хуков
Write/Read - Для unmanaged компонентов можно использовать
UnmanagedPackArrayStrategy<T>
Ошибки ресурсов
Проблема кэширования NamedResource
NamedResource<T> кэширует внутреннюю box-ссылку при первом доступе. Если хранится как readonly или передаётся по значению после первого использования, копия кэша становится устаревшей.
// НЕПРАВИЛЬНО
readonly NamedResource<Config> config = new("main"); // readonly ломает кэш
// ПРАВИЛЬНО
NamedResource<Config> config = new("main"); // мутабельный — кэш работает