Отношения

Отношения — механизм связи сущностей друг с другом через типизированные компоненты-ссылки

  • Link<T> — связь с одной сущностью (обёртка над EntityGID)
  • Links<T> — связь с несколькими сущностями (динамическая коллекция Link<T>)
  • Связи являются обычными компонентами и работают через стандартный API (Add, Ref, Delete, Has)
  • Поддерживают хуки (OnAdd, OnDelete, CopyTo) для автоматизации логики (например, обратных ссылок)

Типы связей

Для определения типа связи реализуйте один из интерфейсов:

// ILinkType — тип для одиночной связи (Link<T>)
// Реализуйте только те хуки, которые нужны
public struct Parent : ILinkType {
    // Вызывается при добавлении связи
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        // self — сущность, к которой добавлена связь
        // link — GID целевой сущности
    }

    // Вызывается при удалении связи
    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        // ...
    }

    // Вызывается при копировании сущности (Clone/CopyTo)
    public void CopyTo<TW>(World<TW>.Entity self, World<TW>.Entity other, EntityGID link) where TW : struct, IWorldType {
        // ...
    }
}

// ILinksType — тип для множественной связи (Links<T>)
// Наследуется от ILinkType, те же хуки
public struct Children : ILinksType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        // ...
    }
}

// Тип без хуков — просто не реализуйте методы
public struct FollowTarget : ILinkType { }

Не оставляйте пустые реализации хуков. Если хук не нужен — не реализуйте его. Нереализованные хуки не вызываются и не создают накладных расходов.


Link<T>

Компонент одиночной связи — обёртка над EntityGID (8 байт).

Link<T> и Links<T> реализуют IDisableable встроенно — entity.Disable<Link<Parent>>() / Enable<Link<Parent>>() работают без дополнительной декларации на ваших link-типах. См. Component / Enable-Disable.

// Свойства
EntityGID value = link.Value;    // GID целевой сущности (только чтение)

// Неявные преобразования
W.Link<Parent> link = entity;              // Entity → Link<T>
W.Link<Parent> link = entity.GID;          // EntityGID → Link<T>
W.Link<Parent> link = entity.GIDCompact;   // EntityGIDCompact → Link<T>
EntityGID gid = link;                      // Link<T> → EntityGID

// Создание через конструктор
var link = new W.Link<Parent>(targetGID);

// Создание через entity.AsLink
W.Link<Parent> link = entity.AsLink<Parent>();

Links<T>

Мультикомпонент — динамическая коллекция Link<T> с автоматическим управлением памятью.

Свойства:

ref var links = ref entity.Ref<W.Links<Children>>();

ushort len = links.Length;       // Количество элементов
ushort cap = links.Capacity;     // Текущая ёмкость
bool empty = links.IsEmpty;      // Пусто
bool notEmpty = links.IsNotEmpty; // Не пусто
bool full = links.IsFull;        // Заполнено до ёмкости

// Доступ по индексу
W.Link<Children> first = links[0];
W.Link<Children> last = links[links.Length - 1];

// Первый и последний элемент
W.Link<Children> f = links.First();
W.Link<Children> l = links.Last();

// Span для чтения
ReadOnlySpan<W.Link<Children>> span = links.AsReadOnlySpan;

// Итерация
foreach (var link in links) {
    if (link.Value.TryUnpack<WT>(out var child)) {
        // ...
    }
}

Добавление:

// TryAdd — не добавляет если уже существует, возвращает false
bool added = links.TryAdd(childLink);

// TryAdd нескольких (от 2 до 4)
links.TryAdd(child1, child2);
links.TryAdd(child1, child2, child3, child4);

// Add — добавляет, в DEBUG бросает ошибку при дубликате
links.Add(childLink);
links.Add(child1, child2);

// Add из массива
links.Add(childArray);
links.Add(childArray, srcIdx: 0, len: 3);

Удаление:

// По значению (возвращает true если найден)
bool removed = links.TryRemove(childLink);

// По значению со swap-remove (не сохраняет порядок, быстрее)
bool removed = links.TryRemoveSwap(childLink);

// По индексу
links.RemoveAt(0);
links.RemoveAtSwap(0);

// Первый / последний
links.RemoveFirst();
links.RemoveFirstSwap();
links.RemoveLast();

// Удалить все (вызывает OnDelete для каждого элемента)
links.Clear();

Поиск:

