Entity

Сущность — структура для идентификации объекта в мире и доступа к его компонентам и тегам

  • Структура размером 4 байта (uint-обёртка над индексом слота)
  • Не содержит счётчика поколений — для устойчивых ссылок используйте EntityGID
  • Все операции с компонентами и тегами доступны через методы на самой сущности

Тип сущности (IEntityType)

Тип сущности — логическая категория, назначаемая при создании. Определяет назначение сущности (юниты, снаряды, эффекты) и управляет размещением в памяти — сущности одного типа внутри кластера хранятся в одних сегментах.

Определение типов сущностей

Типы сущностей определяются как структуры, реализующие IEntityType, с методом byte Id():

public struct Bullet : IEntityType {
    public byte Id() => 1;
}

public struct Enemy : IEntityType {
    public byte Id() => 2;
}

public struct Effect : IEntityType {
    public byte Id() => 3;
}

Встроенный тип Default (Id = 0) регистрируется автоматически при создании мира.

Регистрация

Типы сущностей регистрируются в фазе Created.

Ручная регистрация:

W.Types()
    .EntityType<Bullet>()
    .EntityType<Enemy>()
    .EntityType<Effect>();

Авторегистрация:

W.Types().RegisterAll();

RegisterAll() находит все типы, реализующие IEntityType, в указанных сборках (по умолчанию — typeof(TWorld).Assembly; никакого stack walking, безопасно на Unity IL2CPP, Unity WebGL и NativeAOT) и регистрирует их автоматически. Идентификатор получается из метода Id().

Хуки жизненного цикла (OnCreate / OnDestroy)

Типы сущностей могут определять хуки OnCreate и OnDestroy. Если метод не определён в структуре — он не вызывается. Не нужно оставлять пустые реализации.

public struct Bullet : IEntityType {
    public byte Id() => 1;

    public void OnCreate<TWorld>(World<TWorld>.Entity entity) where TWorld : struct, IWorldType {
        entity.Set(new Velocity { Speed = 100 });
        entity.Set<Active>();
    }

    public void OnDestroy<TWorld>(World<TWorld>.Entity entity, HookReason reason) where TWorld : struct, IWorldType {
        // Логика очистки, отправка событий и т.д.
        // Все компоненты и теги ещё доступны.
    }
}

Типы сущностей с данными

Поскольку IEntityType — struct, он может содержать поля. OnCreate — instance-метод с доступом к полям через this. Это позволяет параметризировать создание без дополнительных аргументов и аллокаций:

public struct Flora : IEntityType {
    public byte Id() => 4;

    public enum Kind : byte { Grass, Bush, Tree }
    public Kind FloraKind;

    public void OnCreate<TWorld>(World<TWorld>.Entity entity) where TWorld : struct, IWorldType {
        entity.Set(new Health { Value = FloraKind == Kind.Tree ? 100 : 10 });
    }
}

// Использование:
var tree = W.NewEntity(new Flora { FloraKind = Flora.Kind.Tree });
var grass = W.NewEntity(new Flora { FloraKind = Flora.Kind.Grass });

Зачем это нужно

Локальность данных при итерации. Компоненты хранятся в SoA-массивах. Когда сущности одного типа расположены в одном сегменте, их компоненты лежат рядом в памяти — процессор эффективно использует кэш-линии.

Уменьшение фрагментации. Без типизации сущности разных видов создавались бы вперемешку. С типизацией дыры от уничтоженных снарядов заполняются новыми снарядами — сегмент остаётся однородным.

Фильтрация в запросах. Фильтры по типу сущности (EntityIs<T>, EntityIsNot<T>, EntityIsAny<T0,T1>) работают на уровне сегментов с нулевой стоимостью per-entity — FilterEntities является no-op. Это самый дешёвый тип фильтра в системе.

entityType и clusterId

Эти два параметра дополняют друг друга:

  • entityTypeлогическая группировка: определяет что это за сущность (юнит, снаряд, эффект). Влияет на размещение в памяти — сущности одного типа хранятся вместе для оптимальной итерации.
  • clusterIdпространственная группировка: определяет где находится сущность (уровень, зона карты, комната). Позволяет ограничивать запросы конкретными областями мира и управлять стримингом.

