refactor: rename iteration to batching and expand scope

Rename `BeginIteration`/`EndIteration` to `BeginBatching`/`EndBatching`
to reflect that the mechanism now covers both entity iteration and
system execution.

- Update `ISystem` and `ITickedSystem` to use `RunImpl` for logic.
- Add `SystemExtensions.Run` to wrap system execution in a batching
  scope.
- Update `SystemGroup` to wrap system execution in a batching scope.
- Update `WorldQueryExtensions` to use the new batching terminology.
This commit is contained in:
hypercross 2026-07-21 09:45:49 +08:00
parent 96d732d6ab
commit e460c0e70f
5 changed files with 79 additions and 38 deletions

View File

@ -2,12 +2,44 @@ namespace OECS;
/// <summary> /// <summary>
/// A system that runs on every tick. /// A system that runs on every tick.
/// Systems receive the world in their <see cref="Run"/> method. /// Implement <see cref="RunImpl"/> with the system logic.
/// Call <see cref="ISystem.Run"/> to execute with batching.
/// </summary> /// </summary>
public interface ISystem public interface ISystem
{ {
/// <summary> /// <summary>
/// Executes the system logic for this tick. /// Implement this with the system's logic for this tick.
/// The caller (e.g., <see cref="ISystem.Run"/>) wraps this
/// in a batching scope so mutations and component accesses
/// are automatically tracked.
/// </summary> /// </summary>
void Run(World world); void RunImpl(World world);
}
/// <summary>
/// Extension methods for running systems with proper batching.
/// </summary>
public static class SystemExtensions
{
/// <summary>
/// Runs a system with a batching scope. Structural mutations are
/// buffered, and <see cref="World.GetComponent{T}"/> calls are
/// auto-tracked for modification until the system returns.
/// </summary>
public static void Run(this ISystem system, World world)
{
world.BeginBatching();
system.RunImpl(world);
world.EndBatching();
}
/// <summary>
/// Runs a ticked system with a batching scope.
/// </summary>
public static void Run(this ITickedSystem system, World world, Tick tick)
{
world.BeginBatching();
system.RunImpl(world, tick);
world.EndBatching();
}
} }

View File

@ -7,7 +7,7 @@ namespace OECS;
public interface ITickedSystem : ISystem public interface ITickedSystem : ISystem
{ {
/// <summary> /// <summary>
/// Executes the system logic with tick information. /// Implement this with the system's ticked logic.
/// </summary> /// </summary>
void Run(World world, Tick tick); void RunImpl(World world, Tick tick);
} }

View File

@ -62,6 +62,7 @@ public class SystemGroup
// sees their effects. // sees their effects.
_world.ExecuteCommands(); _world.ExecuteCommands();
_world.BeginBatching();
foreach (var system in _systems) foreach (var system in _systems)
{ {
if (system is ITickedSystem ticked) if (system is ITickedSystem ticked)
@ -77,10 +78,15 @@ public class SystemGroup
// see the effects of commands enqueued by prior systems. // see the effects of commands enqueued by prior systems.
_world.ExecuteCommands(); _world.ExecuteCommands();
// Flush pending mutations so subsequent systems see
// entity/component changes from commands and previous systems.
_world.FlushPendingMutations();
// Post changes after each system so subscribers see // Post changes after each system so subscribers see
// incremental updates. // incremental updates.
_world.PostChanges(); _world.PostChanges();
} }
_world.EndBatching();
// Drain any remaining commands (e.g., those enqueued outside systems). // Drain any remaining commands (e.g., those enqueued outside systems).
_world.ExecuteCommands(); _world.ExecuteCommands();

View File

