Fixed Collections¶
Components must be unmanaged structs, so a component field can't hold a List<T> or an array directly. When you need a small bounded collection inside a component — equipment slots on a character, active buffs on a unit, a small list of tracked targets — Trecs.Collections provides two size-specialized value types that store their 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.
Read / write split¶
The indexer on both types returns ref readonly T — you read with arr[i], but you can't write through it. Writes go through a separate .Mut(i) extension method that 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 split makes in FixedArray<N> / in FixedList<N> parameters genuinely safe: readers can index them 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 whose storage lives inline in its container. default(FixedArray8<float3>) gives you 8 zeroed slots; there's nothing to initialize.
using Trecs.Collections;
public struct Waypoints : IEntityComponent
{
public FixedArray8<float3> Points;
}
ref Waypoints wp = ref world.Component<Waypoints>(entityIndex).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 that tracks how many slots are live. Capacity is fixed at N; Count starts at 0 and grows with Add up to Capacity.
public struct ContactPoints : IEntityComponent
{
public FixedList16<EntityHandle> Contacts;
}
ref ContactPoints cp = ref world.Component<ContactPoints>(entityIndex).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 the live slots only — bytes past Count (leftover from prior Clear / RemoveAt calls, or uninitialized) are ignored.
Choosing a size¶
Type names pick the footprint. A FixedArray256<float4x4> is 16 KB — per entity, whether every slot is used or not. Pick the smallest variant that covers your worst case, and round up when the worst case doesn't hit a power of two. If you'd want a FixedList<100>, use FixedList128 and accept the slack. Memory is usually pretty cheap.
When to reach for something else¶
- The upper bound varies widely across entities, or is usually far below the cap. A
FixedArray256<T>that's typically empty wastes storage on every entity in the group. Use a heap pointer to an externalNativeList<T>or a managedList<T>instead.
Relation to Unity's FixedList*Bytes¶
Unity's Unity.Collections package ships FixedList32Bytes<T> through FixedList4096Bytes<T> — the same idea on a different axis. The quick differences:
| Trecs | Unity FixedList*Bytes |
|
|---|---|---|
| Sizing axis | Element count (FixedList16<T> = 16 slots, always) |
Total bytes (FixedList64Bytes<T> = ~62 / sizeof(T) slots) |
| Indexer | Returns ref readonly T; writes via .Mut(i) |
Returns T by value (copies per access); use .ElementAt(i) for ref T |
in parameter safety |
Reads safe, writes compile-error | Indexer copies the whole struct per access |
| API surface | Minimal: Add, Clear, RemoveAt, RemoveAtSwapBack, Mut |
Extensive: IndexOf, Contains, Sort, IEnumerable<T>, cross-size equality |
| Count-less variant | FixedArray<N> |
none |
Either works as an inline component buffer. Trecs's types read more naturally at ECS call sites — the element-count axis matches how you think about the bound, and the readonly-indexer / Mut-write split keeps in parameters safe without defensive copies. If your codebase is already using Unity's types, mixing the two is fine; they're independent.