Skip to content

Fixed Collections

Components must be unmanaged structs, so a component field can't hold a List<T> or an array. For small bounded collections inside a component — equipment slots, active buffs, tracked targets — Trecs.Collections provides two size-specialized value types that store elements inline.

Type Shape Use when
FixedArray<N> Fixed length, every slot always live The count is the capacity — no runtime "how many?"
FixedList<N> Fixed capacity, variable Count The count varies per entity up to a known bound

Both require T : unmanaged. Available sizes: 2, 4, 8, 16, 32, 64, 128, 256.

All fixed collections are marked [NonCopyable] because their data is stored inline — copying the struct duplicates the internal storage, and mutations on the copy silently leave the original unchanged. Pass them by ref or in, not by value. Any struct that contains a fixed-collection field is itself non-copyable (the rule propagates transitively). See Copy semantics for details.

Read / write split

The indexer returns ref readonly T — read with arr[i], but you can't write through it. Writes go through .Mut(i), which returns ref T:

ref readonly var x = ref arr[i];  // readonly ref (no copy, works through `in` too)
arr.Mut(i) = 5;                    // write (requires mutable reference to arr)
ref var r = ref arr.Mut(i);        // mutable ref for in-place updates
r += 1;

This makes in FixedArray<N> / in FixedList<N> parameters safe: readers can index freely (no defensive copy, no hidden mutation), and writes fail to compile because Mut is a this ref extension that can't be called through a readonly reference.

void Foo(in FixedArray16<int> arr)
{
    int x = arr[0];      // OK
    arr.Mut(0) = 5;      // COMPILE ERROR: arr is readonly, can't call `Mut`
}

FixedArray<N>

A fixed-length array stored inline. default(FixedArray8<float3>) gives 8 zeroed slots; nothing to initialize.

using Trecs.Collections;

public partial struct Waypoints : IEntityComponent
{
    public FixedArray8<float3> Points;
}

ref Waypoints wp = ref handle.Component<Waypoints>(world).Write;
wp.Points.Mut(0) = new float3(1, 0, 0);
ref readonly float3 p = ref wp.Points[2];   // ref readonly — no element copy
ref float3 m = ref wp.Points.Mut(3);         // mutable ref
m.x += 1.0f;

Members: Length, readonly indexer, == / !=. Extension: Mut(i)ref T.

FixedList<N>

A FixedList<N><T> is a FixedArray<N><T> plus a Count of live slots. Capacity is fixed at N; Count starts at 0 and grows with Add up to Capacity.

public partial struct ContactPoints : IEntityComponent
{
    public FixedList16<EntityHandle> Contacts;
}

ref ContactPoints cp = ref handle.Component<ContactPoints>(world).Write;
cp.Contacts.Add(otherHandle);

// Remove-during-iterate: walk backwards to avoid index skips.
for (int i = cp.Contacts.Count - 1; i >= 0; i--)
{
    if (ShouldRemove(cp.Contacts[i]))
        cp.Contacts.RemoveAtSwapBack(i);
}

Members: Count, Capacity, IsEmpty, readonly indexer, == / !=. Extensions: Mut(i), Add, Clear, RemoveAt, RemoveAtSwapBack.

Operation Cost Preserves order?
Add(item) O(1)
Clear() O(1) (just resets Count; does not zero the buffer)
RemoveAt(i) O(N) (shifts trailing elements) Yes
RemoveAtSwapBack(i) O(1) (overwrites slot i with the last element) No
Mut(i) = x O(1)

== compares Count and live slots only — bytes past Count (leftover from prior Clear / RemoveAt calls, or uninitialized) are ignored.

Choosing a size

The type name picks the footprint. A FixedArray256<float4x4> is 16 KB per entity, used or not. Pick the smallest variant that covers your worst case, rounding up to the next power of two — for a FixedList<100>, use FixedList128 and accept the slack. Memory is usually cheap.

When to reach for something else

  • The upper bound varies widely across entities, or usually sits far below the cap. A FixedArray256<T> that's typically empty wastes storage on every entity in the template. Use a dynamic collection (TrecsList<T> / TrecsArray<T> / TrecsDictionary<TKey, TValue>) instead — the component holds a small inline handle and the elements grow on the world's heap, snapshot-safe.

Relation to Unity's FixedList*Bytes

Unity's Unity.Collections ships FixedList32Bytes<T> through FixedList4096Bytes<T> — same idea, different axis. Differences:

Trecs Unity FixedList*Bytes
Sizing axis Element count (FixedList16<T> = 16 slots, always) Total bytes (FixedList64Bytes<T> = ~62 / sizeof(T) slots)
Element read ref readonly T — zero-copy, including through in parameters T by value — copies the element on every read. .ElementAt(i) returns ref T but isn't callable through in
Element write .Mut(i) returns ref T; not callable through in (compile-error) list[i] = x setter; not callable through in (compile-error)
API surface Minimal: Add, Clear, RemoveAt, RemoveAtSwapBack, Mut Extensive: IndexOf, Contains, Sort, IEnumerable<T>, cross-size equality
Count-less variant FixedArray<N> none