bool exists = links.Contains(childLink);
int idx = links.IndexOf(childLink);

Управление памятью:

links.EnsureSize(10);        // Гарантировать место для 10 дополнительных элементов
links.Resize(32);            // Изменить ёмкость
links.Sort();                // Сортировка

Регистрация

Связи регистрируются как обычные компоненты на этапе создания мира:

W.Create(WorldConfig.Default());

W.Types()
    .Link<Parent>()
    .Links<Children>();

W.Initialize();

Работа со связями

Связи — обычные компоненты. Все стандартные методы работают:

var parent = W.NewEntity<Default>();
var child1 = W.NewEntity<Default>();
var child2 = W.NewEntity<Default>();

// Добавить одиночную связь
child1.Set(new W.Link<Parent>(parent));
child2.Set(new W.Link<Parent>(parent));

// Получить ссылку
ref var parentLink = ref child1.Ref<W.Link<Parent>>();
EntityGID parentGID = parentLink.Value;

// Проверить наличие
bool hasParent = child1.Has<W.Link<Parent>>();

// Удалить связь
child1.Delete<W.Link<Parent>>();

// Добавить множественную связь
ref var children = ref parent.Add<W.Links<Children>>();
children.TryAdd(child1.AsLink<Children>());
children.TryAdd(child2.AsLink<Children>());

// Прочитать множественную связь
ref var kids = ref parent.Ref<W.Links<Children>>();
for (int i = 0; i < kids.Length; i++) {
    if (kids[i].Value.TryUnpack<WT>(out var childEntity)) {
        // работа с дочерней сущностью
    }
}

Extension-методы

Безопасные операции со связями через EntityGID — автоматически проверяют загруженность и актуальность целевой сущности.

// Добавить связь Link<T> к целевой сущности
LinkOppStatus status = targetGID.TryAddLink<WT, Parent>(linkEntity);

// Удалить связь Link<T> у целевой сущности
LinkOppStatus status = targetGID.TryDeleteLink<WT, Parent>(linkEntity);

// Глубокое уничтожение — рекурсивно уничтожает цепочку связанных сущностей
targetGID.DeepDestroyLink<WT, Parent>();

// Глубокое копирование — клонирует целевую сущность и возвращает ссылку на копию
LinkOppStatus status = sourceGID.TryDeepCopyLink<WT, Parent>(out W.Link<Parent> copied);
// Добавить элемент в Links<T> целевой сущности
// Автоматически создаёт компонент Links<T> если его ещё нет
LinkOppStatus status = targetGID.TryAddLinkItem<WT, Children>(linkEntity);

// Удалить элемент из Links<T> целевой сущности
// Автоматически удаляет компонент Links<T> если коллекция стала пустой
LinkOppStatus status = targetGID.TryDeleteLinkItem<WT, Children>(linkEntity);

// Глубокое уничтожение — рекурсивно уничтожает все связанные сущности
targetGID.DeepDestroyLinkItem<WT, Children>();

LinkOppStatus:

// Результат операции
switch (status) {
    case LinkOppStatus.Ok:                // Операция выполнена успешно
    case LinkOppStatus.LinkAlreadyExists: // Связь уже существует (TryAdd)
    case LinkOppStatus.LinkNotExists:     // Связь не найдена (TryDelete)
    case LinkOppStatus.LinkNotLoaded:     // Целевая сущность в выгруженном чанке
    case LinkOppStatus.LinkNotActual:     // GID устарел (сущность уничтожена, слот переиспользован)
}

Примеры связей

Однонаправленная связь (без хуков)

Простейший случай — сущность ссылается на другую без обратной связи.

// Тип без хуков
public struct FollowTarget : ILinkType { }

// Регистрация
W.Types().Link<FollowTarget>();
//  A FollowTarget→ B

var unit = W.NewEntity<Default>();
var target = W.NewEntity<Default>();

// Установить цель для преследования
unit.Set(new W.Link<FollowTarget>(target));

// В системе движения
W.Query().For(static (W.Entity entity, ref W.Link<FollowTarget> follow) => {
    if (follow.Value.TryUnpack<WT>(out var targetEntity)) {
        ref var myPos = ref entity.Ref<Position>();
        ref readonly var targetPos = ref targetEntity.Read<Position>();
        // двигаться к цели
    }
});

Двунаправленная связь один к одному (One-To-One)

