MultiComponent
Мультикомпоненты — оптимизированные компоненты-списки, позволяющие хранить множество однотипных значений на одной сущности
- Все элементы всех мультикомпонентов одного типа для всех сущностей хранятся в едином хранилище — оптимальное потребление памяти
- Вместимость от 4 до 32768 значений в одном компоненте, автоматическое расширение
- Не требует создания массивов или списков внутри компонента — без аллокаций на куче
- Является реализацией компонента, все базовые правила работы аналогичны
- На базе мультикомпонентов реализованы отношения сущностей (
Links<T>) - Контейнер
Multi<TValue>встроенно реализуетIDisableable—entity.Disable<Multi<MyValue>>()/Enable<Multi<MyValue>>()работают без дополнительной декларации. См. Component / Enable-Disable
Определение типа
Тип значения мультикомпонента должен реализовать интерфейс IMultiComponent и быть struct:
// Unmanaged тип — сериализация работает автоматически через bulk memory copy
public struct Item : IMultiComponent {
public int Id;
public float Weight;
}
Не-unmanaged (managed) типы должны реализовать хуки Write/Read для сериализации:
// Managed тип — требует Write/Read хуки для сериализации
public struct NamedItem : IMultiComponent {
public string Name;
public int Count;
public void Write(ref BinaryPackWriter writer) {
writer.Write(in Name);
writer.Write(in Count);
}
public void Read(ref BinaryPackReader reader) {
Name = reader.Read<string>();
Count = reader.ReadInt();
}
}
Стратегия сериализации
Стратегия сериализации элементов выбирается автоматически:
- Для unmanaged типов —
UnmanagedPackArrayStrategy<T>(bulk memory copy, быстрее) - Для managed типов —
StructPackArrayStrategy<T>(поэлементно через хукиWrite/Read)
Для переопределения стратегии или предоставления пользовательской конфигурации реализуйте IMultiComponentConfig<T>:
public struct Item : IMultiComponent, IMultiComponentConfig<Item> {
public int Id;
public float Weight;
public ComponentTypeConfig<W.Multi<Item>> Config<TWorld>() where TWorld : struct, IWorldType => default;
public IPackArrayStrategy<Item> ElementPackStrategy() => new UnmanagedPackArrayStrategy<Item>();
}
Блочная сериализация сегментов
Для снимков чанков/мира/кластеров, когда TValue является unmanaged, можно использовать MultiUnmanagedPackArrayStrategy<TWorld, TValue> для сериализации целых сегментов хранилища одним блоком памяти вместо поэлементных данных каждой сущности. Это заменяет множество мелких per-entity копирований одной bulk-операцией на сегмент и восстанавливает состояние аллокатора напрямую.
Для unmanaged типов MultiUnmanagedPackArrayStrategy применяется автоматически. Для предоставления пользовательской конфигурации:
public struct Item : IMultiComponent, IMultiComponentConfig<Item> {
public int Id;
public float Weight;
public ComponentTypeConfig<W.Multi<Item>> Config<TWorld>() where TWorld : struct, IWorldType => new(
guid: new Guid("...")
);
public IPackArrayStrategy<Item> ElementPackStrategy() => null; // null = авто-определение
}
Эта стратегия сериализует сырые байты структуры Multi<T> плюс сегменты хранилища значений и состояние аллокатора. Entity-level сериализация (EntitiesSnapshot) продолжает использовать per-entity хуки Write/Read — оптимизация применяется только к снимкам чанков/мира/кластеров.
MultiUnmanagedPackArrayStrategy требует чтобы Multi<TValue> удовлетворял ограничению unmanaged. Поскольку поля Multi<T> — все value-типы, это работает для конкретных типов TValue, но нельзя использовать в generic-коде регистрации — указывайте явно для каждого конкретного типа.
Регистрация
W.Create(WorldConfig.Default());
W.Types()
.Multi<Item>() // авто-определение стратегии (UnmanagedPackArrayStrategy для unmanaged типов)
.Multi<NamedItem>(); // managed тип — используется StructPackArrayStrategy с хуками Write/Read
W.Initialize();
Основные операции
Мультикомпонент работает как обычный компонент:
// Добавить (начальная ёмкость — 4 элемента, расширяется автоматически)
ref var items = ref entity.Add<W.Multi<Item>>();
// Получить ссылку
ref var items = ref entity.Ref<W.Multi<Item>>();
// Проверить наличие
bool has = entity.Has<W.Multi<Item>>();
// Удалить (список элементов очищается автоматически)
entity.Delete<W.Multi<Item>>();
// При клонировании и копировании — все элементы копируются автоматически
var clone = entity.Clone();
entity.CopyTo<W.Multi<Item>>(targetEntity);
Свойства
ref var items = ref entity.Ref<W.Multi<Item>>();
ushort len = items.Length; // Количество элементов
ushort cap = items.Capacity; // Текущая ёмкость
bool empty = items.IsEmpty; // Пусто
bool notEmpty = items.IsNotEmpty; // Не пусто
bool full = items.IsFull; // Заполнено до ёмкости
// Доступ по индексу (возвращает ref)
ref var first = ref items[0];
ref var last = ref items[items.Length - 1];
// Первый и последний элемент
ref var f = ref items.First();
ref var l = ref items.Last();
// Span для прямого доступа к памяти
Span<Item> span = items.AsSpan;
ReadOnlySpan<Item> roSpan = items.AsReadOnlySpan;
// Неявное преобразование в Span
Span<Item> span = items;
ReadOnlySpan<Item> roSpan = items;
Добавление
// Один элемент
items.Add(new Item { Id = 1, Weight = 0.5f });
// Несколько (от 2 до 4)
items.Add(
new Item { Id = 1, Weight = 0.5f },
new Item { Id = 2, Weight = 1.0f }
);
items.Add(
new Item { Id = 1, Weight = 0.5f },
new Item { Id = 2, Weight = 1.0f },
new Item { Id = 3, Weight = 1.5f },
new Item { Id = 4, Weight = 2.0f }
);
// Из массива
Item[] array = { new Item { Id = 5 }, new Item { Id = 6 } };
items.Add(array);
// Из среза массива
items.Add(array, srcIdx: 0, len: 1);
// Вставка в указанный индекс (остальные элементы сдвигаются)
items.InsertAt(idx: 1, new Item { Id = 10 });
Управление ёмкостью:
// Гарантировать место для N дополнительных элементов
items.EnsureSize(10);
// Увеличить Length на N (с предварительным расширением если нужно)
items.EnsureCount(5);
// Увеличить Length на N без инициализации данных (низкоуровневая операция)
items.EnsureCountUninitialized(5);
// Установить минимальную ёмкость
items.Resize(32);
Удаление
// По индексу (с сохранением порядка — сдвигает элементы)
items.RemoveAt(idx: 1);
// По индексу (swap-remove — заменяет последним, быстрее, порядок не сохраняется)
items.RemoveAtSwap(idx: 1);
// Первый элемент
items.RemoveFirst(); // с сохранением порядка
items.RemoveFirstSwap(); // swap-remove
// Последний элемент
items.RemoveLast();
// По значению (возвращает true если найден)
bool removed = items.TryRemove(new Item { Id = 1 });
// По значению со swap-remove
bool removed = items.TryRemoveSwap(new Item { Id = 1 });
// Два элемента по значению
items.TryRemove(new Item { Id = 1 }, new Item { Id = 2 });
// Очистить все элементы
items.Clear();
// Сбросить счётчик без очистки данных (низкоуровневая операция)
items.ResetCount();
Поиск
// Индекс элемента (-1 если не найден)
int idx = items.IndexOf(new Item { Id = 1 });
// Проверить наличие
bool exists = items.Contains(new Item { Id = 1 });
// С пользовательским компаратором
bool exists = items.Contains(new Item { Id = 1 }, comparer);
Итерация
// foreach — мутабельный доступ по ссылке
foreach (ref var item in items) {
item.Weight *= 2f;
}
// for — доступ по индексу
for (int i = 0; i < items.Length; i++) {
ref var item = ref items[i];
item.Weight *= 2f;
}
// Через Span
foreach (ref var item in items.AsSpan) {
item.Weight *= 2f;
}
Копирование и сортировка
// Копировать в массив
var array = new Item[items.Length];
items.CopyTo(array);
// Копировать срез
items.CopyTo(array, dstIdx: 0, len: 5);
// Сортировка
items.Sort();
// С пользовательским компаратором
items.Sort(comparer);
Запросы
Мультикомпоненты используются в запросах как обычные компоненты:
// Все сущности с инвентарём
W.Query().For(static (W.Entity entity, ref W.Multi<Item> items) => {
for (int i = 0; i < items.Length; i++) {
ref var item = ref items[i];
// ...
}
});
// С фильтрацией
foreach (var entity in W.Query<All<W.Multi<Item>>>().Entities()) {
ref var items = ref entity.Ref<W.Multi<Item>>();
// ...
}