Skip to content

Commit

Permalink
reuse group collections more efficiently + better parallel approach v…
Browse files Browse the repository at this point in the history
…ia Task.WaitAll
  • Loading branch information
JasonXuDeveloper committed Jan 20, 2025
1 parent e0cdabe commit 04d823b
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 64 deletions.
89 changes: 50 additions & 39 deletions EasyEcs.Core/Context.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

Expand All @@ -21,18 +20,20 @@ public class Context : IAsyncDisposable
private readonly Random _random = new();
private readonly List<Entity> _entities = new();
private readonly List<Entity> _removeList = new();
private readonly SortedList<int, IExecuteSystem> _executeSystems = new();
private readonly List<Task> _executeTasks = new();
private readonly SortedList<int, ExecuteSystemWrapper> _executeSystems = new();
private readonly SortedList<int, IInitSystem> _initSystems = new();
private readonly SortedList<int, IEndSystem> _endSystems = new();
private bool _started;
private bool _disposed;

private readonly List<Entity> _currentEntities = new();
private readonly ConcurrentDictionary<long, List<Entity>> _groupsCache = new();
private static readonly ConcurrentQueue<List<Entity>> Pool = new();

[ThreadStatic] private static List<Entity> _group;
private static List<Entity> Group => _group ??= new();

/// <summary>
/// Called when an error occurs.
/// </summary>
public event Action<Exception> OnError;

/// <summary>
/// The shared context.
Expand All @@ -47,7 +48,7 @@ public class Context : IAsyncDisposable
{
var system = new T();
if (system is IExecuteSystem executeSystem)
_executeSystems.Add(system.Priority, executeSystem);
_executeSystems.Add(system.Priority, new ExecuteSystemWrapper(executeSystem));
if (system is IInitSystem initSystem)
_initSystems.Add(system.Priority, initSystem);
if (system is IEndSystem endSystem)
Expand All @@ -64,7 +65,6 @@ public Entity CreateEntity()
{
var entity = new Entity(this, _random.Next());
_entities.Add(entity);
_currentEntities.Add(entity);
return entity;
}

Expand All @@ -78,7 +78,6 @@ public void DestroyEntity(Entity entity, bool immediate = false)
if (immediate)
{
_entities.Remove(entity);
_currentEntities.Remove(entity);
InvalidateGroupCache();
return;
}
Expand All @@ -91,7 +90,17 @@ public void DestroyEntity(Entity entity, bool immediate = false)
/// <br/>
/// Note: Newly created entities from the current update will not be included.
/// </summary>
public ReadOnlyCollection<Entity> AllEntities => _currentEntities.AsReadOnly();
public PooledCollection<List<Entity>, Entity> AllEntities
{
get
{
var ret = PooledCollection<List<Entity>, Entity>.Create();
var lst = ret.Collection;
lst.Clear();
lst.AddRange(_entities);
return ret;
}
}

/// <summary>
/// Initialize all systems. Use this when starting the context.
Expand All @@ -101,9 +110,6 @@ public async ValueTask<Context> Init()
if (_started)
throw new InvalidOperationException("Context already started.");

// all entities before executing all systems at this update
_currentEntities.Clear();
_currentEntities.AddRange(_entities);
// clear the cache of the groups
InvalidateGroupCache();

Expand All @@ -126,9 +132,6 @@ public async ValueTask DisposeAsync()
if (_disposed)
return;

// all entities before executing all systems at this update
_currentEntities.Clear();
_currentEntities.AddRange(_entities);
// clear the cache of the groups
InvalidateGroupCache();

Expand All @@ -140,7 +143,6 @@ public async ValueTask DisposeAsync()

// clear all entities
_entities.Clear();
_currentEntities.Clear();
// clear all systems
_executeSystems.Clear();
_initSystems.Clear();
Expand All @@ -165,38 +167,39 @@ public async ValueTask Update(bool parallel = true)
if (_disposed)
throw new InvalidOperationException("Context disposed.");

// all entities before executing all systems at this update
_currentEntities.Clear();
_currentEntities.AddRange(_entities);
// clear the cache of the groups
InvalidateGroupCache();
// execute the systems
if (!parallel)
{
foreach (var system in _executeSystems.Values)
{
if (((SystemBase)system).ShouldExecute())
{
await system.OnExecute(this);
}
await system.Update(this);
}
}
else
{
await Parallel.ForEachAsync(_executeSystems.Values, async (system, _) =>
_executeTasks.Clear();
// sort by priority
foreach (var system in _executeSystems.Values)
{
_executeTasks.Add(system.Update(this));
}

try
{
if (((SystemBase)system).ShouldExecute())
{
await system.OnExecute(this);
}
});
await Task.WhenAll(_executeTasks);
}
catch (Exception e)
{
OnError?.Invoke(e);
}
}

// remove entities
foreach (var entity in _removeList)
{
_entities.Remove(entity);
_currentEntities.Remove(entity);
}