@ -19,10 +19,11 @@ public class World : IDisposable
private readonly Dictionary<Type, Entity> _singletonEntities = new(); private readonly Dictionary<Type, Entity> _singletonEntities = new();
private bool _disposed; private bool _disposed;
// Deferred structural mutation support: when iterating entities, // Deferred structural mutation support: when inside a batching scope
// AddComponent, RemoveComponent, and DestroyEntity are buffered and // (e.g., a foreach iteration or a system run), AddComponent,
// applied after the iteration completes. // RemoveComponent, and DestroyEntity are buffered and applied when
private int _iterationDepth; // the outermost scope ends.
private int _batchingDepth;
private readonly List<PendingMutation> _pendingMutations = new(); private readonly List<PendingMutation> _pendingMutations = new();
private enum PendingMutationKind { AddComponent, RemoveComponent, DestroyEntity } private enum PendingMutationKind { AddComponent, RemoveComponent, DestroyEntity }
@ -35,10 +36,10 @@ public class World : IDisposable
public Type ComponentType; public Type ComponentType;
} }
// Auto-dirty-marking: during iteration, component accesses via // Auto-dirty-marking: during a batching scope, component accesses via
// GetComponent<T> are tracked. When the iteration scope ends, // GetComponent<T> are tracked. When the scope ends, all accessed
// all accessed components are automatically marked as modified. // components are automatically marked as modified.
// Outside of iteration, MarkModified<T> must be called explicitly. // Outside of a batching scope, 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();
@ -86,7 +87,7 @@ public class World : IDisposable
/// </summary> /// </summary>
public void DestroyEntity(Entity entity) public void DestroyEntity(Entity entity)
{ {
if (_iterationDepth > 0) if (_batchingDepth > 0)
{ {
_pendingMutations.Add(new PendingMutation _pendingMutations.Add(new PendingMutation
{ {
@ -157,7 +158,7 @@ public class World : IDisposable
/// </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) if (_batchingDepth > 0)
{ {
_pendingMutations.Add(new PendingMutation _pendingMutations.Add(new PendingMutation
{ {
@ -226,7 +227,7 @@ public class World : IDisposable
/// </summary> /// </summary>
public void RemoveComponent<T>(Entity entity) where T : struct public void RemoveComponent<T>(Entity entity) where T : struct
{ {
if (_iterationDepth > 0) if (_batchingDepth > 0)
{ {
_pendingMutations.Add(new PendingMutation _pendingMutations.Add(new PendingMutation
{ {
@ -269,7 +270,7 @@ public class World : IDisposable
/// </summary> /// </summary>
public ref T GetComponent<T>(Entity entity) where T : struct public ref T GetComponent<T>(Entity entity) where T : struct
{ {
if (_iterationDepth > 0) if (_batchingDepth > 0)
_accessedComponents.Add((entity, typeof(T))); _accessedComponents.Add((entity, typeof(T)));
return ref _components.Get<T>(entity); return ref _components.Get<T>(entity);
} }
@ -465,23 +466,25 @@ public class World : IDisposable
// ── Iteration Lifecycle ────────────────────────────────────────── // ── Iteration Lifecycle ──────────────────────────────────────────
/// <summary> /// <summary>
/// Begins an iteration scope. Structural mutations are buffered /// Begins a batching scope. Structural mutations (AddComponent,
/// until <see cref="EndIteration"/> is called. /// RemoveComponent, DestroyEntity) are buffered until the outermost
/// <see cref="EndBatching"/> call. Component accesses via
/// <see cref="GetComponent{T}"/> are auto-tracked for modification.
/// </summary> /// </summary>
internal void BeginIteration() internal void BeginBatching()
{ {
_iterationDepth++; _batchingDepth++;
} }
/// <summary> /// <summary>
/// Ends an iteration scope. When the outermost scope ends, all /// Ends a batching scope. When the outermost scope ends, all
/// buffered structural mutations are applied and auto-tracked /// buffered structural mutations are applied and auto-tracked
/// component modifications are posted. /// component modifications are posted.
/// </summary> /// </summary>
internal void EndIteration() internal void EndBatching()
{ {
_iterationDepth--; _batchingDepth--;
if (_iterationDepth == 0) if (_batchingDepth == 0)
{ {
// Auto-mark all components accessed via GetComponent<T> // Auto-mark all components accessed via GetComponent<T>
// during the iteration as modified. // during the iteration as modified.
@ -499,7 +502,7 @@ public class World : IDisposable
} }
} }
private void FlushPendingMutations() internal void FlushPendingMutations()
{ {
if (_pendingMutations.Count == 0) if (_pendingMutations.Count == 0)
return; return;

View File

@ -117,7 +117,7 @@ public ref struct Select1<T1> where T1 : struct
_set = _store.GetSet(typeof(T1)) as SparseSet<T1>; _set = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_count = _set?.Count ?? 0; _count = _set?.Count ?? 0;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
/// <summary>The current entity handle.</summary> /// <summary>The current entity handle.</summary>
@ -147,7 +147,7 @@ public ref struct Select1<T1> where T1 : struct
public void Dispose() public void Dispose()
{ {
_world.EndIteration(); _world.EndBatching();
} }
private bool PassesWithout() private bool PassesWithout()
@ -188,7 +188,7 @@ public ref struct Select2<T1, T2> where T1 : struct where T2 : struct
_swapped = c2 < c1; _swapped = c2 < c1;
_count = _swapped ? c2 : c1; _count = _swapped ? c2 : c1;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
@ -221,7 +221,7 @@ public ref struct Select2<T1, T2> where T1 : struct where T2 : struct
public Select2<T1, T2> GetEnumerator() => this; public Select2<T1, T2> GetEnumerator() => this;
public void Dispose() => _world.EndIteration(); public void Dispose() => _world.EndBatching();
private bool PassesWithout() private bool PassesWithout()
{ {
@ -268,7 +268,7 @@ public ref struct Select3<T1, T2, T3>
else { _driver = 0; _count = c1; } else { _driver = 0; _count = c1; }
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
@ -310,7 +310,7 @@ public ref struct Select3<T1, T2, T3>
public Select3<T1, T2, T3> GetEnumerator() => this; public Select3<T1, T2, T3> GetEnumerator() => this;
public void Dispose() => _world.EndIteration(); public void Dispose() => _world.EndBatching();
private bool PassesWithout() private bool PassesWithout()
{ {
@ -360,7 +360,7 @@ public ref struct Select4<T1, T2, T3, T4>
_count = min; _count = min;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
@ -412,7 +412,7 @@ public ref struct Select4<T1, T2, T3, T4>
public Select4<T1, T2, T3, T4> GetEnumerator() => this; public Select4<T1, T2, T3, T4> GetEnumerator() => this;
public void Dispose() => _world.EndIteration(); public void Dispose() => _world.EndBatching();
private bool PassesWithout() private bool PassesWithout()
{ {
@ -466,7 +466,7 @@ public ref struct Select5<T1, T2, T3, T4, T5>
_count = min; _count = min;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
@ -530,7 +530,7 @@ public ref struct Select5<T1, T2, T3, T4, T5>
public Select5<T1, T2, T3, T4, T5> GetEnumerator() => this; public Select5<T1, T2, T3, T4, T5> GetEnumerator() => this;
public void Dispose() => _world.EndIteration(); public void Dispose() => _world.EndBatching();
private bool PassesWithout() private bool PassesWithout()
{ {
@ -588,7 +588,7 @@ public ref struct Select6<T1, T2, T3, T4, T5, T6>
_count = min; _count = min;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginBatching();
} }
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
@ -666,7 +666,7 @@ public ref struct Select6<T1, T2, T3, T4, T5, T6>
public Select6<T1, T2, T3, T4, T5, T6> GetEnumerator() => this; public Select6<T1, T2, T3, T4, T5, T6> GetEnumerator() => this;
public void Dispose() => _world.EndIteration(); public void Dispose() => _world.EndBatching();
private bool PassesWithout() private bool PassesWithout()
{ {