MultiComponent
多组件是优化的列表组件,允许在单个实体上存储多个相同类型的值
- 所有实体的所有同类型多组件的所有元素存储在统一存储中 — 最优内存使用
- 每个组件容量从 4 到 32768 个值,自动扩展
- 无需在组件内创建数组或列表 — 零堆分配
- 实现了组件,所有基本规则适用
- 实体关系(
Links<T>)基于多组件构建
类型定义
多组件值类型必须实现接口 IMultiComponent 并且为 struct:
// Unmanaged 类型 — 通过批量内存复制自动序列化
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();
}
}
序列化策略
默认使用 StructPackArrayStrategy<T> 进行逐元素序列化(通过钩子)。 对于 unmanaged 类型,可以使用 UnmanagedPackArrayStrategy<T> 进行批量内存复制(更快)。
可以通过三种方式指定策略:
1. 注册时显式参数:
W.Types()
.Multi<Item>(elementStrategy: new UnmanagedPackArrayStrategy<Item>());
2. 类型上的静态字段/属性(用于通过 RegisterAll 自动注册):
public struct Item : IMultiComponent {
public int Id;
public float Weight;
static readonly IPackArrayStrategy<Item> PackStrategy = new UnmanagedPackArrayStrategy<Item>();
}
3. 默认: StructPackArrayStrategy<T> — 逐元素使用 Write/Read 钩子。
批量段序列化
对于 chunk/world/cluster 快照,当 TValue 为 unmanaged 类型时,可以使用 MultiUnmanagedPackArrayStrategy<TWorld, TValue> 将整个存储段作为内存块序列化,而不是逐实体序列化元素数据。这将许多小的逐实体拷贝替换为每段一次批量操作,并直接恢复分配器状态。
W.Types()
.Multi<Item>(new ComponentTypeConfig<W.Multi<Item>>(
guid: new Guid("..."),
readWriteStrategy: new MultiUnmanagedPackArrayStrategy<MyWorld, Item>()
));
此策略序列化 Multi<T> 结构体的原始字节加上底层值存储段和分配器状态。实体级序列化(EntitiesSnapshot)继续使用逐实体 Write/Read 钩子——此优化仅适用于 chunk/world/cluster 快照。
MultiUnmanagedPackArrayStrategy 要求 Multi<TValue> 满足 unmanaged 约束。由于 Multi<T> 的字段均为值类型,这适用于具体的 TValue 类型,但不能在泛型注册代码中使用——请为每个具体类型显式指定。
注册
W.Create(WorldConfig.Default());
W.Types()
.Multi<Item>() // 默认策略(StructPackArrayStrategy)
.Multi<Item>(elementStrategy: new UnmanagedPackArrayStrategy<Item>()) // 显式 unmanaged 策略
.Multi<NamedItem>(); // 带钩子的 managed 类型
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>>();
// ...
}