diff --git a/OECS/ISystem.cs b/OECS/ISystem.cs index 4ed3d14..9582c4f 100644 --- a/OECS/ISystem.cs +++ b/OECS/ISystem.cs @@ -2,12 +2,44 @@ namespace OECS; /// /// A system that runs on every tick. -/// Systems receive the world in their method. +/// Implement with the system logic. +/// Call to execute with batching. /// public interface ISystem { /// - /// Executes the system logic for this tick. + /// Implement this with the system's logic for this tick. + /// The caller (e.g., ) wraps this + /// in a batching scope so mutations and component accesses + /// are automatically tracked. /// - void Run(World world); + void RunImpl(World world); +} + +/// +/// Extension methods for running systems with proper batching. +/// +public static class SystemExtensions +{ + /// + /// Runs a system with a batching scope. Structural mutations are + /// buffered, and calls are + /// auto-tracked for modification until the system returns. + /// + public static void Run(this ISystem system, World world) + { + world.BeginBatching(); + system.RunImpl(world); + world.EndBatching(); + } + + /// + /// Runs a ticked system with a batching scope. + /// + public static void Run(this ITickedSystem system, World world, Tick tick) + { + world.BeginBatching(); + system.RunImpl(world, tick); + world.EndBatching(); + } } \ No newline at end of file diff --git a/OECS/ITickedSystem.cs b/OECS/ITickedSystem.cs index 7d1addc..757be8f 100644 --- a/OECS/ITickedSystem.cs +++ b/OECS/ITickedSystem.cs @@ -7,7 +7,7 @@ namespace OECS; public interface ITickedSystem : ISystem { /// - /// Executes the system logic with tick information. + /// Implement this with the system's ticked logic. /// - void Run(World world, Tick tick); + void RunImpl(World world, Tick tick); } \ No newline at end of file diff --git a/OECS/SystemGroup.cs b/OECS/SystemGroup.cs index bc4dbc1..d110d67 100644 --- a/OECS/SystemGroup.cs +++ b/OECS/SystemGroup.cs @@ -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(); diff --git a/OECS/World.cs b/OECS/World.cs index 19bdcc2..44e0e3c 100644 --- a/OECS/World.cs +++ b/OECS/World.cs @@ -19,10 +19,11 @@ public class World : IDisposable private readonly Dictionary _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 _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 are tracked. When the iteration scope ends, - // all accessed components are automatically marked as modified. - // Outside of iteration, MarkModified must be called explicitly. + // Auto-dirty-marking: during a batching scope, component accesses via + // GetComponent are tracked. When the scope ends, all accessed + // components are automatically marked as modified. + // Outside of a batching scope, MarkModified 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 /// public void DestroyEntity(Entity entity) { - if (_iterationDepth > 0) + if (_batchingDepth > 0) { _pendingMutations.Add(new PendingMutation { @@ -157,7 +158,7 @@ public class World : IDisposable /// public void AddComponent(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 /// public void RemoveComponent(Entity entity) where T : struct { - if (_iterationDepth > 0) + if (_batchingDepth > 0) { _pendingMutations.Add(new PendingMutation { @@ -269,7 +270,7 @@ public class World : IDisposable /// public ref T GetComponent(Entity entity) where T : struct { - if (_iterationDepth > 0) + if (_batchingDepth > 0) _accessedComponents.Add((entity, typeof(T))); return ref _components.Get(entity); } @@ -465,23 +466,25 @@ public class World : IDisposable // ── Iteration Lifecycle ────────────────────────────────────────── /// - /// Begins an iteration scope. Structural mutations are buffered - /// until is called. + /// Begins a batching scope. Structural mutations (AddComponent, + /// RemoveComponent, DestroyEntity) are buffered until the outermost + /// call. Component accesses via + /// are auto-tracked for modification. /// - internal void BeginIteration() + internal void BeginBatching() { - _iterationDepth++; + _batchingDepth++; } /// - /// 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. /// - internal void EndIteration() + internal void EndBatching() { - _iterationDepth--; - if (_iterationDepth == 0) + _batchingDepth--; + if (_batchingDepth == 0) { // Auto-mark all components accessed via GetComponent // during the iteration as modified. @@ -499,7 +502,7 @@ public class World : IDisposable } } - private void FlushPendingMutations() + internal void FlushPendingMutations() { if (_pendingMutations.Count == 0) return; diff --git a/OECS/WorldQueryExtensions.cs b/OECS/WorldQueryExtensions.cs index 811cc20..bec8a8f 100644 --- a/OECS/WorldQueryExtensions.cs +++ b/OECS/WorldQueryExtensions.cs @@ -117,7 +117,7 @@ public ref struct Select1 where T1 : struct _set = _store.GetSet(typeof(T1)) as SparseSet; _count = _set?.Count ?? 0; _index = -1; - _world.BeginIteration(); + _world.BeginBatching(); } /// The current entity handle. @@ -147,7 +147,7 @@ public ref struct Select1 where T1 : struct public void Dispose() { - _world.EndIteration(); + _world.EndBatching(); } private bool PassesWithout() @@ -188,7 +188,7 @@ public ref struct Select2 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 where T1 : struct where T2 : struct public Select2 GetEnumerator() => this; - public void Dispose() => _world.EndIteration(); + public void Dispose() => _world.EndBatching(); private bool PassesWithout() { @@ -268,7 +268,7 @@ public ref struct Select3 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 public Select3 GetEnumerator() => this; - public void Dispose() => _world.EndIteration(); + public void Dispose() => _world.EndBatching(); private bool PassesWithout() { @@ -360,7 +360,7 @@ public ref struct Select4 _count = min; _index = -1; - _world.BeginIteration(); + _world.BeginBatching(); } public Entity Entity { get; private set; } @@ -412,7 +412,7 @@ public ref struct Select4 public Select4 GetEnumerator() => this; - public void Dispose() => _world.EndIteration(); + public void Dispose() => _world.EndBatching(); private bool PassesWithout() { @@ -466,7 +466,7 @@ public ref struct Select5 _count = min; _index = -1; - _world.BeginIteration(); + _world.BeginBatching(); } public Entity Entity { get; private set; } @@ -530,7 +530,7 @@ public ref struct Select5 public Select5 GetEnumerator() => this; - public void Dispose() => _world.EndIteration(); + public void Dispose() => _world.EndBatching(); private bool PassesWithout() { @@ -588,7 +588,7 @@ public ref struct Select6 _count = min; _index = -1; - _world.BeginIteration(); + _world.BeginBatching(); } public Entity Entity { get; private set; } @@ -666,7 +666,7 @@ public ref struct Select6 public Select6 GetEnumerator() => this; - public void Dispose() => _world.EndIteration(); + public void Dispose() => _world.EndBatching(); private bool PassesWithout() {