diff --git a/README.md b/README.md index 90006c5d..27f1dcb2 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,23 @@ Entities are created as such: Entity entity = world.CreateEntity(); ``` -You should not store entities yourself and rely as much as possible on the returned objects from a world query as those will be updated accordingly to component changes. To clear an entity, simply call its `Dispose` method. ```csharp entity.Dispose(); ``` +You should not store entities yourself and rely as much as possible on the returned objects from a world query as those will be updated accordingly to component changes. +If you absolutely need to store it separately and found related hard-to-find bug, you might consider temporary enabling runtime entity misuse checks: +```csharp +Entity entity = world.CreateEntity(); +Entity.IsMisuseDetectionEnabled = true; // this property is available in release as well, but is not used +entity.Set(); +entity.Dispose(); +if (entity.Has()) { + // in debug configuration you will get entity misuse exception trying to perform Has there as the entity is already dead. +} +``` + Once disposed, you should not use the entity again. If you need a safeguard, you can check the `IsAlive` property: ```csharp #if DEBUG @@ -138,6 +149,15 @@ if (!entity.IsAlive) #endif ``` +There is faster alternative to IsAlive property: +```csharp +if (!entity.IsAliveVersion) +{ + // make sure to only use this if you are sure entity was at least valid before, and its world is still alive + // i.e created from existing valid world through CreateEntity method, and not through default struct constructor +} +``` + You can also make an entity act as if it was disposed so it is removed from world queries while keeping all its components, this is useful when you need to activate/deactivate an entity from your game logic: ```csharp entity.Disable(); @@ -311,6 +331,7 @@ entity.Set(ManagedResource.Create("square.png", "circle.png")); // se textureResourceManager.Manage(_world); ``` This feature only cares for entity components, not the world components. +Plus, it's intended mostly for init, so keep it in mind before trying to use it in any non-init systems as the performance might not be the best. ## Query To perform operations, systems should query entities from the world. This is performed by requesting entities through the world and using the fluent API to create rules @@ -368,9 +389,39 @@ world .AsMap(); ``` Gets an `EntityMap` which maps a single entity with a component type of `TKey` value. Its content is cached for fast access and is automatically updated as you change your entity component composition. -If `When...` rules are present, you should call its `Complete()` method once you are done processing its entities to clear it. +You can think of it as ecs-framework intergated version of `Dictionary`. This is useful if you need O(1) access to an entity based on a key. +For complex situations when you want to compare specific field(s) instead of whole component, you can pass `IEqualityComparer` when creating the map: +```csharp + public static class TestsApplication { + public static void Main() { + World testWorld = new World(); + var comparer = new TestComponentComparer(); + var testComponentMap = testWorld.GetEntities().AsMap(comparer); + + Entity one = testWorld.CreateEntity(); + Entity two = testWorld.CreateEntity(); + Entity three = testWorld.CreateEntity(); + one.Set(new TestComponent() { UniqueID = 1, SomeBool = true }); + two.Set(new TestComponent() { UniqueID = 1, SomeBool = false }); + + // Now we will get error because EntityMap we have only cares about the bool equality + three.Set(new TestComponent() { UniqueID = 2, SomeBool = true }); + } + } + public struct TestComponent { + public byte UniqueID; + public bool SomeBool; + } + public class TestComponentComparer : IEqualityComparer { + bool IEqualityComparer.Equals(TestComponent x, TestComponent y) => x.SomeBool == y.SomeBool; + int IEqualityComparer.GetHashCode(TestComponent obj) => obj.GetHashCode(); + } +``` + +If `When...` rules are present, you should call its `Complete()` method once you are done processing its entities to clear it. + ### AsMultiMap ```csharp world @@ -379,8 +430,11 @@ world .AsMultiMap(); ``` Gets an `EntityMultiMap` which maps multiple entities with a component type of `TKey` value. Its content is cached for fast access and is automatically updated as you change your entity component composition. -If `When...` rules are present, you should call its `Complete()` method once you are done processing its entities to clear it. +You can think of it as ecs-framework intergated version of `Dictionary`. This is useful if you need O(1) access to entities based on a key. +Just like `EntityMap` it supports custom component equality by passing `IEqualityComparer` + +If `When...` rules are present, you should call its `Complete()` method once you are done processing its entities to clear it. ## System Although there is no obligation to use them, a set of base classes is provided to help with the creation of systems: @@ -495,14 +549,15 @@ public sealed class VelocitySystem : AEntitySetSystem It is also possible to declare the component types by using the `WithAttribute` and `WithoutAttribute` on the system type: ```csharp -[With(typeof(Velocity)] +[With] [With(typeof(Position)] public sealed class VelocitySystem : AEntitySetSystem { - public VelocitySystem(World world, IParallelRunner runner) - : base(world, runner) - { - } + // Note the usage of useBuffer paramater in constructor. + // Setting it to false will yield us better performance. + // But now setting removing, or enabling-disabling components on entity that are part of filter (namely, Velocity and Position) is no longer safe. + // Manipulating entity (i.e. disabling it, or disposing) is now also no longer safe. + public VelocitySystem(World world) : base(world, useBuffer: false) { } protected override void Update(float elapsedTime, in Entity entity) { diff --git a/documentation/FAQ.md b/documentation/FAQ.md index 7066f7d7..3795faea 100644 --- a/documentation/FAQ.md +++ b/documentation/FAQ.md @@ -120,6 +120,7 @@ Once the dlls inside your Unity project you will then need to disable `Validate Note that this feature has only been available since a specific version of Unity. You should then be able to use DefaultEcs in your Unity project. Keep in mind that if you choose the IL2CPP backend, some features will not work (the provided serializers) and some others will require extra code (check [AoTHelper](https://github.com/Doraku/DefaultEcs/blob/master/documentation/api/DefaultEcs-AoTHelper.md)). +You may also want to compile the framework manually while removing all System.Reflection.Emit-using code (the serializers code mentioned above) as it causes unity IL2CPP-based projects fail to build. ## How to update systems at half the framerate? diff --git a/documentation/NEXT_RELEASENOTES.txt b/documentation/NEXT_RELEASENOTES.txt index 4c0f0bdc..22314353 100644 --- a/documentation/NEXT_RELEASENOTES.txt +++ b/documentation/NEXT_RELEASENOTES.txt @@ -14,6 +14,7 @@ - added World.SubscribeWorldComponentChanged method (#165) - added World.SubscribeWorldComponentRemoved method (#165) - added World.NotifyChanged method +- added Entity.IsAliveVersion property - added generic WithAttribute - added generic WithoutAttribute - added generic WhenAddedAttribute diff --git a/source/DefaultEcs.sln b/source/DefaultEcs.sln index e407aae3..193ad8bc 100644 --- a/source/DefaultEcs.sln +++ b/source/DefaultEcs.sln @@ -60,17 +60,11 @@ Global {BD4F95D2-FB6F-4317-8B0A-58FBDF536C0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD4F95D2-FB6F-4317-8B0A-58FBDF536C0B}.Release|Any CPU.Build.0 = Release|Any CPU {6D2BA0F6-177E-454B-8AD7-42AF0AD539B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D2BA0F6-177E-454B-8AD7-42AF0AD539B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D2BA0F6-177E-454B-8AD7-42AF0AD539B7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D2BA0F6-177E-454B-8AD7-42AF0AD539B7}.Release|Any CPU.Build.0 = Release|Any CPU {95F4D6A0-FD01-4AA8-B603-5811C9FFAEEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95F4D6A0-FD01-4AA8-B603-5811C9FFAEEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {95F4D6A0-FD01-4AA8-B603-5811C9FFAEEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95F4D6A0-FD01-4AA8-B603-5811C9FFAEEA}.Release|Any CPU.Build.0 = Release|Any CPU {198A12F3-B396-4243-BFD4-B93FA488AB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {198A12F3-B396-4243-BFD4-B93FA488AB76}.Debug|Any CPU.Build.0 = Debug|Any CPU {198A12F3-B396-4243-BFD4-B93FA488AB76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {198A12F3-B396-4243-BFD4-B93FA488AB76}.Release|Any CPU.Build.0 = Release|Any CPU {17280FCB-26FC-4999-8831-2810AF764B86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17280FCB-26FC-4999-8831-2810AF764B86}.Debug|Any CPU.Build.0 = Debug|Any CPU {17280FCB-26FC-4999-8831-2810AF764B86}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/source/DefaultEcs/DefaultEcs.csproj b/source/DefaultEcs/DefaultEcs.csproj index 5e2386b5..63b87c87 100644 --- a/source/DefaultEcs/DefaultEcs.csproj +++ b/source/DefaultEcs/DefaultEcs.csproj @@ -1,7 +1,6 @@  - netstandard1.1; netstandard2.0; netstandard2.1; @@ -13,10 +12,7 @@ - - - - + diff --git a/source/DefaultEcs/Entity.cs b/source/DefaultEcs/Entity.cs index 8ab5b0ce..08dff163 100644 --- a/source/DefaultEcs/Entity.cs +++ b/source/DefaultEcs/Entity.cs @@ -30,8 +30,17 @@ namespace DefaultEcs #endregion + /// + /// Are runtime debug checks in entity methods that are prone to misuse enabled? + /// This is DEBUG configuration-only feature and is not used in release builds. + /// + public static bool IsMisuseDetectionEnabled; + #region Initialisation + private const string NON_WORLD_ENTITY_MESSAGE = "Entity was not created from a World"; + private const string ENTITY_MISUSE_MESSAGE = "Entity misuse detected. Something is wrong!"; + internal Entity(short worldId) { WorldId = worldId; @@ -60,17 +69,45 @@ internal Entity(short worldId, int entityId) public World World => World.Worlds[WorldId]; /// - /// Gets whether the current is alive or not. + /// Gets whether the current is valid (i.e. not disposed and properly created from existing world) or not. /// /// true if the is alive; otherwise, false. [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool IsAlive => WorldId != 0 && World.EntityInfos[EntityId].IsAlive(Version); + public bool IsAlive + { + get + { + if (WorldId == 0) return false; + if (World.EntityInfos.Length > EntityId) + { + var entityInfo = World.EntityInfos[EntityId]; + return entityInfo.IsAlive(Version); + } + return false; + } + } + + /// + /// Returns whether the current version copy is alive. This method is faster and less safe version of + /// Use it when know for sure that entity was valid previously (i.e. it was created from existing world and used before) + /// + /// true if the given copy is latest(alive) version; otherwise, false. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public readonly bool IsAliveVersion + { + get + { + if (WorldId == 0) return false; + var entityInfo = World.EntityInfos[EntityId]; + return Version == entityInfo.Version; + } + } #endregion #region Methods - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ThrowIf(bool actuallyThrow, string message) { if (actuallyThrow) @@ -115,7 +152,8 @@ private void InnerSet(bool isNew) /// was not created from a . public void Enable() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); ref ComponentEnum components = ref Components; if (!components[World.IsEnabledFlag]) @@ -132,7 +170,8 @@ public void Enable() /// was not created from a . public void Disable() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); ref ComponentEnum components = ref Components; if (components[World.IsEnabledFlag]) @@ -159,7 +198,14 @@ public void Disable() /// was not created from a . public void Enable() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif if (Has()) { @@ -181,7 +227,14 @@ public void Enable() /// was not created from a . public void Disable() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif ref ComponentEnum components = ref Components; if (components[ComponentManager.Flag]) @@ -201,7 +254,14 @@ public void Disable() /// Max number of component of type reached. public void Set(in T component) { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif InnerSet(ComponentManager.GetOrCreate(WorldId).Set(EntityId, component)); } @@ -226,9 +286,16 @@ public void Set(in T component) /// Reference does not have a component of type . public void SetSameAs(in Entity reference) { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); ThrowIf(WorldId != reference.WorldId, "Reference Entity comes from a different World"); +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif + ComponentPool pool = ComponentManager.Get(WorldId); ThrowIf(!(pool?.Has(reference.EntityId) ?? false), $"Reference Entity does not have a component of type {typeof(T)}"); @@ -244,7 +311,14 @@ public void SetSameAs(in Entity reference) /// does not have a component of type . public void SetSameAsWorld() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif ComponentPool pool = ComponentManager.Get(WorldId); ThrowIf(!(pool?.Has(0) ?? false), $"World does not have a component of type {typeof(T)}"); @@ -259,6 +333,13 @@ public void SetSameAsWorld() /// The type of the component. public void Remove() { + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif if (ComponentManager.Get(WorldId)?.Remove(EntityId) == true) { ref ComponentEnum components = ref Components; @@ -277,9 +358,16 @@ public void Remove() /// does not have a component of type . public void NotifyChanged() { - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); ThrowIf(!Has(), $"Entity does not have a component of type {typeof(T)}"); +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif + Publisher.Publish(WorldId, new EntityComponentChangedMessage(EntityId, Components)); if (ComponentManager.GetPrevious(WorldId) is ComponentPool previousPool && Has()) @@ -294,7 +382,17 @@ public void NotifyChanged() /// The type of the component. /// true if the has a component of type ; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Has() => ComponentManager.Get(WorldId)?.Has(EntityId) ?? false; + public bool Has() + { +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif + var componentPool = ComponentManager.Get(WorldId); + return componentPool?.Has(EntityId) ?? false; + } /// /// Gets the component of type on the current . @@ -303,7 +401,15 @@ public void NotifyChanged() /// A reference to the component. /// was not created from a or does not have a component of type . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref T Get() => ref ComponentManager.Pools[WorldId].Get(EntityId); + public ref T Get() + { + var typePools = ComponentManager.Pools; + if (WorldId > 0 && WorldId >= typePools.Length) + { + throw new InvalidOperationException("Get failed, because entity does not have component type=" + typeof(T).Name); + } + return ref typePools[WorldId].Get(EntityId); + } /// /// Creates a copy of current with all of its components in the given using the given . @@ -319,7 +425,7 @@ public Entity CopyTo(World world, ComponentCloner cloner) world.ThrowIfNull(); cloner.ThrowIfNull(); - ThrowIf(WorldId == 0, "Entity was not created from a World"); + ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); Entity copy = world.CreateEntity(); @@ -370,6 +476,8 @@ public Entity CopyTo(World world, ComponentCloner cloner) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { + if (WorldId == 0) return; + if (!this.IsAliveVersion) return; Publisher.Publish(WorldId, new EntityDisposingMessage(EntityId)); Publisher.Publish(WorldId, new EntityDisposedMessage(EntityId)); } diff --git a/source/DefaultEcs/Internal/ComponentPool.cs b/source/DefaultEcs/Internal/ComponentPool.cs index 095e868e..6b474b70 100644 --- a/source/DefaultEcs/Internal/ComponentPool.cs +++ b/source/DefaultEcs/Internal/ComponentPool.cs @@ -298,7 +298,15 @@ public bool Remove(int entityId) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref T Get(int entityId) => ref _components[_mapping[entityId]]; + public ref T Get(int entityId) + { + int mappingIndex = _mapping[entityId]; + if (mappingIndex == -1) + { + throw new InvalidOperationException("Get failed, because entity does not have component type=" + typeof(T).Name); + } + return ref _components[mappingIndex]; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span AsSpan() => new(_components, 0, _lastComponentIndex + 1); diff --git a/source/DefaultEcs/Internal/Threading/WorkerBarrier.cs b/source/DefaultEcs/Internal/Threading/WorkerBarrier.cs deleted file mode 100644 index 91bf3975..00000000 --- a/source/DefaultEcs/Internal/Threading/WorkerBarrier.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace DefaultEcs.Internal.Threading -{ - internal sealed class WorkerBarrier : IDisposable - { - #region Fields - - private readonly int _count; - private readonly ManualResetEventSlim _endHandle; - private readonly ManualResetEventSlim _startHandle; - - private bool _allStarted; - private int _runningCount; - - #endregion - - #region Initialisation - - public WorkerBarrier(int workerCount) - { - _count = workerCount; - _endHandle = new ManualResetEventSlim(false); - _startHandle = new ManualResetEventSlim(false); - - _allStarted = false; - _runningCount = 0; - } - - #endregion - - #region Methods - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void StartWorkers() - { - Volatile.Write(ref _allStarted, false); - _startHandle.Set(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Start() - { - _startHandle.Wait(); - - if (Interlocked.Increment(ref _runningCount) == _count) - { - _startHandle.Reset(); - Volatile.Write(ref _allStarted, true); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Signal() - { - while (!Volatile.Read(ref _allStarted)) - { } - - if (Interlocked.Decrement(ref _runningCount) == 0) - { - _endHandle.Set(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WaitForWorkers() - { - _endHandle.Wait(); - _endHandle.Reset(); - } - - #endregion - - #region IDisposable - - public void Dispose() - { - _endHandle.Dispose(); - _startHandle.Dispose(); - } - - #endregion - } -} diff --git a/source/DefaultEcs/System/AEntityMultiMapSystem.cs b/source/DefaultEcs/System/AEntityMultiMapSystem.cs index 3b7e2b06..b0c10590 100644 --- a/source/DefaultEcs/System/AEntityMultiMapSystem.cs +++ b/source/DefaultEcs/System/AEntityMultiMapSystem.cs @@ -45,7 +45,7 @@ public void Run(int index, int maxIndex) #region Fields - private readonly bool _useBuffer; + private readonly bool _useBuffer = true; private readonly IParallelRunner _runner; private readonly Runnable _runnable; private readonly int _minEntityCountByRunnerIndex; @@ -96,7 +96,8 @@ static bool IsIComparable() MultiMap = factory(this); World = MultiMap.World; - _keyComparer = this as IComparer ?? (IsIComparable() ? Comparer.Default : null); + _keyComparer = (this as IComparer) ?? (IsIComparable() ? Comparer.Default : null); + _useBuffer = (runner != null && runner.DegreeOfParallelism > 1) ? false : _useBuffer; _runner = runner ?? DefaultParallelRunner.Default; _runnable = new Runnable(this); _minEntityCountByRunnerIndex = _runner.DegreeOfParallelism > 1 ? minEntityCountByRunnerIndex : int.MaxValue; @@ -120,9 +121,9 @@ protected AEntityMultiMapSystem(EntityMultiMap map, IParallelRunner runner /// Initialise a new instance of the class with the given . /// /// The on which to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntityMultiMapSystem(EntityMultiMap map, bool useBuffer = false) + protected AEntityMultiMapSystem(EntityMultiMap map, bool useBuffer = true) : this(map, null) { _useBuffer = useBuffer; @@ -160,7 +161,7 @@ protected AEntityMultiMapSystem(World world, IParallelRunner runner, int minEnti /// /// The from which to get the instances to process the update. /// The factory used to create the . - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. /// is null. protected AEntityMultiMapSystem(World world, Func> factory, bool useBuffer) @@ -174,9 +175,9 @@ protected AEntityMultiMapSystem(World world, Func, and attributes will be used. /// /// The from which to get the instances to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntityMultiMapSystem(World world, bool useBuffer = false) + protected AEntityMultiMapSystem(World world, bool useBuffer = true) : this(world, DefaultFactory, useBuffer) { } diff --git a/source/DefaultEcs/System/AEntitySetSystem.cs b/source/DefaultEcs/System/AEntitySetSystem.cs index 08074819..9c27cd9e 100644 --- a/source/DefaultEcs/System/AEntitySetSystem.cs +++ b/source/DefaultEcs/System/AEntitySetSystem.cs @@ -40,7 +40,7 @@ public void Run(int index, int maxIndex) #region Fields - private readonly bool _useBuffer; + private readonly bool _useBuffer = true; private readonly IParallelRunner _runner; private readonly Runnable _runnable; private readonly int _minEntityCountByRunnerIndex; @@ -69,6 +69,7 @@ private AEntitySetSystem(Func factory, IParallelRunner runner Set = factory(this); World = Set.World; + _useBuffer = (runner != null && runner.DegreeOfParallelism > 1) ? false : _useBuffer; _runner = runner ?? DefaultParallelRunner.Default; _runnable = new Runnable(this); _minEntityCountByRunnerIndex = _runner.DegreeOfParallelism > 1 ? minEntityCountByRunnerIndex : int.MaxValue; @@ -89,10 +90,9 @@ protected AEntitySetSystem(EntitySet set, IParallelRunner runner, int minEntityC /// Initialise a new instance of the class with the given . /// /// The on which to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntitySetSystem(EntitySet set, bool useBuffer = false) - : this(set, null) + protected AEntitySetSystem(EntitySet set, bool useBuffer = true) : this(set, null) { _useBuffer = useBuffer; } @@ -129,7 +129,7 @@ protected AEntitySetSystem(World world, IParallelRunner runner, int minEntityCou /// /// The from which to get the instances to process the update. /// The factory used to create the . - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. /// is null. protected AEntitySetSystem(World world, Func factory, bool useBuffer) @@ -143,9 +143,9 @@ protected AEntitySetSystem(World world, Func factory, /// To create the inner , and attributes will be used. /// /// The from which to get the instances to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntitySetSystem(World world, bool useBuffer = false) + protected AEntitySetSystem(World world, bool useBuffer = true) : this(world, DefaultFactory, useBuffer) { } diff --git a/source/DefaultEcs/System/AEntitySortedSetSystem.cs b/source/DefaultEcs/System/AEntitySortedSetSystem.cs index 4cc74014..5e3cb3e9 100644 --- a/source/DefaultEcs/System/AEntitySortedSetSystem.cs +++ b/source/DefaultEcs/System/AEntitySortedSetSystem.cs @@ -17,7 +17,7 @@ public abstract class AEntitySortedSetSystem : ISystem> factory /// Initialise a new instance of the class with the given . /// /// The on which to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntitySortedSetSystem(EntitySortedSet sortedSet, bool useBuffer = false) + protected AEntitySortedSetSystem(EntitySortedSet sortedSet, bool useBuffer = true) : this(sortedSet is null ? throw new ArgumentNullException(nameof(sortedSet)) : _ => sortedSet) { _useBuffer = useBuffer; @@ -62,7 +62,7 @@ protected AEntitySortedSetSystem(EntitySortedSet sortedSet, bool use /// /// The from which to get the instances to process the update. /// The factory used to create the . - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. /// is null. protected AEntitySortedSetSystem(World world, Func> factory, bool useBuffer) @@ -76,9 +76,9 @@ protected AEntitySortedSetSystem(World world, Func, and attributes will be used. /// /// The from which to get the instances to process the update. - /// Whether the entities should be copied before being processed. + /// Whether the entities should be copied before being processed. False will yield better performance but is less safe. /// is null. - protected AEntitySortedSetSystem(World world, bool useBuffer = false) + protected AEntitySortedSetSystem(World world, bool useBuffer = true) : this(world, static (o, w) => EntityRuleBuilderFactory.Create(o.GetType())(o, w).AsSortedSet(o as IComparer), useBuffer) { } diff --git a/source/DefaultEcs/System/Attributes.cs b/source/DefaultEcs/System/Attributes.cs index 48286f79..3f3df48a 100644 --- a/source/DefaultEcs/System/Attributes.cs +++ b/source/DefaultEcs/System/Attributes.cs @@ -97,6 +97,9 @@ public class ComponentAttribute : Attribute /// The types of the component. public ComponentAttribute(ComponentFilterType filterType, params Type[] componentTypes) { + if (!Enum.IsDefined(typeof(ComponentFilterType), filterType)) { + throw new ArgumentException(nameof(filterType)); + } ComponentTypes = componentTypes.ThrowIfNull(); FilterType = filterType; } diff --git a/source/DefaultEcs/Threading/DefaultParallelRunner.cs b/source/DefaultEcs/Threading/DefaultParallelRunner.cs index adc5dd25..2acebbe4 100644 --- a/source/DefaultEcs/Threading/DefaultParallelRunner.cs +++ b/source/DefaultEcs/Threading/DefaultParallelRunner.cs @@ -1,14 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; -using System.Threading.Tasks; -using DefaultEcs.Internal.Threading; namespace DefaultEcs.Threading { /// - /// Represents an object used to run an by using multiple . + /// Represents an object used to run an by using multiple . /// public sealed class DefaultParallelRunner : IParallelRunner { @@ -16,11 +12,14 @@ public sealed class DefaultParallelRunner : IParallelRunner internal static readonly DefaultParallelRunner Default = new(1); - private readonly CancellationTokenSource _disposeHandle; - private readonly WorkerBarrier _barrier; - private readonly Task[] _tasks; + private readonly object _syncObject = new object(); + private readonly ManualResetEventSlim _workStartEvent; + private readonly Thread[] _threads; + private readonly bool[] _threadsWorkState; - private IParallelRunnable _currentRunnable; + private volatile bool _isAlive; + private volatile int _pendingTasks; + private volatile IParallelRunnable _currentRunnable; #endregion @@ -29,19 +28,26 @@ public sealed class DefaultParallelRunner : IParallelRunner /// /// Initialises a new instance of the class. /// - /// The number of concurrent used to update an in parallel. + /// The number of concurrent used to update an in parallel. + /// Name prefix for the threads in case you have more than one runner and need to mark them. /// cannot be inferior to one. - public DefaultParallelRunner(int degreeOfParallelism) + public DefaultParallelRunner(int degreeOfParallelism, string threadNamePrefix = null) { - IEnumerable indices = degreeOfParallelism >= 1 ? Enumerable.Range(0, degreeOfParallelism - 1) : throw new ArgumentException("Argument cannot be inferior to one", nameof(degreeOfParallelism)); - - _disposeHandle = new CancellationTokenSource(); - _tasks = indices.Select(index => new Task(Update, index, TaskCreationOptions.LongRunning)).ToArray(); - _barrier = degreeOfParallelism > 1 ? new WorkerBarrier(_tasks.Length) : null; - - foreach (Task task in _tasks) + if (0 >= degreeOfParallelism) + throw new ArgumentException($"Argument {nameof(degreeOfParallelism)} cannot be inferior to one!"); + + _isAlive = true; + threadNamePrefix ??= ""; + _workStartEvent = new ManualResetEventSlim(initialState: false); + _threads = new Thread[degreeOfParallelism - 1]; + _threadsWorkState = new bool[_threads.Length]; + for (int threadIndex = 0; _threads.Length > threadIndex; threadIndex++) { - task.Start(TaskScheduler.Default); + _threads[threadIndex] = new Thread(new ParameterizedThreadStart(this.ThreadExecutionLoop)); + Thread newThread = _threads[threadIndex]; + newThread.Name = threadNamePrefix + $"{nameof(DefaultParallelRunner)} worker {threadIndex + 1}"; + newThread.IsBackground = true; + newThread.Start(threadIndex); } } @@ -49,28 +55,42 @@ public DefaultParallelRunner(int degreeOfParallelism) #region Methods - private void Update(object state) + private void ThreadExecutionLoop(object initObject) { - int index = (int)state; - -#if !NETSTANDARD1_1 - Thread.CurrentThread.Name = $"{nameof(DefaultParallelRunner)} worker {index + 1}"; -#endif - - goto Start; - - Work: - Volatile.Read(ref _currentRunnable).Run(index, _tasks.Length); - - _barrier.Signal(); - - Start: - _barrier.Start(); + int workerIndex = (int)initObject; + while (_isAlive) + { + _workStartEvent.Wait(); + if (!_isAlive) return; + if (!_threadsWorkState[workerIndex]) continue; + + try + { + _currentRunnable?.Run(workerIndex, _threads.Length); + } + finally + { + lock (_syncObject) + { + _pendingTasks--; + _threadsWorkState[workerIndex] = false; + } + } + } + } - if (!_disposeHandle.IsCancellationRequested) + private bool IsAllThreadsStopped() + { + bool result = true; + foreach (Thread thread in _threads) { - goto Work; + if (thread.ThreadState == ThreadState.Running) + { + result = false; + break; + } } + return result; } #endregion @@ -80,7 +100,7 @@ private void Update(object state) /// /// Gets the degree of parallelism used to run an . /// - public int DegreeOfParallelism => _tasks.Length + 1; + public int DegreeOfParallelism => _threads.Length + 1; /// /// Runs the provided . @@ -88,15 +108,31 @@ private void Update(object state) /// The to run. public void Run(IParallelRunnable runnable) { + if (!_isAlive) throw new InvalidOperationException("Runner was already disposed!"); runnable.ThrowIfNull(); - Volatile.Write(ref _currentRunnable, runnable); - - _barrier?.StartWorkers(); - - runnable.Run(_tasks.Length, _tasks.Length); - - _barrier?.WaitForWorkers(); + _currentRunnable = runnable; + if (_threads.Length > 0) + { + _pendingTasks = _threads.Length; + _threadsWorkState.Fill(true); + _workStartEvent.Set(); + try + { + _currentRunnable.Run(index: _threads.Length, maxIndex: _threads.Length); + } + finally + { + SpinWait.SpinUntil(() => _pendingTasks == 0); + _workStartEvent.Reset(); + SpinWait.SpinUntil(IsAllThreadsStopped); + } + } + else + { + _currentRunnable.Run(index: _threads.Length, maxIndex: _threads.Length); + } + _currentRunnable = null; } #endregion @@ -108,14 +144,12 @@ public void Run(IParallelRunnable runnable) /// public void Dispose() { - _disposeHandle.Cancel(); - - _barrier?.StartWorkers(); - - Task.WaitAll(_tasks); - - _barrier?.Dispose(); - _disposeHandle.Dispose(); + if (!_isAlive) return; + _isAlive = false; + _currentRunnable = null; + _workStartEvent.Set(); + Thread.Sleep(millisecondsTimeout: 1); + _workStartEvent.Dispose(); } #endregion diff --git a/source/DefaultEcs/Threading/IParallelRunnable.cs b/source/DefaultEcs/Threading/IParallelRunnable.cs index d8baf78c..c34908a7 100644 --- a/source/DefaultEcs/Threading/IParallelRunnable.cs +++ b/source/DefaultEcs/Threading/IParallelRunnable.cs @@ -8,8 +8,11 @@ public interface IParallelRunnable /// /// Runs the part out of of the process. /// - /// - /// + /// + /// Index of given ecs worker. + /// Using same index as for main thread is preferable to process leftover entities there. + /// + /// Max index for ecs workers void Run(int index, int maxIndex); } }