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>
/// 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>
public interface ISystem
{
/// <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>
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
{
/// <summary>
/// Executes the system logic with tick information.
/// Implement this with the system's ticked logic.
/// </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.
_world.ExecuteCommands();
_world.BeginBatching();
foreach (var system in _systems)
{
if (system is ITickedSystem ticked)
@ -77,10 +78,15 @@ public class SystemGroup
// see the effects of commands enqueued by prior systems.
_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
// incremental updates.
_world.PostChanges();
}
_world.EndBatching();
// Drain any remaining commands (e.g., those enqueued outside systems).
_world.ExecuteCommands();

View File

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

View File

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