├── Lib ├── Entitas.dll ├── DesperateDevs.Utils.dll └── Entitas.CodeGeneration.Attributes.dll ├── global.json ├── NUrumi.Test ├── Model │ ├── PlayerName.cs │ ├── Parent.cs │ ├── Position.cs │ ├── Velocity.cs │ ├── HealthComponent.cs │ ├── TestComponent.cs │ └── TestRegistry.cs ├── NUrumi.Test.csproj ├── EntitiesSetTests.cs ├── RefFieldTests.cs ├── BenchCodeTest.cs ├── ReactiveFieldTests.cs ├── PrimaryKeyTests.cs ├── RelationTests.cs ├── CollectorTests.cs ├── UnsafeComponentStorageTest.cs ├── GroupTests.cs └── ContextTests.cs ├── NUrumi ├── IContextResizeCallback.cs ├── NUrumi.csproj ├── Config.cs ├── Registry.cs ├── Exceptions │ ├── NUrumiExceptions.cs │ ├── NUrumiException.cs │ └── NUrumiComponentNotFoundException.cs ├── ComponentTag.cs ├── IUpdateCallback.cs ├── Component.cs ├── ComponentOfRef.cs ├── ComponentOf.cs ├── GroupFilter.cs ├── ReactiveField.cs ├── RefField.cs ├── IndexField.cs ├── EntitiesSet.cs ├── Field.cs ├── Collector.cs ├── Group.cs ├── Relation.cs └── ComponentStorageData.cs ├── NUrumi.Benchmark ├── Entitas │ ├── PerfTest.EntitasPosition.cs │ ├── PerfTest.EntitasVelocity.cs │ └── Generated │ │ ├── Game │ │ ├── GameEntity.cs │ │ ├── GameAttribute.cs │ │ ├── GameComponentsLookup.cs │ │ ├── GameContext.cs │ │ ├── GameMatcher.cs │ │ └── Components │ │ │ ├── GamePerfTestEntitasPositionComponent.cs │ │ │ └── GamePerfTestEntitasVelocityComponent.cs │ │ ├── Input │ │ ├── InputEntity.cs │ │ ├── InputAttribute.cs │ │ ├── InputComponentsLookup.cs │ │ ├── InputContext.cs │ │ └── InputMatcher.cs │ │ ├── Contexts.cs │ │ └── Feature.cs ├── Program.cs ├── NUrumi.Benchmark.csproj ├── LeoEcs │ ├── entities.cs │ ├── systems.cs │ ├── filters.cs │ └── components.cs └── Bench │ ├── SimpleMoveBench.cs │ └── AddRemoveBench.cs ├── NUrumi.sln.DotSettings ├── LICENSE ├── NUrumi.sln └── .gitignore /Lib/Entitas.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volkovku/NUrumi/main/Lib/Entitas.dll -------------------------------------------------------------------------------- /Lib/DesperateDevs.Utils.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volkovku/NUrumi/main/Lib/DesperateDevs.Utils.dll -------------------------------------------------------------------------------- /Lib/Entitas.CodeGeneration.Attributes.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volkovku/NUrumi/main/Lib/Entitas.CodeGeneration.Attributes.dll -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.0", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/PlayerName.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Test.Model 2 | { 3 | public class PlayerName : Component.OfRef 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /NUrumi/IContextResizeCallback.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi 2 | { 3 | internal interface IContextResizeCallback 4 | { 5 | void ResizeEntities(int newSize); 6 | } 7 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/Parent.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Test.Model 2 | { 3 | public sealed class Parent : Component 4 | { 5 | public IndexField Value; 6 | } 7 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/Position.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NUrumi.Test.Model 4 | { 5 | public sealed class Position : Component.Of 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/Velocity.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NUrumi.Test.Model 4 | { 5 | public sealed class Velocity : Component.Of 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/HealthComponent.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Test.Model 2 | { 3 | public class HealthComponent : Component 4 | { 5 | public ReactiveField Value; 6 | } 7 | } -------------------------------------------------------------------------------- /NUrumi/NUrumi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NUrumi/Config.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi 2 | { 3 | public class Config 4 | { 5 | public int InitialEntitiesCapacity = 1000; 6 | public int InitialReuseEntitiesBarrier = 1000; 7 | public int InitialComponentRecordsCapacity = 512; 8 | } 9 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/TestComponent.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Test.Model 2 | { 3 | public sealed class TestComponent : Component 4 | { 5 | public Field Field1; 6 | public Field Field2; 7 | public Field Field3; 8 | } 9 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/PerfTest.EntitasPosition.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using IComponent = Entitas.IComponent; 3 | 4 | public partial class PerfTest 5 | { 6 | public class EntitasPosition : IComponent 7 | { 8 | public Vector2 Value; 9 | } 10 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/PerfTest.EntitasVelocity.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using IComponent = Entitas.IComponent; 3 | 4 | public partial class PerfTest 5 | { 6 | public class EntitasVelocity : IComponent 7 | { 8 | public Vector2 Value; 9 | } 10 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | namespace NUrumi.Benchmark 4 | { 5 | internal class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /NUrumi.Test/Model/TestRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Test.Model 2 | { 3 | public sealed class TestRegistry : Registry 4 | { 5 | public TestComponent Test; 6 | public Position Position; 7 | public Velocity Velocity; 8 | public Parent Parent; 9 | public HealthComponent Health; 10 | } 11 | } -------------------------------------------------------------------------------- /NUrumi/Registry.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi 2 | { 3 | /// 4 | /// Represents a registry that describes available components in a domain context. 5 | /// 6 | /// A type of derived registry. 7 | public abstract class Registry where TRegistry : Registry 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /NUrumi.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/GameEntity.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.EntityGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class GameEntity : Entitas.Entity { 10 | } 11 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Input/InputEntity.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.EntityGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class InputEntity : Entitas.Entity { 10 | } 11 | -------------------------------------------------------------------------------- /NUrumi/Exceptions/NUrumiExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi.Exceptions 2 | { 3 | public static class NUrumiExceptions 4 | { 5 | public static NUrumiComponentNotFoundException ComponentNotFound( 6 | int entityIndex, 7 | IComponent component, 8 | IField field) 9 | { 10 | return new NUrumiComponentNotFoundException( 11 | "Entity does not have component (" + 12 | $"entity_index={entityIndex}," + 13 | $"component_name={component.GetType().Name}," + 14 | $"component_field={field.Name})"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/GameAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextAttributeGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed class GameAttribute : Entitas.CodeGeneration.Attributes.ContextAttribute { 10 | 11 | public GameAttribute() : base("Game") { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Input/InputAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextAttributeGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed class InputAttribute : Entitas.CodeGeneration.Attributes.ContextAttribute { 10 | 11 | public InputAttribute() : base("Input") { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NUrumi/Exceptions/NUrumiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace NUrumi.Exceptions 5 | { 6 | public class NUrumiException : Exception 7 | { 8 | public NUrumiException() 9 | { 10 | } 11 | 12 | protected NUrumiException(SerializationInfo info, StreamingContext context) : base(info, context) 13 | { 14 | } 15 | 16 | public NUrumiException(string message) : base(message) 17 | { 18 | } 19 | 20 | public NUrumiException(string message, Exception innerException) : base(message, innerException) 21 | { 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /NUrumi/ComponentTag.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace NUrumi 4 | { 5 | public abstract partial class Component where TComponent : Component, new() 6 | { 7 | public abstract class Tag : Component 8 | { 9 | #pragma warning disable CS0649 10 | private Field _field; 11 | #pragma warning restore CS0649 12 | 13 | /// 14 | /// Marks entity with specified tag. 15 | /// 16 | /// An entity identity. 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public void Set(int entityId) => _field.Set(entityId, true); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /NUrumi/Exceptions/NUrumiComponentNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace NUrumi.Exceptions 5 | { 6 | public class NUrumiComponentNotFoundException : NUrumiException 7 | { 8 | public NUrumiComponentNotFoundException() 9 | { 10 | } 11 | 12 | protected NUrumiComponentNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) 13 | { 14 | } 15 | 16 | public NUrumiComponentNotFoundException(string message) : base(message) 17 | { 18 | } 19 | 20 | public NUrumiComponentNotFoundException(string message, Exception innerException) : base(message, innerException) 21 | { 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Input/InputComponentsLookup.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentLookupGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public static class InputComponentsLookup { 10 | 11 | 12 | 13 | public const int TotalComponents = 0; 14 | 15 | public static readonly string[] componentNames = { 16 | 17 | }; 18 | 19 | public static readonly System.Type[] componentTypes = { 20 | 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /NUrumi.Test/NUrumi.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NUrumi/IUpdateCallback.cs: -------------------------------------------------------------------------------- 1 | namespace NUrumi 2 | { 3 | /// 4 | /// Describes a callback of entity update. 5 | /// 6 | public interface IUpdateCallback 7 | { 8 | /// 9 | /// Raises when any component of was added or removed to entity. 10 | /// 11 | /// An index of changed entity. 12 | /// If true - component was added; otherwise - removed. 13 | void BeforeChange(int entityIndex, bool added); 14 | 15 | /// 16 | /// Raises when any component of was added or removed to entity. 17 | /// 18 | /// An index of changed entity. 19 | /// If true - component was added; otherwise - removed. 20 | void AfterChange(int entityIndex, bool added); 21 | } 22 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/NUrumi.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ..\Lib\DesperateDevs.Utils.dll 19 | 20 | 21 | ..\Lib\Entitas.dll 22 | 23 | 24 | ..\Lib\Entitas.CodeGeneration.Attributes.dll 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/GameComponentsLookup.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentLookupGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public static class GameComponentsLookup { 10 | 11 | public const int PerfTestEntitasPosition = 0; 12 | public const int PerfTestEntitasVelocity = 1; 13 | 14 | public const int TotalComponents = 2; 15 | 16 | public static readonly string[] componentNames = { 17 | "PerfTestEntitasPosition", 18 | "PerfTestEntitasVelocity" 19 | }; 20 | 21 | public static readonly System.Type[] componentTypes = { 22 | typeof(PerfTest.EntitasPosition), 23 | typeof(PerfTest.EntitasVelocity) 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/GameContext.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class GameContext : Entitas.Context { 10 | 11 | public GameContext() 12 | : base( 13 | GameComponentsLookup.TotalComponents, 14 | 0, 15 | new Entitas.ContextInfo( 16 | "Game", 17 | GameComponentsLookup.componentNames, 18 | GameComponentsLookup.componentTypes 19 | ), 20 | (entity) => 21 | 22 | #if (ENTITAS_FAST_AND_UNSAFE) 23 | new Entitas.UnsafeAERC(), 24 | #else 25 | new Entitas.SafeAERC(entity), 26 | #endif 27 | () => new GameEntity() 28 | ) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kirill Volkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Input/InputContext.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class InputContext : Entitas.Context { 10 | 11 | public InputContext() 12 | : base( 13 | InputComponentsLookup.TotalComponents, 14 | 0, 15 | new Entitas.ContextInfo( 16 | "Input", 17 | InputComponentsLookup.componentNames, 18 | InputComponentsLookup.componentTypes 19 | ), 20 | (entity) => 21 | 22 | #if (ENTITAS_FAST_AND_UNSAFE) 23 | new Entitas.UnsafeAERC(), 24 | #else 25 | new Entitas.SafeAERC(entity), 26 | #endif 27 | () => new InputEntity() 28 | ) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/GameMatcher.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextMatcherGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class GameMatcher { 10 | 11 | public static Entitas.IAllOfMatcher AllOf(params int[] indices) { 12 | return Entitas.Matcher.AllOf(indices); 13 | } 14 | 15 | public static Entitas.IAllOfMatcher AllOf(params Entitas.IMatcher[] matchers) { 16 | return Entitas.Matcher.AllOf(matchers); 17 | } 18 | 19 | public static Entitas.IAnyOfMatcher AnyOf(params int[] indices) { 20 | return Entitas.Matcher.AnyOf(indices); 21 | } 22 | 23 | public static Entitas.IAnyOfMatcher AnyOf(params Entitas.IMatcher[] matchers) { 24 | return Entitas.Matcher.AnyOf(matchers); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Input/InputMatcher.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextMatcherGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public sealed partial class InputMatcher { 10 | 11 | public static Entitas.IAllOfMatcher AllOf(params int[] indices) { 12 | return Entitas.Matcher.AllOf(indices); 13 | } 14 | 15 | public static Entitas.IAllOfMatcher AllOf(params Entitas.IMatcher[] matchers) { 16 | return Entitas.Matcher.AllOf(matchers); 17 | } 18 | 19 | public static Entitas.IAnyOfMatcher AnyOf(params int[] indices) { 20 | return Entitas.Matcher.AnyOf(indices); 21 | } 22 | 23 | public static Entitas.IAnyOfMatcher AnyOf(params Entitas.IMatcher[] matchers) { 24 | return Entitas.Matcher.AnyOf(matchers); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NUrumi.Test/EntitiesSetTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | 4 | namespace NUrumi.Test 5 | { 6 | [TestFixture] 7 | public class EntitiesSetTests 8 | { 9 | [Test] 10 | public void EntitiesSetTest() 11 | { 12 | var set = new EntitiesSet(10); 13 | set.EntitiesCount.Should().Be(0); 14 | set.Has(1).Should().BeFalse(); 15 | 16 | set.Add(1).Should().Be(EntitiesSet.Applied); 17 | set.Has(1).Should().BeTrue(); 18 | set.Add(1).Should().Be(EntitiesSet.AppliedEarly); 19 | 20 | set.Remove(1).Should().Be(EntitiesSet.Applied); 21 | set.Has(1).Should().BeFalse(); 22 | set.Remove(1).Should().Be(EntitiesSet.AppliedEarly); 23 | 24 | for (var i = 0; i < 10; i++) 25 | { 26 | set.Add(i).Should().Be(EntitiesSet.Applied); 27 | set.Has(i).Should().BeTrue(); 28 | set.EntitiesCount.Should().Be(i + 1); 29 | } 30 | 31 | set.Clear(); 32 | set.EntitiesCount.Should().Be(0); 33 | 34 | for (var i = 0; i < 10; i++) 35 | { 36 | set.Has(i).Should().BeFalse(); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /NUrumi.Test/RefFieldTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | 4 | namespace NUrumi.Test 5 | { 6 | [TestFixture] 7 | public class RefFieldTests 8 | { 9 | [Test] 10 | public void RefFieldTest() 11 | { 12 | var context = new Context(); 13 | var playerName = context.Registry.PlayerName; 14 | 15 | var thor = context 16 | .CreateEntity() 17 | .Set(playerName, "Thor"); 18 | 19 | var loki = context 20 | .CreateEntity() 21 | .Set(playerName, "Loki"); 22 | 23 | thor.Get(playerName).Should().Be("Thor"); 24 | loki.Get(playerName).Should().Be("Loki"); 25 | 26 | thor.Remove(playerName); 27 | thor.TryGet(playerName, out _).Should().BeFalse(); 28 | loki.Get(playerName).Should().Be("Loki"); 29 | 30 | thor.Set(playerName, "Thor"); 31 | thor.Get(playerName).Should().Be("Thor"); 32 | loki.Get(playerName).Should().Be("Loki"); 33 | 34 | thor.TryGet(playerName, out var thorName).Should().BeTrue(); 35 | thorName.Should().Be("Thor"); 36 | 37 | loki.TryGet(playerName, out var lokiName).Should().BeTrue(); 38 | lokiName.Should().Be("Loki"); 39 | } 40 | 41 | public class TestRegistry : Registry 42 | { 43 | public PlayerName PlayerName; 44 | } 45 | 46 | public class PlayerName : Component.OfRef 47 | { 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /NUrumi.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUrumi", "NUrumi\NUrumi.csproj", "{A11855B6-163A-497F-8B17-3DA1227DCFEB}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUrumi.Test", "NUrumi.Test\NUrumi.Test.csproj", "{3E223D29-2027-4020-A4DE-30A844C50D85}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUrumi.Benchmark", "NUrumi.Benchmark\NUrumi.Benchmark.csproj", "{67227FF9-C865-4EA8-8B91-25B835BDD86D}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {A11855B6-163A-497F-8B17-3DA1227DCFEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {A11855B6-163A-497F-8B17-3DA1227DCFEB}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {A11855B6-163A-497F-8B17-3DA1227DCFEB}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {A11855B6-163A-497F-8B17-3DA1227DCFEB}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {3E223D29-2027-4020-A4DE-30A844C50D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {3E223D29-2027-4020-A4DE-30A844C50D85}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {3E223D29-2027-4020-A4DE-30A844C50D85}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {3E223D29-2027-4020-A4DE-30A844C50D85}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {67227FF9-C865-4EA8-8B91-25B835BDD86D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {67227FF9-C865-4EA8-8B91-25B835BDD86D}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {67227FF9-C865-4EA8-8B91-25B835BDD86D}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {67227FF9-C865-4EA8-8B91-25B835BDD86D}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /NUrumi.Test/BenchCodeTest.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using NUnit.Framework; 3 | 4 | namespace NUrumi.Test 5 | { 6 | [TestFixture] 7 | public class BenchCodeTest 8 | { 9 | private const int EntitiesCount = 100000; 10 | 11 | [Test] 12 | public void Test() 13 | { 14 | var context = new Context(); 15 | var urumiOnlyPos = context.CreateGroup(GroupFilter 16 | .Include(context.Registry.Position) 17 | .Exclude(context.Registry.Velocity)); 18 | 19 | var urumiPosAndVel = context.CreateGroup(GroupFilter 20 | .Include(context.Registry.Position) 21 | .Include(context.Registry.Velocity)); 22 | 23 | var positionComponent = context.Registry.Position; 24 | var velocityComponent = context.Registry.Velocity; 25 | var position = positionComponent.Value; 26 | var velocity = velocityComponent.Value; 27 | 28 | for (var i = 0; i < EntitiesCount; i++) 29 | { 30 | var entity = context.CreateEntity(); 31 | entity.Set(position, Vector2.One); 32 | if (i % 2 == 0) 33 | { 34 | entity.Set(velocity, Vector2.Zero); 35 | } 36 | } 37 | 38 | foreach (var entity in urumiOnlyPos) 39 | { 40 | entity.Set(velocity, Vector2.Zero); 41 | } 42 | 43 | foreach (var entity in urumiPosAndVel) 44 | { 45 | context.RemoveEntity(entity); 46 | } 47 | } 48 | 49 | private class UrumiRegistry : Registry 50 | { 51 | public UrumiPosition Position; 52 | public UrumiVelocity Velocity; 53 | } 54 | 55 | private class UrumiPosition : Component 56 | { 57 | public Field Value; 58 | } 59 | 60 | private class UrumiVelocity : Component 61 | { 62 | public Field Value; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/Components/GamePerfTestEntitasPositionComponent.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentEntityApiGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | using System.Numerics; 11 | 12 | public partial class GameEntity { 13 | 14 | public PerfTest.EntitasPosition perfTestEntitasPosition { get { return (PerfTest.EntitasPosition)GetComponent(GameComponentsLookup.PerfTestEntitasPosition); } } 15 | public bool hasPerfTestEntitasPosition { get { return HasComponent(GameComponentsLookup.PerfTestEntitasPosition); } } 16 | 17 | public void AddPerfTestEntitasPosition(Vector2 newValue) { 18 | var index = GameComponentsLookup.PerfTestEntitasPosition; 19 | var component = (PerfTest.EntitasPosition)CreateComponent(index, typeof(PerfTest.EntitasPosition)); 20 | component.Value = newValue; 21 | AddComponent(index, component); 22 | } 23 | 24 | public void ReplacePerfTestEntitasPosition(Vector2 newValue) { 25 | var index = GameComponentsLookup.PerfTestEntitasPosition; 26 | var component = (PerfTest.EntitasPosition)CreateComponent(index, typeof(PerfTest.EntitasPosition)); 27 | component.Value = newValue; 28 | ReplaceComponent(index, component); 29 | } 30 | 31 | public void RemovePerfTestEntitasPosition() { 32 | RemoveComponent(GameComponentsLookup.PerfTestEntitasPosition); 33 | } 34 | } 35 | 36 | //------------------------------------------------------------------------------ 37 | // 38 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentMatcherApiGenerator. 39 | // 40 | // Changes to this file may cause incorrect behavior and will be lost if 41 | // the code is regenerated. 42 | // 43 | //------------------------------------------------------------------------------ 44 | public sealed partial class GameMatcher { 45 | 46 | static Entitas.IMatcher _matcherPerfTestEntitasPosition; 47 | 48 | public static Entitas.IMatcher PerfTestEntitasPosition { 49 | get { 50 | if (_matcherPerfTestEntitasPosition == null) { 51 | var matcher = (Entitas.Matcher)Entitas.Matcher.AllOf(GameComponentsLookup.PerfTestEntitasPosition); 52 | matcher.componentNames = GameComponentsLookup.componentNames; 53 | _matcherPerfTestEntitasPosition = matcher; 54 | } 55 | 56 | return _matcherPerfTestEntitasPosition; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Game/Components/GamePerfTestEntitasVelocityComponent.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentEntityApiGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | using System.Numerics; 11 | 12 | public partial class GameEntity { 13 | 14 | public PerfTest.EntitasVelocity perfTestEntitasVelocity { get { return (PerfTest.EntitasVelocity)GetComponent(GameComponentsLookup.PerfTestEntitasVelocity); } } 15 | public bool hasPerfTestEntitasVelocity { get { return HasComponent(GameComponentsLookup.PerfTestEntitasVelocity); } } 16 | 17 | public void AddPerfTestEntitasVelocity(Vector2 newValue) { 18 | var index = GameComponentsLookup.PerfTestEntitasVelocity; 19 | var component = (PerfTest.EntitasVelocity)CreateComponent(index, typeof(PerfTest.EntitasVelocity)); 20 | component.Value = newValue; 21 | AddComponent(index, component); 22 | } 23 | 24 | public void ReplacePerfTestEntitasVelocity(Vector2 newValue) { 25 | var index = GameComponentsLookup.PerfTestEntitasVelocity; 26 | var component = (PerfTest.EntitasVelocity)CreateComponent(index, typeof(PerfTest.EntitasVelocity)); 27 | component.Value = newValue; 28 | ReplaceComponent(index, component); 29 | } 30 | 31 | public void RemovePerfTestEntitasVelocity() { 32 | RemoveComponent(GameComponentsLookup.PerfTestEntitasVelocity); 33 | } 34 | } 35 | 36 | //------------------------------------------------------------------------------ 37 | // 38 | // This code was generated by Entitas.CodeGeneration.Plugins.ComponentMatcherApiGenerator. 39 | // 40 | // Changes to this file may cause incorrect behavior and will be lost if 41 | // the code is regenerated. 42 | // 43 | //------------------------------------------------------------------------------ 44 | public sealed partial class GameMatcher { 45 | 46 | static Entitas.IMatcher _matcherPerfTestEntitasVelocity; 47 | 48 | public static Entitas.IMatcher PerfTestEntitasVelocity { 49 | get { 50 | if (_matcherPerfTestEntitasVelocity == null) { 51 | var matcher = (Entitas.Matcher)Entitas.Matcher.AllOf(GameComponentsLookup.PerfTestEntitasVelocity); 52 | matcher.componentNames = GameComponentsLookup.componentNames; 53 | _matcherPerfTestEntitasVelocity = matcher; 54 | } 55 | 56 | return _matcherPerfTestEntitasVelocity; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Contexts.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.CodeGeneration.Plugins.ContextsGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | public partial class Contexts : Entitas.IContexts { 10 | 11 | public static Contexts sharedInstance { 12 | get { 13 | if (_sharedInstance == null) { 14 | _sharedInstance = new Contexts(); 15 | } 16 | 17 | return _sharedInstance; 18 | } 19 | set { _sharedInstance = value; } 20 | } 21 | 22 | static Contexts _sharedInstance; 23 | 24 | public GameContext game { get; set; } 25 | public InputContext input { get; set; } 26 | 27 | public Entitas.IContext[] allContexts { get { return new Entitas.IContext [] { game, input }; } } 28 | 29 | public Contexts() { 30 | game = new GameContext(); 31 | input = new InputContext(); 32 | 33 | var postConstructors = System.Linq.Enumerable.Where( 34 | GetType().GetMethods(), 35 | method => System.Attribute.IsDefined(method, typeof(Entitas.CodeGeneration.Attributes.PostConstructorAttribute)) 36 | ); 37 | 38 | foreach (var postConstructor in postConstructors) { 39 | postConstructor.Invoke(this, null); 40 | } 41 | } 42 | 43 | public void Reset() { 44 | var contexts = allContexts; 45 | for (int i = 0; i < contexts.Length; i++) { 46 | contexts[i].Reset(); 47 | } 48 | } 49 | } 50 | 51 | //------------------------------------------------------------------------------ 52 | // 53 | // This code was generated by Entitas.VisualDebugging.CodeGeneration.Plugins.ContextObserverGenerator. 54 | // 55 | // Changes to this file may cause incorrect behavior and will be lost if 56 | // the code is regenerated. 57 | // 58 | //------------------------------------------------------------------------------ 59 | public partial class Contexts { 60 | 61 | #if (!ENTITAS_DISABLE_VISUAL_DEBUGGING && UNITY_EDITOR) 62 | 63 | [Entitas.CodeGeneration.Attributes.PostConstructor] 64 | public void InitializeContextObservers() { 65 | try { 66 | CreateContextObserver(game); 67 | CreateContextObserver(input); 68 | } catch(System.Exception) { 69 | } 70 | } 71 | 72 | public void CreateContextObserver(Entitas.IContext context) { 73 | if (UnityEngine.Application.isPlaying) { 74 | var observer = new Entitas.VisualDebugging.Unity.ContextObserver(context); 75 | UnityEngine.Object.DontDestroyOnLoad(observer.gameObject); 76 | } 77 | } 78 | 79 | #endif 80 | } 81 | -------------------------------------------------------------------------------- /NUrumi/Component.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NUrumi 4 | { 5 | /// 6 | /// Represents a component. 7 | /// 8 | /// A type of derived component. 9 | public abstract partial class Component : 10 | IComponent 11 | where TComponent : Component, new() 12 | { 13 | private int _index; 14 | private IField[] _fields; 15 | 16 | /// 17 | /// An index of component in registry. 18 | /// 19 | public int Index => _index; 20 | 21 | /// 22 | /// A collection of fields associated with this component. 23 | /// 24 | public IReadOnlyList Fields => _fields; 25 | 26 | /// 27 | /// A component data storage. 28 | /// 29 | public ComponentStorageData Storage; 30 | 31 | /// 32 | /// Determines is this component a part of entity with specified identifier. 33 | /// 34 | /// An entity identity. 35 | /// Returns true if this component is a part of entity; otherwise false. 36 | public bool IsAPartOf(int entityId) 37 | { 38 | return Storage.Has(entityId); 39 | } 40 | 41 | /// 42 | /// Removes component from entity with specified identifier. 43 | /// 44 | /// An identifier of an entity. 45 | /// Returns true if component was removed, otherwise false. 46 | public bool RemoveFrom(int entityId) 47 | { 48 | return Storage.Remove(entityId); 49 | } 50 | 51 | ComponentStorageData IComponent.Storage => Storage; 52 | 53 | void IComponent.Init(int index, IField[] fields, ComponentStorageData storage) 54 | { 55 | _index = index; 56 | _fields = fields; 57 | Storage = storage; 58 | OnInit(); 59 | } 60 | 61 | protected virtual void OnInit() 62 | { 63 | } 64 | } 65 | 66 | public static class ComponentCompanion 67 | { 68 | public static bool Has(this int entityId, Component component) 69 | where TComponent : Component, new() 70 | { 71 | return component.IsAPartOf(entityId); 72 | } 73 | 74 | public static bool Remove(this int entityId, Component component) 75 | where TComponent : Component, new() 76 | { 77 | return component.RemoveFrom(entityId); 78 | } 79 | } 80 | 81 | public interface IComponent 82 | { 83 | int Index { get; } 84 | ComponentStorageData Storage { get; } 85 | IReadOnlyList Fields { get; } 86 | void Init(int index, IField[] fields, ComponentStorageData storage); 87 | } 88 | } -------------------------------------------------------------------------------- /NUrumi/ComponentOfRef.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace NUrumi 4 | { 5 | /// 6 | /// Represents a shortcut to components with only one field. 7 | /// 8 | public abstract partial class Component where TComponent : Component, new() 9 | { 10 | /// 11 | /// Represents a shortcut to components with only one field. 12 | /// 13 | public abstract class OfRef : Component where TValue : class 14 | { 15 | #pragma warning disable CS0649 16 | private RefField _field; 17 | #pragma warning restore CS0649 18 | 19 | /// 20 | /// Returns this field value from entity with specified index. 21 | /// 22 | /// An entity identity. 23 | /// A value of this field in entity. 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public TValue Get(int entityId) => _field.Get(entityId); 26 | 27 | /// 28 | /// Try to get this field value from entity with specified index. 29 | /// 30 | /// An entity identity. 31 | /// A field value if exists. 32 | /// Returns true if value exists, otherwise false. 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public bool TryGet(int entityId, out TValue result) => _field.TryGet(entityId, out result); 35 | 36 | /// 37 | /// Sets field value to entity with specified index. 38 | /// 39 | /// An entity identity. 40 | /// A value to set. 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public void Set(int entityId, TValue value) => _field.Set(entityId, value); 43 | 44 | public static implicit operator RefField(OfRef componentOf) 45 | { 46 | return componentOf._field; 47 | } 48 | } 49 | } 50 | 51 | public static class ComponentOfRefCompanion 52 | { 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public static bool TryGet( 55 | this int entityId, 56 | Component.OfRef component, 57 | out TValue value) 58 | where TComponent : Component, new() 59 | where TValue : class 60 | { 61 | return component.TryGet(entityId, out value); 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public static int Set( 66 | this int entityId, 67 | Component.OfRef component, 68 | TValue value) 69 | where TComponent : Component, new() 70 | where TValue : class 71 | { 72 | component.Set(entityId, value); 73 | return entityId; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /NUrumi.Test/ReactiveFieldTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | 5 | namespace NUrumi.Test 6 | { 7 | [TestFixture] 8 | public class ReactiveFieldTests 9 | { 10 | [Test] 11 | public void ReactiveFieldTest() 12 | { 13 | var context = new Context(); 14 | var component = context.Registry.ReactiveComponent; 15 | var changes = 0; 16 | 17 | int changedEntityId = default; 18 | int? changedValue = default; 19 | int assignedValue = default; 20 | 21 | // ReSharper disable once ConvertToLocalFunction 22 | ReactiveField.OnReactiveFieldValueChangedEventHandler subscription = 23 | (changedComponent, changedField, id, oldValue, newValue) => 24 | { 25 | Assert.AreEqual(component, changedComponent); 26 | Assert.AreEqual(component.ReactiveField, changedField); 27 | changes += 1; 28 | changedEntityId = id; 29 | changedValue = oldValue; 30 | assignedValue = newValue; 31 | }; 32 | 33 | component.ReactiveField.OnValueChanged += subscription; 34 | 35 | var testReactiveField = new Action((entityId, newValue) => 36 | { 37 | var changesBefore = changes; 38 | var valueBefore = entityId.TryGet(component.ReactiveField, out var prev) ? (int?) prev : null; 39 | entityId.Set(component.ReactiveField, newValue); 40 | changedEntityId.Should().Be(entityId); 41 | changedValue.Should().Be(valueBefore); 42 | assignedValue.Should().Be(newValue); 43 | changes.Should().Be(changesBefore + 1); 44 | }); 45 | 46 | // It should fire event when reactive field is changed 47 | var rnd = new Random(); 48 | var entity1 = context.CreateEntity(); 49 | var entity2 = context.CreateEntity(); 50 | var entity3 = context.CreateEntity(); 51 | var entity4 = context.CreateEntity(); 52 | var entities = new[] {entity1, entity2, entity3, entity4}; 53 | for (var i = 0; i < 100; i++) 54 | { 55 | var entityId = entities[Math.Abs(rnd.Next()) % entities.Length]; 56 | testReactiveField(entityId, rnd.Next()); 57 | } 58 | 59 | // It should not fire event if not reactive field changed 60 | var lastChanges = changes; 61 | for (var i = 0; i < 100; i++) 62 | { 63 | var entityId = entities[Math.Abs(rnd.Next()) % entities.Length]; 64 | entityId.Set(component.SimpleField, rnd.Next()); 65 | changes.Should().Be(lastChanges); 66 | } 67 | 68 | // It should not fire event if subscription was cancelled 69 | component.ReactiveField.OnValueChanged -= subscription; 70 | for (var i = 0; i < 100; i++) 71 | { 72 | var entityId = entities[Math.Abs(rnd.Next()) % entities.Length]; 73 | entityId.Set(component.ReactiveField, rnd.Next()); 74 | changes.Should().Be(lastChanges); 75 | } 76 | } 77 | 78 | private class TestRegistry : Registry 79 | { 80 | public ReactiveComponent ReactiveComponent; 81 | } 82 | 83 | private class ReactiveComponent : Component 84 | { 85 | public Field SimpleField; 86 | public ReactiveField ReactiveField; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Entitas/Generated/Feature.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by Entitas.VisualDebugging.CodeGeneration.Plugins.FeatureClassGenerator. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | #if (!ENTITAS_DISABLE_VISUAL_DEBUGGING && UNITY_EDITOR) 10 | 11 | public class Feature : Entitas.VisualDebugging.Unity.DebugSystems { 12 | 13 | public Feature(string name) : base(name) { 14 | } 15 | 16 | public Feature() : base(true) { 17 | var typeName = DesperateDevs.Utils.SerializationTypeExtension.ToCompilableString(GetType()); 18 | var shortType = DesperateDevs.Utils.SerializationTypeExtension.ShortTypeName(typeName); 19 | var readableType = DesperateDevs.Utils.StringExtension.ToSpacedCamelCase(shortType); 20 | 21 | initialize(readableType); 22 | } 23 | } 24 | 25 | #elif (!ENTITAS_DISABLE_DEEP_PROFILING && DEVELOPMENT_BUILD) 26 | 27 | public class Feature : Entitas.Systems { 28 | 29 | System.Collections.Generic.List _initializeSystemNames; 30 | System.Collections.Generic.List _executeSystemNames; 31 | System.Collections.Generic.List _cleanupSystemNames; 32 | System.Collections.Generic.List _tearDownSystemNames; 33 | 34 | public Feature(string name) : this() { 35 | } 36 | 37 | public Feature() { 38 | _initializeSystemNames = new System.Collections.Generic.List(); 39 | _executeSystemNames = new System.Collections.Generic.List(); 40 | _cleanupSystemNames = new System.Collections.Generic.List(); 41 | _tearDownSystemNames = new System.Collections.Generic.List(); 42 | } 43 | 44 | public override Entitas.Systems Add(Entitas.ISystem system) { 45 | var systemName = system.GetType().FullName; 46 | 47 | if (system is Entitas.IInitializeSystem) { 48 | _initializeSystemNames.Add(systemName); 49 | } 50 | 51 | if (system is Entitas.IExecuteSystem) { 52 | _executeSystemNames.Add(systemName); 53 | } 54 | 55 | if (system is Entitas.ICleanupSystem) { 56 | _cleanupSystemNames.Add(systemName); 57 | } 58 | 59 | if (system is Entitas.ITearDownSystem) { 60 | _tearDownSystemNames.Add(systemName); 61 | } 62 | 63 | return base.Add(system); 64 | } 65 | 66 | public override void Initialize() { 67 | for (int i = 0; i < _initializeSystems.Count; i++) { 68 | UnityEngine.Profiling.Profiler.BeginSample(_initializeSystemNames[i]); 69 | _initializeSystems[i].Initialize(); 70 | UnityEngine.Profiling.Profiler.EndSample(); 71 | } 72 | } 73 | 74 | public override void Execute() { 75 | for (int i = 0; i < _executeSystems.Count; i++) { 76 | UnityEngine.Profiling.Profiler.BeginSample(_executeSystemNames[i]); 77 | _executeSystems[i].Execute(); 78 | UnityEngine.Profiling.Profiler.EndSample(); 79 | } 80 | } 81 | 82 | public override void Cleanup() { 83 | for (int i = 0; i < _cleanupSystems.Count; i++) { 84 | UnityEngine.Profiling.Profiler.BeginSample(_cleanupSystemNames[i]); 85 | _cleanupSystems[i].Cleanup(); 86 | UnityEngine.Profiling.Profiler.EndSample(); 87 | } 88 | } 89 | 90 | public override void TearDown() { 91 | for (int i = 0; i < _tearDownSystems.Count; i++) { 92 | UnityEngine.Profiling.Profiler.BeginSample(_tearDownSystemNames[i]); 93 | _tearDownSystems[i].TearDown(); 94 | UnityEngine.Profiling.Profiler.EndSample(); 95 | } 96 | } 97 | } 98 | 99 | #else 100 | 101 | public class Feature : Entitas.Systems { 102 | 103 | public Feature(string name) { 104 | } 105 | 106 | public Feature() { 107 | } 108 | } 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /NUrumi/ComponentOf.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace NUrumi 4 | { 5 | public abstract partial class Component where TComponent : Component, new() 6 | { 7 | /// 8 | /// Represents a shortcut to components with only one field. 9 | /// 10 | public abstract class Of : Component where TValue : unmanaged 11 | { 12 | #pragma warning disable CS0649 13 | private Field _field; 14 | #pragma warning restore CS0649 15 | 16 | /// 17 | /// Returns this field value from entity with specified index. 18 | /// 19 | /// An entity identity. 20 | /// A value of this field in entity. 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public TValue Get(int entityId) => _field.Get(entityId); 23 | 24 | /// 25 | /// Returns this field value from entity with specified index as reference. 26 | /// 27 | /// An entity identity. 28 | /// A value of this field in entity. 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public ref TValue GetRef(int entityId) => ref _field.GetRef(entityId); 31 | 32 | /// 33 | /// Returns this field value from entity with specified index as reference. 34 | /// If value does not set then set it as default and returns it as a reference. 35 | /// 36 | /// An entity identity. 37 | /// A value of this field in entity. 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public ref TValue GetOrAdd(int entityId) => ref _field.GetOrAdd(entityId); 40 | 41 | /// 42 | /// Returns this field value from entity with specified index as reference. 43 | /// If value does not set then set it as default and returns it as a reference. 44 | /// 45 | /// An entity identity. 46 | /// A value which should be set if entity does not have value. 47 | /// A value of this field in entity. 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public ref TValue GetOrSet(int entityId, TValue value) => ref _field.GetOrSet(entityId, value); 50 | 51 | /// 52 | /// Try to get this field value from entity with specified index. 53 | /// 54 | /// An entity identity. 55 | /// A field value if exists. 56 | /// Returns true if value exists, otherwise false. 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public bool TryGet(int entityId, out TValue result) => _field.TryGet(entityId, out result); 59 | 60 | /// 61 | /// Sets field value to entity with specified index. 62 | /// 63 | /// An entity identity. 64 | /// A value to set. 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public void Set(int entityId, TValue value) => _field.Set(entityId, value); 67 | 68 | public static implicit operator Field(Of componentOf) 69 | { 70 | return componentOf._field; 71 | } 72 | } 73 | } 74 | 75 | public static class ComponentOfCompanion 76 | { 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | public static int Set( 79 | this int entityId, 80 | Component.Of component, 81 | TValue value) 82 | where TComponent : Component, new() 83 | where TValue : unmanaged 84 | { 85 | component.Set(entityId, value); 86 | return entityId; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /NUrumi.Test/PrimaryKeyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | 5 | namespace NUrumi.Test 6 | { 7 | [TestFixture] 8 | public class PrimaryKeyTests 9 | { 10 | public class Registry : Registry 11 | { 12 | public ExternalIdComponent ExternalIdIndex; 13 | } 14 | 15 | public class ExternalIdComponent : Component.OfPrimaryKey 16 | { 17 | } 18 | 19 | public readonly struct ExternalEntityId : IEquatable 20 | { 21 | public ExternalEntityId(int externalId) 22 | { 23 | ExternalId = externalId; 24 | } 25 | 26 | public readonly int ExternalId; 27 | 28 | public bool Equals(ExternalEntityId other) 29 | { 30 | return ExternalId == other.ExternalId; 31 | } 32 | 33 | public override bool Equals(object obj) 34 | { 35 | return obj is ExternalEntityId other && Equals(other); 36 | } 37 | 38 | public override int GetHashCode() 39 | { 40 | return ExternalId; 41 | } 42 | 43 | public static bool operator ==(ExternalEntityId left, ExternalEntityId right) 44 | { 45 | return left.Equals(right); 46 | } 47 | 48 | public static bool operator !=(ExternalEntityId left, ExternalEntityId right) 49 | { 50 | return !left.Equals(right); 51 | } 52 | } 53 | 54 | [Test] 55 | public void PrimaryKeyTest() 56 | { 57 | // Initialize 58 | var context = new Context(); 59 | var externalIdIndex = context.Registry.ExternalIdIndex; 60 | 61 | var externalId1 = new ExternalEntityId(1); 62 | var externalId2 = new ExternalEntityId(3); 63 | var externalId3 = new ExternalEntityId(5); 64 | var externalId4 = new ExternalEntityId(2); 65 | 66 | // Populate 67 | var entity1 = context 68 | .CreateEntity() 69 | .Set(externalIdIndex, externalId1); 70 | 71 | var entity2 = context 72 | .CreateEntity() 73 | .Set(externalIdIndex, externalId2); 74 | 75 | var entity3 = context 76 | .CreateEntity() 77 | .Set(externalIdIndex, externalId3); 78 | 79 | var entity4 = context 80 | .CreateEntity() 81 | .Set(externalIdIndex, externalId4); 82 | 83 | var entity5 = context 84 | .CreateEntity(); 85 | 86 | // Check values 87 | var checks = new[] 88 | { 89 | ValueTuple.Create(entity1, externalId1), 90 | ValueTuple.Create(entity2, externalId2), 91 | ValueTuple.Create(entity3, externalId3), 92 | ValueTuple.Create(entity4, externalId4), 93 | }; 94 | 95 | foreach (var (entity, requiredExternalId) in checks) 96 | { 97 | entity.Has(externalIdIndex).Should().BeTrue(); 98 | entity.TryGet(externalIdIndex, out var entityExternalId).Should().BeTrue(); 99 | entityExternalId.Should().Be(requiredExternalId); 100 | entity.Get(externalIdIndex).Should().Be(requiredExternalId); 101 | 102 | externalIdIndex.TryGetEntityByKey(requiredExternalId, out var foundEntity).Should().BeTrue(); 103 | foundEntity.Should().Be(entity); 104 | externalIdIndex.GetEntityByKey(requiredExternalId).Should().Be(entity); 105 | } 106 | 107 | entity5.Has(externalIdIndex).Should().BeFalse(); 108 | entity5.TryGet(externalIdIndex, out _).Should().BeFalse(); 109 | new Action(() => entity5.Set(externalIdIndex, externalId1)) 110 | .Should().Throw() 111 | .Which.Message.Should().Contain("Entity for key already exists"); 112 | 113 | entity1.Remove(externalIdIndex); 114 | entity1.Has(externalIdIndex).Should().BeFalse(); 115 | entity1.TryGet(externalIdIndex, out _).Should().BeFalse(); 116 | externalIdIndex.TryGetEntityByKey(externalId1, out _).Should().BeFalse(); 117 | new Action(() => externalIdIndex.GetEntityByKey(externalId1)) 118 | .Should().Throw() 119 | .Which.Message.Should().Contain("Entity not found"); 120 | 121 | entity5.Set(externalIdIndex, externalId1); 122 | entity5.Has(externalIdIndex).Should().BeTrue(); 123 | entity5.TryGet(externalIdIndex, out var entity5ExternalId).Should().BeTrue(); 124 | entity5ExternalId.Should().Be(externalId1); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/LeoEcs/entities.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The Proprietary or MIT-Red License 3 | // Copyright (c) 2012-2022 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System.Runtime.CompilerServices; 7 | 8 | #if ENABLE_IL2CPP 9 | using Unity.IL2CPP.CompilerServices; 10 | #endif 11 | 12 | namespace Leopotam.EcsLite { 13 | public struct EcsPackedEntity { 14 | internal int Id; 15 | internal int Gen; 16 | } 17 | 18 | public struct EcsPackedEntityWithWorld { 19 | internal int Id; 20 | internal int Gen; 21 | internal EcsWorld World; 22 | #if DEBUG 23 | // For using in IDE debugger. 24 | internal object[] DebugComponentsView { 25 | get { 26 | object[] list = null; 27 | if (World != null && World.IsAlive () && World.IsEntityAliveInternal (Id) && World.GetEntityGen (Id) == Gen) { 28 | World.GetComponents (Id, ref list); 29 | } 30 | return list; 31 | } 32 | } 33 | // For using in IDE debugger. 34 | internal int DebugComponentsCount { 35 | get { 36 | if (World != null && World.IsAlive () && World.IsEntityAliveInternal (Id) && World.GetEntityGen (Id) == Gen) { 37 | return World.GetComponentsCount (Id); 38 | } 39 | return 0; 40 | } 41 | } 42 | 43 | // For using in IDE debugger. 44 | public override string ToString () { 45 | if (Id == 0 && Gen == 0) { return "Entity-Null"; } 46 | if (World == null || !World.IsAlive () || !World.IsEntityAliveInternal (Id) || World.GetEntityGen (Id) != Gen) { return "Entity-NonAlive"; } 47 | System.Type[] types = null; 48 | var count = World.GetComponentTypes (Id, ref types); 49 | System.Text.StringBuilder sb = null; 50 | if (count > 0) { 51 | sb = new System.Text.StringBuilder (512); 52 | for (var i = 0; i < count; i++) { 53 | if (sb.Length > 0) { sb.Append (","); } 54 | sb.Append (types[i].Name); 55 | } 56 | } 57 | return $"Entity-{Id}:{Gen} [{sb}]"; 58 | } 59 | #endif 60 | } 61 | 62 | #if ENABLE_IL2CPP 63 | [Il2CppSetOption (Option.NullChecks, false)] 64 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 65 | #endif 66 | public static class EcsEntityExtensions { 67 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 68 | public static EcsPackedEntity PackEntity (this EcsWorld world, int entity) { 69 | EcsPackedEntity packed; 70 | packed.Id = entity; 71 | packed.Gen = world.GetEntityGen (entity); 72 | return packed; 73 | } 74 | 75 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 76 | public static bool Unpack (this in EcsPackedEntity packed, EcsWorld world, out int entity) { 77 | if (!world.IsAlive () || !world.IsEntityAliveInternal (packed.Id) || world.GetEntityGen (packed.Id) != packed.Gen) { 78 | entity = -1; 79 | return false; 80 | } 81 | entity = packed.Id; 82 | return true; 83 | } 84 | 85 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 86 | public static bool EqualsTo (this in EcsPackedEntity a, in EcsPackedEntity b) { 87 | return a.Id == b.Id && a.Gen == b.Gen; 88 | } 89 | 90 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 91 | public static EcsPackedEntityWithWorld PackEntityWithWorld (this EcsWorld world, int entity) { 92 | EcsPackedEntityWithWorld packedEntity; 93 | packedEntity.World = world; 94 | packedEntity.Id = entity; 95 | packedEntity.Gen = world.GetEntityGen (entity); 96 | return packedEntity; 97 | } 98 | 99 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 100 | public static bool Unpack (this in EcsPackedEntityWithWorld packedEntity, out EcsWorld world, out int entity) { 101 | if (packedEntity.World == null || !packedEntity.World.IsAlive () || !packedEntity.World.IsEntityAliveInternal (packedEntity.Id) || packedEntity.World.GetEntityGen (packedEntity.Id) != packedEntity.Gen) { 102 | world = null; 103 | entity = -1; 104 | return false; 105 | } 106 | world = packedEntity.World; 107 | entity = packedEntity.Id; 108 | return true; 109 | } 110 | 111 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 112 | public static bool EqualsTo (this in EcsPackedEntityWithWorld a, in EcsPackedEntityWithWorld b) { 113 | return a.Id == b.Id && a.Gen == b.Gen && a.World == b.World; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /NUrumi.Test/RelationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | 5 | namespace NUrumi.Test 6 | { 7 | [TestFixture] 8 | public class RelationTests 9 | { 10 | [Test] 11 | public void RelationTest() 12 | { 13 | // Configure 14 | // --------- 15 | var context = new Context(); 16 | var likes = context.Registry.Likes; 17 | 18 | var apples = context.CreateEntity(); 19 | var burgers = context.CreateEntity(); 20 | var sweets = context.CreateEntity(); 21 | 22 | var alice = context.CreateEntity(); 23 | var bob = context.CreateEntity(); 24 | 25 | // Populate set of relations 26 | // ------------------------- 27 | alice.Add(likes, apples); 28 | alice.Add(likes, sweets); 29 | 30 | bob.Add(likes, burgers); 31 | bob.Add(likes, sweets); 32 | 33 | // Check what likes alice 34 | // --------------------- 35 | alice.Has(likes, apples).Should().BeTrue(); 36 | alice.Has(likes, sweets).Should().BeTrue(); 37 | alice.Has(likes, burgers).Should().BeFalse(); 38 | 39 | var aliceLikes = alice.Relationship(likes); 40 | aliceLikes.Count.Should().Be(2); 41 | aliceLikes.Contains(apples).Should().BeTrue(); 42 | aliceLikes.Contains(burgers).Should().BeFalse(); 43 | aliceLikes.Contains(sweets).Should().BeTrue(); 44 | 45 | // Check what likes bob 46 | // --------------------- 47 | bob.Has(likes, apples).Should().BeFalse(); 48 | bob.Has(likes, burgers).Should().BeTrue(); 49 | bob.Has(likes, sweets).Should().BeTrue(); 50 | 51 | var bobLikes = bob.Relationship(likes); 52 | bobLikes.Count.Should().Be(2); 53 | bobLikes.Contains(apples).Should().BeFalse(); 54 | bobLikes.Contains(sweets).Should().BeTrue(); 55 | bobLikes.Contains(burgers).Should().BeTrue(); 56 | 57 | // Check who like apples 58 | // --------------------- 59 | var whoLikeApples = apples.Target(likes); 60 | whoLikeApples.Count.Should().Be(1); 61 | whoLikeApples.Contains(alice).Should().BeTrue(); 62 | whoLikeApples.Contains(bob).Should().BeFalse(); 63 | 64 | // Check who like burgers 65 | // ---------------------- 66 | var whoLikeBurgers = burgers.Target(likes); 67 | whoLikeBurgers.Count.Should().Be(1); 68 | whoLikeBurgers.Contains(alice).Should().BeFalse(); 69 | whoLikeBurgers.Contains(bob).Should().BeTrue(); 70 | 71 | // Check who like sweets 72 | // --------------------- 73 | var whoLikeSweets = sweets.Target(likes); 74 | whoLikeSweets.Count.Should().Be(2); 75 | whoLikeSweets.Contains(alice).Should().BeTrue(); 76 | whoLikeSweets.Contains(bob).Should().BeTrue(); 77 | 78 | // Alice don't like apples anymore 79 | // -------------------------------- 80 | alice.Remove(likes, apples); 81 | 82 | aliceLikes = alice.Relationship(likes); 83 | aliceLikes.Count.Should().Be(1); 84 | aliceLikes.Contains(apples).Should().BeFalse(); 85 | aliceLikes.Contains(sweets).Should().BeTrue(); 86 | aliceLikes.Contains(burgers).Should().BeFalse(); 87 | 88 | whoLikeApples = apples.Target(likes); 89 | whoLikeApples.Count.Should().Be(0); 90 | whoLikeApples.Contains(alice).Should().BeFalse(); 91 | whoLikeApples.Contains(bob).Should().BeFalse(); 92 | 93 | // There is no sweets in our menu anymore 94 | // -------------------------------------- 95 | context.RemoveEntity(burgers); 96 | 97 | bobLikes.Count.Should().Be(1); 98 | bobLikes.Contains(apples).Should().BeFalse(); 99 | bobLikes.Contains(sweets).Should().BeTrue(); 100 | bobLikes.Contains(burgers).Should().BeFalse(); 101 | 102 | // Bob goes out 103 | // ------------ 104 | context.RemoveEntity(bob); 105 | 106 | whoLikeApples = apples.Target(likes); 107 | whoLikeApples.Count.Should().Be(0); 108 | whoLikeApples.Contains(alice).Should().BeFalse(); 109 | whoLikeApples.Contains(bob).Should().BeFalse(); 110 | 111 | whoLikeSweets = sweets.Target(likes); 112 | whoLikeSweets.Count.Should().Be(1); 113 | whoLikeSweets.Contains(alice).Should().BeTrue(); 114 | whoLikeSweets.Contains(bob).Should().BeFalse(); 115 | } 116 | 117 | 118 | public class RelationRegistry : Registry 119 | { 120 | public Likes Likes; 121 | } 122 | 123 | public class Likes : Relation 124 | { 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /NUrumi/GroupFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUrumi.Exceptions; 4 | 5 | namespace NUrumi 6 | { 7 | public interface IGroupFilter 8 | { 9 | IReadOnlyCollection Included { get; } 10 | IReadOnlyCollection Excluded { get; } 11 | IGroupFilter Include(IComponent component); 12 | IGroupFilter Exclude(IComponent component); 13 | } 14 | 15 | public static class GroupFilter 16 | { 17 | public static IGroupFilter Include(IComponent component) 18 | { 19 | return new Instance( 20 | new HashSet {component}, 21 | new HashSet(), 22 | component.GetHashCode()); 23 | } 24 | 25 | public static IGroupFilter Exclude(IComponent component) 26 | { 27 | return new Instance( 28 | new HashSet(), 29 | new HashSet {component}, 30 | component.GetHashCode()); 31 | } 32 | 33 | private class Instance : IGroupFilter, IEquatable 34 | { 35 | private readonly HashSet _include; 36 | private readonly HashSet _exclude; 37 | private readonly int _hashCode; 38 | 39 | public IReadOnlyCollection Included => _include; 40 | public IReadOnlyCollection Excluded => _exclude; 41 | 42 | // ReSharper disable once MemberHidesStaticFromOuterClass 43 | public IGroupFilter Include(IComponent component) 44 | { 45 | if (_exclude.Contains(component)) 46 | { 47 | throw new NUrumiException( 48 | $"Group filter already has component '{component.GetType().Name}' " + 49 | "in exclude part"); 50 | } 51 | 52 | return new Instance( 53 | new HashSet(_include) {component}, 54 | _exclude, 55 | (_hashCode * 397) ^ component.GetHashCode()); 56 | } 57 | 58 | // ReSharper disable once MemberHidesStaticFromOuterClass 59 | public IGroupFilter Exclude(IComponent component) 60 | { 61 | if (_include.Contains(component)) 62 | { 63 | throw new NUrumiException( 64 | $"Group filter already has component '{component.GetType().Name}' " + 65 | "in include part"); 66 | } 67 | 68 | return new Instance( 69 | _include, 70 | new HashSet(_exclude) {component}, 71 | (_hashCode * 397) ^ component.GetHashCode()); 72 | } 73 | 74 | internal Instance(HashSet include, HashSet exclude, int hashCode) 75 | { 76 | _include = include; 77 | _exclude = exclude; 78 | _hashCode = hashCode; 79 | } 80 | 81 | public bool Equals(Instance other) 82 | { 83 | if (ReferenceEquals(null, other)) return false; 84 | if (ReferenceEquals(this, other)) return true; 85 | 86 | if (_hashCode != other._hashCode 87 | || _include.Count != other._include.Count 88 | || _exclude.Count != other._exclude.Count) 89 | { 90 | return false; 91 | } 92 | 93 | foreach (var component in _include) 94 | { 95 | if (!other._include.Contains(component)) 96 | { 97 | return false; 98 | } 99 | } 100 | 101 | foreach (var component in _exclude) 102 | { 103 | if (!other._exclude.Contains(component)) 104 | { 105 | return false; 106 | } 107 | } 108 | 109 | return true; 110 | } 111 | 112 | public override bool Equals(object obj) 113 | { 114 | if (ReferenceEquals(null, obj)) return false; 115 | if (ReferenceEquals(this, obj)) return true; 116 | if (obj.GetType() != GetType()) return false; 117 | return Equals((Instance) obj); 118 | } 119 | 120 | public override int GetHashCode() 121 | { 122 | return _hashCode; 123 | } 124 | 125 | public static bool operator ==(Instance left, Instance right) 126 | { 127 | return Equals(left, right); 128 | } 129 | 130 | public static bool operator !=(Instance left, Instance right) 131 | { 132 | return !Equals(left, right); 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Bench/SimpleMoveBench.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using BenchmarkDotNet.Attributes; 4 | using Entitas; 5 | using Leopotam.EcsLite; 6 | 7 | namespace NUrumi.Benchmark.Bench 8 | { 9 | public class SimpleMoveBench 10 | { 11 | private const int EntitiesCount = 100000; 12 | 13 | private readonly Context _urumi; 14 | private readonly Group _urumiGroup; 15 | 16 | private readonly EcsWorld _leo; 17 | private readonly EcsFilter _leoFilter; 18 | 19 | private GameContext _entitas; 20 | private IGroup _entitasGroup; 21 | 22 | public SimpleMoveBench() 23 | { 24 | _urumi = new Context(); 25 | var cmp = _urumi.Registry; 26 | var urumiPosition = cmp.Position; 27 | var urumiVelocity = cmp.Velocity; 28 | _urumiGroup = _urumi.CreateGroup(GroupFilter 29 | .Include(cmp.Position) 30 | .Include(cmp.Velocity)); 31 | 32 | _leo = new EcsWorld(); 33 | _leoFilter = _leo 34 | .Filter() 35 | .Inc() 36 | .End(); 37 | 38 | _entitas = new GameContext(); 39 | _entitasGroup = _entitas.GetGroup(GameMatcher.AllOf( 40 | GameMatcher.PerfTestEntitasPosition, 41 | GameMatcher.PerfTestEntitasVelocity)); 42 | 43 | var leoPosPool = _leo.GetPool(); 44 | var leoVelocityPool = _leo.GetPool(); 45 | 46 | var random = new Random(); 47 | for (var i = 0; i < EntitiesCount; i++) 48 | { 49 | var position = new Vector2(random.Next(), random.Next()); 50 | var velocity = new Vector2(random.Next(), random.Next()); 51 | 52 | var urumiEntity = _urumi.CreateEntity(); 53 | urumiPosition.Set(urumiEntity, position); 54 | 55 | var leo = _leo.NewEntity(); 56 | ref var leoPos = ref leoPosPool.Add(leo); 57 | leoPos.Value = position; 58 | 59 | var ent = _entitas.CreateEntity(); 60 | ent.AddPerfTestEntitasPosition(position); 61 | 62 | if (i % 2 == 0) 63 | { 64 | urumiVelocity.Set(urumiEntity, velocity); 65 | ref var leoVelocity = ref leoVelocityPool.Add(leo); 66 | leoVelocity.Value = velocity; 67 | ent.AddPerfTestEntitasVelocity(velocity); 68 | } 69 | } 70 | } 71 | 72 | [Benchmark] 73 | public int Urumi() 74 | { 75 | var stub = 0; 76 | var cmp = _urumi.Registry; 77 | var positionCmp = cmp.Position; 78 | var velocityCmp = cmp.Velocity; 79 | 80 | foreach (var entity in _urumiGroup) 81 | { 82 | ref var velocity = ref entity.GetRef(velocityCmp); 83 | ref var position = ref entity.GetRef(positionCmp); 84 | position += velocity; 85 | stub += (int) velocity.X; 86 | } 87 | 88 | return stub; 89 | } 90 | 91 | [Benchmark] 92 | public int Leo() 93 | { 94 | var stub = 0; 95 | 96 | var leoPosPool = _leo.GetPool(); 97 | var leoVelocityPool = _leo.GetPool(); 98 | 99 | foreach (var entity in _leoFilter) 100 | { 101 | ref var velocity = ref leoVelocityPool.Get(entity); 102 | ref var position = ref leoPosPool.Get(entity); 103 | position.Value += velocity.Value; 104 | stub += (int) velocity.Value.X; 105 | } 106 | 107 | return stub; 108 | } 109 | 110 | [Benchmark] 111 | public int Entitas() 112 | { 113 | var stub = 0; 114 | foreach (var entity in _entitasGroup) 115 | { 116 | var velocity = entity.perfTestEntitasVelocity.Value; 117 | var positionCmp = entity.perfTestEntitasPosition; 118 | positionCmp.Value += velocity; 119 | stub += (int) velocity.X; 120 | } 121 | 122 | return stub; 123 | } 124 | 125 | private class UrumiRegistry : Registry 126 | { 127 | public UrumiPosition Position; 128 | public UrumiVelocity Velocity; 129 | } 130 | 131 | private class UrumiPosition : Component.Of 132 | { 133 | } 134 | 135 | private class UrumiVelocity : Component.Of 136 | { 137 | } 138 | 139 | private struct LeoPosition 140 | { 141 | public Vector2 Value; 142 | } 143 | 144 | private struct LeoVelocity 145 | { 146 | public Vector2 Value; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/Bench/AddRemoveBench.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using BenchmarkDotNet.Attributes; 4 | using Leopotam.EcsLite; 5 | 6 | namespace NUrumi.Benchmark.Bench 7 | { 8 | public class AddRemoveBench 9 | { 10 | private const int EntitiesCount = 100000; 11 | 12 | [Benchmark] 13 | public void Urumi() 14 | { 15 | var context = new Context(); 16 | var urumiOnlyPos = context.CreateGroup(GroupFilter 17 | .Include(context.Registry.Position) 18 | .Exclude(context.Registry.Velocity)); 19 | 20 | var urumiPosAndVel = context.CreateGroup(GroupFilter 21 | .Include(context.Registry.Position) 22 | .Include(context.Registry.Velocity)); 23 | 24 | var positionComponent = context.Registry.Position; 25 | var velocityComponent = context.Registry.Velocity; 26 | var position = positionComponent.Value; 27 | var velocity = velocityComponent.Value; 28 | 29 | for (var i = 0; i < EntitiesCount; i++) 30 | { 31 | var entity = context.CreateEntity(); 32 | entity.Set(position, Vector2.One); 33 | if (i % 2 == 0) 34 | { 35 | entity.Set(velocity, Vector2.Zero); 36 | } 37 | } 38 | 39 | foreach (var entity in urumiOnlyPos) 40 | { 41 | entity.Set(velocity, Vector2.Zero); 42 | } 43 | 44 | foreach (var entity in urumiPosAndVel) 45 | { 46 | context.RemoveEntity(entity); 47 | } 48 | } 49 | 50 | [Benchmark] 51 | public void Leo() 52 | { 53 | var leo = new EcsWorld(); 54 | var leoOnlyPos = leo 55 | .Filter() 56 | .Exc() 57 | .End(); 58 | 59 | var leoPosAndVel = leo 60 | .Filter() 61 | .Inc() 62 | .End(); 63 | 64 | var position = leo.GetPool(); 65 | var velocity = leo.GetPool(); 66 | 67 | for (var i = 0; i < EntitiesCount; i++) 68 | { 69 | var entity = leo.NewEntity(); 70 | ref var pos = ref position.Add(entity); 71 | pos.Value = Vector2.One; 72 | 73 | if (i % 2 == 0) 74 | { 75 | ref var vel = ref velocity.Add(entity); 76 | vel.Value = Vector2.Zero; 77 | } 78 | } 79 | 80 | foreach (var entity in leoOnlyPos) 81 | { 82 | ref var vel = ref velocity.Add(entity); 83 | vel.Value = Vector2.Zero; 84 | } 85 | 86 | foreach (var entity in leoPosAndVel) 87 | { 88 | leo.DelEntity(entity); 89 | } 90 | } 91 | 92 | private readonly List _iter = new List(); 93 | 94 | [Benchmark] 95 | public void Entitas() 96 | { 97 | var context = new GameContext(); 98 | var entitasOnlyPos = context.GetGroup( 99 | GameMatcher 100 | .AllOf(GameMatcher.PerfTestEntitasPosition) 101 | .NoneOf(GameMatcher.PerfTestEntitasVelocity)); 102 | 103 | var entitasPosAndVel = context.GetGroup(GameMatcher.AllOf( 104 | GameMatcher.PerfTestEntitasPosition, 105 | GameMatcher.PerfTestEntitasVelocity)); 106 | 107 | for (var i = 0; i < EntitiesCount; i++) 108 | { 109 | var entity = context.CreateEntity(); 110 | entity.AddPerfTestEntitasPosition(Vector2.One); 111 | if (i % 2 == 0) 112 | { 113 | entity.AddPerfTestEntitasVelocity(Vector2.Zero); 114 | } 115 | } 116 | 117 | entitasOnlyPos.GetEntities(_iter); 118 | foreach (var entity in _iter) 119 | { 120 | entity.AddPerfTestEntitasVelocity(Vector2.Zero); 121 | } 122 | 123 | entitasPosAndVel.GetEntities(_iter); 124 | foreach (var entity in _iter) 125 | { 126 | entity.Destroy(); 127 | } 128 | } 129 | 130 | private class UrumiRegistry : Registry 131 | { 132 | public UrumiPosition Position; 133 | public UrumiVelocity Velocity; 134 | } 135 | 136 | private class UrumiPosition : Component 137 | { 138 | public Field Value; 139 | } 140 | 141 | private class UrumiVelocity : Component 142 | { 143 | public Field Value; 144 | } 145 | 146 | private struct LeoPosition 147 | { 148 | public Vector2 Value; 149 | } 150 | 151 | private struct LeoVelocity 152 | { 153 | public Vector2 Value; 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /NUrumi.Test/CollectorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using FluentAssertions; 4 | using NUnit.Framework; 5 | using NUrumi.Test.Model; 6 | 7 | namespace NUrumi.Test 8 | { 9 | [TestFixture] 10 | public class CollectorTests 11 | { 12 | [Test] 13 | public void CollectorTest() 14 | { 15 | var context = new Context(); 16 | var position = context.Registry.Position; 17 | var velocity = context.Registry.Velocity; 18 | var health = context.Registry.Health.Value; 19 | 20 | var rnd = new Random(); 21 | 22 | Vector2 RandomVector() 23 | { 24 | return new Vector2(rnd.Next(), rnd.Next()); 25 | } 26 | 27 | var group = context.CreateGroup(GroupFilter.Include(position).Include(velocity)); 28 | var entityForCheckRemove = context 29 | .CreateEntity() 30 | .Set(position, RandomVector()) 31 | .Set(velocity, RandomVector()); 32 | 33 | var collector = context 34 | .CreateCollector() 35 | .WatchEntitiesAddedTo(group) 36 | .WatchEntitiesRemovedFrom(group) 37 | .WatchChangesOf(health); 38 | 39 | var entity1 = context.CreateEntity(); 40 | var entity2 = context.CreateEntity(); 41 | var entity3 = context.CreateEntity(); 42 | var entity4 = context.CreateEntity(); 43 | 44 | // Just created collector should not has any entity 45 | // ------------------------------------------------ 46 | collector.Count.Should().Be(0); 47 | collector.Has(entity1).Should().BeFalse(); 48 | collector.Has(entity2).Should().BeFalse(); 49 | collector.Has(entity3).Should().BeFalse(); 50 | collector.Has(entity4).Should().BeFalse(); 51 | collector.Has(entityForCheckRemove).Should().BeFalse(); 52 | 53 | // Collector should catch entity when it added to group 54 | // ---------------------------------------------------- 55 | entity1.Set(position, RandomVector()); 56 | collector.Count.Should().Be(0); 57 | collector.Has(entity1).Should().BeFalse(); 58 | 59 | entity1.Set(velocity, RandomVector()); 60 | collector.Count.Should().Be(1); 61 | collector.Has(entity1).Should().BeTrue(); 62 | 63 | // Collector should not register changes of same entity twice 64 | // ---------------------------------------------------------- 65 | entity1.Set(health, rnd.Next()); 66 | collector.Count.Should().Be(1); 67 | collector.Has(entity1).Should().BeTrue(); 68 | 69 | // Collector should catch entity when specified field value were changed 70 | // --------------------------------------------------------------------- 71 | entity2.Set(health, rnd.Next()); 72 | collector.Count.Should().Be(2); 73 | collector.Has(entity1).Should().BeTrue(); 74 | collector.Has(entity2).Should().BeTrue(); 75 | collector.Has(entityForCheckRemove).Should().BeFalse(); 76 | 77 | // Collector should catch entity which removed from specified group 78 | // ---------------------------------------------------------------- 79 | entityForCheckRemove.Remove(position); 80 | collector.Count.Should().Be(3); 81 | collector.Has(entity1).Should().BeTrue(); 82 | collector.Has(entity2).Should().BeTrue(); 83 | collector.Has(entityForCheckRemove).Should().BeTrue(); 84 | 85 | // After clearing collector should be empty 86 | // ---------------------------------------- 87 | collector.Clear(); 88 | collector.Count.Should().Be(0); 89 | collector.Has(entity1).Should().BeFalse(); 90 | collector.Has(entity2).Should().BeFalse(); 91 | 92 | // If value of entity not changed it should not be tracked 93 | // ------------------------------------------------------- 94 | var prev = entity2.Get(health); 95 | entity2.Set(health, prev); 96 | collector.Count.Should().Be(0); 97 | collector.Has(entity1).Should().BeFalse(); 98 | collector.Has(entity2).Should().BeFalse(); 99 | 100 | // Otherwise it should be tracked 101 | // ------------------------------------------------------- 102 | entity2.Set(health, prev + 1); 103 | collector.Count.Should().Be(1); 104 | collector.Has(entity1).Should().BeFalse(); 105 | collector.Has(entity2).Should().BeTrue(); 106 | 107 | // After disposing collector should not track any changes 108 | // ------------------------------------------------------ 109 | collector.Dispose(); 110 | 111 | entity3.Set(position, RandomVector()); 112 | entity3.Set(velocity, RandomVector()); 113 | entity4.Set(health, rnd.Next()); 114 | 115 | collector.Count.Should().Be(0); 116 | collector.Has(entity1).Should().BeFalse(); 117 | collector.Has(entity2).Should().BeFalse(); 118 | collector.Has(entity3).Should().BeFalse(); 119 | collector.Has(entity4).Should().BeFalse(); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /NUrumi/ReactiveField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace NUrumi 5 | { 6 | /// 7 | /// Represents a special kind of field which can be used to track self changes. 8 | /// 9 | /// 10 | public class ReactiveField : IField where TValue : unmanaged, IEquatable 11 | { 12 | private int _index; 13 | private int _offset; 14 | private string _name; 15 | private ComponentStorageData _storage; 16 | 17 | /// 18 | /// Describes an event handler of reactive field value changes. 19 | /// 20 | /// A component which field value was changed 21 | /// A field which value was changed 22 | /// An identifier of an entity which value was changed 23 | public delegate void OnReactiveFieldValueChangedEventHandler( 24 | IComponent component, 25 | IField field, 26 | int entityId, 27 | TValue? oldValue, 28 | TValue newValue); 29 | 30 | /// 31 | /// An event which raises when field value changes. 32 | /// 33 | public event OnReactiveFieldValueChangedEventHandler OnValueChanged; 34 | 35 | /// 36 | /// An zero-based index of the field in the component. 37 | /// 38 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 39 | // Use internal fields for performance reasons 40 | public int Index => _index; 41 | 42 | /// 43 | /// An data offset of the field in the component in bytes. 44 | /// 45 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 46 | // Use internal fields for performance reasons 47 | public int Offset => _offset; 48 | 49 | /// 50 | /// A name of this field. 51 | /// 52 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 53 | // Use internal fields for performance reasons 54 | public string Name => _name; 55 | 56 | /// 57 | /// A size of this field value in bytes. 58 | /// 59 | public int ValueSize 60 | { 61 | get 62 | { 63 | unsafe 64 | { 65 | return sizeof(TValue); 66 | } 67 | } 68 | } 69 | 70 | /// 71 | /// Returns this field value from entity with specified index. 72 | /// 73 | /// An entity identity. 74 | /// A value of this field in entity. 75 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 | public TValue Get(int entityId) 77 | { 78 | return _storage.Get(entityId, _offset); 79 | } 80 | 81 | /// 82 | /// Try to get this field value from entity with specified index. 83 | /// 84 | /// An entity identity. 85 | /// A field value if exists. 86 | /// Returns true if value exists, otherwise false. 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | public bool TryGet(int entityId, out TValue result) 89 | { 90 | return _storage.TryGet(entityId, _offset, out result); 91 | } 92 | 93 | /// 94 | /// Sets field value to entity with specified index. 95 | /// 96 | /// An entity identity. 97 | /// A value to set. 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | public void Set(int entityId, TValue value) 100 | { 101 | TValue? oldValue = default; 102 | if (_storage.TryGet(entityId, _offset, out var prev)) 103 | { 104 | if (prev.Equals(value)) 105 | { 106 | return; 107 | } 108 | 109 | oldValue = prev; 110 | } 111 | 112 | _storage.Set(entityId, _offset, value); 113 | 114 | RaiseValueChanged(entityId, oldValue, value); 115 | } 116 | 117 | private void RaiseValueChanged(int entityId, TValue? oldValue, TValue newValue) 118 | { 119 | var handler = OnValueChanged; 120 | handler?.Invoke(_storage.Component, this, entityId, oldValue, newValue); 121 | } 122 | 123 | void IField.Init(string name, int fieldIndex, int fieldOffset, ComponentStorageData storage) 124 | { 125 | _name = name; 126 | _index = fieldIndex; 127 | _offset = fieldOffset; 128 | _storage = storage; 129 | } 130 | } 131 | 132 | public static class ReactiveFieldCompanion 133 | { 134 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 135 | public static bool TryGet( 136 | this int entityId, 137 | ReactiveField field, 138 | out TValue value) 139 | where TValue : unmanaged, IEquatable 140 | { 141 | return field.TryGet(entityId, out value); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /NUrumi/RefField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace NUrumi 5 | { 6 | /// 7 | /// Represents a field for store class type values. 8 | /// It less efficient than working with struct types but can be useful in some cases. 9 | /// 10 | /// 11 | public class RefField : 12 | IField, 13 | IUpdateCallback, 14 | IContextResizeCallback 15 | where TValue : class 16 | { 17 | private int _index; 18 | private int _offset; 19 | private string _name; 20 | private ComponentStorageData _storage; 21 | 22 | private TValue[] _values = new TValue[100]; 23 | private int?[] _entities; 24 | private int _valuesCount; 25 | private int[] _freeValues = new int[100]; 26 | private int _freeValuesCount; 27 | 28 | /// 29 | /// An zero-based index of the field in the component. 30 | /// 31 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 32 | // Use internal fields for performance reasons 33 | public int Index => _index; 34 | 35 | /// 36 | /// An data offset of the field in the component in bytes. 37 | /// 38 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 39 | // Use internal fields for performance reasons 40 | public int Offset => _offset; 41 | 42 | /// 43 | /// A name of this field. 44 | /// 45 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 46 | // Use internal fields for performance reasons 47 | public string Name => _name; 48 | 49 | /// 50 | /// A size of this field value in bytes. 51 | /// 52 | public int ValueSize => sizeof(int); 53 | 54 | /// 55 | /// Returns this field value from entity with specified index. 56 | /// 57 | /// An entity identity. 58 | /// A value of this field in entity. 59 | public TValue Get(int entityIndex) 60 | { 61 | var ix = _storage.Get(entityIndex, _offset); 62 | return _values[ix]; 63 | } 64 | 65 | /// 66 | /// Try to get this field value from entity with specified index. 67 | /// 68 | /// An entity identity. 69 | /// A field value if exists. 70 | /// Returns true if value exists, otherwise false. 71 | public bool TryGet(int entityIndex, out TValue result) 72 | { 73 | if (!_storage.TryGet(entityIndex, _offset, out var ix)) 74 | { 75 | result = default; 76 | return false; 77 | } 78 | 79 | result = _values[ix]; 80 | return true; 81 | } 82 | 83 | public void Set(int entityIndex, TValue value) 84 | { 85 | int ix; 86 | if (_freeValuesCount > 0) 87 | { 88 | var freeIx = _freeValuesCount - 1; 89 | _freeValuesCount = freeIx; 90 | ix = _freeValues[freeIx]; 91 | } 92 | else 93 | { 94 | ix = _valuesCount; 95 | _valuesCount++; 96 | if (ix == _values.Length) 97 | { 98 | Array.Resize(ref _values, _valuesCount << 1); 99 | } 100 | } 101 | 102 | _values[ix] = value; 103 | _entities[entityIndex] = ix; 104 | _storage.Set(entityIndex, _offset, ix); 105 | } 106 | 107 | void IField.Init(string name, int fieldIndex, int fieldOffset, ComponentStorageData storage) 108 | { 109 | _name = name; 110 | _index = fieldIndex; 111 | _offset = fieldOffset; 112 | _entities = new int?[storage.EntitiesCapacity]; 113 | _storage = storage; 114 | _storage.AddUpdateCallback(this); 115 | } 116 | 117 | void IUpdateCallback.BeforeChange(int entityIndex, bool added) 118 | { 119 | } 120 | 121 | void IUpdateCallback.AfterChange(int entityIndex, bool added) 122 | { 123 | if (added) 124 | { 125 | return; 126 | } 127 | 128 | var optValueIndex = _entities[entityIndex]; 129 | if (!optValueIndex.HasValue) 130 | { 131 | return; 132 | } 133 | 134 | var ix = optValueIndex.Value; 135 | _values[ix] = default; 136 | 137 | var freeIx = _freeValuesCount; 138 | _freeValuesCount++; 139 | if (freeIx == _freeValues.Length) 140 | { 141 | Array.Resize(ref _freeValues, freeIx << 1); 142 | } 143 | 144 | _freeValues[freeIx] = ix; 145 | } 146 | 147 | void IContextResizeCallback.ResizeEntities(int newSize) 148 | { 149 | Array.Resize(ref _entities, newSize); 150 | } 151 | } 152 | 153 | public static class RefFieldCompanion 154 | { 155 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 156 | public static bool TryGet( 157 | this int entityId, 158 | RefField field, 159 | out TValue value) where TValue : class 160 | { 161 | return field.TryGet(entityId, out value); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /NUrumi.Test/UnsafeComponentStorageTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | using NUrumi.Exceptions; 4 | 5 | namespace NUrumi.Test 6 | { 7 | [TestFixture] 8 | public class UnsafeComponentStorageTest 9 | { 10 | public class TestComponent : Component 11 | { 12 | public Field Field1; 13 | public Field Field2; 14 | } 15 | 16 | public class TestRegistry : Registry 17 | { 18 | public TestComponent Test; 19 | } 20 | 21 | [Test] 22 | public void CommonScenario() 23 | { 24 | var context = new Context(); 25 | 26 | var storage = context.Registry.Test.Storage; 27 | var field1 = context.Registry.Test.Field1.Offset; 28 | var field2 = context.Registry.Test.Field2.Offset; 29 | var entity1 = 0; 30 | var entity2 = 1; 31 | var entity3 = 3; 32 | 33 | field1.Should().Be(ComponentStorageData.ReservedSize + 0); 34 | field2.Should().Be(ComponentStorageData.ReservedSize + 4); 35 | 36 | storage.Has(entity1).Should().BeFalse(); 37 | storage.EntitiesCount.Should().Be(0); 38 | 39 | // Set values for 1st entity 40 | storage.Set(entity1, field1, 1); 41 | storage.Has(entity1).Should().BeTrue(); 42 | storage.Get(entity1, field1).Should().Be(1); 43 | storage.EntitiesCount.Should().Be(1); 44 | 45 | storage.Set(entity1, field2, 2); 46 | storage.Has(entity1).Should().BeTrue(); 47 | storage.Get(entity1, field2).Should().Be(2); 48 | storage.EntitiesCount.Should().Be(1); 49 | 50 | // Set values for 2nd entity 51 | storage.Set(entity2, field1, 3); 52 | storage.Has(entity2).Should().BeTrue(); 53 | storage.Get(entity2, field1).Should().Be(3); 54 | storage.EntitiesCount.Should().Be(2); 55 | 56 | storage.Set(entity2, field2, 4); 57 | storage.Has(entity2).Should().BeTrue(); 58 | storage.Get(entity2, field2).Should().Be(4); 59 | storage.EntitiesCount.Should().Be(2); 60 | 61 | // Ensure that 1st entity values does not affected 62 | storage.Get(entity1, field1).Should().Be(1); 63 | storage.Get(entity1, field2).Should().Be(2); 64 | 65 | // Remove 1st entity 66 | storage.Remove(entity1).Should().BeTrue(); 67 | storage.Has(entity1).Should().BeFalse(); 68 | storage.Has(entity2).Should().BeTrue(); 69 | storage.EntitiesCount.Should().Be(1); 70 | 71 | storage.Get(entity2, field1).Should().Be(3); 72 | storage.Get(entity2, field2).Should().Be(4); 73 | 74 | // Use recycled record to new entity 75 | storage.Set(entity3, field1, 5); 76 | storage.Set(entity3, field2, 6); 77 | storage.Has(entity3).Should().BeTrue(); 78 | storage.Get(entity3, field1).Should().Be(5); 79 | storage.Get(entity3, field2).Should().Be(6); 80 | storage.EntitiesCount.Should().Be(2); 81 | 82 | storage.Has(entity1).Should().BeFalse(); 83 | storage.Has(entity2).Should().BeTrue(); 84 | storage.Get(entity2, field1).Should().Be(3); 85 | storage.Get(entity2, field2).Should().Be(4); 86 | 87 | // Set value for 1st entity again 88 | storage.Set(entity1, field1, 7); 89 | storage.Set(entity1, field2, 8); 90 | 91 | storage.Has(entity1).Should().BeTrue(); 92 | storage.Has(entity2).Should().BeTrue(); 93 | storage.Has(entity3).Should().BeTrue(); 94 | 95 | storage.Get(entity1, field1).Should().Be(7); 96 | storage.Get(entity1, field2).Should().Be(8); 97 | 98 | storage.Get(entity2, field1).Should().Be(3); 99 | storage.Get(entity2, field2).Should().Be(4); 100 | 101 | storage.Get(entity3, field1).Should().Be(5); 102 | storage.Get(entity3, field2).Should().Be(6); 103 | 104 | storage.EntitiesCount.Should().Be(3); 105 | 106 | // Remove all entities 107 | storage.Remove(entity1).Should().BeTrue(); 108 | storage.EntitiesCount.Should().Be(2); 109 | 110 | storage.Remove(entity2).Should().BeTrue(); 111 | storage.EntitiesCount.Should().Be(1); 112 | 113 | storage.Remove(entity3).Should().BeTrue(); 114 | storage.EntitiesCount.Should().Be(0); 115 | 116 | storage.Has(entity1).Should().BeFalse(); 117 | storage.Has(entity2).Should().BeFalse(); 118 | storage.Has(entity3).Should().BeFalse(); 119 | 120 | storage 121 | .Invoking(s => s.Get(entity1, field1)) 122 | .Should().Throw(); 123 | 124 | storage 125 | .Invoking(s => s.Get(entity1, field2)) 126 | .Should().Throw(); 127 | 128 | storage 129 | .Invoking(s => s.Get(entity2, field1)) 130 | .Should().Throw(); 131 | 132 | storage 133 | .Invoking(s => s.Get(entity2, field2)) 134 | .Should().Throw(); 135 | 136 | storage 137 | .Invoking(s => s.Get(entity3, field1)) 138 | .Should().Throw(); 139 | 140 | storage 141 | .Invoking(s => s.Get(entity3, field2)) 142 | .Should().Throw(); 143 | 144 | storage.EntitiesCount.Should().Be(0); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /NUrumi/IndexField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace NUrumi 6 | { 7 | /// 8 | /// Represents a field that indexes the entities by it value. 9 | /// 10 | /// A type of indexed value. 11 | // ReSharper disable once ClassNeverInstantiated.Global 12 | public sealed class IndexField : 13 | IField, 14 | IUpdateCallback, 15 | IContextResizeCallback 16 | where TValue : unmanaged, IEquatable 17 | { 18 | private readonly Dictionary _entities = new Dictionary(); 19 | private TValue?[] _currentValues; 20 | 21 | private int _index; 22 | private int _offset; 23 | private string _name; 24 | private ComponentStorageData _storage; 25 | 26 | /// 27 | /// An zero-based index of the field in the component. 28 | /// 29 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 30 | // Use internal fields for performance reasons 31 | public int Index => _index; 32 | 33 | /// 34 | /// An data offset of the field in the component in bytes. 35 | /// 36 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 37 | // Use internal fields for performance reasons 38 | public int Offset => _offset; 39 | 40 | /// 41 | /// A name of this field. 42 | /// 43 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 44 | // Use internal fields for performance reasons 45 | public string Name => _name; 46 | 47 | /// 48 | /// A size of this field value in bytes. 49 | /// 50 | public int ValueSize 51 | { 52 | get 53 | { 54 | unsafe 55 | { 56 | return sizeof(TValue); 57 | } 58 | } 59 | } 60 | 61 | /// 62 | /// Returns this field value from entity with specified index. 63 | /// 64 | /// An entity identity. 65 | /// A value of this field in entity. 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public TValue Get(int entityId) 68 | { 69 | return _storage.Get(entityId, _offset); 70 | } 71 | 72 | /// 73 | /// Try to get this field value from entity with specified index. 74 | /// 75 | /// An entity identity. 76 | /// A field value if exists. 77 | /// Returns true if value exists, otherwise false. 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public bool TryGet(int entityId, out TValue result) 80 | { 81 | return _storage.TryGet(entityId, _offset, out result); 82 | } 83 | 84 | /// 85 | /// Sets field value to entity with specified index. 86 | /// 87 | /// An entity identity. 88 | /// A value to set. 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public void Set(int entityId, TValue value) 91 | { 92 | if (_storage.TryGet(entityId, _offset, out var prev)) 93 | { 94 | if (prev.Equals(value)) 95 | { 96 | return; 97 | } 98 | 99 | _entities[prev].Remove(entityId); 100 | } 101 | 102 | _storage.Set(entityId, _offset, value); 103 | _currentValues[entityId] = value; 104 | 105 | if (!_entities.TryGetValue(value, out var index)) 106 | { 107 | index = new EntitiesSet(_storage.EntitiesCapacity); 108 | _entities.Add(value, index); 109 | } 110 | 111 | index.Add(entityId); 112 | } 113 | 114 | /// 115 | /// Returns an enumerator of entities associated with the specified value. 116 | /// 117 | /// A value to search. 118 | /// An enumerator of entities associated with specified value. 119 | public EntitiesSet GetEntitiesAssociatedWith(TValue value) 120 | { 121 | return _entities.TryGetValue(value, out var index) ? index : EntitiesSet.Empty; 122 | } 123 | 124 | void IField.Init(string name, int fieldIndex, int fieldOffset, ComponentStorageData storage) 125 | { 126 | _name = name; 127 | _index = fieldIndex; 128 | _offset = fieldOffset; 129 | _currentValues = new TValue?[storage.EntitiesCapacity]; 130 | _storage = storage; 131 | _storage.AddUpdateCallback(this); 132 | } 133 | 134 | void IUpdateCallback.BeforeChange(int entityIndex, bool added) 135 | { 136 | } 137 | 138 | void IUpdateCallback.AfterChange(int entityId, bool added) 139 | { 140 | if (added) 141 | { 142 | return; 143 | } 144 | 145 | var currValue = _currentValues[entityId]; 146 | if (!currValue.HasValue) 147 | { 148 | return; 149 | } 150 | 151 | _entities[currValue.Value].Remove(entityId); 152 | } 153 | 154 | void IContextResizeCallback.ResizeEntities(int newSize) 155 | { 156 | Array.Resize(ref _currentValues, newSize); 157 | foreach (var entitiesSet in _entities.Values) 158 | { 159 | entitiesSet.ResizeEntities(newSize); 160 | } 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /NUrumi/EntitiesSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using NUrumi.Exceptions; 4 | 5 | namespace NUrumi 6 | { 7 | public sealed class EntitiesSet 8 | { 9 | public const int Deferred = -1; 10 | public const int AppliedEarly = 0; 11 | public const int Applied = 1; 12 | 13 | public static readonly EntitiesSet Empty = new EntitiesSet(0); 14 | 15 | private int[] _entitiesIndex; 16 | private int[] _denseEntities; 17 | private int _entitiesCount; 18 | 19 | private int[] _deferredOperations; 20 | private int _deferredOperationsCount; 21 | private int _locksCount; 22 | 23 | public EntitiesSet(int entitiesCapacity) 24 | { 25 | _entitiesIndex = new int[entitiesCapacity]; 26 | _denseEntities = new int[entitiesCapacity]; 27 | _entitiesCount = 0; 28 | 29 | _deferredOperations = new int[100]; 30 | _deferredOperationsCount = 0; 31 | _locksCount = 0; 32 | } 33 | 34 | public int EntitiesCount 35 | { 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | get => _entitiesCount; 38 | } 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public int GetEntities(ref int[] result) 42 | { 43 | var entitiesCount = _entitiesCount; 44 | if (result == null) 45 | { 46 | result = new int[entitiesCount]; 47 | } 48 | else if (result.Length < entitiesCount) 49 | { 50 | Array.Resize(ref result, entitiesCount); 51 | } 52 | 53 | Array.Copy(_denseEntities, result, entitiesCount); 54 | return entitiesCount; 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public Enumerator GetEnumerator() 59 | { 60 | _locksCount += 1; 61 | return new Enumerator(this); 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | internal void ResizeEntities(int newSize) 66 | { 67 | Array.Resize(ref _entitiesIndex, newSize); 68 | } 69 | 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | public int Add(int entityIndex) 72 | { 73 | return AddDeferredOperation(false, entityIndex) 74 | ? Deferred 75 | : AddWithoutLockChecks(entityIndex); 76 | } 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public int Remove(int entityIndex) 80 | { 81 | return AddDeferredOperation(false, entityIndex) 82 | ? Deferred 83 | : RemoveWithoutLockChecks(entityIndex); 84 | } 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | public bool Has(int entityIndex) 88 | { 89 | return _entitiesIndex[entityIndex] != 0; 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | public void Clear() 94 | { 95 | if (_locksCount > 0) 96 | { 97 | throw new NUrumiException("Can't clear when entity set is locked."); 98 | } 99 | 100 | for (var i = 0; i < _entitiesCount; i++) 101 | { 102 | var ix = _denseEntities[i]; 103 | _entitiesIndex[ix] = 0; 104 | } 105 | 106 | _entitiesCount = 0; 107 | } 108 | 109 | private int AddWithoutLockChecks(int entityIndex) 110 | { 111 | var denseIndex = _entitiesIndex[entityIndex]; 112 | if (denseIndex != 0) 113 | { 114 | // Already added 115 | return AppliedEarly; 116 | } 117 | 118 | denseIndex = _entitiesCount; 119 | if (denseIndex == _denseEntities.Length) 120 | { 121 | Array.Resize(ref _denseEntities, _entitiesCount << 1); 122 | } 123 | 124 | _entitiesIndex[entityIndex] = denseIndex + 1; 125 | _denseEntities[denseIndex] = entityIndex; 126 | _entitiesCount += 1; 127 | 128 | return Applied; 129 | } 130 | 131 | private int RemoveWithoutLockChecks(int entityIndex) 132 | { 133 | var denseIndex = _entitiesIndex[entityIndex]; 134 | if (denseIndex == 0) 135 | { 136 | // Already removed` 137 | return AppliedEarly; 138 | } 139 | 140 | if (denseIndex == _entitiesCount) 141 | { 142 | _entitiesIndex[entityIndex] = 0; 143 | _entitiesCount -= 1; 144 | return Applied; 145 | } 146 | 147 | _entitiesCount -= 1; 148 | var lastIndex = _denseEntities[_entitiesCount]; 149 | _denseEntities[denseIndex - 1] = lastIndex; 150 | _entitiesIndex[lastIndex] = denseIndex; 151 | _entitiesIndex[entityIndex] = 0; 152 | 153 | return Applied; 154 | } 155 | 156 | private void Unlock() 157 | { 158 | _locksCount -= 1; 159 | if (_locksCount == 0 && _deferredOperationsCount > 0) 160 | { 161 | for (var i = 0; i < _deferredOperationsCount; i++) 162 | { 163 | ref var operation = ref _deferredOperations[i]; 164 | if (operation >= 0) 165 | { 166 | AddWithoutLockChecks(operation); 167 | } 168 | else 169 | { 170 | RemoveWithoutLockChecks(-(operation + 1)); 171 | } 172 | } 173 | 174 | _deferredOperationsCount = 0; 175 | } 176 | } 177 | 178 | private bool AddDeferredOperation(bool added, int entityIndex) 179 | { 180 | if (_locksCount == 0) 181 | { 182 | return false; 183 | } 184 | 185 | var ix = ++_deferredOperationsCount - 1; 186 | if (ix == _deferredOperations.Length) 187 | { 188 | Array.Resize(ref _deferredOperations, ix << 1); 189 | } 190 | 191 | _deferredOperations[ix] = added ? entityIndex : -entityIndex - 1; 192 | return true; 193 | } 194 | 195 | public struct Enumerator : IDisposable 196 | { 197 | private readonly EntitiesSet _set; 198 | private readonly int[] _entities; 199 | private readonly int _count; 200 | private int _idx; 201 | 202 | public Enumerator(EntitiesSet set) 203 | { 204 | _set = set; 205 | _entities = set._denseEntities; 206 | _count = set._entitiesCount; 207 | _idx = -1; 208 | } 209 | 210 | public int Current 211 | { 212 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 213 | get => _entities[_idx]; 214 | } 215 | 216 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 217 | public bool MoveNext() 218 | { 219 | return ++_idx < _count; 220 | } 221 | 222 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 223 | public void Dispose() 224 | { 225 | _set.Unlock(); 226 | } 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/LeoEcs/systems.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The Proprietary or MIT-Red License 3 | // Copyright (c) 2012-2022 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System.Collections.Generic; 7 | using System.Runtime.CompilerServices; 8 | 9 | #if ENABLE_IL2CPP 10 | using Unity.IL2CPP.CompilerServices; 11 | #endif 12 | 13 | namespace Leopotam.EcsLite { 14 | public interface IEcsSystem { } 15 | 16 | public interface IEcsPreInitSystem : IEcsSystem { 17 | void PreInit (EcsSystems systems); 18 | } 19 | 20 | public interface IEcsInitSystem : IEcsSystem { 21 | void Init (EcsSystems systems); 22 | } 23 | 24 | public interface IEcsRunSystem : IEcsSystem { 25 | void Run (EcsSystems systems); 26 | } 27 | 28 | public interface IEcsDestroySystem : IEcsSystem { 29 | void Destroy (EcsSystems systems); 30 | } 31 | 32 | public interface IEcsPostDestroySystem : IEcsSystem { 33 | void PostDestroy (EcsSystems systems); 34 | } 35 | 36 | #if ENABLE_IL2CPP 37 | [Il2CppSetOption (Option.NullChecks, false)] 38 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 39 | #endif 40 | public class EcsSystems { 41 | readonly EcsWorld _defaultWorld; 42 | readonly Dictionary _worlds; 43 | readonly List _allSystems; 44 | readonly object _shared; 45 | IEcsRunSystem[] _runSystems; 46 | int _runSystemsCount; 47 | 48 | public EcsSystems (EcsWorld defaultWorld, object shared = null) { 49 | _defaultWorld = defaultWorld; 50 | _shared = shared; 51 | _worlds = new Dictionary (32); 52 | _allSystems = new List (128); 53 | } 54 | 55 | public Dictionary GetAllNamedWorlds () { 56 | return _worlds; 57 | } 58 | 59 | public int GetAllSystems (ref IEcsSystem[] list) { 60 | var itemsCount = _allSystems.Count; 61 | if (itemsCount == 0) { return 0; } 62 | if (list == null || list.Length < itemsCount) { 63 | list = new IEcsSystem[_allSystems.Capacity]; 64 | } 65 | for (int i = 0, iMax = itemsCount; i < iMax; i++) { 66 | list[i] = _allSystems[i]; 67 | } 68 | return itemsCount; 69 | } 70 | 71 | public int GetRunSystems (ref IEcsRunSystem[] list) { 72 | var itemsCount = _runSystemsCount; 73 | if (itemsCount == 0) { return 0; } 74 | if (list == null || list.Length < itemsCount) { 75 | list = new IEcsRunSystem[_runSystems.Length]; 76 | } 77 | for (int i = 0, iMax = itemsCount; i < iMax; i++) { 78 | list[i] = _runSystems[i]; 79 | } 80 | return itemsCount; 81 | } 82 | 83 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 84 | public T GetShared () where T : class { 85 | return _shared as T; 86 | } 87 | 88 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 89 | public EcsWorld GetWorld (string name = null) { 90 | if (name == null) { 91 | return _defaultWorld; 92 | } 93 | _worlds.TryGetValue (name, out var world); 94 | return world; 95 | } 96 | 97 | public void Destroy () { 98 | for (var i = _allSystems.Count - 1; i >= 0; i--) { 99 | if (_allSystems[i] is IEcsDestroySystem destroySystem) { 100 | destroySystem.Destroy (this); 101 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 102 | var worldName = CheckForLeakedEntities (); 103 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {destroySystem.GetType ().Name}.Destroy()."); } 104 | #endif 105 | } 106 | } 107 | for (var i = _allSystems.Count - 1; i >= 0; i--) { 108 | if (_allSystems[i] is IEcsPostDestroySystem postDestroySystem) { 109 | postDestroySystem.PostDestroy (this); 110 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 111 | var worldName = CheckForLeakedEntities (); 112 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {postDestroySystem.GetType ().Name}.PostDestroy()."); } 113 | #endif 114 | } 115 | } 116 | _allSystems.Clear (); 117 | _runSystems = null; 118 | } 119 | 120 | public EcsSystems AddWorld (EcsWorld world, string name) { 121 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 122 | if (string.IsNullOrEmpty (name)) { throw new System.Exception ("World name cant be null or empty."); } 123 | #endif 124 | _worlds[name] = world; 125 | return this; 126 | } 127 | 128 | public EcsSystems Add (IEcsSystem system) { 129 | _allSystems.Add (system); 130 | if (system is IEcsRunSystem) { 131 | _runSystemsCount++; 132 | } 133 | return this; 134 | } 135 | 136 | public void Init () { 137 | if (_runSystemsCount > 0) { 138 | _runSystems = new IEcsRunSystem[_runSystemsCount]; 139 | } 140 | foreach (var system in _allSystems) { 141 | if (system is IEcsPreInitSystem initSystem) { 142 | initSystem.PreInit (this); 143 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 144 | var worldName = CheckForLeakedEntities (); 145 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {initSystem.GetType ().Name}.PreInit()."); } 146 | #endif 147 | } 148 | } 149 | var runIdx = 0; 150 | foreach (var system in _allSystems) { 151 | if (system is IEcsInitSystem initSystem) { 152 | initSystem.Init (this); 153 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 154 | var worldName = CheckForLeakedEntities (); 155 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {initSystem.GetType ().Name}.Init()."); } 156 | #endif 157 | } 158 | if (system is IEcsRunSystem runSystem) { 159 | _runSystems[runIdx++] = runSystem; 160 | } 161 | } 162 | } 163 | 164 | public void Run () { 165 | for (int i = 0, iMax = _runSystemsCount; i < iMax; i++) { 166 | _runSystems[i].Run (this); 167 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 168 | var worldName = CheckForLeakedEntities (); 169 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {_runSystems[i].GetType ().Name}.Run()."); } 170 | #endif 171 | } 172 | } 173 | 174 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 175 | public string CheckForLeakedEntities () { 176 | if (_defaultWorld.CheckForLeakedEntities ()) { return "default"; } 177 | foreach (var pair in _worlds) { 178 | if (pair.Value.CheckForLeakedEntities ()) { 179 | return pair.Key; 180 | } 181 | } 182 | return null; 183 | } 184 | #endif 185 | } 186 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Common IntelliJ Platform excludes 17 | .idea/ 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /NUrumi/Field.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace NUrumi 4 | { 5 | /// 6 | /// Represents a field of component which can effective operate with struct types. 7 | /// 8 | /// A type of component field. 9 | public sealed class Field : IField where TValue : unmanaged 10 | { 11 | private int _index; 12 | private int _offset; 13 | private string _name; 14 | private ComponentStorageData _storage; 15 | 16 | /// 17 | /// An zero-based index of the field in the component. 18 | /// 19 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 20 | // Use internal fields for performance reasons 21 | public int Index => _index; 22 | 23 | /// 24 | /// An data offset of the field in the component in bytes. 25 | /// 26 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 27 | // Use internal fields for performance reasons 28 | public int Offset => _offset; 29 | 30 | /// 31 | /// A name of this field. 32 | /// 33 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 34 | // Use internal fields for performance reasons 35 | public string Name => _name; 36 | 37 | /// 38 | /// A size of this field value in bytes. 39 | /// 40 | public int ValueSize 41 | { 42 | get 43 | { 44 | unsafe 45 | { 46 | return sizeof(TValue); 47 | } 48 | } 49 | } 50 | 51 | /// 52 | /// Returns this field value from entity with specified index. 53 | /// 54 | /// An entity identity. 55 | /// A value of this field in entity. 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public TValue Get(int entityId) 58 | { 59 | return _storage.Get(entityId, _offset); 60 | } 61 | 62 | /// 63 | /// Returns this field value from entity with specified index as reference. 64 | /// 65 | /// An entity identity. 66 | /// A value of this field in entity. 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public ref TValue GetRef(int entityId) 69 | { 70 | return ref _storage.Get(entityId, _offset); 71 | } 72 | 73 | /// 74 | /// Returns this field value from entity with specified index as reference. 75 | /// If value does not set then set it as default and returns it as a reference. 76 | /// 77 | /// An entity identity. 78 | /// A value of this field in entity. 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public ref TValue GetOrAdd(int entityId) 81 | { 82 | return ref _storage.GetOrSet(entityId, _offset); 83 | } 84 | 85 | /// 86 | /// Returns this field value from entity with specified index as reference. 87 | /// If value does not set then set it as default and returns it as a reference. 88 | /// 89 | /// An entity identity. 90 | /// A value which should be set if entity does not have value. 91 | /// A value of this field in entity. 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | public ref TValue GetOrSet(int entityId, TValue value) 94 | { 95 | return ref _storage.GetOrSet(entityId, _offset, value); 96 | } 97 | 98 | /// 99 | /// Try to get this field value from entity with specified index. 100 | /// 101 | /// An entity identity. 102 | /// A field value if exists. 103 | /// Returns true if value exists, otherwise false. 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public bool TryGet(int entityId, out TValue result) 106 | { 107 | return _storage.TryGet(entityId, _offset, out result); 108 | } 109 | 110 | /// 111 | /// Sets field value to entity with specified index. 112 | /// 113 | /// An entity identity. 114 | /// A value to set. 115 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 116 | public void Set(int entityId, TValue value) 117 | { 118 | _storage.Set(entityId, _offset, value); 119 | } 120 | 121 | void IField.Init(string name, int fieldIndex, int fieldOffset, ComponentStorageData storage) 122 | { 123 | _name = name; 124 | _index = fieldIndex; 125 | _offset = fieldOffset; 126 | _storage = storage; 127 | } 128 | } 129 | 130 | public static class FieldCompanion 131 | { 132 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 133 | public static TValue Get(this int entityId, IField field) where TValue : unmanaged 134 | { 135 | return field.Get(entityId); 136 | } 137 | 138 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 139 | public static TValue Get(this int entityId, RefField field) where TValue : class 140 | { 141 | return field.Get(entityId); 142 | } 143 | 144 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 145 | public static TValue Get(this int entityId, Component.OfRef component) 146 | where TComponent : Component, new() 147 | where TValue : class 148 | { 149 | return component.Get(entityId); 150 | } 151 | 152 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 153 | public static ref TValue GetRef(this int entityId, Field field) where TValue : unmanaged 154 | { 155 | return ref field.GetRef(entityId); 156 | } 157 | 158 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 159 | public static ref TValue GetRef( 160 | this int entityId, 161 | Component.Of component) 162 | where TComponent : Component, new() 163 | where TValue : unmanaged 164 | { 165 | return ref component.GetRef(entityId); 166 | } 167 | 168 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 169 | public static bool TryGet( 170 | this int entityId, 171 | Field field, 172 | out TValue value) 173 | where TValue : unmanaged 174 | { 175 | return field.TryGet(entityId, out value); 176 | } 177 | 178 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 179 | public static int Set( 180 | this int entityId, 181 | TField field, 182 | TValue value) 183 | where TField : IField 184 | { 185 | field.Set(entityId, value); 186 | return entityId; 187 | } 188 | } 189 | 190 | public interface IField : IField 191 | { 192 | TValue Get(int entityIndex); 193 | bool TryGet(int entityIndex, out TValue value); 194 | void Set(int entityIndex, TValue value); 195 | } 196 | 197 | public interface IField 198 | { 199 | int Index { get; } 200 | int Offset { get; } 201 | int ValueSize { get; } 202 | string Name { get; } 203 | void Init(string name, int fieldIndex, int fieldOffset, ComponentStorageData storage); 204 | } 205 | } -------------------------------------------------------------------------------- /NUrumi.Test/GroupTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using FluentAssertions; 4 | using NUnit.Framework; 5 | using NUrumi.Test.Model; 6 | 7 | namespace NUrumi.Test 8 | { 9 | [TestFixture] 10 | public class GroupTests 11 | { 12 | [Test] 13 | public void GroupsShouldCorrectManageEntities() 14 | { 15 | var context = new Context(); 16 | var position = context.Registry.Position; 17 | var velocity = context.Registry.Velocity; 18 | 19 | var groupPosition = context.CreateGroup(GroupFilter.Include(context.Registry.Position)); 20 | var groupVelocity = context.CreateGroup(GroupFilter.Include(context.Registry.Velocity)); 21 | var groupWithBoth = context.CreateGroup( 22 | GroupFilter 23 | .Include(context.Registry.Position) 24 | .Include(context.Registry.Velocity)); 25 | 26 | var entityWithPosition = context.CreateEntity(); 27 | position.Set(entityWithPosition, Vector2.One); 28 | groupWithBoth.EntitiesCount.Should().Be(0); 29 | 30 | var entityWithVelocity = context.CreateEntity(); 31 | velocity.Set(entityWithVelocity, Vector2.One); 32 | groupWithBoth.EntitiesCount.Should().Be(0); 33 | 34 | var entityWithBoth1 = context.CreateEntity(); 35 | position.Set(entityWithBoth1, Vector2.Zero); 36 | velocity.Set(entityWithBoth1, Vector2.One); 37 | groupWithBoth.EntitiesCount.Should().Be(1); 38 | 39 | var entityWithBoth2 = context.CreateEntity(); 40 | position.Set(entityWithBoth2, Vector2.One); 41 | velocity.Set(entityWithBoth2, Vector2.Zero); 42 | groupWithBoth.EntitiesCount.Should().Be(2); 43 | 44 | var entitiesInGroup = (int[]) null; 45 | var entitiesInGroupCount = groupWithBoth.GetEntities(ref entitiesInGroup); 46 | entitiesInGroupCount.Should().Be(2); 47 | entitiesInGroup.Length.Should().Be(2); 48 | entitiesInGroup[0].Should().Be(entityWithBoth1); 49 | entitiesInGroup[1].Should().Be(entityWithBoth2); 50 | 51 | context.Registry.Velocity.RemoveFrom(entityWithBoth1).Should().Be(true); 52 | groupWithBoth.EntitiesCount.Should().Be(1); 53 | 54 | context.Registry.Position.RemoveFrom(entityWithBoth2).Should().Be(true); 55 | groupWithBoth.EntitiesCount.Should().Be(0); 56 | 57 | groupPosition.EntitiesCount.Should().Be(2); 58 | groupPosition.GetEntities(ref entitiesInGroup); 59 | entitiesInGroup[0].Should().Be(entityWithPosition); 60 | entitiesInGroup[1].Should().Be(entityWithBoth1); 61 | 62 | groupVelocity.EntitiesCount.Should().Be(2); 63 | groupVelocity.GetEntities(ref entitiesInGroup); 64 | entitiesInGroup[0].Should().Be(entityWithVelocity); 65 | entitiesInGroup[1].Should().Be(entityWithBoth2); 66 | } 67 | 68 | [Test] 69 | public void GroupsShouldCorrectManageEntitiesWithDeferredOperations() 70 | { 71 | var context = new Context(); 72 | var positionComponent = context.Registry.Position; 73 | var position = context.Registry.Position; 74 | var velocity = context.Registry.Velocity; 75 | var group = context.CreateGroup( 76 | GroupFilter 77 | .Include(context.Registry.Position) 78 | .Include(context.Registry.Velocity)); 79 | 80 | var entitiesCount = 100; 81 | for (var i = 0; i < entitiesCount; i++) 82 | { 83 | var entity = context.CreateEntity(); 84 | entity.Set(position, Vector2.One); 85 | entity.Set(velocity, Vector2.Zero); 86 | group.EntitiesCount.Should().Be(i + 1); 87 | } 88 | 89 | var ix = 0; 90 | foreach (var entity in group) 91 | { 92 | if (ix % 2 == 0) 93 | { 94 | entity.Remove(positionComponent); 95 | } 96 | 97 | group.EntitiesCount.Should().Be(entitiesCount); 98 | ix += 1; 99 | } 100 | 101 | group.EntitiesCount.Should().Be(entitiesCount / 2); 102 | } 103 | 104 | [Test] 105 | public void GroupsShouldRaiseEventsWhenEntityAddedOrRemoved() 106 | { 107 | var context = new Context(); 108 | var position = context.Registry.Position; 109 | var velocity = context.Registry.Velocity; 110 | var group = context.CreateGroup(GroupFilter.Include(position).Include(velocity)); 111 | 112 | var changesCount = 0; 113 | var changedEntity = -1; 114 | var changeOperation = false; 115 | 116 | void TestAdd(int entityIndex, Vector2 newPosition, Vector2 newVelocity) 117 | { 118 | var changesBefore = changesCount; 119 | changeOperation = false; 120 | 121 | entityIndex.Set(position, newPosition); 122 | changesCount.Should().Be(changesBefore); 123 | entityIndex.Set(velocity, newVelocity); 124 | changesCount.Should().Be(changesBefore + 1); 125 | changedEntity.Should().Be(entityIndex); 126 | changeOperation.Should().BeTrue(); 127 | } 128 | 129 | void TestAddedEarly(int entityIndex, Vector2 newPosition, Vector2 newVelocity) 130 | { 131 | var changesBefore = changesCount; 132 | entityIndex.Set(position, newPosition); 133 | entityIndex.Set(velocity, newVelocity); 134 | changesCount.Should().Be(changesBefore); 135 | } 136 | 137 | void TestRemove(int entityIndex) 138 | { 139 | var changesBefore = changesCount; 140 | changeOperation = true; 141 | 142 | entityIndex.Remove(position); 143 | changesCount.Should().Be(changesBefore + 1); 144 | changesBefore = changesCount; 145 | 146 | changeOperation.Should().BeFalse(); 147 | entityIndex.Remove(velocity); 148 | changesCount.Should().Be(changesBefore); 149 | } 150 | 151 | var rnd = new Random(); 152 | 153 | Vector2 RandomVector() 154 | { 155 | return new Vector2(rnd.Next(), rnd.Next()); 156 | } 157 | 158 | group.OnGroupChanged += (entity, add) => 159 | { 160 | changesCount += 1; 161 | changedEntity = entity; 162 | changeOperation = add; 163 | }; 164 | 165 | var entity1 = context.CreateEntity(); 166 | var entity2 = context.CreateEntity(); 167 | var entity3 = context.CreateEntity(); 168 | 169 | TestAdd(entity1, RandomVector(), RandomVector()); 170 | TestAddedEarly(entity1, RandomVector(), RandomVector()); 171 | TestAddedEarly(entity1, RandomVector(), RandomVector()); 172 | 173 | TestAdd(entity2, RandomVector(), RandomVector()); 174 | TestAddedEarly(entity2, RandomVector(), RandomVector()); 175 | 176 | TestAdd(entity3, RandomVector(), RandomVector()); 177 | TestAddedEarly(entity3, RandomVector(), RandomVector()); 178 | 179 | TestRemove(entity2); 180 | TestAdd(entity2, RandomVector(), RandomVector()); 181 | TestAddedEarly(entity2, RandomVector(), RandomVector()); 182 | 183 | // Deferred 184 | var changesBeforeDeferred = changesCount; 185 | foreach (var entity in group) 186 | { 187 | entity.Remove(position); 188 | changesCount.Should().Be(changesBeforeDeferred); 189 | entity.Remove(velocity); 190 | changesCount.Should().Be(changesBeforeDeferred); 191 | } 192 | 193 | changesCount.Should().Be(changesBeforeDeferred + 3); 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/LeoEcs/filters.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The Proprietary or MIT-Red License 3 | // Copyright (c) 2012-2022 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System; 7 | using System.Runtime.CompilerServices; 8 | 9 | #if ENABLE_IL2CPP 10 | using Unity.IL2CPP.CompilerServices; 11 | #endif 12 | 13 | namespace Leopotam.EcsLite { 14 | #if LEOECSLITE_FILTER_EVENTS 15 | public interface IEcsFilterEventListener { 16 | void OnEntityAdded (int entity); 17 | void OnEntityRemoved (int entity); 18 | } 19 | #endif 20 | #if ENABLE_IL2CPP 21 | [Il2CppSetOption (Option.NullChecks, false)] 22 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 23 | #endif 24 | public sealed class EcsFilter { 25 | readonly EcsWorld _world; 26 | readonly EcsWorld.Mask _mask; 27 | int[] _denseEntities; 28 | int _entitiesCount; 29 | internal int[] SparseEntities; 30 | int _lockCount; 31 | DelayedOp[] _delayedOps; 32 | int _delayedOpsCount; 33 | #if LEOECSLITE_FILTER_EVENTS 34 | IEcsFilterEventListener[] _eventListeners = new IEcsFilterEventListener[4]; 35 | int _eventListenersCount; 36 | #endif 37 | 38 | internal EcsFilter (EcsWorld world, EcsWorld.Mask mask, int denseCapacity, int sparseCapacity) { 39 | _world = world; 40 | _mask = mask; 41 | _denseEntities = new int[denseCapacity]; 42 | SparseEntities = new int[sparseCapacity]; 43 | _entitiesCount = 0; 44 | _delayedOps = new DelayedOp[512]; 45 | _delayedOpsCount = 0; 46 | _lockCount = 0; 47 | } 48 | 49 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 50 | public EcsWorld GetWorld () { 51 | return _world; 52 | } 53 | 54 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 55 | public int GetEntitiesCount () { 56 | return _entitiesCount; 57 | } 58 | 59 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 60 | public int[] GetRawEntities () { 61 | return _denseEntities; 62 | } 63 | 64 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 65 | public int[] GetSparseIndex () { 66 | return SparseEntities; 67 | } 68 | 69 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 70 | public Enumerator GetEnumerator () { 71 | _lockCount++; 72 | return new Enumerator (this); 73 | } 74 | 75 | #if LEOECSLITE_FILTER_EVENTS 76 | public void AddEventListener (IEcsFilterEventListener eventListener) { 77 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 78 | if (eventListener == null) { throw new Exception ("Listener is null."); } 79 | #endif 80 | if (_eventListeners.Length == _eventListenersCount) { 81 | Array.Resize (ref _eventListeners, _eventListenersCount << 1); 82 | } 83 | _eventListeners[_eventListenersCount++] = eventListener; 84 | } 85 | 86 | public void RemoveEventListener (IEcsFilterEventListener eventListener) { 87 | for (var i = 0; i < _eventListenersCount; i++) { 88 | if (_eventListeners[i] == eventListener) { 89 | _eventListenersCount--; 90 | // cant fill gap with last element due listeners order is important. 91 | Array.Copy (_eventListeners, i + 1, _eventListeners, i, _eventListenersCount - i); 92 | break; 93 | } 94 | } 95 | } 96 | #endif 97 | 98 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 99 | internal void ResizeSparseIndex (int capacity) { 100 | Array.Resize (ref SparseEntities, capacity); 101 | } 102 | 103 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 104 | internal EcsWorld.Mask GetMask () { 105 | return _mask; 106 | } 107 | 108 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 109 | internal void AddEntity (int entity) { 110 | if (AddDelayedOp (true, entity)) { return; } 111 | if (_entitiesCount == _denseEntities.Length) { 112 | Array.Resize (ref _denseEntities, _entitiesCount << 1); 113 | } 114 | _denseEntities[_entitiesCount++] = entity; 115 | SparseEntities[entity] = _entitiesCount; 116 | #if LEOECSLITE_FILTER_EVENTS 117 | ProcessEventListeners (true, entity); 118 | #endif 119 | } 120 | 121 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 122 | internal void RemoveEntity (int entity) { 123 | if (AddDelayedOp (false, entity)) { return; } 124 | var idx = SparseEntities[entity] - 1; 125 | SparseEntities[entity] = 0; 126 | _entitiesCount--; 127 | if (idx < _entitiesCount) { 128 | _denseEntities[idx] = _denseEntities[_entitiesCount]; 129 | SparseEntities[_denseEntities[idx]] = idx + 1; 130 | } 131 | #if LEOECSLITE_FILTER_EVENTS 132 | ProcessEventListeners (false, entity); 133 | #endif 134 | } 135 | 136 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 137 | bool AddDelayedOp (bool added, int entity) { 138 | if (_lockCount <= 0) { return false; } 139 | if (_delayedOpsCount == _delayedOps.Length) { 140 | Array.Resize (ref _delayedOps, _delayedOpsCount << 1); 141 | } 142 | ref var op = ref _delayedOps[_delayedOpsCount++]; 143 | op.Added = added; 144 | op.Entity = entity; 145 | return true; 146 | } 147 | 148 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 149 | void Unlock () { 150 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 151 | if (_lockCount <= 0) { throw new Exception ($"Invalid lock-unlock balance for \"{GetType ().Name}\"."); } 152 | #endif 153 | _lockCount--; 154 | if (_lockCount == 0 && _delayedOpsCount > 0) { 155 | for (int i = 0, iMax = _delayedOpsCount; i < iMax; i++) { 156 | ref var op = ref _delayedOps[i]; 157 | if (op.Added) { 158 | AddEntity (op.Entity); 159 | } else { 160 | RemoveEntity (op.Entity); 161 | } 162 | } 163 | _delayedOpsCount = 0; 164 | } 165 | } 166 | 167 | #if LEOECSLITE_FILTER_EVENTS 168 | void ProcessEventListeners (bool isAdd, int entity) { 169 | if (isAdd) { 170 | for (var i = 0; i < _eventListenersCount; i++) { 171 | _eventListeners[i].OnEntityAdded (entity); 172 | } 173 | } else { 174 | for (var i = 0; i < _eventListenersCount; i++) { 175 | _eventListeners[i].OnEntityRemoved (entity); 176 | } 177 | } 178 | } 179 | #endif 180 | 181 | public struct Enumerator : IDisposable { 182 | readonly EcsFilter _filter; 183 | readonly int[] _entities; 184 | readonly int _count; 185 | int _idx; 186 | 187 | public Enumerator (EcsFilter filter) { 188 | _filter = filter; 189 | _entities = filter._denseEntities; 190 | _count = filter._entitiesCount; 191 | _idx = -1; 192 | } 193 | 194 | public int Current { 195 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 196 | get => _entities[_idx]; 197 | } 198 | 199 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 200 | public bool MoveNext () { 201 | return ++_idx < _count; 202 | } 203 | 204 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 205 | public void Dispose () { 206 | _filter.Unlock (); 207 | } 208 | } 209 | 210 | struct DelayedOp { 211 | public bool Added; 212 | public int Entity; 213 | } 214 | } 215 | } -------------------------------------------------------------------------------- /NUrumi/Collector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUrumi.Exceptions; 4 | 5 | namespace NUrumi 6 | { 7 | /// 8 | /// Represents a collection of entities which collected through observation of context changes. 9 | /// 10 | public sealed class Collector : 11 | IDisposable, 12 | IContextResizeCallback 13 | where TRegistry : Registry, new() 14 | { 15 | private readonly Context _context; 16 | private readonly EntitiesSet _collectedEntities; 17 | private readonly HashSet> _listenedOnAddGroups = new HashSet>(); 18 | private readonly HashSet> _listenedOnRemoveGroups = new HashSet>(); 19 | private readonly List _unsubscribeActions = new List(); 20 | private bool _disposed; 21 | 22 | internal Collector(Context context, int initialCapacity) 23 | { 24 | _context = context; 25 | _collectedEntities = new EntitiesSet(initialCapacity); 26 | } 27 | 28 | ~Collector() 29 | { 30 | Dispose(); 31 | } 32 | 33 | /// 34 | /// Gets count of collected entities. 35 | /// 36 | public int Count => _collectedEntities.EntitiesCount; 37 | 38 | /// 39 | /// Determines is specified entity was collected. 40 | /// 41 | /// An entity to check 42 | /// If entity in collector returns true; otherwise false 43 | public bool Has(int entity) => _collectedEntities.Has(entity); 44 | 45 | /// 46 | /// Watches entities which were added to specified group. 47 | /// 48 | /// A group to watch. 49 | /// Returns an instance of this collector. 50 | public Collector WatchEntitiesAddedTo(Group group) 51 | { 52 | if (_disposed) 53 | { 54 | throw new ObjectDisposedException("Collector already disposed"); 55 | } 56 | 57 | if (!_listenedOnAddGroups.Add(group)) 58 | { 59 | throw new NUrumiException($"Collector already listen on add to group: {group}"); 60 | } 61 | 62 | if (group.Context != _context) 63 | { 64 | throw new NUrumiException("Can't listen events from other context."); 65 | } 66 | 67 | var action = new Group.GroupChangedEvent( 68 | (entity, added) => 69 | { 70 | if (!added) 71 | { 72 | return; 73 | } 74 | 75 | _collectedEntities.Add(entity); 76 | }); 77 | 78 | group.OnGroupChanged += action; 79 | _unsubscribeActions.Add(() => group.OnGroupChanged -= action); 80 | _listenedOnAddGroups.Add(group); 81 | 82 | return this; 83 | } 84 | 85 | /// 86 | /// Watches entities which were removed from specified group. 87 | /// 88 | /// A group to watch. 89 | /// Returns an instance of this collector. 90 | public Collector WatchEntitiesRemovedFrom(Group group) 91 | { 92 | if (_disposed) 93 | { 94 | throw new ObjectDisposedException("Collector already disposed"); 95 | } 96 | 97 | if (!_listenedOnRemoveGroups.Add(group)) 98 | { 99 | throw new NUrumiException($"Collector already listen on remove from group: {group}"); 100 | } 101 | 102 | if (group.Context != _context) 103 | { 104 | throw new NUrumiException("Can't listen events from other context."); 105 | } 106 | 107 | var action = new Group.GroupChangedEvent( 108 | (entity, added) => 109 | { 110 | if (added) 111 | { 112 | return; 113 | } 114 | 115 | _collectedEntities.Add(entity); 116 | }); 117 | 118 | group.OnGroupChanged += action; 119 | _unsubscribeActions.Add(() => group.OnGroupChanged -= action); 120 | _listenedOnRemoveGroups.Add(group); 121 | 122 | return this; 123 | } 124 | 125 | /// 126 | /// Watches entities which were added or removed from specified group. 127 | /// 128 | /// A group to watch. 129 | /// Returns an instance of this collector. 130 | public Collector WatchEntitiesAddedOrRemovedFrom(Group group) 131 | { 132 | if (_disposed) 133 | { 134 | throw new ObjectDisposedException("Collector already disposed"); 135 | } 136 | 137 | if (!_listenedOnAddGroups.Add(group)) 138 | { 139 | throw new NUrumiException($"Collector already listen on add to group: {group}"); 140 | } 141 | 142 | if (!_listenedOnRemoveGroups.Add(group)) 143 | { 144 | throw new NUrumiException($"Collector already listen on remove from group: {group}"); 145 | } 146 | 147 | if (group.Context != _context) 148 | { 149 | throw new NUrumiException("Can't listen events from other context."); 150 | } 151 | 152 | var action = new Group.GroupChangedEvent((entity, added) => _collectedEntities.Add(entity)); 153 | 154 | group.OnGroupChanged += action; 155 | _unsubscribeActions.Add(() => group.OnGroupChanged -= action); 156 | _listenedOnRemoveGroups.Add(group); 157 | 158 | return this; 159 | } 160 | 161 | /// 162 | /// Watches entities which field value was changed. 163 | /// 164 | /// A field to watch. 165 | /// Returns an instance of this collector. 166 | public Collector WatchChangesOf(ReactiveField field) 167 | where TValue : unmanaged, IEquatable 168 | { 169 | if (_disposed) 170 | { 171 | throw new ObjectDisposedException("Collector already disposed"); 172 | } 173 | 174 | var action = new ReactiveField.OnReactiveFieldValueChangedEventHandler( 175 | (component, _, entity, value, newValue) => _collectedEntities.Add(entity)); 176 | 177 | field.OnValueChanged += action; 178 | _unsubscribeActions.Add(() => field.OnValueChanged -= action); 179 | 180 | return this; 181 | } 182 | 183 | /// 184 | /// Removes all collected entities from this collector. 185 | /// 186 | public void Clear() 187 | { 188 | _collectedEntities.Clear(); 189 | } 190 | 191 | /// 192 | /// Enumerates all collected entities. 193 | /// 194 | /// Returns an enumerator of collected entities. 195 | public EntitiesSet.Enumerator GetEnumerator() 196 | { 197 | return _collectedEntities.GetEnumerator(); 198 | } 199 | 200 | public void Dispose() 201 | { 202 | if (_disposed) 203 | { 204 | return; 205 | } 206 | 207 | foreach (var unsubscribeAction in _unsubscribeActions) 208 | { 209 | unsubscribeAction(); 210 | } 211 | 212 | _unsubscribeActions.Clear(); 213 | _collectedEntities.Clear(); 214 | _disposed = true; 215 | } 216 | 217 | void IContextResizeCallback.ResizeEntities(int newSize) 218 | { 219 | _collectedEntities.ResizeEntities(newSize); 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /NUrumi.Benchmark/LeoEcs/components.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The Proprietary or MIT-Red License 3 | // Copyright (c) 2012-2022 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System; 7 | using System.Runtime.CompilerServices; 8 | 9 | #if ENABLE_IL2CPP 10 | using Unity.IL2CPP.CompilerServices; 11 | #endif 12 | 13 | namespace Leopotam.EcsLite { 14 | public interface IEcsPool { 15 | void Resize (int capacity); 16 | bool Has (int entity); 17 | void Del (int entity); 18 | void AddRaw (int entity, object dataRaw); 19 | object GetRaw (int entity); 20 | void SetRaw (int entity, object dataRaw); 21 | int GetId (); 22 | Type GetComponentType (); 23 | } 24 | 25 | public interface IEcsAutoReset where T : struct { 26 | void AutoReset (ref T c); 27 | } 28 | 29 | #if ENABLE_IL2CPP 30 | [Il2CppSetOption (Option.NullChecks, false)] 31 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 32 | #endif 33 | public sealed class EcsPool : IEcsPool where T : struct { 34 | readonly Type _type; 35 | readonly EcsWorld _world; 36 | readonly int _id; 37 | readonly AutoResetHandler _autoReset; 38 | // 1-based index. 39 | T[] _denseItems; 40 | int[] _sparseItems; 41 | int _denseItemsCount; 42 | int[] _recycledItems; 43 | int _recycledItemsCount; 44 | #if ENABLE_IL2CPP && !UNITY_EDITOR 45 | T _autoresetFakeInstance; 46 | #endif 47 | 48 | internal EcsPool (EcsWorld world, int id, int denseCapacity, int sparseCapacity, int recycledCapacity) { 49 | _type = typeof (T); 50 | _world = world; 51 | _id = id; 52 | _denseItems = new T[denseCapacity + 1]; 53 | _sparseItems = new int[sparseCapacity]; 54 | _denseItemsCount = 1; 55 | _recycledItems = new int[recycledCapacity]; 56 | _recycledItemsCount = 0; 57 | var isAutoReset = typeof (IEcsAutoReset).IsAssignableFrom (_type); 58 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 59 | if (!isAutoReset && _type.GetInterface ("IEcsAutoReset`1") != null) { 60 | throw new Exception ($"IEcsAutoReset should have <{typeof (T).Name}> constraint for component \"{typeof (T).Name}\"."); 61 | } 62 | #endif 63 | if (isAutoReset) { 64 | var autoResetMethod = typeof (T).GetMethod (nameof (IEcsAutoReset.AutoReset)); 65 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 66 | if (autoResetMethod == null) { 67 | throw new Exception ( 68 | $"IEcsAutoReset<{typeof (T).Name}> explicit implementation not supported, use implicit instead."); 69 | } 70 | #endif 71 | _autoReset = (AutoResetHandler) Delegate.CreateDelegate ( 72 | typeof (AutoResetHandler), 73 | #if ENABLE_IL2CPP && !UNITY_EDITOR 74 | _autoresetFakeInstance, 75 | #else 76 | null, 77 | #endif 78 | autoResetMethod); 79 | } 80 | } 81 | 82 | #if UNITY_2020_3_OR_NEWER 83 | [UnityEngine.Scripting.Preserve] 84 | #endif 85 | void ReflectionSupportHack () { 86 | _world.GetPool (); 87 | _world.Filter ().Exc ().End (); 88 | } 89 | 90 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 91 | public EcsWorld GetWorld () { 92 | return _world; 93 | } 94 | 95 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 96 | public int GetId () { 97 | return _id; 98 | } 99 | 100 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 101 | public Type GetComponentType () { 102 | return _type; 103 | } 104 | 105 | void IEcsPool.Resize (int capacity) { 106 | Array.Resize (ref _sparseItems, capacity); 107 | } 108 | 109 | object IEcsPool.GetRaw (int entity) { 110 | return Get (entity); 111 | } 112 | 113 | void IEcsPool.SetRaw (int entity, object dataRaw) { 114 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 115 | if (dataRaw == null || dataRaw.GetType () != _type) { throw new Exception ("Invalid component data, valid \"{typeof (T).Name}\" instance required."); } 116 | if (_sparseItems[entity] <= 0) { throw new Exception ($"Component \"{typeof (T).Name}\" not attached to entity."); } 117 | #endif 118 | _denseItems[_sparseItems[entity]] = (T) dataRaw; 119 | } 120 | 121 | void IEcsPool.AddRaw (int entity, object dataRaw) { 122 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 123 | if (dataRaw == null || dataRaw.GetType () != _type) { throw new Exception ("Invalid component data, valid \"{typeof (T).Name}\" instance required."); } 124 | #endif 125 | ref var data = ref Add (entity); 126 | data = (T) dataRaw; 127 | } 128 | 129 | public T[] GetRawDenseItems () { 130 | return _denseItems; 131 | } 132 | 133 | public ref int GetRawDenseItemsCount () { 134 | return ref _denseItemsCount; 135 | } 136 | 137 | public int[] GetRawSparseItems () { 138 | return _sparseItems; 139 | } 140 | 141 | public int[] GetRawRecycledItems () { 142 | return _recycledItems; 143 | } 144 | 145 | public ref int GetRawRecycledItemsCount () { 146 | return ref _recycledItemsCount; 147 | } 148 | 149 | public ref T Add (int entity) { 150 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 151 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 152 | if (_sparseItems[entity] > 0) { throw new Exception ($"Component \"{typeof (T).Name}\" already attached to entity."); } 153 | #endif 154 | int idx; 155 | if (_recycledItemsCount > 0) { 156 | idx = _recycledItems[--_recycledItemsCount]; 157 | } else { 158 | idx = _denseItemsCount; 159 | if (_denseItemsCount == _denseItems.Length) { 160 | Array.Resize (ref _denseItems, _denseItemsCount << 1); 161 | } 162 | _denseItemsCount++; 163 | _autoReset?.Invoke (ref _denseItems[idx]); 164 | } 165 | _sparseItems[entity] = idx; 166 | _world.OnEntityChangeInternal (entity, _id, true); 167 | _world.Entities[entity].ComponentsCount++; 168 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 169 | _world.RaiseEntityChangeEvent (entity); 170 | #endif 171 | return ref _denseItems[idx]; 172 | } 173 | 174 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 175 | public ref T Get (int entity) { 176 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 177 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 178 | if (_sparseItems[entity] == 0) { throw new Exception ($"Cant get \"{typeof (T).Name}\" component - not attached."); } 179 | #endif 180 | return ref _denseItems[_sparseItems[entity]]; 181 | } 182 | 183 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 184 | public bool Has (int entity) { 185 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 186 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 187 | #endif 188 | return _sparseItems[entity] > 0; 189 | } 190 | 191 | public void Del (int entity) { 192 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 193 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 194 | #endif 195 | ref var sparseData = ref _sparseItems[entity]; 196 | if (sparseData > 0) { 197 | _world.OnEntityChangeInternal (entity, _id, false); 198 | if (_recycledItemsCount == _recycledItems.Length) { 199 | Array.Resize (ref _recycledItems, _recycledItemsCount << 1); 200 | } 201 | _recycledItems[_recycledItemsCount++] = sparseData; 202 | if (_autoReset != null) { 203 | _autoReset.Invoke (ref _denseItems[sparseData]); 204 | } else { 205 | _denseItems[sparseData] = default; 206 | } 207 | sparseData = 0; 208 | ref var entityData = ref _world.Entities[entity]; 209 | entityData.ComponentsCount--; 210 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 211 | _world.RaiseEntityChangeEvent (entity); 212 | #endif 213 | if (entityData.ComponentsCount == 0) { 214 | _world.DelEntity (entity); 215 | } 216 | } 217 | } 218 | 219 | delegate void AutoResetHandler (ref T component); 220 | } 221 | } -------------------------------------------------------------------------------- /NUrumi/Group.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace NUrumi 6 | { 7 | /// 8 | /// Represents a group of entities. 9 | /// 10 | public sealed class Group : IUpdateCallback where TRegistry : Registry, new() 11 | { 12 | private readonly bool[] _conditions; 13 | private readonly ComponentStorageData[] _componentStorages; 14 | private readonly bool _singleInclude; 15 | private readonly EntitiesSet _entities; 16 | private readonly List _deferredOperations; 17 | private int _locksCount; 18 | 19 | /// 20 | /// An event which raised when the group was changed. 21 | /// 22 | /// An index of entity which was added or removed from group. 23 | /// If true - an entity was added; otherwise - removed. 24 | public delegate void GroupChangedEvent(int entityIndex, bool add); 25 | 26 | /// 27 | /// Creates a new group with specified filter and initial capacity. 28 | /// 29 | /// A context of this group. 30 | /// A filter of entities as composition of components. 31 | /// An initial entities capacity. 32 | /// Returns new group. 33 | public static Group Create(Context context, IGroupFilter filter, int entitiesCapacity) 34 | { 35 | var componentsCount = filter.Included.Count + filter.Excluded.Count; 36 | var conditions = new bool[componentsCount]; 37 | var componentStorages = new ComponentStorageData[componentsCount]; 38 | var group = new Group(context, conditions, componentStorages, entitiesCapacity); 39 | 40 | var i = 0; 41 | foreach (var component in filter.Included) 42 | { 43 | conditions[i] = true; 44 | componentStorages[i] = component.Storage; 45 | component.Storage.AddUpdateCallback(group); 46 | i++; 47 | } 48 | 49 | foreach (var component in filter.Excluded) 50 | { 51 | conditions[i] = true; 52 | componentStorages[i] = component.Storage; 53 | component.Storage.AddUpdateCallback(group); 54 | i++; 55 | } 56 | 57 | return group; 58 | } 59 | 60 | private Group( 61 | Context context, 62 | bool[] conditions, 63 | ComponentStorageData[] componentStorages, 64 | int entitiesCapacity) 65 | { 66 | Context = context; 67 | _conditions = conditions; 68 | _componentStorages = componentStorages; 69 | _singleInclude = conditions.Length == 1 && conditions[0]; 70 | _entities = new EntitiesSet(entitiesCapacity); 71 | _deferredOperations = new List(); 72 | } 73 | 74 | /// 75 | /// An event which raised when this group changed. 76 | /// 77 | public event GroupChangedEvent OnGroupChanged; 78 | 79 | /// 80 | /// A context which owns this group. 81 | /// 82 | public readonly Context Context; 83 | 84 | /// 85 | /// Count of entities in this group. 86 | /// 87 | public int EntitiesCount => _entities.EntitiesCount; 88 | 89 | /// 90 | /// Returns entities to specified array. 91 | /// If array length is less then required it will be automatically extended. 92 | /// 93 | /// A destination array. 94 | /// Returns a count of written entities. 95 | public int GetEntities(ref int[] result) 96 | { 97 | return _entities.GetEntities(ref result); 98 | } 99 | 100 | /// 101 | /// Enumerates an entities in this group. 102 | /// 103 | /// Returns an enumerator of entities. 104 | public Enumerator GetEnumerator() 105 | { 106 | _locksCount += 1; 107 | return new Enumerator(this, _entities.GetEnumerator()); 108 | } 109 | 110 | void IUpdateCallback.BeforeChange(int entityIndex, bool added) 111 | { 112 | } 113 | 114 | void IUpdateCallback.AfterChange(int entityIndex, bool added) 115 | { 116 | if (_singleInclude) 117 | { 118 | if (added) 119 | { 120 | AddInternal(entityIndex); 121 | } 122 | else 123 | { 124 | RemoveInternal(entityIndex); 125 | } 126 | 127 | return; 128 | } 129 | 130 | Update(entityIndex); 131 | } 132 | 133 | internal void Update(int entityIndex) 134 | { 135 | for (var i = 0; i < _conditions.Length; i++) 136 | { 137 | if (_conditions[i] == _componentStorages[i].Has(entityIndex)) 138 | { 139 | continue; 140 | } 141 | 142 | RemoveInternal(entityIndex); 143 | return; 144 | } 145 | 146 | AddInternal(entityIndex); 147 | } 148 | 149 | internal void ResizeEntities(int newSize) 150 | { 151 | _entities.ResizeEntities(newSize); 152 | } 153 | 154 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 155 | private void AddInternal(int entityIndex) 156 | { 157 | if (OnGroupChanged == null) 158 | { 159 | _entities.Add(entityIndex); 160 | return; 161 | } 162 | 163 | var resolution = _entities.Add(entityIndex); 164 | if (resolution == EntitiesSet.AppliedEarly) 165 | { 166 | return; 167 | } 168 | 169 | if (resolution == EntitiesSet.Applied) 170 | { 171 | var h = OnGroupChanged; 172 | h?.Invoke(entityIndex, true); 173 | return; 174 | } 175 | 176 | var ix = _deferredOperations.BinarySearch(entityIndex); 177 | if (ix < 0) 178 | { 179 | _deferredOperations.Insert(-(ix + 1), entityIndex); 180 | } 181 | } 182 | 183 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 184 | private void RemoveInternal(int entityIndex) 185 | { 186 | if (OnGroupChanged == null) 187 | { 188 | _entities.Remove(entityIndex); 189 | return; 190 | } 191 | 192 | var resolution = _entities.Remove(entityIndex); 193 | if (resolution == EntitiesSet.AppliedEarly) 194 | { 195 | return; 196 | } 197 | 198 | if (resolution == EntitiesSet.Applied) 199 | { 200 | var h = OnGroupChanged; 201 | h?.Invoke(entityIndex, false); 202 | return; 203 | } 204 | 205 | var ix = _deferredOperations.BinarySearch(entityIndex); 206 | if (ix < 0) 207 | { 208 | _deferredOperations.Insert(-(ix + 1), entityIndex); 209 | } 210 | } 211 | 212 | private void Unlock() 213 | { 214 | _locksCount -= 1; 215 | 216 | if (_locksCount == 0 && _deferredOperations.Count > 0) 217 | { 218 | var h = OnGroupChanged; 219 | if (h == null) 220 | { 221 | _deferredOperations.Clear(); 222 | return; 223 | } 224 | 225 | for (var i = 0; i < _deferredOperations.Count; i++) 226 | { 227 | var entity = _deferredOperations[i]; 228 | h(entity, _entities.Has(entity)); 229 | } 230 | 231 | _deferredOperations.Clear(); 232 | } 233 | } 234 | 235 | public struct Enumerator : IDisposable 236 | { 237 | private readonly Group _group; 238 | private EntitiesSet.Enumerator _setEnumerator; 239 | 240 | public Enumerator(Group group, EntitiesSet.Enumerator setEnumerator) 241 | { 242 | _group = group; 243 | _setEnumerator = setEnumerator; 244 | } 245 | 246 | public int Current 247 | { 248 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 249 | get => _setEnumerator.Current; 250 | } 251 | 252 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 253 | public bool MoveNext() 254 | { 255 | return _setEnumerator.MoveNext(); 256 | } 257 | 258 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 259 | public void Dispose() 260 | { 261 | _setEnumerator.Dispose(); 262 | _group.Unlock(); 263 | } 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /NUrumi/Relation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NUrumi 5 | { 6 | /// 7 | /// Represents a relationship between two entities. 8 | /// Definitions: 9 | /// * relationship - used to refer to first element of pair 10 | /// * target - used to refer to second element of pair 11 | /// 12 | /// Component which represents a kind of relationship: Likes, ChildOf and etc. 13 | public class Relation : 14 | Component, 15 | IUpdateCallback 16 | where TRelationshipKind : Component, new() 17 | { 18 | internal readonly Queue> Pool = new Queue>(); 19 | 20 | #pragma warning disable CS0649 21 | internal RefField> Direct; 22 | internal RefField> Reverse; 23 | #pragma warning restore CS0649 24 | 25 | protected override void OnInit() 26 | { 27 | base.OnInit(); 28 | Storage.AddUpdateCallback(this); 29 | } 30 | 31 | void IUpdateCallback.BeforeChange(int entity, bool added) 32 | { 33 | if (added) 34 | { 35 | return; 36 | } 37 | 38 | RemoveDirect(entity, out var direct); 39 | RemoveReverse(entity, out var reverse); 40 | 41 | if (direct != null) 42 | { 43 | direct.Clear(); 44 | Pool.Enqueue(direct); 45 | } 46 | 47 | if (reverse != null) 48 | { 49 | reverse.Clear(); 50 | Pool.Enqueue(reverse); 51 | } 52 | } 53 | 54 | void IUpdateCallback.AfterChange(int entity, bool added) 55 | { 56 | } 57 | 58 | private void RemoveDirect(int entity, out HashSet direct) 59 | { 60 | if (!entity.TryGet(Direct, out direct)) 61 | { 62 | return; 63 | } 64 | 65 | if (direct.Count == 0) 66 | { 67 | return; 68 | } 69 | 70 | foreach (var otherEntity in direct) 71 | { 72 | if (otherEntity.TryGet(Reverse, out var reverse)) 73 | { 74 | reverse.Remove(entity); 75 | } 76 | } 77 | } 78 | 79 | private void RemoveReverse(int entity, out HashSet reverse) 80 | { 81 | if (!entity.TryGet(Reverse, out reverse)) 82 | { 83 | return; 84 | } 85 | 86 | if (reverse.Count == 0) 87 | { 88 | return; 89 | } 90 | 91 | foreach (var otherEntity in reverse) 92 | { 93 | if (otherEntity.TryGet(Direct, out var direct)) 94 | { 95 | direct.Remove(entity); 96 | } 97 | } 98 | } 99 | } 100 | 101 | public static class Relation 102 | { 103 | /// 104 | /// Adds relationship between entity and other entity. 105 | /// 106 | /// A first entity in relation. 107 | /// Component which represents a kind of relationship 108 | /// A second entity in relation. 109 | /// Component which represents a kind of relationship 110 | public static int Add( 111 | this int entity, 112 | Relation relationKind, 113 | int otherEntity) 114 | where TRelationshipKind : Component, new() 115 | { 116 | if (!entity.TryGet(relationKind.Direct, out var direct)) 117 | { 118 | direct = CreateHashSet(relationKind); 119 | entity.Set(relationKind.Direct, direct); 120 | } 121 | 122 | if (!direct.Add(otherEntity)) 123 | { 124 | return entity; 125 | } 126 | 127 | if (!otherEntity.TryGet(relationKind.Reverse, out var reverse)) 128 | { 129 | reverse = CreateHashSet(relationKind); 130 | otherEntity.Set(relationKind.Reverse, reverse); 131 | } 132 | 133 | reverse.Add(entity); 134 | 135 | return entity; 136 | } 137 | 138 | /// 139 | /// Removes relationship between two entities. 140 | /// 141 | /// A first entity in relation. 142 | /// Component which represents a kind of relationship. 143 | /// A second entity in relation. 144 | /// Component which represents a kind of relationship. 145 | public static int Remove( 146 | this int entity, 147 | Relation relationKind, 148 | int otherEntity) 149 | where TRelationshipKind : Component, new() 150 | { 151 | if (!entity.TryGet(relationKind.Direct, out var direct)) 152 | { 153 | return entity; 154 | } 155 | 156 | direct.Remove(otherEntity); 157 | otherEntity.Get(relationKind.Reverse).Remove(entity); 158 | 159 | return entity; 160 | } 161 | 162 | /// 163 | /// Returns a collection of entities which associated with specified entity. 164 | /// 165 | /// An entity which relations should be return. 166 | /// Component which represents a kind of relationship. 167 | /// Component which represents a kind of relationship. 168 | /// Returns a collection of entities which associated with specified entity. 169 | public static IReadOnlyCollection Relationship( 170 | this int entity, 171 | Relation relationKind) 172 | where TRelationshipKind : Component, new() 173 | { 174 | if (!entity.TryGet(relationKind.Direct, out var direct)) 175 | { 176 | return Array.Empty(); 177 | } 178 | 179 | return direct; 180 | } 181 | 182 | /// 183 | /// Returns a collection of entities which associated with specified entity in reverse order. 184 | /// 185 | /// An entity which reverse relations should be return. 186 | /// Component which represents a kind of relationship. 187 | /// Component which represents a kind of relationship. 188 | /// Returns a collection of entities which associated with specified entity. 189 | public static IReadOnlyCollection Target( 190 | this int entity, 191 | Relation relationKind) 192 | where TRelationshipKind : Component, new() 193 | { 194 | if (!entity.TryGet(relationKind.Reverse, out var reverse)) 195 | { 196 | return Array.Empty(); 197 | } 198 | 199 | return reverse; 200 | } 201 | 202 | /// 203 | /// Determines is entity has relationship with other entity. 204 | /// 205 | /// A first entity in relation. 206 | /// Component which represents a kind of relationship. 207 | /// A second entity in relation. 208 | /// Component which represents a kind of relationship. 209 | /// Returns true if relationship exists; otherwise returns false. 210 | public static bool Has( 211 | this int entity, 212 | Relation relationKind, 213 | int otherEntity) 214 | where TRelationshipKind : Component, new() 215 | { 216 | return entity.TryGet(relationKind.Direct, out var direct) && direct.Contains(otherEntity); 217 | } 218 | 219 | /// 220 | /// Determines is entity has reverse-relationship with other entity. 221 | /// 222 | /// A first entity in relation. 223 | /// Component which represents a kind of relationship. 224 | /// A second entity in relation. 225 | /// Component which represents a kind of relationship. 226 | /// Returns true if reverse-relationship exists; otherwise returns false. 227 | public static bool Targets( 228 | this int entity, 229 | Relation relationKind, 230 | int otherEntity) 231 | where TRelationshipKind : Component, new() 232 | { 233 | return entity.TryGet(relationKind.Reverse, out var reverse) && reverse.Contains(otherEntity); 234 | } 235 | 236 | private static HashSet CreateHashSet(Relation component) 237 | where TComponent : Component, new() 238 | { 239 | return component.Pool.Count > 0 ? component.Pool.Dequeue() : new HashSet(); 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /NUrumi.Test/ContextTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using FluentAssertions; 4 | using NUnit.Framework; 5 | using NUrumi.Test.Model; 6 | 7 | namespace NUrumi.Test 8 | { 9 | [TestFixture] 10 | public class ContextTests 11 | { 12 | [Test] 13 | public void ContextRegistryInitialization() 14 | { 15 | var context = new Context(); 16 | context.Registry.Test.Should().NotBeNull(); 17 | context.Registry.Test.Index.Should().Be(0); 18 | 19 | context.Registry.Test.Field1.Index.Should().Be(0); 20 | context.Registry.Test.Field1.Offset.Should().Be(ComponentStorageData.ReservedSize + 0); 21 | context.Registry.Test.Field1.ValueSize.Should().Be(4); 22 | context.Registry.Test.Field1.Name.Should().Be("Field1"); 23 | 24 | context.Registry.Test.Field2.Index.Should().Be(1); 25 | context.Registry.Test.Field2.Offset.Should().Be(ComponentStorageData.ReservedSize + 4); 26 | context.Registry.Test.Field2.ValueSize.Should().Be(1); 27 | context.Registry.Test.Field2.Name.Should().Be("Field2"); 28 | 29 | context.Registry.Test.Field3.Index.Should().Be(2); 30 | context.Registry.Test.Field3.Offset.Should().Be(ComponentStorageData.ReservedSize + 5); 31 | context.Registry.Test.Field3.ValueSize.Should().Be(8); 32 | context.Registry.Test.Field3.Name.Should().Be("Field3"); 33 | } 34 | 35 | [Test] 36 | public void ContextEntities() 37 | { 38 | var context = new Context(config: new Config 39 | { 40 | InitialEntitiesCapacity = 1, 41 | InitialReuseEntitiesBarrier = 2 42 | }); 43 | 44 | // Create entities 45 | var entity1 = context.CreateEntity(); 46 | context.IsAlive(entity1, out var entity1Gen).Should().BeTrue(); 47 | context.LiveEntitiesCount.Should().Be(1); 48 | entity1.Should().Be(1); 49 | entity1Gen.Should().Be(1); 50 | 51 | var entity2 = context.CreateEntity(); 52 | context.IsAlive(entity2, out var entity2Gen).Should().BeTrue(); 53 | context.LiveEntitiesCount.Should().Be(2); 54 | entity2.Should().Be(2); 55 | entity2Gen.Should().Be(1); 56 | 57 | var entity3 = context.CreateEntity(); 58 | context.IsAlive(entity3, out var entity3Gen).Should().BeTrue(); 59 | context.LiveEntitiesCount.Should().Be(3); 60 | entity3.Should().Be(3); 61 | entity3Gen.Should().Be(1); 62 | 63 | // Ensure that new entities does not affect each other 64 | context.IsAlive(entity1, out _).Should().BeTrue(); 65 | context.IsAlive(entity2, out _).Should().BeTrue(); 66 | context.IsAlive(entity3, out _).Should().BeTrue(); 67 | 68 | // Remove entity in a middle position 69 | context.RemoveEntity(entity2).Should().BeTrue(); 70 | context.IsAlive(entity1, out _).Should().BeTrue(); 71 | context.IsAlive(entity2, out _).Should().BeFalse(); 72 | context.IsAlive(entity3, out _).Should().BeTrue(); 73 | context.LiveEntitiesCount.Should().Be(2); 74 | context.RecycledEntitiesCount.Should().Be(1); 75 | 76 | // It should not reuse entities till reuse barrier not reached 77 | var entity4 = context.CreateEntity(); 78 | var entity5 = context.CreateEntity(); 79 | context.LiveEntitiesCount.Should().Be(4); 80 | context.IsAlive(entity4, out var entity4Gen).Should().BeTrue(); 81 | context.IsAlive(entity5, out var entity5Gen).Should().BeTrue(); 82 | entity4.Should().Be(4); 83 | entity4Gen.Should().Be(1); 84 | entity5.Should().Be(5); 85 | entity5Gen.Should().Be(1); 86 | 87 | // It should reuse entity index when reuse barrier was reached 88 | context.RemoveEntity(entity5); 89 | context.RemoveEntity(entity4); 90 | context.LiveEntitiesCount.Should().Be(2); 91 | context.RecycledEntitiesCount.Should().Be(3); 92 | context.IsAlive(entity1, out _).Should().BeTrue(); 93 | context.IsAlive(entity2, out _).Should().BeFalse(); 94 | context.IsAlive(entity3, out _).Should().BeTrue(); 95 | context.IsAlive(entity4, out _).Should().BeFalse(); 96 | context.IsAlive(entity5, out _).Should().BeFalse(); 97 | 98 | var entity6 = context.CreateEntity(); 99 | var entity7 = context.CreateEntity(); 100 | context.IsAlive(entity1, entity1Gen).Should().BeTrue(); 101 | context.IsAlive(entity2, entity2Gen).Should().BeFalse(); 102 | context.IsAlive(entity3, entity3Gen).Should().BeTrue(); 103 | context.IsAlive(entity4, entity4Gen).Should().BeFalse(); 104 | context.IsAlive(entity5, entity5Gen).Should().BeFalse(); 105 | context.IsAlive(entity6, out var entity6Gen).Should().BeTrue(); 106 | context.IsAlive(entity7, out var entity7Gen).Should().BeTrue(); 107 | 108 | context.LiveEntitiesCount.Should().Be(4); 109 | context.RecycledEntitiesCount.Should().Be(1); 110 | 111 | entity6.Should().Be(2); 112 | entity6Gen.Should().Be(2); 113 | entity7.Should().Be(5); 114 | entity7Gen.Should().Be(2); 115 | } 116 | 117 | [Test] 118 | public void SetFieldValuesByRef() 119 | { 120 | var context = new Context(); 121 | var position = context.Registry.Position; 122 | var velocity = context.Registry.Velocity; 123 | 124 | var entityId = context.CreateEntity(); 125 | position.Set(entityId, Vector2.Zero); 126 | velocity.Set(entityId, Vector2.One); 127 | 128 | ref var positionRef = ref position.GetRef(entityId); 129 | ref var velocityRef = ref velocity.GetRef(entityId); 130 | positionRef += velocityRef; 131 | 132 | position.Get(entityId).Should().Be(Vector2.One); 133 | } 134 | 135 | [Test] 136 | public void EntityValues() 137 | { 138 | var context = new Context(); 139 | var testComponent = context.Registry.Test; 140 | var field1 = context.Registry.Test.Field1; 141 | var field2 = context.Registry.Test.Field2; 142 | var field3 = context.Registry.Test.Field3; 143 | 144 | var entityId = context.CreateEntity(); 145 | field1.Set(entityId, int.MaxValue / 2); 146 | field2.Set(entityId, 2); 147 | field3.Set(entityId, long.MaxValue / 2); 148 | 149 | context.IsAlive(entityId, out _).Should().BeTrue(); 150 | testComponent.IsAPartOf(entityId).Should().BeTrue(); 151 | field1.Get(entityId).Should().Be(int.MaxValue / 2); 152 | field2.Get(entityId).Should().Be(2); 153 | field3.Get(entityId).Should().Be(long.MaxValue / 2); 154 | } 155 | 156 | 157 | [Test] 158 | public void QueriesDeferredOperations() 159 | { 160 | var context = new Context(); 161 | var positionComponent = context.Registry.Position; 162 | var position = context.Registry.Position; 163 | var velocity = context.Registry.Velocity; 164 | var group = context.CreateGroup( 165 | GroupFilter 166 | .Include(context.Registry.Position) 167 | .Include(context.Registry.Velocity)); 168 | 169 | var entitiesCount = 100; 170 | for (var i = 0; i < entitiesCount; i++) 171 | { 172 | var entity = context.CreateEntity(); 173 | entity.Set(position, Vector2.One); 174 | entity.Set(velocity, Vector2.Zero); 175 | group.EntitiesCount.Should().Be(i + 1); 176 | } 177 | 178 | var ix = 0; 179 | foreach (var entity in group) 180 | { 181 | if (ix % 2 == 0) 182 | { 183 | entity.Remove(positionComponent); 184 | } 185 | 186 | group.EntitiesCount.Should().Be(entitiesCount); 187 | ix += 1; 188 | } 189 | 190 | group.EntitiesCount.Should().Be(entitiesCount / 2); 191 | } 192 | 193 | [Test] 194 | public void IndexField() 195 | { 196 | var context = new Context(); 197 | var parentComponent = context.Registry.Parent; 198 | var parent = parentComponent.Value; 199 | 200 | var parentEntity = context.CreateEntity(); 201 | 202 | var childEntity1 = context.CreateEntity(); 203 | childEntity1.Set(parent, parentEntity); 204 | 205 | var childEntity2 = context.CreateEntity(); 206 | childEntity2.Set(parent, parentEntity); 207 | 208 | var childEntity3 = context.CreateEntity(); 209 | childEntity3.Set(parent, parentEntity); 210 | 211 | var children = new List(); 212 | foreach (var child in parent.GetEntitiesAssociatedWith(parentEntity)) 213 | { 214 | children.Add(child); 215 | } 216 | 217 | children.Count.Should().Be(3); 218 | children.Contains(childEntity1).Should().BeTrue(); 219 | children.Contains(childEntity2).Should().BeTrue(); 220 | children.Contains(childEntity3).Should().BeTrue(); 221 | 222 | parentComponent.RemoveFrom(childEntity2); 223 | 224 | children.Clear(); 225 | foreach (var child in parent.GetEntitiesAssociatedWith(parentEntity)) 226 | { 227 | children.Add(child); 228 | } 229 | 230 | children.Count.Should().Be(2); 231 | children.Contains(childEntity1).Should().BeTrue(); 232 | children.Contains(childEntity2).Should().BeFalse(); 233 | children.Contains(childEntity3).Should().BeTrue(); 234 | } 235 | } 236 | } -------------------------------------------------------------------------------- /NUrumi/ComponentStorageData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using NUrumi.Exceptions; 6 | 7 | namespace NUrumi 8 | { 9 | public sealed class ComponentStorageData 10 | { 11 | public const int ReservedSize = sizeof(int); // index 12 | 13 | internal readonly IComponent Component; 14 | private unsafe byte*[] _entities; 15 | 16 | private readonly int _componentSize; 17 | private int _recordsCapacity; 18 | private unsafe byte* _records; 19 | private unsafe byte* _zero; 20 | private unsafe byte* _recordsLast; 21 | private int _recordsCount; 22 | 23 | private IUpdateCallback[] _updateCallbacks = new IUpdateCallback[10]; 24 | private int _updateCallbacksCount; 25 | 26 | public unsafe ComponentStorageData( 27 | IComponent component, 28 | int componentSize, 29 | int entitiesInitialCapacity, 30 | int recordsInitialCapacity) 31 | { 32 | Component = component; 33 | _entities = new byte*[entitiesInitialCapacity]; 34 | 35 | _componentSize = componentSize + ReservedSize; 36 | _records = (byte*) Marshal.AllocHGlobal(recordsInitialCapacity * _componentSize); 37 | _zero = (byte*) Marshal.AllocHGlobal(_componentSize); 38 | _recordsCapacity = recordsInitialCapacity; 39 | _recordsCount = 0; 40 | _recordsLast = _records; 41 | FillWithZero(_records, _recordsCapacity * _componentSize); 42 | FillWithZero(_zero, _componentSize); 43 | } 44 | 45 | public unsafe int EntitiesCapacity 46 | { 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | get => _entities.Length; 49 | } 50 | 51 | public int EntitiesCount 52 | { 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | get => _recordsCount; 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public unsafe void ResizeEntities(int newSize) 59 | { 60 | var newEntities = new byte*[newSize]; 61 | Array.Copy(_entities, newEntities, _entities.Length); 62 | _entities = newEntities; 63 | } 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public unsafe bool Has(int entityIndex) 67 | { 68 | return _entities[entityIndex] != null; 69 | } 70 | 71 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 72 | public unsafe ref TValue Get( 73 | int entityIndex, 74 | int fieldOffset) 75 | where TValue : unmanaged 76 | { 77 | var record = _entities[entityIndex]; 78 | if (record != null) 79 | { 80 | return ref *(TValue*) (record + fieldOffset); 81 | } 82 | 83 | // Extract method for performance optimization 84 | // .net generates a lot of boilerplate IL code if exception is thrown in this scope 85 | return ref *(TValue*) ThrowComponentNotFound(fieldOffset, entityIndex); 86 | } 87 | 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | public ref TValue GetOrSet( 90 | int entityIndex, 91 | int fieldOffset) 92 | where TValue : unmanaged 93 | { 94 | return ref GetOrSet(entityIndex, fieldOffset, default); 95 | } 96 | 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public unsafe ref TValue GetOrSet( 99 | int entityIndex, 100 | int fieldOffset, 101 | TValue value) 102 | where TValue : unmanaged 103 | { 104 | var record = _entities[entityIndex]; 105 | if (record == null) 106 | { 107 | Set(entityIndex, fieldOffset, ref value); 108 | } 109 | 110 | return ref *(TValue*) (record + fieldOffset); 111 | } 112 | 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public unsafe bool TryGet( 115 | int entityIndex, 116 | int fieldOffset, 117 | out TValue result) 118 | where TValue : unmanaged 119 | { 120 | var record = _entities[entityIndex]; 121 | if (record == null) 122 | { 123 | result = default; 124 | return false; 125 | } 126 | 127 | result = *(TValue*) (record + fieldOffset); 128 | return true; 129 | } 130 | 131 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 132 | public void Set( 133 | int entityIndex, 134 | int fieldOffset, 135 | TValue value) 136 | where TValue : unmanaged 137 | { 138 | Set(entityIndex, fieldOffset, ref value); 139 | } 140 | 141 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 142 | public unsafe void Set( 143 | int entityIndex, 144 | int fieldOffset, 145 | ref TValue value) 146 | where TValue : unmanaged 147 | { 148 | var entities = _entities; 149 | var record = entities[entityIndex]; 150 | if (record == null) 151 | { 152 | NotifyBeforeChanges(entityIndex, true); 153 | 154 | var recordIndex = _recordsCount; 155 | if (recordIndex == _recordsCapacity - 1) 156 | { 157 | var newCapacity = _recordsCapacity << 1; 158 | var newSize = newCapacity * _componentSize; 159 | var oldSize = _recordsCapacity * _componentSize; 160 | var newRecords = (byte*) Marshal.AllocHGlobal(newSize); 161 | FillWithZero(newRecords + oldSize, newSize - oldSize); 162 | Buffer.MemoryCopy(_records, newRecords, newSize, oldSize); 163 | Marshal.FreeHGlobal((IntPtr) _records); 164 | _records = newRecords; 165 | _recordsCapacity = newCapacity; 166 | 167 | record = _records; 168 | for (var i = 0; i < recordIndex; i++) 169 | { 170 | _entities[*(int*) record] = record; 171 | record += _componentSize; 172 | } 173 | } 174 | else 175 | { 176 | record = _records + (recordIndex * _componentSize); 177 | } 178 | 179 | *(int*) record = entityIndex; 180 | _recordsCount += 1; 181 | _recordsLast = record; 182 | entities[entityIndex] = record; 183 | 184 | NotifyAfterChanges(entityIndex, true); 185 | } 186 | 187 | *(TValue*) (record + fieldOffset) = value; 188 | } 189 | 190 | public unsafe bool Remove(int entityIndex) 191 | { 192 | var entities = _entities; 193 | ref var record = ref entities[entityIndex]; 194 | if (record == null) 195 | { 196 | // Record already removed 197 | return false; 198 | } 199 | 200 | NotifyBeforeChanges(entityIndex, false); 201 | 202 | var lastRecord = _recordsLast; 203 | var componentSize = _componentSize; 204 | if (record == lastRecord) 205 | { 206 | Buffer.MemoryCopy(_zero, record, componentSize, componentSize); 207 | _recordsCount -= 1; 208 | _recordsLast -= componentSize; 209 | record = null; 210 | 211 | NotifyAfterChanges(entityIndex, false); 212 | 213 | return true; 214 | } 215 | 216 | var recordEntityIndex = *(int*) lastRecord; 217 | entities[recordEntityIndex] = record; 218 | 219 | Buffer.MemoryCopy(lastRecord, record, componentSize, componentSize); 220 | Buffer.MemoryCopy(_zero, lastRecord, componentSize, componentSize); 221 | 222 | record = null; 223 | _recordsCount -= 1; 224 | _recordsLast -= componentSize; 225 | 226 | NotifyAfterChanges(entityIndex, false); 227 | 228 | return true; 229 | } 230 | 231 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 232 | private void NotifyBeforeChanges(int entityIndex, bool added) 233 | { 234 | var queriesCount = _updateCallbacksCount; 235 | if (queriesCount == 0) 236 | { 237 | return; 238 | } 239 | 240 | var queries = _updateCallbacks; 241 | for (var i = 0; i < queriesCount; i++) 242 | { 243 | queries[i].BeforeChange(entityIndex, added); 244 | } 245 | } 246 | 247 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 248 | private void NotifyAfterChanges(int entityIndex, bool added) 249 | { 250 | var queriesCount = _updateCallbacksCount; 251 | if (queriesCount == 0) 252 | { 253 | return; 254 | } 255 | 256 | var queries = _updateCallbacks; 257 | for (var i = 0; i < queriesCount; i++) 258 | { 259 | queries[i].AfterChange(entityIndex, added); 260 | } 261 | } 262 | 263 | private unsafe void* ThrowComponentNotFound(int fieldOffset, int entityIndex) 264 | { 265 | var field = Component.Fields.Single(_ => _.Offset == fieldOffset); 266 | throw NUrumiExceptions.ComponentNotFound(entityIndex, Component, field); 267 | } 268 | 269 | internal void AddUpdateCallback(IUpdateCallback callback) 270 | { 271 | var index = _updateCallbacksCount; 272 | if (index == _updateCallbacks.Length) 273 | { 274 | Array.Resize(ref _updateCallbacks, _updateCallbacksCount << 1); 275 | } 276 | 277 | _updateCallbacks[index] = callback; 278 | _updateCallbacksCount += 1; 279 | } 280 | 281 | private static unsafe void FillWithZero(byte* array, int size) 282 | { 283 | var p = array; 284 | for (var i = 0; i < size; i++) 285 | { 286 | *p = 0; 287 | p += 1; 288 | } 289 | } 290 | } 291 | } --------------------------------------------------------------------------------