Сегментация работает на пересечении этих параметров: внутри каждого кластера для каждого типа выделяются отдельные сегменты.


Создание

// Создание сущности с типом по умолчанию (Id = 0)
W.Entity entity = W.NewEntity<Default>();

// С конкретным типом — OnCreate хук вызывается автоматически
W.Entity entity = W.NewEntity<Bullet>();
W.Entity entity = W.NewEntity<Enemy>(clusterId: LEVEL_1_CLUSTER);

// С данными в структуре типа сущности
W.Entity entity = W.NewEntity(new Flora { FloraKind = Flora.Kind.Tree });

// С компонентами — Set возвращает Entity, можно использовать как цепочку
W.Entity entity = W.NewEntity<Bullet>().Set(new Position { Value = Vector3.One });
W.Entity entity = W.NewEntity<Bullet>().Set(
    new Position { Value = Vector3.One },
    new Velocity { Value = 10f },
    new Damage { Value = 5 }
);

// Создание в конкретном чанке
W.Entity entity = W.NewEntityInChunk<Bullet>(chunkIdx: chunkIdx);

// Создание по GID (для десериализации и сетевой синхронизации)
W.Entity entity = W.NewEntityByGID<Default>(gid);

// Не-дженерик перегрузки (тип сущности известен только в runtime, например при десериализации)
byte entityTypeId = EntityTypeInfo<Bullet>.Id;
W.Entity entity = W.NewEntity(entityTypeId, clusterId: LEVEL_1_CLUSTER);
W.Entity entity = W.NewEntityInChunk(entityTypeId, chunkIdx: chunkIdx);
W.Entity entity = W.NewEntityByGID(entityTypeId, gid);

Создание в зависимом мире (Try):

Зависимый мир (Independent = false) делит пространство слотов с другими мирами. Если выделенные ему слоты исчерпаны, создание сущности невозможно.

// Вернёт false, если в зависимом мире закончились выделенные слоты
if (W.TryNewEntity<Bullet>(out var entity, clusterId: LEVEL_1_CLUSTER)) {
    entity.Set(new Position { Value = Vector3.Zero });
}

Массовое создание

uint count = 1000;

// Без компонентов
W.NewEntities<Default>(count);

// С компонентами по типу (от 1 до 5 типов)
W.NewEntities<Default, Position>(count);
W.NewEntities<Default, Position, Velocity>(count);

// С компонентами по значению (от 1 до 8 компонентов)
W.NewEntities<Default>(count, new Position { Value = Vector3.Zero });
W.NewEntities<Default>(count,
    new Position { Value = Vector3.Zero },
    new Velocity { Value = 1f }
);

// С делегатом инициализации каждой сущности
W.NewEntities<Default, Position>(count, onCreate: static entity => {
    entity.Set<Unit>();
});

// С кластером
W.NewEntities<Default, Position>(count, clusterId: LEVEL_1_CLUSTER);

// Полная перегрузка: значения + кластер + делегат
W.NewEntities<Default>(count,
    new Position { Value = Vector3.Zero },
    clusterId: LEVEL_1_CLUSTER,
    onCreate: static entity => {
        entity.Set<Unit>();
    }
);

Свойства

W.Entity entity = W.NewEntity<Bullet>();

uint id = entity.ID;                         // Внутренний индекс слота
EntityGID gid = entity.GID;                  // Глобальный идентификатор (8 байт)
EntityGIDCompact gidC = entity.GIDCompact;   // Компактный идентификатор (4 байта)
ushort version = entity.Version;             // Счётчик поколений слота
ushort clusterId = entity.ClusterId;         // Идентификатор кластера
byte entityType = entity.EntityType;         // Тип сущности (0–255)
uint chunkId = entity.ChunkID;              // Индекс чанка

