diff --git a/Source/Demos/Sandbox/GameMain.cs b/Source/Demos/Sandbox/GameMain.cs index a35cc05e..55cda396 100644 --- a/Source/Demos/Sandbox/GameMain.cs +++ b/Source/Demos/Sandbox/GameMain.cs @@ -13,6 +13,7 @@ namespace Sandbox private ComponentMapper _transformMapper; public MyRenderSystem(GraphicsDevice graphicsDevice) + : base(Aspect.All(typeof(Transform2))) { _spriteBatch = new SpriteBatch(graphicsDevice); } @@ -26,7 +27,7 @@ namespace Sandbox { _spriteBatch.Begin(samplerState: SamplerState.PointClamp); - foreach (var entity in World.EntityManager.Entities) + foreach (var entity in GetEntities()) { var transform = _transformMapper.GetComponent(entity); diff --git a/Source/MonoGame.Extended.Entities/Aspect.cs b/Source/MonoGame.Extended.Entities/Aspect.cs new file mode 100644 index 00000000..03c7d63f --- /dev/null +++ b/Source/MonoGame.Extended.Entities/Aspect.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities +{ + public class Aspect + { + private Aspect() + { + AllSet = new BitArray(32); + ExclusionSet = new BitArray(32); + OneSet = new BitArray(32); + } + + public BitArray AllSet { get; } + public BitArray ExclusionSet { get; } + public BitArray OneSet { get; } + + public static Builder All(params Type[] types) + { + return new Builder().All(types); + } + + public static Builder One(params Type[] types) + { + return new Builder().One(types); + } + + public static Builder Exclude(params Type[] types) + { + return new Builder().Exclude(types); + } + + public bool IsInterested(BitArray componentBits) + { + if (!AllSet.IsEmpty() && !componentBits.ContainsAll(AllSet)) + return false; + + if (!ExclusionSet.IsEmpty() && ExclusionSet.Intersects(componentBits)) + return false; + + if (!OneSet.IsEmpty() && !OneSet.Intersects(componentBits)) + return false; + + return true; + } + + public class Builder + { + public Builder() + { + AllTypes = new Bag(); + ExclusionTypes = new Bag(); + OneTypes = new Bag(); + } + + public Bag AllTypes { get; } + public Bag ExclusionTypes { get; } + public Bag OneTypes { get; } + + public Builder All(params Type[] types) + { + foreach (var type in types) + AllTypes.Add(type); + + return this; + } + + public Builder One(params Type[] types) + { + foreach (var type in types) + OneTypes.Add(type); + + return this; + } + + public Builder Exclude(params Type[] types) + { + foreach (var type in types) + ExclusionTypes.Add(type); + + return this; + } + + public Aspect Build(ComponentManager componentManager) + { + var aspect = new Aspect(); + Associate(componentManager, AllTypes, aspect.AllSet); + Associate(componentManager, OneTypes, aspect.OneSet); + Associate(componentManager, ExclusionTypes, aspect.ExclusionSet); + return aspect; + } + + // ReSharper disable once ParameterTypeCanBeEnumerable.Local + private static void Associate(ComponentManager componentManager, Bag types, BitArray bitArray) + { + foreach (var type in types) + { + var id = componentManager.GetComponentTypeId(type); + bitArray.Set(id, true); + } + } + } + } + + +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/BitArrayExtensions.cs b/Source/MonoGame.Extended.Entities/BitArrayExtensions.cs new file mode 100644 index 00000000..2ca0737c --- /dev/null +++ b/Source/MonoGame.Extended.Entities/BitArrayExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; + +namespace MonoGame.Extended.Entities +{ + public static class BitArrayExtensions + { + public static bool IsEmpty(this BitArray bitArray) + { + for (var i = 0; i < bitArray.Length; i++) + { + if (bitArray[i]) + return false; + } + + return true; + } + + public static bool ContainsAll(this BitArray bitArray, BitArray other) + { + var otherBitsLength = other.Length; + var bitsLength = bitArray.Length; + + for (var i = bitsLength; i < otherBitsLength; i++) + { + if (other[i]) + return false; + } + + var s = Math.Min(bitsLength, otherBitsLength); + + for (var i = 0; s > i; i++) + { + if ((bitArray[i] & other[i]) != other[i]) + return false; + } + + return true; + } + + public static bool Intersects(this BitArray bitArray, BitArray other) + { + var s = Math.Min(bitArray.Length, other.Length); + + for (var i = 0; s > i; i++) + { + if (bitArray[i] & other[i]) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/ComponentManager.cs b/Source/MonoGame.Extended.Entities/ComponentManager.cs index c8441be1..1caf5653 100644 --- a/Source/MonoGame.Extended.Entities/ComponentManager.cs +++ b/Source/MonoGame.Extended.Entities/ComponentManager.cs @@ -9,6 +9,7 @@ namespace MonoGame.Extended.Entities public class ComponentManager : UpdateSystem { public ComponentManager() + : base(Aspect.All()) { _mappers = new Bag(); _componentTypes = new Dictionary(); @@ -17,10 +18,9 @@ namespace MonoGame.Extended.Entities private readonly Bag _mappers; private readonly Dictionary _componentTypes; - private ComponentMapper CreateMapperForType() + private ComponentMapper CreateMapperForType(int id) where T : class { - var id = _mappers.Count; var mapper = new ComponentMapper(id); _mappers[id] = mapper; return mapper; @@ -29,12 +29,22 @@ namespace MonoGame.Extended.Entities public ComponentMapper GetMapper() where T : class { - if (_componentTypes.TryGetValue(typeof(T), out var id)) + var id = GetComponentTypeId(typeof(T)); + + if (_mappers[id] != null) return _mappers[id] as ComponentMapper; - var mapper = CreateMapperForType(); - _componentTypes.Add(typeof(T), mapper.Id); - return mapper; + return CreateMapperForType(id); + } + + public int GetComponentTypeId(Type type) + { + if (_componentTypes.TryGetValue(type, out var id)) + return id; + + id = _componentTypes.Count; + _componentTypes.Add(type, id); + return id; } public override void Initialize(ComponentManager componentManager) diff --git a/Source/MonoGame.Extended.Entities/ComponentMapper.cs b/Source/MonoGame.Extended.Entities/ComponentMapper.cs index b8a18ce7..cd85c84a 100644 --- a/Source/MonoGame.Extended.Entities/ComponentMapper.cs +++ b/Source/MonoGame.Extended.Entities/ComponentMapper.cs @@ -24,7 +24,6 @@ namespace MonoGame.Extended.Entities Components = new Bag(); } - public int Id { get; } public Bag Components { get; } public void CreateComponent(int entityId, T component) diff --git a/Source/MonoGame.Extended.Entities/EntityManager.cs b/Source/MonoGame.Extended.Entities/EntityManager.cs index 1636c0ba..93de5238 100644 --- a/Source/MonoGame.Extended.Entities/EntityManager.cs +++ b/Source/MonoGame.Extended.Entities/EntityManager.cs @@ -7,6 +7,7 @@ namespace MonoGame.Extended.Entities public class EntityManager : UpdateSystem { public EntityManager(ComponentManager componentManager) + : base(Aspect.All()) { _componentManager = componentManager; Entities = new Bag(128); diff --git a/Source/MonoGame.Extended.Entities/EntityWorld.cs b/Source/MonoGame.Extended.Entities/EntityWorld.cs index 3f808bb4..bd8c620e 100644 --- a/Source/MonoGame.Extended.Entities/EntityWorld.cs +++ b/Source/MonoGame.Extended.Entities/EntityWorld.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using MonoGame.Extended.Collections; using MonoGame.Extended.Entities.Systems; diff --git a/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs b/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs index 2785d2b1..30d8f295 100644 --- a/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs +++ b/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs @@ -1,8 +1,26 @@ -namespace MonoGame.Extended.Entities.Systems +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities.Systems { public abstract class BaseSystem { + private readonly Aspect.Builder _aspectBuilder; + + protected BaseSystem(Aspect.Builder aspect) + { + _aspectBuilder = aspect; + } + + protected Aspect Aspect { get; private set; } + public EntityWorld World { get; internal set; } + public abstract void Initialize(ComponentManager componentManager); + + protected Bag GetEntities() + { + var aspect = _aspectBuilder.Build(World.ComponentManager); + return World.EntityManager.Entities; + } } } \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/Systems/DrawSystem.cs b/Source/MonoGame.Extended.Entities/Systems/DrawSystem.cs index 517a742b..f164a47c 100644 --- a/Source/MonoGame.Extended.Entities/Systems/DrawSystem.cs +++ b/Source/MonoGame.Extended.Entities/Systems/DrawSystem.cs @@ -4,7 +4,8 @@ namespace MonoGame.Extended.Entities.Systems { public abstract class DrawSystem : BaseSystem { - protected DrawSystem() + protected DrawSystem(Aspect.Builder aspect) + : base(aspect) { } diff --git a/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs b/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs index c1757c86..f4c18d38 100644 --- a/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs +++ b/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs @@ -4,7 +4,8 @@ namespace MonoGame.Extended.Entities.Systems { public abstract class UpdateSystem : BaseSystem { - protected UpdateSystem() + protected UpdateSystem(Aspect.Builder aspect) + : base(aspect) { } diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs new file mode 100644 index 00000000..9f76dc56 --- /dev/null +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs @@ -0,0 +1,69 @@ +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Sprites; +using Xunit; + +namespace MonoGame.Extended.Entities.Tests +{ + public class AspectBuilderTests + { + [Fact] + public void MatchAllTypes() + { + var builder = new Aspect.Builder() + .All(typeof(Transform2), typeof(Sprite)); + + Assert.Equal(2, builder.AllTypes.Count); + Assert.Contains(typeof(Transform2), builder.AllTypes); + Assert.Contains(typeof(Sprite), builder.AllTypes); + } + + [Fact] + public void MatchAllTypesIsEmpty() + { + var builder = new Aspect.Builder() + .All(); + + Assert.Empty(builder.AllTypes); + Assert.Empty(builder.OneTypes); + Assert.Empty(builder.ExclusionTypes); + } + + [Fact] + public void MatchOneOfType() + { + var builder = new Aspect.Builder() + .One(typeof(Transform2), typeof(Sprite)); + + Assert.Equal(2, builder.OneTypes.Count); + Assert.Contains(typeof(Transform2), builder.OneTypes); + Assert.Contains(typeof(Sprite), builder.OneTypes); + } + + [Fact] + public void ExcludeTypes() + { + var builder = new Aspect.Builder() + .Exclude(typeof(Transform2), typeof(Sprite)); + + Assert.Equal(2, builder.ExclusionTypes.Count); + Assert.Contains(typeof(Transform2), builder.ExclusionTypes); + Assert.Contains(typeof(Sprite), builder.ExclusionTypes); + } + + [Fact] + public void BuildAspect() + { + var componentManager = new ComponentManager(); + var builder = new Aspect.Builder() + .All(typeof(Transform2), typeof(Sprite)) + .One(typeof(string)) + .Exclude(typeof(Texture2D)); + + var aspect = builder.Build(componentManager); + + Assert.NotEmpty(aspect.AllSet); + Assert.NotEmpty(aspect.OneSet); + Assert.NotEmpty(aspect.ExclusionSet); + } + } +} \ No newline at end of file diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/AspectTests.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/AspectTests.cs new file mode 100644 index 00000000..8917524e --- /dev/null +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/AspectTests.cs @@ -0,0 +1,88 @@ +using System.Collections; +using MonoGame.Extended.Sprites; +using Xunit; + +namespace MonoGame.Extended.Entities.Tests +{ + public class DummyComponent + { + } + + public class AspectTests + { + private readonly ComponentManager _componentManager; + private readonly BitArray _entityA; + private readonly BitArray _entityB; + + public AspectTests() + { + _componentManager = new ComponentManager(); + _entityA = new BitArray(3) + { + [_componentManager.GetComponentTypeId(typeof(Transform2))] = true, + [_componentManager.GetComponentTypeId(typeof(Sprite))] = true, + [_componentManager.GetComponentTypeId(typeof(DummyComponent))] = true + }; + _entityB = new BitArray(3) + { + [_componentManager.GetComponentTypeId(typeof(Transform2))] = true, + [_componentManager.GetComponentTypeId(typeof(Sprite))] = true, + }; + } + + [Fact] + public void EmptyAspectMatchesAllComponents() + { + var componentManager = new ComponentManager(); + var emptyAspect = Aspect.All() + .Build(componentManager); + + Assert.True(emptyAspect.IsInterested(_entityA)); + Assert.True(emptyAspect.IsInterested(_entityB)); + } + + [Fact] + public void IsInterestedInAllComponents() + { + var allAspect = Aspect + .All(typeof(Sprite), typeof(Transform2), typeof(DummyComponent)) + .Build(_componentManager); + + Assert.True(allAspect.IsInterested(_entityA)); + Assert.False(allAspect.IsInterested(_entityB)); + } + + [Fact] + public void IsInterestedInEitherOneOfTheComponents() + { + var eitherOneAspect = Aspect + .One(typeof(Transform2), typeof(DummyComponent)) + .Build(_componentManager); + + Assert.True(eitherOneAspect.IsInterested(_entityA)); + Assert.True(eitherOneAspect.IsInterested(_entityB)); + } + + [Fact] + public void IsInterestedInJustOneComponent() + { + var oneAspect = Aspect + .One(typeof(DummyComponent)) + .Build(_componentManager); + + Assert.True(oneAspect.IsInterested(_entityA)); + Assert.False(oneAspect.IsInterested(_entityB)); + } + + [Fact] + public void IsInterestedInExcludingOneComponent() + { + var oneAspect = Aspect + .Exclude(typeof(DummyComponent)) + .Build(_componentManager); + + Assert.False(oneAspect.IsInterested(_entityA)); + Assert.True(oneAspect.IsInterested(_entityB)); + } + } +} \ No newline at end of file diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs new file mode 100644 index 00000000..0802dfd0 --- /dev/null +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs @@ -0,0 +1,32 @@ +using MonoGame.Extended.Sprites; +using Xunit; + +namespace MonoGame.Extended.Entities.Tests +{ + public class ComponentManagerTests + { + [Fact] + public void GetMapperForType() + { + var componentManager = new ComponentManager(); + var transformMapper = componentManager.GetMapper(); + var spriteMapper = componentManager.GetMapper(); + + Assert.IsType>(transformMapper); + Assert.IsType>(spriteMapper); + Assert.Equal(0, transformMapper.Id); + Assert.Equal(1, spriteMapper.Id); + Assert.Same(spriteMapper, componentManager.GetMapper()); + } + + [Fact] + public void GetComponentTypeId() + { + var componentManager = new ComponentManager(); + + Assert.Equal(0, componentManager.GetComponentTypeId(typeof(Transform2))); + Assert.Equal(1, componentManager.GetComponentTypeId(typeof(Sprite))); + Assert.Equal(0, componentManager.GetComponentTypeId(typeof(Transform2))); + } + } +} \ No newline at end of file diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs index 346170e9..3757bd34 100644 --- a/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs @@ -1,19 +1,7 @@ -using System; -using MonoGame.Extended.Entities.Systems; -using Xunit; +using Xunit; namespace MonoGame.Extended.Entities.Tests { - public class ComponentManagerTests - { - [Fact] - public void CreateComponentManager() - { - var manager = new ComponentManager(); - throw new NotImplementedException(); - } - } - public class ComponentMapperTests { [Fact] diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateBasic.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateBasic.cs index b2b48f9d..3f96a7fd 100644 --- a/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateBasic.cs +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateBasic.cs @@ -1,5 +1,4 @@ -using MonoGame.Extended.Entities; -using MonoGame.Extended.Entities.Legacy; +using MonoGame.Extended.Entities.Legacy; namespace MonoGame.Extended.Gui.Tests.Implementation { diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateUsingManager.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateUsingManager.cs index 8d70553b..b1f5ee06 100644 --- a/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateUsingManager.cs +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/Implementation/EntityTemplateUsingManager.cs @@ -1,5 +1,4 @@ -using MonoGame.Extended.Entities; -using MonoGame.Extended.Entities.Legacy; +using MonoGame.Extended.Entities.Legacy; namespace MonoGame.Extended.Gui.Tests.Implementation {