Query
Queries are a mechanism for searching entities and their components in the world
- All queries require no caching, are stack-allocated, and can be used on-the-fly
- Support filtering by components, tags, entity status, and clusters
- Two iteration modes:
Strict(default, faster) andFlexible(additionally allows destroying / disabling / enabling other snapshot entities during iteration). In both modes, entities outside the iteration snapshot — created mid-iteration or not matching the filter — are not blocked.
Filters
Types for describing filtering. Each occupies 1 byte and requires no initialization.
// Given 5 entities in the world:
// Components Tags EntityType
// Entity 1: Position, Velocity Unit Npc
// Entity 2: Position, Name Player Npc
// Entity 3: Position, Velocity, Name Unit, Player Npc
// Entity 4: Velocity — Bullet
// Entity 5: Position■, Velocity Unit Bullet
// (■ = disabled)
//
// Examples below show which entities pass each filter
Components:
// All — presence of ALL enabled components (from 1 to 8 types)
All<Position, Velocity, Direction> all = default;
// AllOnlyDisabled — presence of ALL disabled components
AllOnlyDisabled<Position> disabled = default;
// AllWithDisabled — presence of ALL components (any state)
AllWithDisabled<Position, Velocity> any = default;
// None — absence of enabled components (from 1 to 8 types)
None<Position, Name> none = default;
// NoneWithDisabled — absence of components (any state)
NoneWithDisabled<Position> noneAll = default;
// Any — presence of at least one enabled component (from 2 to 8 types)
Any<Position, Velocity> any = default;
// AnyOnlyDisabled — at least one disabled
AnyOnlyDisabled<Position, Velocity> anyDis = default;
// AnyWithDisabled — at least one (any state)
AnyWithDisabled<Position, Velocity> anyAll = default;
// Note: All five *Disabled families above (AllOnlyDisabled, AllWithDisabled,
// NoneWithDisabled, AnyOnlyDisabled, AnyWithDisabled) constrain their type
// parameters to `struct, IComponent, IDisableable`. Components without the
// IDisableable marker cannot be used here — compile-time error.
// See features/component.md#enabledisable.
// Results for the entities above:
// All<Position, Velocity> → 1, 3
// AllOnlyDisabled<Position> → 5
// AllWithDisabled<Position, Velocity> → 1, 3, 5
// None<Name> → 1, 4, 5
// NoneWithDisabled<Position> → 4
// Any<Position, Name> → 1, 2, 3
// AnyOnlyDisabled<Position, Velocity> → 5
// AnyWithDisabled<Position, Name> → 1, 2, 3, 5
Tags:
Tags use the same filters as components — All<>, None<>, Any<> and their variants. No separate tag filter types exist.
// All — presence of ALL specified tags (from 1 to 8 types)
All<Unit, Player> tagAll = default;
// None — absence of specified tags (from 1 to 8 types)
None<Unit, Player> tagNone = default;
// Any — at least one of the specified tags (from 2 to 8 types)
Any<Unit, Player> tagAny = default;
// Results for the entities above:
// All<Unit, Player> → 3
// None<Unit> → 2, 4
// Any<Unit, Player> → 1, 2, 3, 5
Change tracking:
// AllAdded — ALL specified components were added since last ClearTracking (from 1 to 5 types)
AllAdded<Position> added = default;
AllAdded<Position, Velocity> addedMulti = default;
// AnyAdded — AT LEAST ONE of the specified components was added (from 2 to 5 types)
AnyAdded<Position, Velocity> anyAdded = default;
// NoneAdded — NONE of the specified components were added (from 1 to 5 types)
NoneAdded<Position> noneAdded = default;
// AllDeleted — ALL specified components were deleted since last ClearTracking (from 1 to 5 types)
AllDeleted<Position> deleted = default;
// AnyDeleted — AT LEAST ONE was deleted (from 2 to 5 types)
AnyDeleted<Position, Velocity> anyDeleted = default;
// NoneDeleted — NONE were deleted (from 1 to 5 types)
NoneDeleted<Position> noneDeleted = default;
// AllChanged — ALL specified components were accessed via ref since last ClearChangedTracking (from 1 to 5 types)
// Requires the component type to implement ITrackableChanged
AllChanged<Position> changed = default;
// AnyChanged — AT LEAST ONE was changed (from 2 to 5 types)
AnyChanged<Position, Velocity> anyChanged = default;
// NoneChanged — NONE were changed (from 1 to 5 types)
NoneChanged<Position> noneChanged = default;
// AllAdded / AnyAdded / NoneAdded / AllDeleted / AnyDeleted / NoneDeleted
// also work with tags — use the same filters for both components and tags
// Created — entity was created since last ClearCreatedTracking
// (requires WorldConfig.TrackCreated = true, no type parameters)
Created created = default;
// Combination with other filters
foreach (var entity in W.Query<AllAdded<Position>, All<Velocity, Unit>>().Entities()) {
ref var pos = ref entity.Ref<Position>();
// process newly added entities with Position
}
In delegate-based iteration (For), ref parameters mark the component as Changed, while in parameters do not. Use in for read-only access to avoid unnecessary Changed marks. See Changed Tracking for details.
Entity types:
// EntityIs — exactly this entity type (1 type parameter)
EntityIs<Bullet> entityIs = default;
// EntityIsNot — exclude entity types (from 1 to 5 types)
EntityIsNot<Effect> entityIsNot = default;
// EntityIsAny — match any of the specified entity types (from 2 to 5 types)
EntityIsAny<Bullet, Rocket> entityIsAny = default;
// Results for the entities above:
// EntityIs<Npc> → 1, 2, 3
// EntityIsNot<Bullet> → 1, 2, 3
// EntityIsAny<Npc, Bullet> → 1, 2, 3, 4, 5
And / Or — composite filters:
And and Or allow grouping multiple filters into a single type. This is useful for:
- Passing a complex filter as one generic parameter — store in a field, pass to a method, or use as a type argument
- Building filters that basic types cannot express — e.g., “entities with component set A or component set B”
And — all conditions must match (from 2 to 6 filters):
And<All<Position, Velocity>, None<Name>, Any<Unit, Player>> filter = default;
// Via factory method (type inference)
var filter = And.By(
default(All<Position, Velocity>),
default(None<Name>),
default(Any<Unit, Player>)
);
// Use case: pass a composite filter to a helper method
void ProcessMovable(And<All<Position, Velocity>, None<Frozen>> filter) {
foreach (var entity in W.Query(filter).Entities()) {
entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}
}
Or — at least one condition must match (from 2 to 6 filters):
Or enables combinationally complex filtering that basic filter types cannot express.
// Melee fighters OR ranged fighters — completely different component sets,
// cannot be expressed with a single All/Any/None combination
Or<All<MeleeWeapon, Damage>, All<RangedWeapon, Ammo>> fighters = default;
// Rebuild spatial index when Position was added, removed, or modified
Or<AllAdded<Position>, AllDeleted<Position>, AllChanged<Position>> spatialChanged = default;
// Process both UI buttons (ClickArea + Label) and world interactables (Collider + Interaction)
Or<All<ClickArea, Label>, All<Collider, Interaction>> clickable = default;
// Via factory method
var filter = Or.By(
default(All<MeleeWeapon, Damage>),
default(All<RangedWeapon, Ammo>)
);
// Results for the entities above:
// Or<All<Position, Velocity>, All<Position, Name>>
// Entity 1: Pos✓ Vel✓ → ✓ (passes first)
// Entity 2: Pos✓ Name✓ → ✓ (passes second)
// Entity 3: Pos✓ Vel✓ Name✓ → ✓ (passes both)
// Entity 4: Pos✗ → ✗
// → Result: 1, 2, 3, 5
Nesting:
// And and Or can be nested for arbitrarily complex logic
// (A and B and C) or (A and B and D):
Or<All<A, B, C>, All<A, B, D>> complex = default;
// All visible entities that are either alive units or active effects:
And<All<Visible>, Or<All<Unit, Alive>, All<Effect, Active>>> visibleAlive = default;
Entity iteration
// Iterate over all entities without filtering
foreach (var entity in W.Query().Entities()) {
Console.WriteLine(entity.PrettyString);
}
// With filter via generic (from 1 to 8 filters)
foreach (var entity in W.Query<All<Position, Velocity>>().Entities()) {
entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}
// With multiple filters
foreach (var entity in W.Query<All<Position, Velocity>, None<Name>>().Entities()) {
entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}
// Via filter value
var all = default(All<Position, Velocity>);
foreach (var entity in W.Query(all).Entities()) {
entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}
// Via And/Or — group filters into a single type for passing to methods or storing in fields
var filter = default(And<All<Position, Velocity>, None<Name>>);
foreach (var entity in W.Query(filter).Entities()) {
entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}
// Flexible mode — allows destroying / disabling / enabling other snapshot entities during iteration
foreach (var entity in W.Query<All<Position>>().EntitiesFlexible()) {
// safe here: another.Destroy(), another.Disable(), another.Enable() — for snapshot entities too
// still forbidden (asserts in DEBUG): another.Delete<Position>(), another.Disable<Position>(), etc. on snapshot entities
// note: creating new entities and configuring them inside the loop is always allowed (any mode)
}
// Find the first matching entity
if (W.Query<All<Position>>().Any(out var found)) {
// found — first entity with Position
}
// Get the only entity (error in debug if more than one found)
if (W.Query<All<Position>>().One(out var single)) {
// single — the only entity with Position
}
// Test whether a given entity belongs to the query result
// - checks the entity's lifecycle state (default: only Enabled)
// - checks cluster membership (if clusters are provided)
// - applies the query filter via Entity.IsMatch
if (W.Query<All<Position, Velocity>>().Contains(entity)) {
// entity is enabled and passes the filter
}
// With optional parameters
W.Query<All<Position>>().Contains(
entity,
entities: EntityStatusType.Any, // Enabled (default), Disabled, Any
clusters: stackalloc ushort[] { 1, 2 } // empty = any cluster
);
// Count matching entities (full scan)
int count = W.Query<All<Position>>().EntitiesCount();
Delegate-based iteration (For)
Optimized iteration via delegates — unrolls loops under the hood.
// Over all entities
W.Query().For(entity => {
Console.WriteLine(entity.PrettyString);
});
// By components (from 1 to 6 types)
// Components in the delegate automatically act as an All filter
W.Query().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
// With entity in delegate
W.Query().For(static (W.Entity entity, ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
// With user data (to avoid delegate allocations)
W.Query().For(deltaTime, static (ref float dt, ref Position pos, in Velocity vel) => {
pos.Value += vel.Value * dt;
});
// With ref data (for accumulating results)
int count = 0;
W.Query().For(ref count, static (ref int counter, W.Entity entity, ref Position pos) => {
counter++;
});
// With tuple of multiple parameters
W.Query().For((deltaTime, gravity), static (ref (float dt, float g) data, ref Position pos, ref Velocity vel) => {
vel.Value += data.g * data.dt;
pos.Value += vel.Value * data.dt;
});
Readonly components (Read):
When a component is only read and not modified, use in instead of ref in delegates. This tells the change tracking system not to mark the component as changed.
// Last N components as readonly via `in`
W.Query().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value; // Position — writable (ref), Velocity — readonly (in)
});
// All components readonly
W.Query().For(static (in Position pos, in Velocity vel) => {
Console.WriteLine(pos.Value + vel.Value);
});
// With entity
W.Query().For(static (W.Entity entity, ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
// With user data
W.Query().For(ref result, static (ref float res, in Position pos, in Velocity vel) => {
res += pos.Value.Length;
});
Read variants are available when changed tracking is enabled (default). Can be disabled via FFS_ECS_DISABLE_CHANGED_TRACKING define.
With additional filtering:
// Components in the delegate act as an All filter,
// additional filters are specified directly on Query and don't require specifying delegate components
W.Query<Any<Unit, Player>>().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
// With multiple filters
W.Query<None<Name>, Any<Unit, Player>>().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
// Via value
var filter = default(Any<Unit, Player>);
W.Query(filter).For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
Entity and component status:
W.Query().For(
static (ref Position pos, ref Velocity vel) => {
// ...
},
entities: EntityStatusType.Disabled, // Enabled (default), Disabled, Any
components: ComponentStatus.Disabled // Enabled (default), Disabled, Any
);
Single entity search (Search)
Iteration with early exit on first match. All components in search delegates are readonly (in).
if (W.Query().Search(out W.Entity found,
(W.Entity entity, in Position pos, in Health health) => {
return pos.Value.x > 100 && health.Current < 50;
})) {
// found — first entity matching the condition
}
Function structs (IQuery / IQueryBlock)
Function structs instead of delegates — for optimization, state passing, or extracting logic. Function structs use a fluent builder API on WorldQuery — unlike delegates, component types are not specified via generic parameters on For, but via the builder chain.
IQuery — per-entity callback:
The interface hierarchy uses nested types for write/read access control (from 1 to 6 components total):
IQuery.Write<T0, T1>— all components writable (ref)IQuery.Read<T0, T1>— all components readonly (in)IQuery.Write<T0>.Read<T1>— first writable, rest readonly
// All writable — IQuery.Write
readonly struct MoveFunction : W.IQuery.Write<Position, Velocity> {
public void Invoke(W.Entity entity, ref Position pos, ref Velocity vel) {
pos.Value += vel.Value;
}
}
// Fluent API: Write<...>() specifies writable components, then For<TFunction>() executes
W.Query().Write<Position, Velocity>().For<MoveFunction>();
// With a value
W.Query().Write<Position, Velocity>().For(new MoveFunction());
// Via ref (to preserve state after iteration)
var func = new MoveFunction();
W.Query().Write<Position, Velocity>().For(ref func);
// Mixed write/read — IQuery.Write<>.Read<>
readonly struct ApplyVelocity : W.IQuery.Write<Position>.Read<Velocity> {
public void Invoke(W.Entity entity, ref Position pos, in Velocity vel) {
pos.Value += vel.Value;
}
}
// Chain: Write<writable>().Read<readonly>().For<TFunction>()
W.Query().Write<Position>().Read<Velocity>().For<ApplyVelocity>();
// All readonly — IQuery.Read
readonly struct PrintPositions : W.IQuery.Read<Position, Velocity> {
public void Invoke(W.Entity entity, in Position pos, in Velocity vel) {
Console.WriteLine(pos.Value + vel.Value);
}
}
W.Query().Read<Position, Velocity>().For<PrintPositions>();
// With additional filtering
W.Query<None<Name>, Any<Unit, Player>>()
.Write<Position, Velocity>().For<MoveFunction>();
// Combining system and IQuery
public struct MoveSystem : ISystem, W.IQuery.Write<Position>.Read<Velocity> {
private float _speed;
public void Update() {
_speed = W.GetResource<GameConfig>().Speed;
W.Query<All<Unit>>()
.Write<Position>().Read<Velocity>().For(ref this);
}
public void Invoke(W.Entity entity, ref Position pos, in Velocity vel) {
pos.Value += vel.Value * _speed;
}
}
WorldQuery methods
Delegates — component types are inferred from the lambda:
| Method | Components |
|---|---|
For(delegate) | 1–6, ref or in per component |
ForParallel(delegate) | 1–6, ref or in per component |
Search(out entity, delegate) | 1–6, all in |
Function structs — component access via builder:
| Method | Components | Access |
|---|---|---|
Write<1‑6>() | 1–6 | all ref |
Write<1‑5>().Read<1‑5>() | 2–6 total | first ref, rest in |
Read<1‑6>() | 1–6 | all in |
Block function structs — same pattern, unmanaged only:
| Method | Components | Access |
|---|---|---|
WriteBlock<1‑6>() | 1–6 | all Block<T> |
WriteBlock<1‑5>().Read<1‑5>() | 2–6 total | Block<T> + BlockR<T> |
ReadBlock<1‑6>() | 1–6 | all BlockR<T> |
Each builder provides For<F>() and ForParallel<F>(). Read / ReadBlock require change tracking (enabled by default, disable via FFS_ECS_DISABLE_CHANGED_TRACKING).
Parallel processing
Parallel processing requires enabling at world creation: set ThreadCount > 0 in WorldConfig (or use WorldConfig.MaxThreads()). Inside parallel iteration, only the current iterated entity may be modified or destroyed. Forbidden: creating entities, modifying other entities, reading events. Sending events (SendEvent) is thread-safe (when there is no concurrent reading of the same type, see Events for details). Always uses QueryMode.Strict.
// Delegate — the first parameter, minEntitiesPerThread is named (default 256)
W.Query().ForParallel(
static (W.Entity entity, ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
},
minEntitiesPerThread: 50000
);
// Without entity — components only
W.Query().ForParallel(
static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
},
minEntitiesPerThread: 50000
);
// With user data
W.Query().ForParallel(deltaTime,
static (ref float dt, ref Position pos, in Velocity vel) => {
pos.Value += vel.Value * dt;
},
minEntitiesPerThread: 50000
);
// With filtering
W.Query<None<Name>, Any<Unit, Player>>().ForParallel(
static (W.Entity entity) => {
entity.Add<Name>();
},
minEntitiesPerThread: 50000
);
// Via function struct
W.Query().Write<Position>().Read<Velocity>().ForParallel<ApplyVelocity>(
minEntitiesPerThread: 50000
);
// workersLimit — limit the number of threads (0 = use all available)
W.Query().ForParallel(
static (ref Position pos) => { /* ... */ },
minEntitiesPerThread: 10000,
workersLimit: 4
);
Block iteration (ForBlock)
Low-level iteration via function structs — for unmanaged components, provides Block<T> (writable) and BlockR<T> (readonly) wrappers with direct pointers to data arrays.
The interface hierarchy mirrors IQuery (from 1 to 6 unmanaged components total):
IQueryBlock.Write<T0, T1>— all components writable (Block<T>)IQueryBlock.Read<T0, T1>— all components readonly (BlockR<T>)IQueryBlock.Write<T0>.Read<T1>— first writable, rest readonly
// All writable — IQueryBlock.Write
readonly struct MoveBlock : W.IQueryBlock.Write<Position, Velocity> {
public void Invoke(uint count, EntityBlock entitiesBlock,
Block<Position> positions, Block<Velocity> velocities) {
for (uint i = 0; i < count; i++) {
positions[i].Value += velocities[i].Value;
}
}
}
// Fluent API: WriteBlock<...>().For<TFunction>()
W.Query().WriteBlock<Position, Velocity>().For<MoveBlock>();
// Mixed write/read — WriteBlock<>.Read<>
readonly struct ApplyVelocityBlock : W.IQueryBlock.Write<Position>.Read<Velocity> {
public void Invoke(uint count, EntityBlock entitiesBlock,
Block<Position> positions, BlockR<Velocity> velocities) {
for (uint i = 0; i < count; i++) {
positions[i].Value += velocities[i].Value;
}
}
}
W.Query().WriteBlock<Position>().Read<Velocity>().For<ApplyVelocityBlock>();
// All readonly — ReadBlock<>
readonly struct SumPositionsBlock : W.IQueryBlock.Read<Position> {
public void Invoke(uint count, EntityBlock entitiesBlock, BlockR<Position> positions) {
for (uint i = 0; i < count; i++) {
// read-only access
}
}
}
W.Query().ReadBlock<Position>().For<SumPositionsBlock>();
// Via ref (to preserve state)
var func = new MoveBlock();
W.Query().WriteBlock<Position, Velocity>().For(ref func);
// Parallel version
W.Query().WriteBlock<Position, Velocity>().ForParallel<MoveBlock>(minEntitiesPerThread: 50000);
Batch operations
Bulk operations on all entities matching a filter — without writing a loop. Can be orders of magnitude faster than manual iteration via For: instead of per-entity processing, batch operations work with bitmasks — in the best case, adding or removing a component/tag for 64 entities is a single bitwise operation. Support call chaining — multiple operations can be performed in a single pass.
// Add component to all entities (from 1 to 5 types)
W.Query<All<Position>>().BatchSet(new Velocity { Value = 1f });
// Delete component from all
W.Query<All<Position, Velocity>>().BatchDelete<Velocity>();
// Disable/enable component on all
W.Query<All<Position>>().BatchDisable<Position>();
W.Query<AllOnlyDisabled<Position>>().BatchEnable<Position>();
// Tags: set, delete, toggle, apply by condition (from 1 to 5 types)
W.Query<All<Position>>().BatchSet<Unit>();
W.Query<All<Unit>>().BatchDelete<Unit>();
W.Query<All<Position>>().BatchToggle<Unit>();
W.Query<All<Position>>().BatchApply<Unit>(true);
// Chaining
W.Query<All<Position>>()
.BatchSet(new Velocity { Value = 1f })
.BatchSet<Unit>()
.BatchDisable<Position>();
Destroying and unloading entities
// Destroy all entities matching a filter
W.Query<All<Position>>().BatchDestroy();
// With parameters
W.Query<All<Unit>>().BatchDestroy(
entities: EntityStatusType.Any,
mode: QueryMode.Flexible
);
// Unload all entities matching a filter
// (marks as unloaded, removes components/tags, but preserves entity IDs and versions)
W.Query<All<Position>>().BatchUnload();
// With parameters
W.Query<All<Unit>>().BatchUnload(
entities: EntityStatusType.Any,
mode: QueryMode.Flexible
);
Clusters
All query methods (Entities, For, ForParallel, Search, Batch*, BatchDestroy, BatchUnload) accept a clusters parameter:
ReadOnlySpan<ushort> clusters = stackalloc ushort[] { 2, 5, 12 };
foreach (var entity in W.Query<All<Position>>().Entities(clusters: clusters)) {
// iteration only over entities from clusters 2, 5, 12
}
W.Query().For(static (W.Entity entity, ref Position pos) => {
// ...
}, clusters: clusters);
QueryMode
For For, Search, Entities methods:
-
QueryMode.Strict(default) — the DEBUG assert is precise: on non-current entities that belong to the iteration snapshot, only operations on filter-typesTthat could remove the cached match are blocked, plus entity-levelDestroy/Disable(when iteratingEnabled) /Enable(when iteratingDisabled):Filter Blocked on a non-current snapshot entity All<T>Delete<T>,Disable<T>AllOnlyDisabled<T>Delete<T>,Enable<T>AllWithDisabled<T>Delete<T>None<T>Add<T>,Set<T>,Enable<T>Operations on types not in the filter, on entities outside the snapshot (created mid-iteration or not matching the filter), and on the current entity are not blocked. Strict is the fastest mode (uses fast-path for fully-populated blocks).
-
QueryMode.Flexible— same blockers on filter-types as Strict, but additionally allows entity-levelDestroy/Disable/Enableon other snapshot entities; such entities are correctly excluded from the remaining iteration via cached bitmask updates. Slower — re-reads the cached mask per entity.
var anotherEntity = W.NewEntity<Default>();
anotherEntity.Add<Position>();
// Strict: destroying another snapshot entity during iteration — error in DEBUG
foreach (var entity in W.Query<All<Position>>().Entities()) {
anotherEntity.Destroy(); // ERROR in DEBUG (anotherEntity is in the snapshot)
// OK — entities created mid-iteration are NOT in the snapshot
var fresh = W.NewEntity<Default>();
fresh.Add<Position>();
fresh.Set(new Velocity { ... });
}
// Flexible: destroy/disable/enable of another snapshot entity is allowed
foreach (var entity in W.Query<All<Position>>().EntitiesFlexible()) {
anotherEntity.Destroy(); // OK — excluded from the rest of the iteration
// anotherEntity.Delete<Position>(); // still ERROR in DEBUG — filtered-type mutation on another snapshot entity
}
// For For/Search via parameter
W.Query().For(static (ref Position pos) => {
// ...
}, queryMode: QueryMode.Flexible);
Flexible is useful when iteration logic destroys or toggles (Disable/Enable) other snapshot entities — for example, pruning child entities during a parent traversal or bulk-deactivating entities affected by an AoE effect. It does not lift the filter-type blockers — such mutations must be deferred (e.g. collected into a buffer and applied after the foreach). In other cases, prefer Strict for performance. Note that creating new entities inside the loop and configuring them is always allowed in both modes — newly created entities are not part of the iteration snapshot.