Sets¶
A set is a lightweight membership flag: an entity is either in the set or it isn't. Sets are independent of an entity's components and tags, and iteration visits only the members.
Defining a set¶
To restrict membership to entities carrying specific tags, use the generic form. Adding an entity without those tags asserts:
Registering sets¶
Adding, removing, and clearing¶
All set access goes through World.Set<T>(), which exposes three timing modes:
| Call shape | Timing | Description |
|---|---|---|
.DeferredAdd / .DeferredRemove / .DeferredClear |
Submission-deferred | Queued; applied at the next Submit() |
.Write |
Synchronous | Read+write view (main thread, syncs outstanding jobs) |
.Read |
Synchronous | Read-only view (main thread, syncs outstanding writers) |
Deferred¶
Queued during system execution; applied at the next submission. Safe during iteration:
World.Set<HighlightedParticle>().DeferredAdd(particle.Handle(World));
World.Set<HighlightedParticle>().DeferredRemove(particle.Handle(World));
World.Set<HighlightedParticle>().DeferredClear();
A queued DeferredClear() supersedes any DeferredAdd / DeferredRemove queued for the same set in the same submission, regardless of call order. For sequential semantics within a single frame ("clear, then add these"), use the immediate APIs below.
From a Burst job, NativeWorldAccessor.Set<T>() returns a NativeSetAccessor<T> with the same DeferredAdd / DeferredRemove / DeferredClear methods. For non-deferred mutations from jobs, use a NativeSetCommandBuffer<T> instead — see the immediate section below.
Immediate¶
Set<T>().Write returns a synced view; its Add / Remove / Clear take effect right away. The sync runs once at acquisition, so cache the view for tight loops.
// Main thread — via SetWrite<T>
var highlighted = World.Set<HighlightedParticle>().Write;
highlighted.Add(handle);
highlighted.Remove(handle);
highlighted.Clear();
In a Burst job, use a NativeSetCommandBuffer<T> captured as a job field:
// In a Burst job
highlightedBuffer.Add(handle, world);
highlightedBuffer.Remove(handle, world);
highlightedBuffer.Clear();
Don't mutate a set while iterating it
An immediate Add / Remove / Clear on the same set you're currently iterating throws in debug builds. In release builds iteration corrupts silently — entries get skipped, revisited, or (when an Add grows the buffer) read from freed memory.
To mutate a set you're iterating, prefer the deferred APIs — or stage the changes in a NativeList<EntityHandle> and apply them after the loop.
Querying by set¶
// ForEachEntity
[ForEachEntity(Set = typeof(HighlightedParticle))]
void Execute(in ParticleView particle) { /* only visits set members */ }
// Aspect query
foreach (var particle in ParticleView.Query(World).InSet<HighlightedParticle>())
{
particle.Color = Color.yellow;
}
// Count
int highlighted = World.Query().InSet<HighlightedParticle>().Count();
Per-frame staging¶
A common pattern: use a set as a per-frame scratch list. One system clears and populates it; downstream systems iterate only the members. Avoids recomputing the same predicate in every consumer (rendering, physics sync, audio cues, etc.).
To make this work within a single frame, use the immediate APIs. Deferred set ops only land at the next submission, so a downstream system in the same frame would see last frame's contents.
public partial class CullingSystem : ISystem
{
public void Execute()
{
// Cache the writer once — Set<T>().Write does a sync up front.
var visible = World.Set<VisibleThisFrame>().Write;
visible.Clear();
foreach (var r in Renderable.Query(World).WithTags<GameTags.Renderable>())
{
if (Frustum.Intersects(r.Bounds))
visible.Add(r.Handle(World));
}
}
partial struct Renderable : IAspect, IRead<Bounds> { }
}
[ExecuteAfter(typeof(CullingSystem))]
public partial class RenderSystem : ISystem
{
[ForEachEntity(Set = typeof(VisibleThisFrame))]
void Render(in MeshInfo mesh, in WorldTransform xform) { ... }
}
Notes:
- Cache the
SetWrite<T>returned bySet<T>().Writeoutside the loop. Each.Writeaccess syncs outstanding job writes; caching syncs once, then writes hit the buffer directly. - From a Burst job, capture a
NativeSetCommandBuffer<T>as a field for thread-safeAdd/Remove/Clear. Job-sideClearwipes pre-existing contents and supersedes anyAdd/Removequeued in the same writer-job-cycle — analogous toSet<T>().DeferredClear().
Sets vs Partitions¶
| Partitions | Sets | |
|---|---|---|
| Cost of change | All memory move to new buffer | Lightweight add/remove from index |
| Iteration | All entities with that tag are contiguous in memory | Sparse — only set members are visited |
| Best for | Core identity, maximum cache locality | Dynamic membership, temporary flags, filtering |
Both partitions and sets can represent state. Tag changes move entity data in memory, giving dense iteration. Set changes are cheap but iteration is sparse. See Entity Subset Patterns for a deeper comparison.
See also¶
- Sample 08 — Sets: a complete example of producer/consumer set membership across systems.