MultiComponent

Multi-components are optimized list-components that allow storing multiple values of the same type on a single entity

  • All elements of all multi-components of one type for all entities are stored in a unified storage — optimal memory usage
  • Capacity from 4 to 32768 values per component, automatic expansion
  • No need to create arrays or lists inside a component — zero heap allocations
  • Implements component, all base rules apply
  • Entity relations (Links<T>) are built on top of multi-components
  • The container component Multi<TValue> implements IDisableable out of the box — entity.Disable<Multi<MyValue>>() / Enable<Multi<MyValue>>() work without extra declaration. See Component / Enable-Disable

Type definition

The multi-component value type must implement the interface IMultiComponent and be a struct:

// Unmanaged type — serialization works automatically via bulk memory copy
public struct Item : IMultiComponent {
    public int Id;
    public float Weight;
}

Non-unmanaged (managed) types must implement Write/Read hooks for serialization:

// Managed type — requires Write/Read hooks for serialization
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();
    }
}

Serialization strategy

The element serialization strategy is selected automatically:

  • For unmanaged types — UnmanagedPackArrayStrategy<T> (bulk memory copy, faster)
  • For managed types — StructPackArrayStrategy<T> (per-element via Write/Read hooks)

To override the strategy or provide custom configuration, implement 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>();
}

Bulk segment serialization

For chunk/world/cluster snapshots, when TValue is unmanaged, you can use MultiUnmanagedPackArrayStrategy<TWorld, TValue> to serialize entire storage segments as memory blocks instead of per-entity element data. This replaces many small per-entity copies with one bulk operation per segment and restores the allocator state directly.

For unmanaged types, MultiUnmanagedPackArrayStrategy is applied automatically. To provide custom configuration:

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 = auto-detect
}

This strategy serializes the raw Multi<T> struct bytes plus the underlying value storage segments and allocator state. Entity-level serialization (EntitiesSnapshot) continues using per-entity Write/Read hooks — this optimization only applies to chunk/world/cluster snapshots.

MultiUnmanagedPackArrayStrategy requires Multi<TValue> to satisfy the unmanaged constraint. Since Multi<T> fields are all value types, this works for concrete TValue types but cannot be used in generic registration code — specify it explicitly for each concrete type.


Registration

W.Create(WorldConfig.Default());

W.Types()
    .Multi<Item>()         // auto-detected strategy (UnmanagedPackArrayStrategy for unmanaged types)
    .Multi<NamedItem>();   // managed type — uses StructPackArrayStrategy with Write/Read hooks

W.Initialize();

Basic operations

Multi-components work like regular components:

// Add (initial capacity — 4 elements, expands automatically)
ref var items = ref entity.Add<W.Multi<Item>>();

// Get reference
ref var items = ref entity.Ref<W.Multi<Item>>();

// Check presence
bool has = entity.Has<W.Multi<Item>>();

// Delete (element list is cleared automatically)
entity.Delete<W.Multi<Item>>();

// On clone and copy — all elements are copied automatically
var clone = entity.Clone();
entity.CopyTo<W.Multi<Item>>(targetEntity);

Properties

ref var items = ref entity.Ref<W.Multi<Item>>();

ushort len = items.Length;       // Number of elements
ushort cap = items.Capacity;     // Current capacity
bool empty = items.IsEmpty;      // Empty
bool notEmpty = items.IsNotEmpty; // Not empty
bool full = items.IsFull;        // Filled to capacity

// Index access (returns ref)
ref var first = ref items[0];
ref var last = ref items[items.Length - 1];

// First and last element
ref var f = ref items.First();
ref var l = ref items.Last();

// Span for direct memory access
Span<Item> span = items.AsSpan;
ReadOnlySpan<Item> roSpan = items.AsReadOnlySpan;

// Implicit conversion to Span
Span<Item> span = items;
ReadOnlySpan<Item> roSpan = items;

Adding

// Single element
items.Add(new Item { Id = 1, Weight = 0.5f });

// Multiple (from 2 to 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 }
);

// From array
Item[] array = { new Item { Id = 5 }, new Item { Id = 6 } };
items.Add(array);

// From array slice
items.Add(array, srcIdx: 0, len: 1);

// Insert at index (remaining elements are shifted)
items.InsertAt(idx: 1, new Item { Id = 10 });

Capacity management:

// Ensure space for N additional elements
items.EnsureSize(10);

// Increase Length by N (with pre-expansion if needed)
items.EnsureCount(5);

// Increase Length by N without initializing data (low-level operation)
items.EnsureCountUninitialized(5);

// Set minimum capacity
items.Resize(32);

Removing

// By index (order-preserving — shifts elements)
items.RemoveAt(idx: 1);

// By index (swap-remove — replaces with last, faster, order not preserved)
items.RemoveAtSwap(idx: 1);

// First element
items.RemoveFirst();       // order-preserving
items.RemoveFirstSwap();   // swap-remove

// Last element
items.RemoveLast();

// By value (returns true if found)
bool removed = items.TryRemove(new Item { Id = 1 });

// By value with swap-remove
bool removed = items.TryRemoveSwap(new Item { Id = 1 });

// Two elements by value
items.TryRemove(new Item { Id = 1 }, new Item { Id = 2 });

// Clear all elements
items.Clear();

// Reset count without clearing data (low-level operation)
items.ResetCount();

// Element index (-1 if not found)
int idx = items.IndexOf(new Item { Id = 1 });

// Check presence
bool exists = items.Contains(new Item { Id = 1 });

// With custom comparer
bool exists = items.Contains(new Item { Id = 1 }, comparer);

Iteration

// foreach — mutable access by reference
foreach (ref var item in items) {
    item.Weight *= 2f;
}

// for — access by index
for (int i = 0; i < items.Length; i++) {
    ref var item = ref items[i];
    item.Weight *= 2f;
}

// Via Span
foreach (ref var item in items.AsSpan) {
    item.Weight *= 2f;
}

Copying and sorting

// Copy to array
var array = new Item[items.Length];
items.CopyTo(array);

// Copy slice
items.CopyTo(array, dstIdx: 0, len: 5);

// Sort
items.Sort();

// With custom comparer
items.Sort(comparer);

Queries

Multi-components are used in queries like regular components:

// All entities with inventory
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];
        // ...
    }
});

// With filtering
foreach (var entity in W.Query<All<W.Multi<Item>>>().Entities()) {
    ref var items = ref entity.Ref<W.Multi<Item>>();
    // ...
}

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