Component
Компонент — наделяет сущность данными и свойствами
- Представлен в виде пользовательской структуры с маркер-интерфейсом
IComponent - Представлен как struct исключительно по соображениям производительности (SoA-хранение)
- Поддерживает хуки жизненного цикла:
OnAdd,OnDelete,CopyTo,Write,Read - Может быть включён/отключён без удаления данных — opt-in через маркер
IDisableable
Пример:
public struct Position : IComponent {
public Vector3 Value;
}
public struct Velocity : IComponent {
public float Value;
}
public struct Name : IComponent {
public string Val;
}
Требуется регистрация в мире между созданием и инициализацией
W.Create(WorldConfig.Default());
//...
// Простая регистрация без конфигурации (подходит для большинства случаев)
W.Types()
.Component<Position>()
.Component<Velocity>()
.Component<Name>();
// Конфигурация предоставляется через реализацию IComponentConfig<T> на структуре компонента
// (см. пример ниже)
//...
W.Initialize();
Параметр noDataLifecycle управляет жизненным циклом данных компонента. По умолчанию (noDataLifecycle: false) фреймворк пре-инициализирует хранилище значением defaultValue и сбрасывает данные к defaultValue при удалении — entity.Add<T>() возвращает настроенное значение по умолчанию. При noDataLifecycle: true инициализация и очистка не выполняются — полезно для высокочастотных unmanaged-типов. Если OnDelete определён, хук отвечает за очистку вне зависимости от этого флага.
Для предоставления конфигурации реализуйте интерфейс IComponentConfig<T> на структуре компонента. И ручная регистрация, и RegisterAll() используют его автоматически:
public struct Health : IComponent, IComponentConfig<Health> {
public float Value;
public ComponentTypeConfig<Health> Config() => new(
defaultValue: new Health { Value = 100f }
);
}
Параметры ComponentTypeConfig<T>:
guid— стабильный идентификатор для сериализации (по умолчанию — автоматически из имени типа)version— версия схемы данных для миграции (по умолчанию — 0)noDataLifecycle— отключить управление данными фреймворком (по умолчанию — false). Приfalseфреймворк предварительно инициализирует хранилище значениемdefaultValueи сбрасывает данные кdefaultValueпри удалении. Приtrueинициализация и очистка не выполняются — полезно для высокочастотных unmanaged типов. Если определёнOnDelete, хук выполняет очистку независимо от этого флагаreadWriteStrategy— стратегия бинарной сериализации (по умолчанию — авто-определение)defaultValue— значение по умолчанию для инициализации и удаления (по умолчанию — нет)
Отслеживание изменений включается реализацией интерфейсов-маркеров на самом типе компонента (не через параметры конфига): ITrackableAdded, ITrackableDeleted, ITrackableChanged. См. Отслеживание изменений.
Создание сущностей с компонентами:
// Создать пустую сущность (без компонентов и тегов)
W.Entity entity = W.NewEntity<Default>();
// Создать сущность с указанием типа и кластера
W.Entity entity = W.NewEntity<Default>(clusterId: 0);
// Создать сущность с компонентами — Set возвращает Entity (перегрузки от 1 до 8 компонентов)
W.Entity entity = W.NewEntity<Default>().Set(new Position { Value = Vector3.One });
W.Entity entity = W.NewEntity<Default>().Set(
new Position { Value = Vector3.One },
new Velocity { Value = 1f }
);
Добавление компонентов:
// Add без значения: если компонент уже есть → вернёт ref к существующему, хуки НЕ вызываются
// Если нет → инициализирует default, вызовет OnAdd
ref var position = ref entity.Add<Position>();
// С флагом isNew: isNew=true если компонент добавлен впервые
ref var position = ref entity.Add<Position>(out bool isNew);
// Добавление нескольких компонентов за один вызов (перегрузки от 2 до 5)
entity.Add<Position, Velocity>();
// Set с значением: ВСЕГДА перезаписывает данные
// Если компонент уже есть → OnDelete(старый) → замена → OnAdd(новый)
// Если нет → установка значения → OnAdd
ref var position = ref entity.Set(new Position { Value = Vector3.One });
// Установка нескольких компонентов с значениями (перегрузки от 2 до 12)
entity.Set(new Position { Value = Vector3.One }, new Velocity { Value = 1f });
Add<T>() без значения и Set<T>(T value) с значением имеют разную семантику хуков. Без значения: если компонент уже существует, хуки не вызываются, возвращается ref к текущим данным. С значением: данные всегда перезаписываются с полным циклом OnDelete → замена → OnAdd.
Доступ к данным:
// Получить мутабельную ref-ссылку на компонент (чтение и запись)
// НЕ помечает как Changed — используйте Mut<T>() для отслеживаемого доступа
ref var velocity = ref entity.Ref<Velocity>();
velocity.Value += 10f;
// Получить readonly ref-ссылку на компонент — НЕ помечает как Changed
ref readonly var pos = ref entity.Read<Position>();
var x = pos.Value.x; // чтение OK, без пометки Changed
// Получить отслеживаемую мутабельную ref-ссылку — помечает как Changed, если компонент реализует ITrackableChanged
ref var pos = ref entity.Mut<Position>();
pos.Value += delta; // данные изменены И помечены как Changed
Ref<T>() НЕ помечает Changed. Используйте Mut<T>() когда нужен трекинг изменений для фильтров AllChanged<T>. В делегатах запросов (For) параметры ref автоматически используют семантику Mut.
Основные операции:
// Получить количество компонентов на сущности
int count = entity.ComponentsCount();
// Проверить наличие компонента (перегрузки от 1 до 3 — проверяет ВСЕ указанные)
// Проверяет наличие независимо от состояния enabled/disabled
bool has = entity.Has<Position>();
bool hasBoth = entity.Has<Position, Velocity>();
bool hasAll = entity.Has<Position, Velocity, Name>();
// Проверить наличие хотя бы одного из указанных компонентов (перегрузки от 2 до 3)
bool hasAny = entity.HasAny<Position, Velocity>();
bool hasAny3 = entity.HasAny<Position, Velocity, Name>();
// Удалить компонент (перегрузки от 1 до 5)
// Вызывает OnDelete если компонент был; вернёт true если удалён, false если не было
bool deleted = entity.Delete<Position>();
entity.Delete<Position, Velocity>();
entity.Delete<Position, Velocity, Name>();
Enable/Disable:
Disable/Enable — opt-in для каждого типа компонента через маркер-интерфейс IDisableable. Только компоненты с маркером IDisableable аллоцируют per-component disabled-битмаску, открывают Disable<T>()/Enable<T>()/HasDisabled<T>()/HasEnabled<T>() на сущности и могут использоваться в *Disabled фильтрах. Компоненты без маркера не платят за disabled-состояние ни памятью, ни байтами в сериализации.
// Помечаем компонент как disableable
public struct Position : IComponent, IDisableable {
public Vector3 Value;
}
// Отключить компонент — данные сохраняются, но сущность исключается из стандартных запросов
// Вернёт ToggleResult: MissingComponent, Unchanged или Changed
ToggleResult disabled = entity.Disable<Position>();
entity.Disable<Position, Velocity>();
entity.Disable<Position, Velocity, Name>();
// Включить компонент обратно
// Вернёт ToggleResult: MissingComponent, Unchanged или Changed
ToggleResult enabled = entity.Enable<Position>();
entity.Enable<Position, Velocity>();
entity.Enable<Position, Velocity, Name>();
// Проверить что ВСЕ указанные компоненты включены (перегрузки от 1 до 3)
bool posEnabled = entity.HasEnabled<Position>();
bool bothEnabled = entity.HasEnabled<Position, Velocity>();
// Проверить что хотя бы один включён (перегрузки от 2 до 3)
bool anyEnabled = entity.HasEnabledAny<Position, Velocity>();
// Проверить что ВСЕ указанные компоненты отключены (перегрузки от 1 до 3)
bool posDisabled = entity.HasDisabled<Position>();
bool bothDisabled = entity.HasDisabled<Position, Velocity>();
// Проверить что хотя бы один отключён (перегрузки от 2 до 3)
bool anyDisabled = entity.HasDisabledAny<Position, Velocity>();
Все методы Disable*/Enable*/Has*Disabled/Has*Enabled имеют констрейнт T : struct, IComponent, IDisableable — вызов на типе без маркера это ошибка компиляции. То же касается фильтров AllOnlyDisabled<T>, AllWithDisabled<T>, NoneWithDisabled<T>, AnyOnlyDisabled<>, AnyWithDisabled<>.
Отключённые компоненты не попадают в стандартные фильтры запросов (All, None, Any), но данные сохраняются в памяти. Используйте WithDisabled/OnlyDisabled варианты фильтров для работы с отключёнными компонентами.
Встроенные типы Multi<TValue> (multi-component), Link<TLinkType> и Links<TLinkType> (отношения) уже реализуют IDisableable — Disable/Enable на отношениях и multi-компонентах работает без правок на стороне пользователя.
Копирование и перемещение:
var source = W.NewEntity<Default>().Set(new Position(), new Velocity());
var target = W.NewEntity<Default>();
// Скопировать указанные компоненты на другую сущность (перегрузки от 1 до 5)
// Исходная сущность сохраняет свои компоненты
// Если хук CopyTo переопределён — вызывается пользовательская логика копирования
// Если хук CopyTo НЕ переопределён — выполняется побитовое копирование через Add + сохранение disabled-состояния
// Вернёт true (для одного) если компонент был у источника
bool copied = source.CopyTo<Position>(target);
source.CopyTo<Position, Velocity>(target);
// Переместить указанные компоненты на другую сущность (перегрузки от 1 до 5)
// Выполняет Copy на target, затем Delete с source (OnDelete вызывается на source)
bool moved = source.MoveTo<Position>(target);
source.MoveTo<Position, Velocity>(target);
Фильтры запросов:
Фильтры по компонентам описаны в разделе Запросы — Компоненты.
Хуки жизненного цикла:
Интерфейс IComponent предоставляет хуки с пустыми реализациями по умолчанию — переопределяйте только те, которые нужны.
Не оставляйте пустые реализации хуков. Если хук не нужен — не реализуйте его. Нереализованные хуки не вызываются и не создают накладных расходов.
public struct Cooldown : IComponent {
public float Duration;
public float Elapsed;
// Вызывается после добавления компонента или перезаписи значения через Set(value)
public void OnAdd<TWorld>(World<TWorld>.Entity self) where TWorld : struct, IWorldType {
Elapsed = 0f; // сбросить таймер при каждом применении
}
// Вызывается перед удалением компонента (Delete), перед перезаписью (Set с значением),
// а также при уничтожении сущности для каждого её компонента
//
// Параметр `reason` указывает причину удаления:
// HookReason.Default — явное удаление или уничтожение сущности
// HookReason.UnloadEntity — выгрузка сущности/чанка
// HookReason.WorldDestroy — сброс/уничтожение мира
public void OnDelete<TWorld>(World<TWorld>.Entity self, HookReason reason) where TWorld : struct, IWorldType { }
// Пользовательская логика копирования для CopyTo / MoveTo / Clone
// Если НЕ переопределён — побитовое копирование + сохранение disabled-состояния
// Если переопределён — полностью заменяет стандартную логику копирования
public void CopyTo<TWorld>(World<TWorld>.Entity self, World<TWorld>.Entity other, bool disabled)
where TWorld : struct, IWorldType {
ref var copy = ref other.Add<Cooldown>();
copy.Duration = Duration;
copy.Elapsed = 0f; // клон начинает с нуля
}
// Сериализация — запись компонента в бинарный поток
// Обязательно для EntitiesSnapshot (все типы), и для non-unmanaged типов в любых снимках
public void Write<TWorld>(ref BinaryPackWriter writer, World<TWorld>.Entity self)
where TWorld : struct, IWorldType {
writer.WriteFloat(Duration);
writer.WriteFloat(Elapsed);
}
// Десериализация — чтение компонента из бинарного потока
// Параметр version позволяет мигрировать данные между версиями схемы
public void Read<TWorld>(ref BinaryPackReader reader, World<TWorld>.Entity self, byte version, bool disabled)
where TWorld : struct, IWorldType {
Duration = reader.ReadFloat();
Elapsed = reader.ReadFloat();
}
}
Порядок вызова хуков при Set(value) для существующего компонента: OnDelete(старое значение) → замена данных → OnAdd(новое значение). При Delete или уничтожении сущности вызывается только OnDelete.
Отладка:
// Собрать все компоненты сущности в список (для инспектора/отладки)
// Список очищается перед заполнением
var components = new List<IComponent>();
entity.GetAllComponents(components);