_removeList.Clear();
Expand All @@ -205,11 +208,14 @@ await Parallel.ForEachAsync(_executeSystems.Values, async (system, _) =>
/// <summary>
/// Get all entities that have the specified components.
/// <br/>
/// Note: You should save the result (via LINQ) if you want to call <see cref="GroupOf"/> again
/// Note: remember to dispose the returned enumerable. (i.e. using)
/// <code>
/// using var entities = context.GroupOf(typeof(Component1), typeof(Component2));
/// </code>
/// </summary>
/// <param name="components"></param>
/// <returns></returns>
public ReadOnlyCollection<Entity> GroupOf(params Type[] components)
public PooledCollection<List<Entity>, Entity> GroupOf(params Type[] components)
{
// compute an id for these components
long group = 0;
Expand All @@ -218,12 +224,16 @@ public ReadOnlyCollection<Entity> GroupOf(params Type[] components)
group += component.GetHashCode();
}

Group.Clear();
// request a pooled enumerable
var ret = PooledCollection<List<Entity>, Entity>.Create();
var lst = ret.Collection;
lst.Clear();

// if we have already looked up this group at this update, we simply copy the results
if (_groupsCache.TryGetValue(group, out var entities))
{
Group.AddRange(entities);
return Group.AsReadOnly();
lst.AddRange(entities);
return ret;
}

// attempt to reuse a list
Expand All @@ -233,7 +243,8 @@ public ReadOnlyCollection<Entity> GroupOf(params Type[] components)
}

// iterate over all entities and check if they have the components, then cache it
foreach (var entity in _currentEntities)
using var allEntities = AllEntities;
foreach (var entity in allEntities)
{
if (entity.HasComponents(components))
{
Expand All @@ -243,8 +254,8 @@ public ReadOnlyCollection<Entity> GroupOf(params Type[] components)

// try cache it
_groupsCache.TryAdd(group, entities);
Group.AddRange(entities);
return Group.AsReadOnly();
lst.AddRange(entities);
return ret;
}

/// <summary>
Expand Down
28 changes: 28 additions & 0 deletions EasyEcs.Core/ExecuteSystemWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Tasks;

namespace EasyEcs.Core;

/// <summary>
/// A wrapper for an <see cref="IExecuteSystem"/> that will only execute the system at certain intervals.
/// Used by Context.
/// </summary>
internal class ExecuteSystemWrapper
{
private readonly IExecuteSystem _system;
private int _counter;

public ExecuteSystemWrapper(IExecuteSystem system)
{
_system = system;
}

internal Task Update(Context context)
{
if (_system.ExecuteFrequency == 1 || (_counter++ > 0 && _counter % _system.ExecuteFrequency == 0))
{
return _system.OnExecute(context);
}

return Task.CompletedTask;
}
}
2 changes: 1 addition & 1 deletion EasyEcs.Core/IEndSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace EasyEcs.Core;

public interface IEndSystem
{
ValueTask OnEnd(Context context);
Task OnEnd(Context context);
}
3 changes: 2 additions & 1 deletion EasyEcs.Core/IExecuteSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ namespace EasyEcs.Core;

public interface IExecuteSystem
{
ValueTask OnExecute(Context context);
int ExecuteFrequency => 1;
Task OnExecute(Context context);
}
2 changes: 1 addition & 1 deletion EasyEcs.Core/IInitSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace EasyEcs.Core;

public interface IInitSystem
{
ValueTask OnInit(Context context);
Task OnInit(Context context);
}
83 changes: 83 additions & 0 deletions EasyEcs.Core/PooledCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace EasyEcs.Core;

/// <summary>
/// Represents a pooled collection. Use this for iterating (i.e. foreach) over a collection. Remember to dispose it.
/// </summary>
/// <typeparam name="TCollection"></typeparam>
/// <typeparam name="TElement"></typeparam>
public class PooledCollection<TCollection, TElement> : ICollection<TElement>, IDisposable
where TCollection : ICollection<TElement>, new()
{
internal TCollection Collection;
private static readonly ConcurrentQueue<PooledCollection<TCollection, TElement>> Pool = new();

/// <summary>
/// Create a pooled collection.
/// </summary>
/// <returns></returns>
public static PooledCollection<TCollection, TElement> Create()
{
if (Pool.TryDequeue(out var pooled))
{
return pooled;
}

return new PooledCollection<TCollection, TElement>();
}

public TElement this[int index] => Collection.ElementAt(index);

private PooledCollection()
{
Collection = new TCollection();
}

public IEnumerator<TElement> GetEnumerator()
{
return Collection.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

public void Dispose()
{
Pool.Enqueue(this);
}

public void Add(TElement item)
{
Collection.Add(item);
}

public void Clear()
{
Collection.Clear();
}

public bool Contains(TElement item)
{
return Collection.Contains(item);
}

public void CopyTo(TElement[] array, int arrayIndex)
{
Collection.CopyTo(array, arrayIndex);
}

public bool Remove(TElement item)
{
return Collection.Remove(item);
}

public int Count => Collection.Count;
public bool IsReadOnly => Collection.IsReadOnly;
}
10 changes: 1 addition & 9 deletions EasyEcs.Core/SystemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,9 @@ namespace EasyEcs.Core;
/// <summary>
/// A system updates in a certain frequency and should gather some entities and apply some logic.
/// <br/>
/// Systems in a context are sorted and updated by priority.
/// Systems in a context are sorted and invoked by priority.
/// </summary>
public abstract class SystemBase
{
public virtual int Priority => 0;
public virtual int ExecuteFrequency => 1;

private int _counter;

internal bool ShouldExecute()
{
return ExecuteFrequency == 1 || (_counter++ > 0 && _counter % ExecuteFrequency == 0);
}
}
2 changes: 2 additions & 0 deletions EasyEcs.UnitTest/SimpleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class SimpleTest
public async Task TrivialTest()
{
var ctx = new Context();
ctx.OnError += e => Assert.Fail(e.Message);

// Add systems to the context
ctx
Expand Down Expand Up @@ -80,6 +81,7 @@ public async Task TrivialTest()
public async Task DestroyTest()
{
var ctx = new Context();
ctx.OnError += e => Assert.Fail(e.Message);

// Add systems to the context
ctx
Expand Down
Loading

0 comments on commit 04d823b

Please sign in to comment.