Five predictable-allocation container types, a FIFO (First-In, First-Out) queue, a double-ended queue, a binary-heap priority queue, a LIFO (Last-In, First-Out) stack, and a fixed-capacity ring buffer, each with a Burst-compatible DOTS (Data-Oriented Technology Stack) variant for job code and an optional synchronized wrapper for cross-thread use.
Summary
Scylla Core provides five sequential container types under Scylla.Core.Structures:
ScyllaQueue<T>, a FIFO (First-In, First-Out) queue.ScyllaDeque<T>, a double-ended queue that pushes and pops at both ends.ScyllaPriorityQueue<T>, a binary-heap priority queue.ScyllaStack<T>, a LIFO (Last-In, First-Out) stack.ScyllaRingBuffer<T>, a fixed-capacity circular buffer.
All five are array-backed, all five implement the shared IScyllaCollection<T> contract described in Collections Overview, and all five expose a fluent builder, an optional synchronized wrapper, and a Burst-compatible DOTS counterpart.
The five differ in ordering and capacity behavior. Queue, deque, and stack grow automatically (the backing array doubles) unless .FixedCapacity() is set on the builder, in which case a full container simply refuses new items. The priority queue follows the same growable-or-fixed rule but orders by a comparer rather than by arrival: ScyllaPriorityQueueOrder.Min (the default) dequeues the smallest value first, ScyllaPriorityQueueOrder.Max dequeues the largest first. The ring buffer is the odd one out: its capacity is always fixed at construction, and instead of a growth policy it has an OverwriteOnFull flag that decides whether a write to a full buffer fails or silently evicts the oldest entry.
Each managed type has a *DOTS struct counterpart, ScyllaQueueDOTS<T>, ScyllaDequeDOTS<T>, ScyllaPriorityQueueDOTS<TValue>, ScyllaStackDOTS<T>, and ScyllaRingBufferDOTS<T>, backed by a NativeArray<T> instead of a managed array. The DOTS variants are always fixed-capacity, require an unmanaged element type, and must be disposed explicitly; there is no growable mode and no synchronized wrapper. The priority queue’s DOTS form stores an integer priority alongside a plain unmanaged payload (ScyllaPriorityQueueNode<TValue>) rather than requiring an interface implementation, since Burst-compiled code cannot dispatch through IScyllaPriorityQueueItem the way managed code can.
Thread safety follows the pattern set out in the Collections Overview: every managed type exposes AsSynchronized() (or the builder’s .Synchronized() step) to obtain a Synchronized* wrapper that locks each operation, and none of the five is thread-safe on its own. This page focuses on the operations specific to each of the five shapes; the shared interface, capability flags, and builder mechanics live on the overview page.
Use the queue, stack, and buffer family for:
- Ordered event, command, or spawn-request queues (
ScyllaQueue<T>). - Rewind buffers, undo histories, and input windows that need access from both ends (
ScyllaDeque<T>). - Priority dispatch: AI directive queues, wave spawners, task schedulers (
ScyllaPriorityQueue<T>). - Single-direction undo stacks and explicit-state recursion walkers, for example a non-recursive dungeon splitter (
ScyllaStack<T>). - Rolling windows of recent history: frame times, input samples, telemetry (
ScyllaRingBuffer<T>). - Burst-compiled jobs that need any of the above without touching managed memory (the
*DOTSvariants). - Cross-thread producer/consumer hand-offs, such as a loader thread feeding the main thread (the synchronized wrappers).
Features
- Five container shapes behind one contract.
ScyllaQueue<T>,ScyllaDeque<T>,ScyllaPriorityQueue<T>,ScyllaStack<T>, andScyllaRingBuffer<T>all implementIScyllaCollection<T>, so code written against the interface swaps concrete types in one line. - Automatic growth or fixed capacity, per instance. Queue, deque, priority queue, and stack double their backing array on demand unless
.FixedCapacity()was set at construction, in which caseTryAdd(and its shape-specific alias) returnsfalseinstead of allocating. - A fixed-capacity ring buffer with an overwrite policy.
ScyllaRingBuffer<T>never grows; itsOverwriteOnFullflag chooses between rejecting a write to a full buffer and silently discarding the oldest entry to make room. - Min-heap or max-heap ordering on the priority queue.
ScyllaPriorityQueueOrder.Min(default) dequeues the smallest value first;ScyllaPriorityQueueOrder.Maxdequeues the largest. Both modes share the same sift-up/sift-down implementation internally. - A ready-made comparer for prioritized items. Implement
IScyllaPriorityQueueItem(a single get-onlyQueuePriorityproperty) on an item type and passScyllaPriorityQueueItemComparer<T>.Defaultto the queue instead of writing a customIComparer<T>. - Deque access from both ends.
TryPushFront/TryPushBack,TryPopFront/TryPopBack, andTryPeekFront/TryPeekBackall run in O(1), with the genericIScyllaCollection<T>.TryAdd/TryRemovemapped to the back and front respectively (FIFO through the interface). - Bidirectional iteration on the ring buffer.
GetEnumerator()walks oldest-to-newest;AsReversed()returns a lightweight struct wrapper for newest-to-oldestforeach, both without heap allocation. - Synchronized wrappers per type.
.AsSynchronized(syncRoot)or the builder’s.Synchronized()step returns a nestedSynchronized*class (SynchronizedScyllaQueue,SynchronizedScyllaDeque, and so on) that locks every operation and exposes a stableSyncRootfor compound atomic sequences. - Burst-compatible DOTS variants for all five.
ScyllaQueueDOTS<T>,ScyllaDequeDOTS<T>,ScyllaPriorityQueueDOTS<TValue>,ScyllaStackDOTS<T>, andScyllaRingBufferDOTS<T>are[BurstCompile]structs overNativeArray<T>, constrained tounmanagedelement types, and require an explicitAllocator. - Optional overwrite-on-full in the DOTS layer too.
ScyllaQueueDOTS<T>,ScyllaDequeDOTS<T>,ScyllaStackDOTS<T>, andScyllaRingBufferDOTS<T>all accept anoverwriteOnFullconstructor argument;ScyllaPriorityQueueDOTS<TValue>does not, since it has no natural “opposite end” to evict from and simply returnsfalsewhen full. - Allocation-free enumeration and export. Every managed type exposes a value-type
Enumeratorforforeachon the concrete type, plusCopyTo(Span<T>)andCopyTo(T[], int)overloads for exporting a snapshot without boxing. - Membership checks where a linear scan makes sense.
Contains(item, comparer = null)is available on all five managed types (O(n)); the DOTS variants omit it, since unmanaged types have no built-in default equality comparer to fall back on. - Consistent fluent builders.
CreateBuilder(),.WithCapacity(n), and either.Build()(returnsIScyllaCollection<T>, may be a synchronized wrapper) or.BuildConcrete()(returns the concrete type, throws if.Synchronized()was called) follow the same shape across the family.
The containers
Five container types cover five distinct access patterns. Pick the row whose key operations match the code you are about to write, not the type that happens to be closest at hand.
| Type | Order | Key operations | Complexity | DOTS variant |
|---|---|---|---|---|
ScyllaQueue<T> | FIFO | TryEnqueue, TryDequeue, TryPeekFront | O(1) amortized enqueue and dequeue | ScyllaQueueDOTS<T> |
ScyllaDeque<T> | Double-ended | TryPushFront/TryPushBack, TryPopFront/TryPopBack, TryPeekFront/TryPeekBack | O(1) amortized at both ends | ScyllaDequeDOTS<T> |
ScyllaPriorityQueue<T> | Min-heap or max-heap | TryEnqueue, TryDequeue, TryPeekTop | O(log n) enqueue and dequeue, O(1) peek | ScyllaPriorityQueueDOTS<TValue> |
ScyllaStack<T> | LIFO | TryPush, TryPop, TryPeekTop | O(1) amortized push and pop | ScyllaStackDOTS<T> |
ScyllaRingBuffer<T> | Fixed-capacity FIFO (circular) | TryWrite, TryRead, TryPeekOldest/TryPeekNewest, PeekAt | O(1) write, read, and peek | ScyllaRingBufferDOTS<T> |
The generic IScyllaCollection<T> names (TryAdd, TryRemove, TryPeek) are always available too; each type maps them onto the operation that matches its natural insertion order (enqueue/dequeue for the queue and ring buffer, push/pop for the stack, push-back/pop-front for the deque, enqueue/dequeue by priority for the priority queue). Reach for the shape-specific names at the call site for readability; reach for the generic names when writing code that works across container types.
The DOTS variants do not mirror this naming one-for-one. ScyllaQueueDOTS<T> and ScyllaStackDOTS<T> expose only the interface-bridge names TryAdd/TryRemove/TryPeek, with no TryEnqueue/TryDequeue or TryPush/TryPop aliases. ScyllaDequeDOTS<T> keeps its full TryPushFront/TryPushBack/TryPopFront/TryPopBack/TryPeekFront/TryPeekBack set alongside the bridge. ScyllaRingBufferDOTS<T> keeps TryPeekOldest/TryPeekNewest/TryPeekAt but has no TryWrite/TryRead (use TryAdd/TryRemove instead). ScyllaPriorityQueueDOTS<TValue> keeps TryEnqueue/TryDequeue/TryPeek as explicit (int priority, TValue value) overloads alongside the node-typed interface bridge.
Recipes
Each recipe below is a self-contained answer for one of the five container types, plus one for the DOTS variants and one for the synchronized wrappers. Jump to the one matching the problem in front of you.
Route gameplay events through a FIFO queue
Problem. Several systems deal damage in the same frame, sometimes several times to the same target, and the order those hits land in matters for combo counters and death attribution. Applying damage the instant a system computes it means the resolution order depends on component iteration order rather than when the hit actually happened.
Solution. ScyllaQueue<T> enqueues events as they occur and drains them in the same order once per frame. Fix the capacity so a damage burst never triggers a mid-frame allocation.
using Scylla.Core.Structures;
/* A damage event carries what dealt it, to whom, and how much. */
public readonly struct DamageEvent
{
public readonly int SourceID;
public readonly int TargetID;
public readonly float Amount;
public DamageEvent(int sourceID, int targetID, float amount)
{
SourceID = sourceID;
TargetID = targetID;
Amount = amount;
}
}
/* Pre-size for a busy frame and fix the capacity so a damage burst
* never triggers a mid-frame allocation. */
var damageQueue = ScyllaQueue<DamageEvent>.CreateBuilder()
.WithCapacity(128)
.FixedCapacity()
.BuildConcrete();
/* Every system that deals damage enqueues an event instead of
* applying it directly, so resolution order matches occurrence order. */
damageQueue.TryEnqueue(new DamageEvent(sourceID: 12, targetID: 47, amount: 18f));
damageQueue.TryEnqueue(new DamageEvent(sourceID: 3, targetID: 47, amount: 6f));
/* Preview the next event without consuming it, useful for an
* "incoming hit" indicator that reads before the damage lands. */
if (damageQueue.TryPeekFront(out var next))
{
ShowIncomingHitMarker(next.TargetID);
}
/* Drain the queue once per frame, oldest event first. */
while (damageQueue.TryDequeue(out var evt))
{
ApplyDamage(evt.TargetID, evt.Amount);
}
Scrub a rewind buffer from both ends
Problem. A time-manipulation mechanic (or a fighting-game input buffer, or a replay scrubber) needs to append the newest simulation frame every tick, step backward through recent frames on demand, and drop the oldest frame once the history window is full, all without reallocating every tick.
Solution. ScyllaDeque<T> pushes and pops at either end in O(1). Append new frames at the back, rewind by popping from the back, and trim old history by popping from the front.
/* One recorded frame of simulation state. */
public struct SimFrame
{
public float Time;
public Vector3 PlayerPosition;
}
/* 300 frames covers five seconds of history at 60 fps. */
var rewindBuffer = ScyllaDeque<SimFrame>.CreateBuilder()
.WithCapacity(300)
.BuildConcrete();
/* Each simulation tick appends the newest frame to the back. */
rewindBuffer.TryPushBack(new SimFrame { Time = currentTime, PlayerPosition = player.position });
/* Once the window is full, drop the oldest frame from the front
* before appending, keeping memory bounded without reallocating. */
if (rewindBuffer.Count >= 300)
{
rewindBuffer.TryPopFront(out _);
}
/* Rewinding: pop from the back to step the player backward one frame
* at a time, restoring position from each popped frame. */
if (isRewinding && rewindBuffer.TryPopBack(out var frame))
{
player.position = frame.PlayerPosition;
}
/* Preview the most recently recorded frame without consuming it,
* useful for a "hold to rewind" indicator that shows where rewind lands. */
rewindBuffer.TryPeekBack(out var latest);
Dispatch AI directives in priority order
Problem. An AI director collects directives from several systems in the same tick, a boss phase transition, a wave-spawn request, an ambient patrol update, and needs to execute the most urgent one first regardless of which system posted it first.
Solution. ScyllaPriorityQueue<T> dequeues by priority rather than arrival order. Implement IScyllaPriorityQueueItem on the directive type and pass ScyllaPriorityQueueItemComparer<T>.Default instead of writing a custom comparer.
/* A directive carries its own urgency via QueuePriority; under Min
* ordering (the default) lower values are more urgent. */
public sealed class AIDirective : IScyllaPriorityQueueItem
{
public int QueuePriority { get; }
public string Description { get; }
public AIDirective(int priority, string description)
{
QueuePriority = priority;
Description = description;
}
}
/* Order by QueuePriority using the ready-made comparer instead of
* writing a custom IComparer<AIDirective>. */
var directiveQueue = ScyllaPriorityQueue<AIDirective>.CreateBuilder()
.WithCapacity(32)
.WithComparer(ScyllaPriorityQueueItemComparer<AIDirective>.Default)
.MinHeap()
.BuildConcrete();
directiveQueue.TryEnqueue(new AIDirective(priority: 5, description: "Patrol waypoint C"));
directiveQueue.TryEnqueue(new AIDirective(priority: 1, description: "Boss phase transition"));
directiveQueue.TryEnqueue(new AIDirective(priority: 3, description: "Reinforce east flank"));
/* The director drains one directive per tick; the boss phase
* transition (priority 1) comes out first regardless of enqueue order. */
if (directiveQueue.TryDequeue(out var urgent))
{
Log.Info($"Executing: {urgent.Description}", LogCategory.Game);
}
Give a level editor an undo history
Problem. A level editor needs an undo stack: every tile paint, entity move, or delete operation pushes a reversible command, Ctrl+Z pops and reverts the most recent one, and the undo menu item wants to preview what it would revert without actually doing so.
Solution. ScyllaStack<T> is the LIFO counterpart to the queue. TryPush records a command, TryPeekTop previews it, and TryPop reverts it.
public abstract class EditorCommand
{
public abstract void Undo();
public abstract string Description { get; }
}
/* Cap undo depth at 100 steps; once the fixed-capacity stack is full,
* TryPush simply refuses further commands instead of growing forever. */
var undoStack = ScyllaStack<EditorCommand>.CreateBuilder()
.WithCapacity(100)
.FixedCapacity()
.BuildConcrete();
/* Every designer action pushes its undo command. */
var recorded = undoStack.TryPush(new TilePaintCommand(tile, gridPos));
if (!recorded)
{
Log.Warning("Undo history is full; oldest actions are no longer recorded.", LogCategory.Editor);
}
/* Show what "Undo" would do in the menu tooltip without performing it. */
if (undoStack.TryPeekTop(out var pending))
{
SetUndoMenuLabel($"Undo {pending.Description}");
}
/* Ctrl+Z: pop and revert. */
if (undoStack.TryPop(out var last))
{
last.Undo();
}
Track a rolling window of recent frame times
Problem. A performance overlay wants to graph the last two seconds of frame times, and a smoothing filter wants the last few samples of some other per-frame value. Both need “the last N things, oldest automatically discarded when full” without manual pruning.
Solution. ScyllaRingBuffer<T> is a fixed-capacity circular buffer. With OverwriteOnFull enabled, writing past capacity silently drops the oldest sample instead of failing.
/* 120 samples covers two seconds of history at 60 fps. */
var frameTimes = ScyllaRingBuffer<float>.CreateBuilder()
.WithCapacity(120)
.OverwriteOnFull()
.BuildConcrete();
/* Push a new sample every frame; once the buffer fills, the oldest
* sample is discarded automatically to make room. */
frameTimes.TryWrite(Time.deltaTime);
/* Draw the graph left to right, oldest sample first. */
foreach (var sample in frameTimes)
{
PlotNextPoint(sample);
}
/* Or draw newest-first, without allocating a reversed copy. */
foreach (var sample in frameTimes.AsReversed())
{
PlotNextPointReversed(sample);
}
/* Read a specific sample without disturbing the buffer, for example
* the frame time four samples back for a smoothing calculation. */
if (frameTimes.TryPeekAt(frameTimes.Count - 4, out var older))
{
SmoothWithSample(older);
}
Run queue and stack operations inside a Burst job
Problem. A collision-resolution pass fans work out across a Burst-compiled job and needs a FIFO work list and a LIFO result list that the job can touch without boxing or managed-heap access.
Solution. ScyllaQueueDOTS<T> and ScyllaStackDOTS<T> are [BurstCompile] structs over NativeArray<T>. Both expose only the generic TryAdd/TryRemove/TryPeek names, not the TryEnqueue/TryPush naming their managed counterparts use.
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
[BurstCompile]
public struct CollisionResolutionJob : IJob
{
public ScyllaQueueDOTS<int> PendingContacts; /* work list, FIFO */
public ScyllaStackDOTS<int> ResolvedContacts; /* result list, LIFO */
public void Execute()
{
/* ScyllaQueueDOTS<T> exposes only the interface-bridge names.
* There is no TryEnqueue/TryDequeue alias on the DOTS variant. */
while (PendingContacts.TryRemove(out var contactID))
{
/* ScyllaStackDOTS<T> is the same story: TryAdd/TryRemove/TryPeek
* only, no TryPush/TryPop naming like the managed ScyllaStack<T>. */
ResolvedContacts.TryAdd(contactID);
}
}
}
/* TempJob suits containers created and consumed within the same
* few frames as the scheduled job. */
var job = new CollisionResolutionJob
{
PendingContacts = ScyllaQueueDOTS<int>.CreateBuilder()
.WithCapacity(256)
.WithAllocator(Allocator.TempJob)
.Build(),
ResolvedContacts = ScyllaStackDOTS<int>.CreateBuilder()
.WithCapacity(256)
.WithAllocator(Allocator.TempJob)
.Build()
};
/* Fill PendingContacts from the main thread before scheduling, then: */
var handle = job.Schedule();
handle.Complete();
/* Dispose after the job completes. DOTS containers are not
* garbage-collected and leak native memory if forgotten. */
job.PendingContacts.Dispose();
job.ResolvedContacts.Dispose();
Hand work between threads with the synchronized wrappers
Problem. A background streaming thread finishes loading assets at its own pace and needs to hand each finished asset to the main thread, which applies loaded assets once per frame. Writing a bespoke lock around a plain queue every time this pattern comes up is exactly the kind of thing worth not repeating.
Solution. Every managed container’s .Synchronized() builder step (or AsSynchronized() on a built instance) returns a wrapper that locks each operation internally. For a multi-step sequence, such as draining until empty, lock the shared SyncRoot once so the two threads cannot interleave mid-sequence.
/* Build the synchronized wrapper up front so both threads only ever
* see the thread-safe API, never the raw unsynchronized queue. */
var loadedAssets = ScyllaQueue<AssetHandle>.CreateBuilder()
.WithCapacity(64)
.FixedCapacity()
.Synchronized()
.Build();
/* Streaming thread: enqueue as each asset finishes loading. TryAdd
* returning false is a back-pressure signal (loader is outpacing the
* main thread), not an error condition. */
void OnAssetLoaded(AssetHandle handle)
{
if (!loadedAssets.TryAdd(handle))
{
Log.Warning("Asset hand-off queue is full; loader is outpacing the main thread.", LogCategory.Streaming);
}
}
/* Main thread: drain once per frame. Lock the shared SyncRoot for the
* whole loop so the streaming thread cannot enqueue between the
* IsEmpty check and the TryRemove call. */
lock (loadedAssets.SyncRoot)
{
while (!loadedAssets.IsEmpty)
{
loadedAssets.TryRemove(out var handle);
ApplyLoadedAsset(handle);
}
}
API Sketch
The public surface across the five container types, their supporting types, and their DOTS variants. See the API reference for full signatures, remarks, and exception contracts on every member.
namespace Scylla.Core.Structures
{
/* ------------------------------------------------------------------ */
/* SHARED CONTRACT (full detail on the Collections Overview page) */
/* ------------------------------------------------------------------ */
public interface IScyllaCollection<T> : IScyllaCollection
{
bool TryAdd(T item);
bool TryRemove(out T item);
bool TryPeek(out T item);
int CopyTo(Span<T> destination);
int CopyTo(T[] destination, int destinationIndex);
}
/* ------------------------------------------------------------------ */
/* QUEUE (FIFO) */
/* ------------------------------------------------------------------ */
public sealed class ScyllaQueue<T> : IScyllaCollection<T>
{
public ScyllaQueue(int capacity = 4, bool fixedCapacity = false);
public int Capacity { get; }
public bool IsFixedCapacity { get; }
public bool IsFull { get; }
public bool TryEnqueue(T item);
public bool TryDequeue(out T item);
public bool TryPeekFront(out T item);
public bool Contains(T item, IEqualityComparer<T> comparer = null);
public void EnsureCapacity(int minCapacity);
public Enumerator GetEnumerator();
public SynchronizedScyllaQueue AsSynchronized(object syncRoot = null);
public static Builder CreateBuilder();
public sealed class Builder
{
public Builder WithCapacity(int capacity);
public Builder FixedCapacity(bool value = true);
public Builder Synchronized(object syncRoot = null);
public IScyllaCollection<T> Build();
public ScyllaQueue<T> BuildConcrete();
}
public sealed class SynchronizedScyllaQueue : IScyllaCollection<T>
{
/* Mirrors the unsynchronized surface; every call locks internally. */
}
}
/* ------------------------------------------------------------------ */
/* DEQUE (double-ended) */
/* ------------------------------------------------------------------ */
public sealed class ScyllaDeque<T> : IScyllaCollection<T>
{
public ScyllaDeque(int capacity = 4, bool fixedCapacity = false);
public int Capacity { get; }
public bool IsFixedCapacity { get; }
public bool IsFull { get; }
public bool TryPushFront(T item);
public bool TryPushBack(T item);
public bool TryPopFront(out T item);
public bool TryPopBack(out T item);
public bool TryPeekFront(out T item);
public bool TryPeekBack(out T item);
public bool Contains(T item, IEqualityComparer<T> comparer = null);
public void EnsureCapacity(int minCapacity);
public Enumerator GetEnumerator();
public SynchronizedScyllaDeque AsSynchronized(object syncRoot = null);
public static Builder CreateBuilder();
/* Builder and SynchronizedScyllaDeque mirror the queue's shape. */
}
/* ------------------------------------------------------------------ */
/* PRIORITY QUEUE (binary heap) */
/* ------------------------------------------------------------------ */
public enum ScyllaPriorityQueueOrder { Min = 0, Max = 1 }
public interface IScyllaPriorityQueueItem
{
int QueuePriority { get; }
}
public sealed class ScyllaPriorityQueueItemComparer<T> : IComparer<T>
where T : IScyllaPriorityQueueItem
{
public static readonly ScyllaPriorityQueueItemComparer<T> Default;
public int Compare(T x, T y);
}
public sealed class ScyllaPriorityQueue<T> : IScyllaCollection<T>
{
public ScyllaPriorityQueue(int capacity = 4, bool fixedCapacity = false,
ScyllaPriorityQueueOrder order = ScyllaPriorityQueueOrder.Min, IComparer<T> comparer = null);
public int Capacity { get; }
public bool IsFixedCapacity { get; }
public bool IsFull { get; }
public bool TryEnqueue(T item);
public bool TryDequeue(out T item);
public bool TryPeekTop(out T item);
public bool Contains(T item, IEqualityComparer<T> comparer = null);
public void EnsureCapacity(int minCapacity);
public Enumerator GetEnumerator();
public SynchronizedScyllaPriorityQueue AsSynchronized(object syncRoot = null);
public static Builder CreateBuilder();
public sealed class Builder
{
public Builder WithCapacity(int capacity);
public Builder FixedCapacity(bool value = true);
public Builder WithComparer(IComparer<T> comparer);
public Builder MinHeap();
public Builder MaxHeap();
public Builder Synchronized(object syncRoot = null);
public IScyllaCollection<T> Build();
public ScyllaPriorityQueue<T> BuildConcrete();
}
}
/* ------------------------------------------------------------------ */
/* STACK (LIFO) */
/* ------------------------------------------------------------------ */
public sealed class ScyllaStack<T> : IScyllaCollection<T>
{
public ScyllaStack(int capacity = 4, bool fixedCapacity = false);
public int Capacity { get; }
public bool IsFixedCapacity { get; }
public bool IsFull { get; }
public bool TryPush(T item);
public bool TryPop(out T item);
public bool TryPeekTop(out T item);
public bool Contains(T item, IEqualityComparer<T> comparer = null);
public void EnsureCapacity(int minCapacity);
public Enumerator GetEnumerator();
public SynchronizedScyllaStack AsSynchronized(object syncRoot = null);
public static Builder CreateBuilder();
/* Builder and SynchronizedScyllaStack mirror the queue's shape. */
}
/* ------------------------------------------------------------------ */
/* RING BUFFER (fixed-capacity circular buffer) */
/* ------------------------------------------------------------------ */
public sealed class ScyllaRingBuffer<T> : IScyllaCollection<T>
{
public ScyllaRingBuffer(int capacity, bool overwriteOnFull = false);
public int Capacity { get; }
public bool IsFull { get; }
public int FreeCount { get; }
public bool OverwriteOnFull { get; }
public bool TryWrite(T item);
public bool TryRead(out T item);
public bool TryPeekOldest(out T item);
public bool TryPeekNewest(out T item);
public T PeekAt(int index);
public bool TryPeekAt(int index, out T item);
public bool Contains(T item, IEqualityComparer<T> comparer = null);
public int RemoveWhere(Predicate<T> match);
public Enumerator GetEnumerator();
public ReverseEnumerable AsReversed();
public SynchronizedScyllaRingBuffer AsSynchronized(object syncRoot = null);
public static Builder CreateBuilder();
public sealed class Builder
{
public Builder WithCapacity(int capacity);
public Builder OverwriteOnFull(bool value = true);
public Builder Synchronized(object syncRoot = null);
public IScyllaCollection<T> Build();
public ScyllaRingBuffer<T> BuildConcrete();
}
}
/* ------------------------------------------------------------------ */
/* DOTS VARIANTS */
/* Burst-compatible, fixed-capacity, unmanaged, disposable structs. */
/* No Contains, no synchronized wrapper; ThreadSafety is always */
/* Unsynchronized and SyncRoot is always null. */
/* ------------------------------------------------------------------ */
public struct ScyllaQueueDOTS<T> : IScyllaCollection<T>, IDisposable
where T : unmanaged
{
public ScyllaQueueDOTS(int capacity, Allocator allocator, bool overwriteOnFull = false,
NativeArrayOptions options = NativeArrayOptions.UninitializedMemory);
public bool IsCreated { get; }
public int Capacity { get; }
public bool IsFull { get; }
public int FreeCount { get; }
public bool OverwriteOnFull { get; }
/* TryAdd / TryRemove / TryPeek only, no TryEnqueue / TryDequeue alias. */
public void Dispose();
public static Builder CreateBuilder();
/* Builder: WithAllocator (required), WithCapacity, OverwriteOnFull, WithNativeArrayOptions. */
}
public struct ScyllaDequeDOTS<T> : IScyllaCollection<T>, IDisposable
where T : unmanaged
{
public ScyllaDequeDOTS(int capacity, Allocator allocator, bool overwriteOnFull = false,
NativeArrayOptions options = NativeArrayOptions.UninitializedMemory);
public bool TryPushFront(T item);
public bool TryPushBack(T item);
public bool TryPopFront(out T item);
public bool TryPopBack(out T item);
public bool TryPeekFront(out T item);
public bool TryPeekBack(out T item);
/* Plus the TryAdd / TryRemove / TryPeek interface bridge. */
public void Dispose();
public static Builder CreateBuilder();
}
public struct ScyllaPriorityQueueNode<TValue> where TValue : unmanaged
{
public int Priority;
public TValue Value;
public ScyllaPriorityQueueNode(int priority, TValue value);
}
public struct ScyllaPriorityQueueDOTS<TValue>
: IScyllaCollection<ScyllaPriorityQueueNode<TValue>>, IDisposable
where TValue : unmanaged
{
public ScyllaPriorityQueueDOTS(int capacity, Allocator allocator,
ScyllaPriorityQueueOrder order = ScyllaPriorityQueueOrder.Min,
NativeArrayOptions options = NativeArrayOptions.UninitializedMemory);
public ScyllaPriorityQueueOrder Order { get; }
public bool TryEnqueue(int priority, TValue value);
public bool TryDequeue(out int priority, out TValue value);
public bool TryPeek(out int priority, out TValue value);
/* Plus the node-typed TryAdd / TryRemove / TryPeek interface bridge. */
public void Dispose();
public static Builder CreateBuilder();
/* Builder: WithAllocator (required), WithCapacity, MinHeap/MaxHeap/WithOrder,
* WithNativeArrayOptions. No OverwriteOnFull; this variant is always
* fixed-capacity, and TryEnqueue returns false once full. */
}
public struct ScyllaStackDOTS<T> : IScyllaCollection<T>, IDisposable
where T : unmanaged
{
public ScyllaStackDOTS(int capacity, Allocator allocator, bool overwriteOnFull = false,
NativeArrayOptions options = NativeArrayOptions.UninitializedMemory);
/* TryAdd / TryRemove / TryPeek only, no TryPush / TryPop alias. */
public void Dispose();
public static Builder CreateBuilder();
}
public struct ScyllaRingBufferDOTS<T> : IScyllaCollection<T>, IDisposable
where T : unmanaged
{
public ScyllaRingBufferDOTS(int capacity, Allocator allocator, bool overwriteOnFull = false,
NativeArrayOptions options = NativeArrayOptions.UninitializedMemory);
public bool TryPeekOldest(out T item);
public bool TryPeekNewest(out T item);
public bool TryPeekAt(int index, out T item);
/* TryAdd / TryRemove serve as write / read; there is no TryWrite / TryRead alias. */
public Enumerator GetEnumerator();
public ReverseEnumerable AsReversed();
public void Dispose();
public static Builder CreateBuilder();
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Builder reference
All five managed builders share the same core steps: .WithCapacity(n) sets the initial buffer size, .Synchronized(syncRoot = null) wraps the result in the type’s Synchronized* class, .Build() returns IScyllaCollection<T> (possibly a synchronized wrapper), and .BuildConcrete() returns the concrete type directly (throwing InvalidOperationException if .Synchronized() was called). Only the settings below differ per container.
Queue, deque, and stack builders
These three share an identical settings surface beyond the common steps above.
| Setter | Default | Effect |
|---|---|---|
WithCapacity(int) | 4 | Initial buffer capacity. Throws ArgumentOutOfRangeException if less than 1. |
FixedCapacity(bool = true) | false | Disables automatic growth. TryAdd (and the shape-specific alias) returns false when full instead of resizing. |
Synchronized(syncRoot = null) | not synchronized | Wraps the result in the type’s Synchronized* class, sharing the given lock object or a private one. |
Priority queue builder
Adds ordering and comparer selection on top of the shared queue/deque/stack settings.
| Setter | Default | Effect |
|---|---|---|
WithComparer(IComparer<T>) | Comparer<T>.Default | Custom ordering. Pass ScyllaPriorityQueueItemComparer<T>.Default to order by IScyllaPriorityQueueItem.QueuePriority. |
MinHeap() | active by default | Smallest value (by comparer) dequeues first. |
MaxHeap() | – | Largest value (by comparer) dequeues first. |
Ring buffer builder
The ring buffer has no FixedCapacity() step, since its capacity is always fixed; it trades that for an overwrite policy instead.
| Setter | Default | Effect |
|---|---|---|
WithCapacity(int) | 4 | Fixed maximum element count. The constructor itself has no default and requires this value. |
OverwriteOnFull(bool = true) | false | When true, a write to a full buffer discards the oldest element instead of returning false. |
Synchronized(syncRoot = null) | not synchronized | Same as the other four containers. |
DOTS builders
All five *DOTS builders require WithAllocator(Allocator); calling Build() or BuildConcrete() without it throws InvalidOperationException. There is no Synchronized() step in this layer, and Build()/BuildConcrete() are functionally identical since no synchronized wrapper exists for DOTS types.
| Setter | Default | Effect |
|---|---|---|
WithAllocator(Allocator) | none, required | Native allocator for the backing NativeArray<T>. Allocator.None throws ArgumentException. |
WithCapacity(int) | 4 | Fixed capacity; DOTS variants never grow past this value. |
WithNativeArrayOptions(NativeArrayOptions) | UninitializedMemory | Pass ClearMemory to zero-initialize the buffer at allocation time. |
OverwriteOnFull(bool = true) | false | Available on ScyllaQueueDOTS<T>, ScyllaDequeDOTS<T>, ScyllaStackDOTS<T>, ScyllaRingBufferDOTS<T>. Not on ScyllaPriorityQueueDOTS<TValue>. |
MinHeap() / MaxHeap() / WithOrder(ScyllaPriorityQueueOrder) | Min | ScyllaPriorityQueueDOTS<TValue> only. |
Best Practices
- Match the container to the access pattern, not to habit. A queue used as a stack wastes its order tracking; a priority queue used for equal-priority items wastes heap maintenance. See the Collections Overview table for the broader family this page’s five types belong to.
- Pre-size every container. Pass
capacityat construction or.WithCapacity(n)on the builder. The default is a small4, and growth reallocates; a known peak size deserves one allocation, not several. - Reach for
FixedCapacity()in hot paths. Combined with pre-sizing, a fixed-capacity queue, deque, priority queue, or stack never allocates at runtime. The ring buffer gets the same benefit automatically, since it is always fixed-capacity. - Give the priority queue a real comparer. Either implement
IScyllaPriorityQueueItemand useScyllaPriorityQueueItemComparer<T>.Default, or supply an explicitIComparer<T>. Relying onComparer<T>.Defaultfor a type that does not implementIComparable<T>throws at the first comparison, not at construction. - Choose the ring buffer’s
OverwriteOnFulldeliberately.truesuits rolling history (frame times, recent positions) where the newest data always matters more than the oldest.falsesuits a bounded command buffer that should signal back-pressure instead of silently dropping data. - Reach for the synchronized wrapper only when genuinely cross-thread. Every wrapper locks on every operation; single-thread hot paths pay that cost for no benefit. Use the unsynchronized type there and reserve
.Synchronized()for real producer/consumer hand-offs. - Dispose every DOTS instance exactly once. None of the five
*DOTStypes are garbage-collected. Pair construction with aDispose()call, ideally via ausingstatement or a job-completion callback. - Do not assume DOTS method names match the managed type.
ScyllaQueueDOTS<T>andScyllaStackDOTS<T>drop theTryEnqueue/TryPushnaming in favor of the bareTryAdd/TryRemove/TryPeekinterface names; check the API Sketch above before porting managed code into a job. - Prefer the concrete type’s
Enumeratorover interface-based iteration in hot loops.foreachonScyllaQueue<T>(or any of the five) resolves to a value-type enumerator with no allocation; iterating throughIScyllaCollection<T>as anIEnumerable<T>would box it. - Use
CopyTofor snapshot export instead of a manual loop. Both theSpan<T>and array overloads use block operations internally on the managed types and are the documented allocation-free path for pulling a copy out through the interface. - Treat a
falsereturn fromTryAdd(or its alias) as back-pressure, not an error. It means a fixed-capacity container is full or a non-overwriting ring buffer rejected a write; decide whether to retry, drop, or block based on the gameplay context, not by treating it as an exception path. - Pool the elements themselves when they are expensive to construct. These containers manage storage slots, not object lifetimes; pair a
ScyllaQueue<T>of pooled handles with an actual pool (see Object Pools) whenTwraps something costly to allocate, such as a particle system instance.
Pitfalls
Related
- Collections Overview
- Maps and Dictionaries
- Specialized Structures
- Object Pools
- API reference:
Scylla.Core.Structures.IScyllaCollection`1 - API reference:
Scylla.Core.Structures.ScyllaQueue`1 - API reference:
Scylla.Core.Structures.ScyllaQueueDOTS`1 - API reference:
Scylla.Core.Structures.ScyllaDeque`1 - API reference:
Scylla.Core.Structures.ScyllaDequeDOTS`1 - API reference:
Scylla.Core.Structures.ScyllaPriorityQueue`1 - API reference:
Scylla.Core.Structures.ScyllaPriorityQueueDOTS`1 - API reference:
Scylla.Core.Structures.IScyllaPriorityQueueItem - API reference:
Scylla.Core.Structures.ScyllaPriorityQueueOrder - API reference:
Scylla.Core.Structures.ScyllaPriorityQueueItemComparer`1 - API reference:
Scylla.Core.Structures.ScyllaStack`1 - API reference:
Scylla.Core.Structures.ScyllaStackDOTS`1 - API reference:
Scylla.Core.Structures.ScyllaRingBuffer`1 - API reference:
Scylla.Core.Structures.ScyllaRingBufferDOTS`1