Disposable Collections
PurrNet provides pooled, disposable collections for GC‑friendly state and deterministic iteration:
DisposableList<T>DisposableDictionary<TKey, TValue>DisposableArray<T>
Use these in your STATE structs and long‑lived prediction data. They integrate with packing/history to duplicate safely and dispose cleanly.
Why Disposable Collections
Minimize allocations by renting from pools under the hood (
ListPool,DictionaryPool,ArrayPool).Deterministic iteration for dictionaries via an internal stable key list.
Codegen/packing support: deep copy via
Duplicate()during history snapshots and deltas.
General Rules
Always create via
Create(...)factory; do not usenew List<T>(),new Dictionary<,>(), or constructors.Always call
Dispose()when you are done with the collection.If a collection lives inside a
STATE, dispose it inSTATE.Dispose().Do not struct‑copy a disposable collection and dispose both; use
.Duplicate()to create an independent copy.
DisposableList
Create:
var list = DisposableList<MyType>.Create(capacity);orCreate()orCreate(IEnumerable<T>).Use like a regular
List<T>(Add, indexer,Count, etc.).Dispose:
list.Dispose();In
STATE, implement dispose:
public struct InventoryState : IPredictedData<InventoryState>
{
public DisposableList<int> items;
public void Dispose() { items.Dispose(); }
}DisposableDictionary<TKey, TValue>
Create:
var dict = DisposableDictionary<PlayerID, PredictedObjectID>.Create();Iteration order is deterministic using an internal key list.
Use:
Add, indexer,TryGetValue,Remove,ContainsKey.Example in a
STATE(from PlayerSpawner):players = DisposableDictionary<PlayerID, PredictedObjectID>.Create();Dispose in
STATE.Dispose():
public void Dispose() { players.Dispose(); }Tip: When enumerating, foreach (var (k, v) in dict) is safe and stable. Avoid mutating structure while iterating.
DisposableArray
Fixed‑size, pooled array with optional
Resize(int)growth.Create:
var arr = DisposableArray<byte>.Create(size);No
Add/Remove/Insert/Clear; indexer for read/write.Dispose:
arr.Dispose();
Copying and History
The prediction history uses packers that deep‑copy disposable collections by calling
Duplicate()under the hood.This ensures snapshots are independent. You should still implement
Dispose()on yourSTATEto release each snapshot when it is discarded.
Anti‑pattern:
Avoid
var b = a; b.Dispose(); a.Dispose();on disposable structs — both point to the same pooled container. Usevar b = a.Duplicate();if you truly need a separate copy.
Short‑Lived Temporaries
For per‑frame or function‑local scratch collections, prefer non‑disposable pools:
var tmp = ListPool<T>.Instantiate(); ... ListPool<T>.Destroy(tmp);var tmp = DictionaryPool<K,V>.Instantiate(); ... DictionaryPool<K,V>.Destroy(tmp);
These do not need to be part of state and should not be stored across frames.
Leak Checks (Editor)
When
PURR_LEAKS_CHECKis enabled in Editor, pooled allocations are tracked and usage is updated on access. This helps catch missedDispose()/Destroy()calls during development.
IDuplicate and Performance
The packer copies state snapshots via
Packer.Copy<T>(value).If
T : IDuplicate<T>, the packer callsDuplicate()directly instead of serializing/deserializing to clone.Implementing
IDuplicate<T>on your custom structs nested insideSTATEcan significantly reduce GC and CPU during prediction history copies and reconciliation.
Example:
using PurrNet.Packing;
public struct MySubState : IDuplicate<MySubState>
{
public DisposableList<int> indices;
public float weight;
public MySubState Duplicate()
{
return new MySubState {
indices = indices.Duplicate(), // deep copy pooled list
weight = weight
};
}
}
public struct MyState : IPredictedData<MyState>
{
public MySubState data;
public void Dispose() { data.indices.Dispose(); }
}Tips:
Implement
IEquatable<T>as well for fast equality checks (Packer.AreEqual) used in delta packing.Disposable collections already implement
IDuplicate<T>; use and dispose them correctly to benefit automatically.
Last updated