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)
|
||||
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.
|
||||
// Use the iterator API with early-exit via break.
|
||||
var query = world.Query().With<Cell>().Without<Mark>().Build();
|
||||
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)
|
||||
target = entity;
|
||||
});
|
||||
if (iter.Current1.Row == Row && iter.Current1.Col == Col)
|
||||
{
|
||||
target = iter.CurrentEntity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
return; // Cell already occupied or invalid position.
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ public class RenderSystem : ISystem
|
|||
for (int c = 0; c < 3; 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();
|
||||
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(" Tic-Tac-Toe");
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@ public class WinCheckSystem : ISystem
|
|||
if (state.Status != GameStatus.Playing)
|
||||
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];
|
||||
|
||||
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.
|
||||
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 _disposed;
|
||||
|
||||
#if DEBUG
|
||||
// Tracks entities whose components were accessed via GetComponent<T>
|
||||
// but never had MarkModified<T> called. Used to warn about missing
|
||||
// MarkModified calls after a system runs.
|
||||
// Deferred structural mutation support: when iterating entities,
|
||||
// AddComponent, RemoveComponent, and DestroyEntity are buffered and
|
||||
// applied after the iteration completes.
|
||||
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)> _markedModified = new();
|
||||
#endif
|
||||
|
||||
public World()
|
||||
{
|
||||
|
|
@ -75,6 +90,21 @@ public class World : IDisposable
|
|||
/// from the source entities.
|
||||
/// </summary>
|
||||
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))
|
||||
return;
|
||||
|
|
@ -135,6 +165,23 @@ public class World : IDisposable
|
|||
/// the reverse index is updated automatically.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
|
|
@ -170,6 +217,22 @@ public class World : IDisposable
|
|||
/// the reverse index is updated automatically.
|
||||
/// </summary>
|
||||
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.
|
||||
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"/>
|
||||
/// for the given entity. Throws if the entity does not have the component.
|
||||
///
|
||||
/// After mutating the component through this reference, you must call
|
||||
/// <see cref="MarkModified{T}"/> so that observers are notified.
|
||||
/// In DEBUG builds, a warning is logged if you forget.
|
||||
/// During iteration (inside <see cref="ForEach"/> or a <c>Select</c> loop),
|
||||
/// components accessed through this method are automatically marked as
|
||||
/// modified when the iteration scope ends. Outside of iteration,
|
||||
/// <see cref="MarkModified{T}"/> must be called explicitly.
|
||||
/// </summary>
|
||||
public ref T GetComponent<T>(Entity entity) where T : struct
|
||||
{
|
||||
#if DEBUG
|
||||
if (_iterationDepth > 0)
|
||||
_accessedComponents.Add((entity, typeof(T)));
|
||||
#endif
|
||||
return ref _components.Get<T>(entity);
|
||||
}
|
||||
|
||||
|
|
@ -226,16 +289,14 @@ public class World : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Marks a component of type <typeparamref name="T"/> on the given entity
|
||||
/// as modified. Must be called after mutating a component via
|
||||
/// <see cref="GetComponent{T}"/> so that observers are notified.
|
||||
/// as modified. Only needed outside of iteration scopes — during
|
||||
/// <see cref="ForEach"/> or a <c>Select</c> loop, components accessed
|
||||
/// via <see cref="GetComponent{T}"/> are auto-marked.
|
||||
/// </summary>
|
||||
public void MarkModified<T>(Entity entity) where T : struct
|
||||
{
|
||||
_changes.Pending.MarkComponentModified(entity, typeof(T));
|
||||
|
||||
#if DEBUG
|
||||
_markedModified.Add((entity, typeof(T)));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -245,9 +306,6 @@ public class World : IDisposable
|
|||
/// </summary>
|
||||
public void PostChanges()
|
||||
{
|
||||
#if DEBUG
|
||||
WarnUnmarkedModifications();
|
||||
#endif
|
||||
_changes.Post();
|
||||
}
|
||||
|
||||
|
|
@ -371,14 +429,9 @@ public class World : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// one component type.
|
||||
///
|
||||
/// <b>Caution:</b> Adding or removing components (including destroying
|
||||
/// 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.
|
||||
/// one component type. Structural mutations (AddComponent, RemoveComponent,
|
||||
/// DestroyEntity) made during iteration are deferred and applied after
|
||||
/// the iteration completes.
|
||||
/// </summary>
|
||||
public void ForEach<T1>(
|
||||
QueryDescriptor query,
|
||||
|
|
@ -386,12 +439,20 @@ public class World : IDisposable
|
|||
where T1 : struct
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
QueryExecutor.ForEach(_components, query, action);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndIteration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// two component types.
|
||||
/// two component types. Structural mutations are deferred.
|
||||
/// </summary>
|
||||
public void ForEach<T1, T2>(
|
||||
QueryDescriptor query,
|
||||
|
|
@ -399,12 +460,20 @@ public class World : IDisposable
|
|||
where T1 : struct where T2 : struct
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1), typeof(T2));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
QueryExecutor.ForEach(_components, query, action);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndIteration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// three component types.
|
||||
/// three component types. Structural mutations are deferred.
|
||||
/// </summary>
|
||||
public void ForEach<T1, T2, T3>(
|
||||
QueryDescriptor query,
|
||||
|
|
@ -412,12 +481,20 @@ public class World : IDisposable
|
|||
where T1 : struct where T2 : struct where T3 : struct
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
QueryExecutor.ForEach(_components, query, action);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndIteration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// four component types.
|
||||
/// four component types. Structural mutations are deferred.
|
||||
/// </summary>
|
||||
public void ForEach<T1, T2, T3, T4>(
|
||||
QueryDescriptor query,
|
||||
|
|
@ -425,12 +502,20 @@ public class World : IDisposable
|
|||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
QueryExecutor.ForEach(_components, query, action);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndIteration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// five component types.
|
||||
/// five component types. Structural mutations are deferred.
|
||||
/// </summary>
|
||||
public void ForEach<T1, T2, T3, T4, T5>(
|
||||
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
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
QueryExecutor.ForEach(_components, query, action);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndIteration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all entities matching the query, providing ref access to
|
||||
/// six component types.
|
||||
/// six component types. Structural mutations are deferred.
|
||||
/// </summary>
|
||||
public void ForEach<T1, T2, T3, T4, T5, T6>(
|
||||
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
|
||||
{
|
||||
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
|
||||
BeginIteration();
|
||||
try
|
||||
{
|
||||
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 ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -512,28 +683,4 @@ public class World : IDisposable
|
|||
_disposed = true;
|
||||
_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