Замкнутая пара — обе сущности ссылаются друг на друга одним типом.

//    MarriedTo
//  A ────────→ B
//  A ←──────── B
//    MarriedTo

public struct MarriedTo : ILinkType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLink<TW, MarriedTo>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLink<TW, MarriedTo>(self);
    }
}

W.Types().Link<MarriedTo>();
var alice = W.NewEntity<Default>();
var bob = W.NewEntity<Default>();

// Достаточно установить с одной стороны — обратная создастся автоматически
alice.Set(new W.Link<MarriedTo>(bob));
// Теперь: alice имеет Link<MarriedTo> → bob
//         bob имеет Link<MarriedTo> → alice

// Удаление тоже двустороннее
alice.Delete<W.Link<MarriedTo>>();
// Теперь: оба компонента удалены

Двунаправленная связь один к одному (разные типы)

Две сущности связаны разными типами связей.

//  A ←Rider── Mount──→ B

public struct Mount : ILinkType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLink<TW, Rider>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLink<TW, Rider>(self);
    }
}

public struct Rider : ILinkType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLink<TW, Mount>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLink<TW, Mount>(self);
    }
}

W.Types()
    .Link<Mount>()
    .Link<Rider>();
var player = W.NewEntity<Default>();
var horse = W.NewEntity<Default>();

player.Set(new W.Link<Mount>(horse));
// player имеет Link<Mount> → horse
// horse имеет Link<Rider> → player

Двунаправленная связь один ко многим (One-To-Many)

Родитель и дети — классическая иерархия.

//      ←Parent  Children→ child1
//     /
//  parent ←Parent  Children→ child2
//     \
//      ←Parent  Children→ child3

public struct Parent : ILinkType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLinkItem<TW, Children>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLinkItem<TW, Children>(self);
    }
}

public struct Children : ILinksType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLink<TW, Parent>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLink<TW, Parent>(self);
    }
}

W.Types()
    .Link<Parent>()
    .Links<Children>();
var father = W.NewEntity<Default>();
var son = W.NewEntity<Default>();
var daughter = W.NewEntity<Default>();

// Установить связь с любой стороны
son.Set(new W.Link<Parent>(father));
daughter.Set(new W.Link<Parent>(father));
// father автоматически получит Links<Children> → [son, daughter]

// Или добавить со стороны родителя
ref var kids = ref father.Ref<W.Links<Children>>();
var newChild = W.NewEntity<Default>();
kids.TryAdd(newChild.AsLink<Children>());
// newChild автоматически получит Link<Parent> → father

withCyclicHooks: false (значение по умолчанию) в extension-методах TryAddLink/TryDeleteLink/TryAddLinkItem/TryDeleteLinkItem — это оптимизация: при вызове из хука не нужно вызывать хук у противоположной стороны, так как он уже выполняется.


Однонаправленная связь ко многим (To-Many)

Сущность ссылается на несколько других без обратной связи.

//      Targets→ B
//     /
//  A── Targets→ C
//     \
//      Targets→ D

public struct Targets : ILinksType { }

W.Types().Links<Targets>();
var turret = W.NewEntity<Default>();
var enemy1 = W.NewEntity<Default>();
var enemy2 = W.NewEntity<Default>();

ref var targets = ref turret.Add<W.Links<Targets>>();
targets.TryAdd(enemy1.AsLink<Targets>());
targets.TryAdd(enemy2.AsLink<Targets>());

Двунаправленная связь многие ко многим (Many-To-Many)

Обе стороны хранят коллекции ссылок друг на друга.

//      ←Owners  Memberships→ groupA
//     /
//  user1 ←Owners  Memberships→ groupB
//
//  user2 ←Owners  Memberships→ groupA

public struct Memberships : ILinksType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLinkItem<TW, Owners>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLinkItem<TW, Owners>(self);
    }
}

public struct Owners : ILinksType {
    public void OnAdd<TW>(World<TW>.Entity self, EntityGID link) where TW : struct, IWorldType {
        link.TryAddLinkItem<TW, Memberships>(self);
    }

    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        link.TryDeleteLinkItem<TW, Memberships>(self);
    }
}

W.Types()
    .Links<Memberships>()
    .Links<Owners>();
var user1 = W.NewEntity<Default>();
var user2 = W.NewEntity<Default>();
var groupA = W.NewEntity<Default>();
var groupB = W.NewEntity<Default>();

// Добавить user1 в обе группы
ref var memberships = ref user1.Add<W.Links<Memberships>>();
memberships.TryAdd(groupA.AsLink<Memberships>());
memberships.TryAdd(groupB.AsLink<Memberships>());
// groupA и groupB автоматически получат Links<Owners> → [user1]

// Добавить user2 в groupA
ref var memberships2 = ref user2.Add<W.Links<Memberships>>();
memberships2.TryAdd(groupA.AsLink<Memberships>());
// groupA теперь имеет Links<Owners> → [user1, user2]

Для снимков чанков/мира/кластеров с unmanaged типами связей LinksUnmanagedPackArrayStrategy применяется автоматически — ручная настройка не требуется.

Для предоставления пользовательской конфигурации при регистрации связей реализуйте ILinksConfig<T> на типе связи:

public struct MyLinkType : ILinksType, ILinksConfig<MyLinkType> {
    public ComponentTypeConfig<W.Links<MyLinkType>> Config<TWorld>() where TWorld : struct, IWorldType => new(
        guid: new Guid("...")
    );
}

Работает идентично MultiUnmanagedPackArrayStrategy — подробности см. в блочная сериализация мульти-компонентов.


Многопоточность

В ForParallel разрешено модифицировать только текущую итерируемую сущность. Хуки связей, которые изменяют состояние других сущностей (например, добавляют обратную ссылку родителю), вызовут ошибку в DEBUG при параллельной итерации.

Для работы со связями в параллельных запросах используйте событияSendEvent потокобезопасен (при отсутствии одновременного чтения того же типа, подробнее см. События) и может вызываться из любого потока. Логику обработки событий выполняйте в основном потоке после завершения параллельной итерации.

Пример: отложенное удаление связи через события

// 1. Определяем событие
public struct DeleteLinkEvent<TLink> : IEvent where TLink : unmanaged, ILinkType {
    public EntityGID Target;    // сущность, у которой нужно удалить связь
    public EntityGID Link;      // значение связи для проверки
}

// 2. Регистрируем событие и создаём ресивер
W.Types().Event<DeleteLinkEvent<Parent>>();
var deleteLinkReceiver = W.RegisterEventReceiver<DeleteLinkEvent<Parent>>();

// Сохраняем ресивер в ресурсах мира для доступа из системы
W.SetResource(deleteLinkReceiver);
// 3. Определяем тип связи БЕЗ хуков изменяющих другие сущности
public struct Parent : ILinkType {
    // В OnDelete вместо прямого изменения родителя — отправляем событие
    public void OnDelete<TW>(World<TW>.Entity self, EntityGID link, HookReason reason) where TW : struct, IWorldType {
        World<TW>.SendEvent(new DeleteLinkEvent<Parent> {
            Target = link,
            Link = self.GID
        });
    }
}
// 4. Параллельная итерация — безопасна, хук отправляет событие вместо прямой модификации
W.Query().ForParallel(
    static (W.Entity entity, ref W.Link<Parent> parent) => {
        if (someCondition) {
            entity.Delete<W.Link<Parent>>();
            // OnDelete отправит DeleteLinkEvent вместо модификации родителя
        }
    },
    minEntitiesPerThread: 1000
);

// 5. В основном потоке обрабатываем все события
ref var receiver = ref W.GetResource<EventReceiver<WT, DeleteLinkEvent<Parent>>>();
receiver.ReadAll(static (W.Event<DeleteLinkEvent<Parent>> e) => {
    // Теперь безопасно модифицировать другие сущности
    ref var data = ref e.Value;
    data.Target.TryDeleteLinkItem<WT, Children>(data.Link.Unpack<WT>());
});

Запросы

Компоненты связей используются в запросах как любые другие компоненты:

// Все сущности с родителем
foreach (var entity in W.Query<All<W.Link<Parent>>>().Entities()) {
    ref var parentLink = ref entity.Ref<W.Link<Parent>>();
    // ...
}

// Все сущности с детьми, но без родителя (корневые)
W.Query<All<W.Links<Children>>, None<W.Link<Parent>>>()
    .For(static (W.Entity entity, ref W.Links<Children> kids) => {
        // корневые сущности
    });

// Через делегат
W.Query().For(static (ref W.Link<Parent> parent) => {
    if (parent.Value.TryUnpack<WT>(out var parentEntity)) {
        // ...
    }
});

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