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
- 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.