bool alive = entity.IsNotDestroyed;          // Не уничтожена
bool destroyed = entity.IsDestroyed;         // Уничтожена
bool enabled = entity.IsEnabled;             // Включена (участвует в запросах)
bool disabled = entity.IsDisabled;           // Отключена
bool selfOwned = entity.IsSelfOwned;         // Сегмент принадлежит этому миру

// Проверки типа сущности
bool isBullet = entity.Is<Bullet>();                    // Точное совпадение типа
bool isProjectile = entity.IsAny<Bullet, Rocket>();     // Любой из типов
bool isNotEffect = entity.IsNot<Effect>();              // Не этот тип
bool isNotVfx = entity.IsNot<Effect, Particle>();       // Ни один из указанных

string info = entity.PrettyString;           // Отладочная строка

Жизненный цикл

// Отключить сущность — исключается из стандартных запросов, но сохраняет все данные
entity.Disable();

// Включить обратно
entity.Enable();

// Уничтожить — удаляет все компоненты (с вызовом OnDelete), теги, освобождает слот
// Возвращает bool: true если сущность была жива и уничтожена, false если уже уничтожена
entity.Destroy();

// Выгрузить из памяти — сущность становится невидимой, но её ID сохраняется
// Используется для стриминга (временная выгрузка с последующей загрузкой через сериализацию)
entity.Unload();

// Инкрементировать версию без уничтожения — все ранее полученные GID станут невалидными
entity.UpVersion();

Клонирование и перенос

// Клонировать сущность — создаёт новую сущность с копией всех компонентов и тегов
W.Entity clone = entity.Clone();

// Клонировать в другой кластер
W.Entity clone = entity.Clone(clusterId: OTHER_CLUSTER);

// Копировать все компоненты и теги в существующую сущность
// Если у целевой сущности уже есть совпадающие компоненты — они перезаписываются
entity.CopyTo(targetEntity);

// Перенести все данные в существующую сущность и уничтожить исходную
entity.MoveTo(targetEntity);

// Перенести в другой кластер — создаёт новую сущность, копирует данные, уничтожает исходную
W.Entity moved = entity.MoveTo(clusterId: OTHER_CLUSTER);

Компоненты

API работы с компонентами описан в разделе Компоненты.


Теги

API работы с тегами описан в разделе Теги.


Мульти-компоненты

API мульти-компонентов описан в разделе Мульти-компоненты.


Отношения

API отношений между сущностями описан в разделе Отношения.


Проверка по фильтру запроса

IsMatch<TFilter> проверяет, проходит ли сущность тот же фильтр, что используется в Query<TFilter>.

// Проверка по типу фильтра
bool ok = entity.IsMatch<All<Position, Velocity>>();

// С передачей значения фильтра — удобно для составных And/Or
var filter = And.By(default(None<Stunned>), default(All<Player>));
bool ready = entity.IsMatch(filter);

Отладка

// Отладочная строка с полной информацией
string info = entity.PrettyString;

// Количество компонентов (включённых + отключённых)
int compCount = entity.ComponentsCount();

// Количество тегов
int tagCount = entity.TagsCount();

// Получить все компоненты (список очищается перед заполнением)
var components = new List<IComponent>();
entity.GetAllComponents(components);

// Получить все теги (список очищается перед заполнением)
var tags = new List<ITag>();
entity.GetAllTags(tags);

Фильтры запросов по типу сущности

Фильтры по типу сущности описаны в разделе Запросы — Типы сущностей.


Операторы и преобразования

W.Entity a = W.NewEntity<Default>();
W.Entity b = W.NewEntity<Default>();

// Сравнение по индексу слота (без проверки версии)
bool eq = a == b;
bool neq = a != b;

// Неявное преобразование Entity → EntityGID (8 байт)
EntityGID gid = entity;

// Явное преобразование Entity → EntityGIDCompact (4 байта)
// В DEBUG бросит ошибку если Chunk >= 4 или ClusterId >= 4
EntityGIDCompact compact = (EntityGIDCompact)entity;

// Преобразование в типизированную связь (для системы отношений)
Link<ChildOf> link = entity.AsLink<ChildOf>();

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