feat: add deferred structural mutations and entity iterators
Implement deferred structural mutations (AddComponent, RemoveComponent, DestroyEntity) to allow safe modification of the world during iteration. Additionally, introduce a new `EntityIterator` API providing zero- allocation `ref struct` iterators (`Select<T1>`, `Select<T1, T2>`, etc.) to support manual iteration with early-exit capabilities. As part of these changes, component access via `GetComponent<T>` during iteration now automatically marks components as modified.
This commit is contained in:
parent
9aa2a71257
commit
f1eddc75ae
|
|
@ -20,20 +20,20 @@ public struct PlaceMarkCommand : ICommand
|
||||||
if (state.Status != GameStatus.Playing)
|
if (state.Status != GameStatus.Playing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Copy to locals so the lambda in ForEach can capture them
|
|
||||||
// (this is a struct, so `this` cannot be captured directly).
|
|
||||||
int row = Row;
|
|
||||||
int col = Col;
|
|
||||||
|
|
||||||
// Find the cell entity at (Row, Col) that has no Mark.
|
// Find the cell entity at (Row, Col) that has no Mark.
|
||||||
|
// Use the iterator API with early-exit via break.
|
||||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
||||||
Entity? target = null;
|
Entity? target = null;
|
||||||
|
|
||||||
world.ForEach(query, (Entity entity, ref Cell cell) =>
|
using var iter = world.Select<Cell>(query);
|
||||||
|
while (iter.MoveNext())
|
||||||
{
|
{
|
||||||
if (cell.Row == row && cell.Col == col)
|
if (iter.Current1.Row == Row && iter.Current1.Col == Col)
|
||||||
target = entity;
|
{
|
||||||
});
|
target = iter.CurrentEntity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (target == null)
|
if (target == null)
|
||||||
return; // Cell already occupied or invalid position.
|
return; // Cell already occupied or invalid position.
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,14 @@ public class RenderSystem : ISystem
|
||||||
for (int c = 0; c < 3; c++)
|
for (int c = 0; c < 3; c++)
|
||||||
grid[r, c] = '.';
|
grid[r, c] = '.';
|
||||||
|
|
||||||
// Fill in placed marks.
|
// Fill in placed marks using the iterator API.
|
||||||
var markQuery = world.Query().With<Cell>().With<Mark>().Build();
|
var markQuery = world.Query().With<Cell>().With<Mark>().Build();
|
||||||
world.ForEach(markQuery, (Entity entity, ref Cell cell, ref Mark mark) =>
|
using var markIter = world.Select<Cell, Mark>(markQuery);
|
||||||
|
while (markIter.MoveNext())
|
||||||
{
|
{
|
||||||
grid[cell.Row, cell.Col] = mark.Player == Player.X ? 'X' : 'O';
|
grid[markIter.Current1.Row, markIter.Current1.Col] =
|
||||||
});
|
markIter.Current2.Player == Player.X ? 'X' : 'O';
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" Tic-Tac-Toe");
|
Console.WriteLine(" Tic-Tac-Toe");
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,14 @@ public class WinCheckSystem : ISystem
|
||||||
if (state.Status != GameStatus.Playing)
|
if (state.Status != GameStatus.Playing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Build a 3×3 grid of marks.
|
// Build a 3×3 grid of marks using the iterator API.
|
||||||
var grid = new Player[3, 3];
|
var grid = new Player[3, 3];
|
||||||
|
|
||||||
world.ForEach(Query, (Entity entity, ref Cell cell, ref Mark mark) =>
|
using var iter = world.Select<Cell, Mark>(Query);
|
||||||
|
while (iter.MoveNext())
|
||||||
{
|
{
|
||||||
grid[cell.Row, cell.Col] = mark.Player;
|
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Check rows.
|
// Check rows.
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace OECS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides zero-allocation ref struct iterators for querying entities.
|
||||||
|
/// Use with <c>foreach</c> or manual <c>while (iter.MoveNext())</c>.
|
||||||
|
/// Supports <c>break</c>, <c>continue</c>, and early returns.
|
||||||
|
/// </summary>
|
||||||
|
public static class EntityIterator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an iterator over entities matching the query, with ref access
|
||||||
|
/// to one component type.
|
||||||
|
/// </summary>
|
||||||
|
public static Select1<T1> Select<T1>(
|
||||||
|
this World world, QueryDescriptor query)
|
||||||
|
where T1 : struct
|
||||||
|
{
|
||||||
|
return new Select1<T1>(world, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an iterator over entities matching the query, with ref access
|
||||||
|
/// to two component types.
|
||||||
|
/// </summary>
|
||||||
|
public static Select2<T1, T2> Select<T1, T2>(
|
||||||
|
this World world, QueryDescriptor query)
|
||||||
|
where T1 : struct where T2 : struct
|
||||||
|
{
|
||||||
|
return new Select2<T1, T2>(world, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an iterator over entities matching the query, with ref access
|
||||||
|
/// to three component types.
|
||||||
|
/// </summary>
|
||||||
|
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
||||||
|
this World world, QueryDescriptor query)
|
||||||
|
where T1 : struct where T2 : struct where T3 : struct
|
||||||
|
{
|
||||||
|
return new Select3<T1, T2, T3>(world, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ref struct iterator for queries with one component type.
|
||||||
|
/// </summary>
|
||||||
|
public ref struct Select1<T1> where T1 : struct
|
||||||
|
{
|
||||||
|
private readonly World _world;
|
||||||
|
private readonly SparseSet<T1>? _set;
|
||||||
|
private readonly ComponentStore _store;
|
||||||
|
private readonly IReadOnlySet<Type> _without;
|
||||||
|
private int _index;
|
||||||
|
private readonly int _count;
|
||||||
|
|
||||||
|
internal Select1(World world, QueryDescriptor query)
|
||||||
|
{
|
||||||
|
_world = world;
|
||||||
|
_store = world.Components;
|
||||||
|
_without = query.Without;
|
||||||
|
_set = _store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||||
|
_count = _set?.Count ?? 0;
|
||||||
|
_index = -1;
|
||||||
|
|
||||||
|
_world.BeginIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity CurrentEntity { get; private set; }
|
||||||
|
public ref T1 Current1 => ref _set!.Get(CurrentEntity);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_set == null) return false;
|
||||||
|
while (++_index < _count)
|
||||||
|
{
|
||||||
|
CurrentEntity = _set.DenseEntities[_index];
|
||||||
|
if (CurrentEntity.Id == 1) continue; // Skip singleton.
|
||||||
|
if (!PassesWithout()) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Select1<T1> GetEnumerator() => this;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_world.EndIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PassesWithout()
|
||||||
|
{
|
||||||
|
foreach (var type in _without)
|
||||||
|
{
|
||||||
|
var set = _store.GetSet(type);
|
||||||
|
if (set != null && set.Contains(CurrentEntity))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ref struct iterator for queries with two component types.
|
||||||
|
/// </summary>
|
||||||
|
public ref struct Select2<T1, T2>
|
||||||
|
where T1 : struct where T2 : struct
|
||||||
|
{
|
||||||
|
private readonly World _world;
|
||||||
|
private readonly SparseSet<T1>? _set1;
|
||||||
|
private readonly SparseSet<T2>? _set2;
|
||||||
|
private readonly ComponentStore _store;
|
||||||
|
private readonly IReadOnlySet<Type> _without;
|
||||||
|
private int _index;
|
||||||
|
private readonly int _count;
|
||||||
|
|
||||||
|
internal Select2(World world, QueryDescriptor query)
|
||||||
|
{
|
||||||
|
_world = world;
|
||||||
|
_store = world.Components;
|
||||||
|
_without = query.Without;
|
||||||
|
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||||
|
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||||
|
_count = _set1?.Count ?? 0;
|
||||||
|
_index = -1;
|
||||||
|
|
||||||
|
_world.BeginIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity CurrentEntity { get; private set; }
|
||||||
|
public ref T1 Current1 => ref _set1!.Get(CurrentEntity);
|
||||||
|
public ref T2 Current2 => ref _set2!.Get(CurrentEntity);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_set1 == null || _set2 == null) return false;
|
||||||
|
while (++_index < _count)
|
||||||
|
{
|
||||||
|
CurrentEntity = _set1.DenseEntities[_index];
|
||||||
|
if (CurrentEntity.Id == 1) continue;
|
||||||
|
if (!_set2.Contains(CurrentEntity)) continue;
|
||||||
|
if (!PassesWithout()) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Select2<T1, T2> GetEnumerator() => this;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_world.EndIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PassesWithout()
|
||||||
|
{
|
||||||
|
foreach (var type in _without)
|
||||||
|
{
|
||||||
|
var set = _store.GetSet(type);
|
||||||
|
if (set != null && set.Contains(CurrentEntity))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ref struct iterator for queries with three component types.
|
||||||
|
/// </summary>
|
||||||
|
public ref struct Select3<T1, T2, T3>
|
||||||
|
where T1 : struct where T2 : struct where T3 : struct
|
||||||
|
{
|
||||||
|
private readonly World _world;
|
||||||
|
private readonly SparseSet<T1>? _set1;
|
||||||
|
private readonly SparseSet<T2>? _set2;
|
||||||
|
private readonly SparseSet<T3>? _set3;
|
||||||
|
private readonly ComponentStore _store;
|
||||||
|
private readonly IReadOnlySet<Type> _without;
|
||||||
|
private int _index;
|
||||||
|
private readonly int _count;
|
||||||
|
|
||||||
|
internal Select3(World world, QueryDescriptor query)
|
||||||
|
{
|
||||||
|
_world = world;
|
||||||
|
_store = world.Components;
|
||||||
|
_without = query.Without;
|
||||||
|
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||||
|
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||||
|
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||||
|
_count = _set1?.Count ?? 0;
|
||||||
|
_index = -1;
|
||||||
|
|
||||||
|
_world.BeginIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity CurrentEntity { get; private set; }
|
||||||
|
public ref T1 Current1 => ref _set1!.Get(CurrentEntity);
|
||||||
|
public ref T2 Current2 => ref _set2!.Get(CurrentEntity);
|
||||||
|
public ref T3 Current3 => ref _set3!.Get(CurrentEntity);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_set1 == null || _set2 == null || _set3 == null) return false;
|
||||||
|
while (++_index < _count)
|
||||||
|
{
|
||||||
|
CurrentEntity = _set1.DenseEntities[_index];
|
||||||
|
if (CurrentEntity.Id == 1) continue;
|
||||||
|
if (!_set2.Contains(CurrentEntity)) continue;
|
||||||
|
if (!_set3.Contains(CurrentEntity)) continue;
|
||||||
|
if (!PassesWithout()) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Select3<T1, T2, T3> GetEnumerator() => this;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_world.EndIteration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PassesWithout()
|
||||||
|
{
|
||||||
|
foreach (var type in _without)
|
||||||
|
{
|
||||||
|
var set = _store.GetSet(type);
|
||||||
|
if (set != null && set.Contains(CurrentEntity))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,13 +24,28 @@ public class World : IDisposable
|
||||||
private bool _singletonCreated;
|
private bool _singletonCreated;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
#if DEBUG
|
// Deferred structural mutation support: when iterating entities,
|
||||||
// Tracks entities whose components were accessed via GetComponent<T>
|
// AddComponent, RemoveComponent, and DestroyEntity are buffered and
|
||||||
// but never had MarkModified<T> called. Used to warn about missing
|
// applied after the iteration completes.
|
||||||
// MarkModified calls after a system runs.
|
private int _iterationDepth;
|
||||||
|
private readonly List<PendingMutation> _pendingMutations = new();
|
||||||
|
|
||||||
|
private enum PendingMutationKind { AddComponent, RemoveComponent, DestroyEntity }
|
||||||
|
|
||||||
|
private struct PendingMutation
|
||||||
|
{
|
||||||
|
public PendingMutationKind Kind;
|
||||||
|
public Entity Entity;
|
||||||
|
public object? Component; // boxed struct for AddComponent
|
||||||
|
public Type ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-dirty-marking: during iteration, component accesses via
|
||||||
|
// GetComponent<T> are tracked. When the iteration scope ends,
|
||||||
|
// all accessed components are automatically marked as modified.
|
||||||
|
// Outside of iteration, MarkModified<T> must be called explicitly.
|
||||||
private readonly HashSet<(Entity Entity, Type ComponentType)> _accessedComponents = new();
|
private readonly HashSet<(Entity Entity, Type ComponentType)> _accessedComponents = new();
|
||||||
private readonly HashSet<(Entity Entity, Type ComponentType)> _markedModified = new();
|
private readonly HashSet<(Entity Entity, Type ComponentType)> _markedModified = new();
|
||||||
#endif
|
|
||||||
|
|
||||||
public World()
|
public World()
|
||||||
{
|
{
|
||||||
|
|
@ -75,6 +90,21 @@ public class World : IDisposable
|
||||||
/// from the source entities.
|
/// from the source entities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DestroyEntity(Entity entity)
|
public void DestroyEntity(Entity entity)
|
||||||
|
{
|
||||||
|
if (_iterationDepth > 0)
|
||||||
|
{
|
||||||
|
_pendingMutations.Add(new PendingMutation
|
||||||
|
{
|
||||||
|
Kind = PendingMutationKind.DestroyEntity,
|
||||||
|
Entity = entity
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyEntityImpl(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DestroyEntityImpl(Entity entity)
|
||||||
{
|
{
|
||||||
if (!_allocator.IsAlive(entity))
|
if (!_allocator.IsAlive(entity))
|
||||||
return;
|
return;
|
||||||
|
|
@ -135,6 +165,23 @@ public class World : IDisposable
|
||||||
/// the reverse index is updated automatically.
|
/// the reverse index is updated automatically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddComponent<T>(Entity entity, T component) where T : struct
|
public void AddComponent<T>(Entity entity, T component) where T : struct
|
||||||
|
{
|
||||||
|
if (_iterationDepth > 0)
|
||||||
|
{
|
||||||
|
_pendingMutations.Add(new PendingMutation
|
||||||
|
{
|
||||||
|
Kind = PendingMutationKind.AddComponent,
|
||||||
|
Entity = entity,
|
||||||
|
Component = component,
|
||||||
|
ComponentType = typeof(T)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddComponentImpl(entity, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddComponentImpl<T>(Entity entity, T component) where T : struct
|
||||||
{
|
{
|
||||||
ThrowIfNotAlive(entity);
|
ThrowIfNotAlive(entity);
|
||||||
|
|
||||||
|
|
@ -170,6 +217,22 @@ public class World : IDisposable
|
||||||
/// the reverse index is updated automatically.
|
/// the reverse index is updated automatically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RemoveComponent<T>(Entity entity) where T : struct
|
public void RemoveComponent<T>(Entity entity) where T : struct
|
||||||
|
{
|
||||||
|
if (_iterationDepth > 0)
|
||||||
|
{
|
||||||
|
_pendingMutations.Add(new PendingMutation
|
||||||
|
{
|
||||||
|
Kind = PendingMutationKind.RemoveComponent,
|
||||||
|
Entity = entity,
|
||||||
|
ComponentType = typeof(T)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveComponentImpl<T>(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveComponentImpl<T>(Entity entity) where T : struct
|
||||||
{
|
{
|
||||||
// Update relationship index before removal.
|
// Update relationship index before removal.
|
||||||
if (typeof(IRelationship).IsAssignableFrom(typeof(T)))
|
if (typeof(IRelationship).IsAssignableFrom(typeof(T)))
|
||||||
|
|
@ -192,15 +255,15 @@ public class World : IDisposable
|
||||||
/// Returns a reference to the component of type <typeparamref name="T"/>
|
/// Returns a reference to the component of type <typeparamref name="T"/>
|
||||||
/// for the given entity. Throws if the entity does not have the component.
|
/// for the given entity. Throws if the entity does not have the component.
|
||||||
///
|
///
|
||||||
/// After mutating the component through this reference, you must call
|
/// During iteration (inside <see cref="ForEach"/> or a <c>Select</c> loop),
|
||||||
/// <see cref="MarkModified{T}"/> so that observers are notified.
|
/// components accessed through this method are automatically marked as
|
||||||
/// In DEBUG builds, a warning is logged if you forget.
|
/// modified when the iteration scope ends. Outside of iteration,
|
||||||
|
/// <see cref="MarkModified{T}"/> must be called explicitly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ref T GetComponent<T>(Entity entity) where T : struct
|
public ref T GetComponent<T>(Entity entity) where T : struct
|
||||||
{
|
{
|
||||||
#if DEBUG
|
if (_iterationDepth > 0)
|
||||||
_accessedComponents.Add((entity, typeof(T)));
|
_accessedComponents.Add((entity, typeof(T)));
|
||||||
#endif
|
|
||||||
return ref _components.Get<T>(entity);
|
return ref _components.Get<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,16 +289,14 @@ public class World : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a component of type <typeparamref name="T"/> on the given entity
|
/// Marks a component of type <typeparamref name="T"/> on the given entity
|
||||||
/// as modified. Must be called after mutating a component via
|
/// as modified. Only needed outside of iteration scopes — during
|
||||||
/// <see cref="GetComponent{T}"/> so that observers are notified.
|
/// <see cref="ForEach"/> or a <c>Select</c> loop, components accessed
|
||||||
|
/// via <see cref="GetComponent{T}"/> are auto-marked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void MarkModified<T>(Entity entity) where T : struct
|
public void MarkModified<T>(Entity entity) where T : struct
|
||||||
{
|
{
|
||||||
_changes.Pending.MarkComponentModified(entity, typeof(T));
|
_changes.Pending.MarkComponentModified(entity, typeof(T));
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
_markedModified.Add((entity, typeof(T)));
|
_markedModified.Add((entity, typeof(T)));
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -245,9 +306,6 @@ public class World : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PostChanges()
|
public void PostChanges()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
WarnUnmarkedModifications();
|
|
||||||
#endif
|
|
||||||
_changes.Post();
|
_changes.Post();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -371,14 +429,9 @@ public class World : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// one component type.
|
/// one component type. Structural mutations (AddComponent, RemoveComponent,
|
||||||
///
|
/// DestroyEntity) made during iteration are deferred and applied after
|
||||||
/// <b>Caution:</b> Adding or removing components (including destroying
|
/// the iteration completes.
|
||||||
/// entities) during iteration may cause entities to be skipped or
|
|
||||||
/// processed twice. This is a consequence of the sparse set's
|
|
||||||
/// swap-remove strategy. If you need to make structural changes,
|
|
||||||
/// collect the affected entities first, then apply changes after
|
|
||||||
/// the iteration.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1>(
|
public void ForEach<T1>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -386,12 +439,20 @@ public class World : IDisposable
|
||||||
where T1 : struct
|
where T1 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1));
|
ValidateQueryTypes(query, typeof(T1));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// two component types.
|
/// two component types. Structural mutations are deferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1, T2>(
|
public void ForEach<T1, T2>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -399,12 +460,20 @@ public class World : IDisposable
|
||||||
where T1 : struct where T2 : struct
|
where T1 : struct where T2 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1), typeof(T2));
|
ValidateQueryTypes(query, typeof(T1), typeof(T2));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// three component types.
|
/// three component types. Structural mutations are deferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1, T2, T3>(
|
public void ForEach<T1, T2, T3>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -412,12 +481,20 @@ public class World : IDisposable
|
||||||
where T1 : struct where T2 : struct where T3 : struct
|
where T1 : struct where T2 : struct where T3 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3));
|
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// four component types.
|
/// four component types. Structural mutations are deferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1, T2, T3, T4>(
|
public void ForEach<T1, T2, T3, T4>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -425,12 +502,20 @@ public class World : IDisposable
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
|
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
|
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// five component types.
|
/// five component types. Structural mutations are deferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1, T2, T3, T4, T5>(
|
public void ForEach<T1, T2, T3, T4, T5>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -438,12 +523,20 @@ public class World : IDisposable
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
|
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
|
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Iterates all entities matching the query, providing ref access to
|
/// Iterates all entities matching the query, providing ref access to
|
||||||
/// six component types.
|
/// six component types. Structural mutations are deferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ForEach<T1, T2, T3, T4, T5, T6>(
|
public void ForEach<T1, T2, T3, T4, T5, T6>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
|
|
@ -451,8 +544,86 @@ public class World : IDisposable
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
|
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
|
||||||
{
|
{
|
||||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
|
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
|
||||||
|
BeginIteration();
|
||||||
|
try
|
||||||
|
{
|
||||||
QueryExecutor.ForEach(_components, query, action);
|
QueryExecutor.ForEach(_components, query, action);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EndIteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins an iteration scope. Structural mutations are buffered
|
||||||
|
/// until <see cref="EndIteration"/> is called.
|
||||||
|
/// </summary>
|
||||||
|
internal void BeginIteration()
|
||||||
|
{
|
||||||
|
_iterationDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends an iteration scope. When the outermost scope ends, all
|
||||||
|
/// buffered structural mutations are applied and auto-tracked
|
||||||
|
/// component modifications are posted.
|
||||||
|
/// </summary>
|
||||||
|
internal void EndIteration()
|
||||||
|
{
|
||||||
|
_iterationDepth--;
|
||||||
|
if (_iterationDepth == 0)
|
||||||
|
{
|
||||||
|
// Auto-mark all components accessed via GetComponent<T>
|
||||||
|
// during the iteration as modified.
|
||||||
|
foreach (var (entity, componentType) in _accessedComponents)
|
||||||
|
{
|
||||||
|
if (!_markedModified.Contains((entity, componentType)))
|
||||||
|
{
|
||||||
|
_changes.Pending.MarkComponentModified(entity, componentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_accessedComponents.Clear();
|
||||||
|
_markedModified.Clear();
|
||||||
|
|
||||||
|
FlushPendingMutations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushPendingMutations()
|
||||||
|
{
|
||||||
|
if (_pendingMutations.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var m in _pendingMutations)
|
||||||
|
{
|
||||||
|
switch (m.Kind)
|
||||||
|
{
|
||||||
|
case PendingMutationKind.AddComponent:
|
||||||
|
// Use reflection to call AddComponentImpl<T>.
|
||||||
|
var addMethod = typeof(World).GetMethod(
|
||||||
|
nameof(AddComponentImpl),
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||||
|
var genericAdd = addMethod.MakeGenericMethod(m.ComponentType);
|
||||||
|
genericAdd.Invoke(this, [m.Entity, m.Component]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PendingMutationKind.RemoveComponent:
|
||||||
|
var removeMethod = typeof(World).GetMethod(
|
||||||
|
nameof(RemoveComponentImpl),
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||||
|
var genericRemove = removeMethod.MakeGenericMethod(m.ComponentType);
|
||||||
|
genericRemove.Invoke(this, [m.Entity]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PendingMutationKind.DestroyEntity:
|
||||||
|
DestroyEntityImpl(m.Entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingMutations.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Internal Access ───────────────────────────────────────────────
|
// ── Internal Access ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -512,28 +683,4 @@ public class World : IDisposable
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_changes.Dispose();
|
_changes.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
/// <summary>
|
|
||||||
/// Logs warnings for any components that were accessed via
|
|
||||||
/// <see cref="GetComponent{T}"/> but never had
|
|
||||||
/// <see cref="MarkModified{T}"/> called.
|
|
||||||
/// </summary>
|
|
||||||
private void WarnUnmarkedModifications()
|
|
||||||
{
|
|
||||||
foreach (var (entity, componentType) in _accessedComponents)
|
|
||||||
{
|
|
||||||
if (!_markedModified.Contains((entity, componentType)))
|
|
||||||
{
|
|
||||||
Debug.WriteLine(
|
|
||||||
$"[OECS] Warning: Component '{componentType.Name}' on {entity} " +
|
|
||||||
$"was accessed via GetComponent but MarkModified was never called. " +
|
|
||||||
$"Observers will not be notified of the change.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_accessedComponents.Clear();
|
|
||||||
_markedModified.Clear();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue