From b5ccec62dbbf188ea52e6c3690e66dda46dd3875 Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 4 May 2023 18:34:47 +0300 Subject: [PATCH 01/10] Remove samples building from .sln (Fix for sln build on non-windows OS'es) IDE support should still work for them on solution open as ActiveCfg entries were not removed --- source/DefaultEcs.sln | 6 ------ 1 file changed, 6 deletions(-) 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 From c34a6715d134d6584b9130f299221951f0a2688a Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 4 May 2023 18:55:40 +0300 Subject: [PATCH 02/10] Meaningful Entity.Get exception message --- source/DefaultEcs/Entity.cs | 38 ++++++++++++++------- source/DefaultEcs/Internal/ComponentPool.cs | 10 +++++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/source/DefaultEcs/Entity.cs b/source/DefaultEcs/Entity.cs index 8ab5b0ce..fd6f3cd6 100644 --- a/source/DefaultEcs/Entity.cs +++ b/source/DefaultEcs/Entity.cs @@ -32,6 +32,8 @@ namespace DefaultEcs #region Initialisation + private const string NON_WORLD_ENTITY_MESSAGE = "Entity was not created from a World"; + internal Entity(short worldId) { WorldId = worldId; @@ -70,7 +72,7 @@ internal Entity(short worldId, int entityId) #region Methods - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ThrowIf(bool actuallyThrow, string message) { if (actuallyThrow) @@ -115,7 +117,7 @@ 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); ref ComponentEnum components = ref Components; if (!components[World.IsEnabledFlag]) @@ -132,7 +134,7 @@ 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); ref ComponentEnum components = ref Components; if (components[World.IsEnabledFlag]) @@ -159,7 +161,7 @@ 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 (Has()) { @@ -181,7 +183,7 @@ 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); ref ComponentEnum components = ref Components; if (components[ComponentManager.Flag]) @@ -201,7 +203,7 @@ 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); InnerSet(ComponentManager.GetOrCreate(WorldId).Set(EntityId, component)); } @@ -226,7 +228,7 @@ 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"); ComponentPool pool = ComponentManager.Get(WorldId); @@ -244,7 +246,7 @@ 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); ComponentPool pool = ComponentManager.Get(WorldId); ThrowIf(!(pool?.Has(0) ?? false), $"World does not have a component of type {typeof(T)}"); @@ -277,7 +279,7 @@ 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)}"); Publisher.Publish(WorldId, new EntityComponentChangedMessage(EntityId, Components)); @@ -294,7 +296,11 @@ 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() + { + var componentPool = ComponentManager.Get(WorldId); + return componentPool?.Has(EntityId) ?? false; + } /// /// Gets the component of type on the current . @@ -303,7 +309,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 +333,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(); 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); From 7bc060587e416b932776d5b56e16dde3f1ea6be9 Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 4 May 2023 18:59:10 +0300 Subject: [PATCH 03/10] Entity.IsAlive world.trim fix + new faster Entity.IsAliveVersion property --- source/DefaultEcs/Entity.cs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/source/DefaultEcs/Entity.cs b/source/DefaultEcs/Entity.cs index fd6f3cd6..eec15c72 100644 --- a/source/DefaultEcs/Entity.cs +++ b/source/DefaultEcs/Entity.cs @@ -62,11 +62,38 @@ 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 + { + var entityInfo = World.EntityInfos[EntityId]; + return Version == entityInfo.Version; + } + } #endregion From 658a6f4ebaa0d84e586f64fa208a3dc304f94081 Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 4 May 2023 20:09:14 +0300 Subject: [PATCH 04/10] Systems use entity buffering by default Except for threading, for performance reasons --- source/DefaultEcs/System/AEntityMultiMapSystem.cs | 15 ++++++++------- source/DefaultEcs/System/AEntitySetSystem.cs | 14 +++++++------- .../DefaultEcs/System/AEntitySortedSetSystem.cs | 12 ++++++------ source/DefaultEcs/System/Attributes.cs | 3 +++ 4 files changed, 24 insertions(+), 20 deletions(-) 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; } From b0aaeb0c6f27c8bce9f439edab1405867b172b73 Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 4 May 2023 21:44:00 +0300 Subject: [PATCH 05/10] HelpPage update --- README.md | 58 +++++++++++++++++++++++++---- documentation/NEXT_RELEASENOTES.txt | 1 + 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 90006c5d..b651c998 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,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. + // 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 +320,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 +378,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 +419,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 +538,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/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 From 1774536b19971e3cd01f5c0a9a7502b95b2093e1 Mon Sep 17 00:00:00 2001 From: nrader Date: Thu, 1 Jun 2023 17:10:42 +0300 Subject: [PATCH 06/10] IsAliveVersion default entity struct crash fix --- source/DefaultEcs/Entity.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/source/DefaultEcs/Entity.cs b/source/DefaultEcs/Entity.cs index eec15c72..544e294b 100644 --- a/source/DefaultEcs/Entity.cs +++ b/source/DefaultEcs/Entity.cs @@ -90,6 +90,7 @@ public readonly bool IsAliveVersion { get { + if (WorldId == 0) return false; var entityInfo = World.EntityInfos[EntityId]; return Version == entityInfo.Version; } From bd474319aaebb020c12a06eeaffeb6e438994e35 Mon Sep 17 00:00:00 2001 From: nrader Date: Sun, 2 Jul 2023 15:16:58 +0300 Subject: [PATCH 07/10] Entity misuse runtime detection(and its on/off usage property) for debug configurations --- source/DefaultEcs/Entity.cs | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/source/DefaultEcs/Entity.cs b/source/DefaultEcs/Entity.cs index 544e294b..08dff163 100644 --- a/source/DefaultEcs/Entity.cs +++ b/source/DefaultEcs/Entity.cs @@ -30,9 +30,16 @@ 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) { @@ -146,6 +153,7 @@ private void InnerSet(bool isNew) public void Enable() { ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); ref ComponentEnum components = ref Components; if (!components[World.IsEnabledFlag]) @@ -163,6 +171,7 @@ public void Enable() public void Disable() { ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); ref ComponentEnum components = ref Components; if (components[World.IsEnabledFlag]) @@ -191,6 +200,13 @@ public void Enable() { ThrowIf(WorldId == 0, NON_WORLD_ENTITY_MESSAGE); +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif + if (Has()) { ref ComponentEnum components = ref Components; @@ -213,6 +229,13 @@ public void Disable() { 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]) { @@ -233,6 +256,13 @@ public void Set(in T component) { 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)); } @@ -259,6 +289,13 @@ public void SetSameAs(in Entity reference) 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)}"); @@ -276,6 +313,13 @@ public void SetSameAsWorld() { 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)}"); @@ -289,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; @@ -310,6 +361,13 @@ public void NotifyChanged() 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()) @@ -326,6 +384,12 @@ public void NotifyChanged() [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() { +#if DEBUG + if (IsMisuseDetectionEnabled) + { + ThrowIf(!this.IsAliveVersion, ENTITY_MISUSE_MESSAGE); + } +#endif var componentPool = ComponentManager.Get(WorldId); return componentPool?.Has(EntityId) ?? false; } @@ -412,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)); } From c323b80e678c067baf7a5f252a7b88fbeb6995f3 Mon Sep 17 00:00:00 2001 From: nrader Date: Sun, 2 Jul 2023 15:58:08 +0300 Subject: [PATCH 08/10] Help update --- README.md | 15 +++++++++++++-- documentation/FAQ.md | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b651c998..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 @@ -142,7 +153,7 @@ 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. + // 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 } ``` 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? From 34470a3abd41a1f6278916582d1228230e629f96 Mon Sep 17 00:00:00 2001 From: Dmitry Uvarov Date: Thu, 29 Jun 2023 16:09:59 +0300 Subject: [PATCH 09/10] Drop .NetStandard 1.1 support --- source/DefaultEcs/DefaultEcs.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 @@ - - - - + From d8929f3bb16f33764e222b011fd212258c387a11 Mon Sep 17 00:00:00 2001 From: nrader Date: Fri, 30 Jun 2023 18:19:42 +0300 Subject: [PATCH 10/10] Thread-based DefaultParallelRunner --- .../Internal/Threading/WorkerBarrier.cs | 86 ----------- .../Threading/DefaultParallelRunner.cs | 140 +++++++++++------- .../DefaultEcs/Threading/IParallelRunnable.cs | 7 +- 3 files changed, 92 insertions(+), 141 deletions(-) delete mode 100644 source/DefaultEcs/Internal/Threading/WorkerBarrier.cs 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/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); } }