diff --git a/Source/Demos/Sandbox/Components/Raindrop.cs b/Source/Demos/Sandbox/Components/Raindrop.cs new file mode 100644 index 00000000..db5f113b --- /dev/null +++ b/Source/Demos/Sandbox/Components/Raindrop.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace Sandbox.Components +{ + public class Raindrop + { + public Vector2 Velocity; + } +} \ No newline at end of file diff --git a/Source/Demos/Sandbox/GameMain.cs b/Source/Demos/Sandbox/GameMain.cs index df304bfd..44d258c7 100644 --- a/Source/Demos/Sandbox/GameMain.cs +++ b/Source/Demos/Sandbox/GameMain.cs @@ -3,85 +3,11 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MonoGame.Extended; using MonoGame.Extended.Entities; -using MonoGame.Extended.Entities.Systems; +using Sandbox.Components; +using Sandbox.Systems; namespace Sandbox { - public class Raindrop - { - public Vector2 Velocity; - } - - public class RainfallSystem : UpdateSystem - { - private ComponentMapper _transformMapper; - private ComponentMapper _raindropMapper; - - public RainfallSystem() - : base(Aspect.All(typeof(Transform2), typeof(Raindrop))) - { - } - - public override void Initialize(ComponentManager componentManager) - { - _transformMapper = componentManager.GetMapper(); - _raindropMapper = componentManager.GetMapper(); - } - - public override void Update(GameTime gameTime) - { - var elapsedSeconds = gameTime.GetElapsedSeconds(); - - foreach (var entity in GetEntities()) - { - var transform = _transformMapper.Get(entity); - var raindrop = _raindropMapper.Get(entity); - - raindrop.Velocity += new Vector2(0, 10) * elapsedSeconds; - transform.Position += raindrop.Velocity * elapsedSeconds; - - if (transform.Position.Y >= 480) - transform.Position = new Vector2(transform.Position.X, 0); - } - } - } - - public class MyRenderSystem : DrawSystem - { - private readonly GraphicsDevice _graphicsDevice; - private readonly SpriteBatch _spriteBatch; - private ComponentMapper _transformMapper; - - public MyRenderSystem(GraphicsDevice graphicsDevice) - : base(Aspect.All(typeof(Transform2))) - { - _graphicsDevice = graphicsDevice; - _spriteBatch = new SpriteBatch(graphicsDevice); - } - - public override void Initialize(ComponentManager componentManager) - { - _transformMapper = componentManager.GetMapper(); - } - - public override void Draw(GameTime gameTime) - { - _graphicsDevice.Clear(Color.Black); - - _spriteBatch.Begin(samplerState: SamplerState.PointClamp); - - foreach (var entity in GetEntities()) - { - var transform = _transformMapper.Get(entity); - - _spriteBatch.FillRectangle(transform.Position, new Size2(3, 3), Color.LightBlue); - } - - _spriteBatch.End(); - } - - } - public class MainGame : Game { // ReSharper disable once NotAccessedField.Local @@ -100,7 +26,7 @@ namespace Sandbox { _spriteBatch = new SpriteBatch(GraphicsDevice); _world = new EntityWorld(); - _world.RegisterSystem(new MyRenderSystem(GraphicsDevice)); + _world.RegisterSystem(new RenderSystem(GraphicsDevice)); _world.RegisterSystem(new RainfallSystem()); var random = new FastRandom(); diff --git a/Source/Demos/Sandbox/Systems/RainfallSystem.cs b/Source/Demos/Sandbox/Systems/RainfallSystem.cs new file mode 100644 index 00000000..6a9cbcd2 --- /dev/null +++ b/Source/Demos/Sandbox/Systems/RainfallSystem.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended; +using MonoGame.Extended.Entities; +using MonoGame.Extended.Entities.Systems; +using Sandbox.Components; + +namespace Sandbox.Systems +{ + public class RainfallSystem : EntityUpdateSystem + { + private ComponentMapper _transformMapper; + private ComponentMapper _raindropMapper; + + public RainfallSystem() + : base(Aspect.All(typeof(Transform2), typeof(Raindrop))) + { + } + + public override void Initialize(IComponentMapperService mapperService) + { + _transformMapper = mapperService.GetMapper(); + _raindropMapper = mapperService.GetMapper(); + } + + public override void Update(GameTime gameTime) + { + var elapsedSeconds = gameTime.GetElapsedSeconds(); + + foreach (var entity in ActiveEntities) + { + var transform = _transformMapper.Get(entity); + var raindrop = _raindropMapper.Get(entity); + + raindrop.Velocity += new Vector2(0, 10) * elapsedSeconds; + transform.Position += raindrop.Velocity * elapsedSeconds; + + if (transform.Position.Y >= 480) + transform.Position = new Vector2(transform.Position.X, 0); + } + } + } +} \ No newline at end of file diff --git a/Source/Demos/Sandbox/Systems/RenderSystem.cs b/Source/Demos/Sandbox/Systems/RenderSystem.cs new file mode 100644 index 00000000..89f0e141 --- /dev/null +++ b/Source/Demos/Sandbox/Systems/RenderSystem.cs @@ -0,0 +1,43 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended; +using MonoGame.Extended.Entities; +using MonoGame.Extended.Entities.Systems; + +namespace Sandbox.Systems +{ + public class RenderSystem : EntityDrawSystem + { + private readonly GraphicsDevice _graphicsDevice; + private readonly SpriteBatch _spriteBatch; + private ComponentMapper _transformMapper; + + public RenderSystem(GraphicsDevice graphicsDevice) + : base(Aspect.All(typeof(Transform2))) + { + _graphicsDevice = graphicsDevice; + _spriteBatch = new SpriteBatch(graphicsDevice); + } + + public override void Initialize(IComponentMapperService mapperService) + { + _transformMapper = mapperService.GetMapper(); + } + + public override void Draw(GameTime gameTime) + { + _graphicsDevice.Clear(Color.Black); + _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + + foreach (var entity in ActiveEntities) + { + var transform = _transformMapper.Get(entity); + + _spriteBatch.FillRectangle(transform.Position, new Size2(3, 3), Color.LightBlue); + } + + _spriteBatch.End(); + } + + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/ComponentManager.cs b/Source/MonoGame.Extended.Entities/ComponentManager.cs index 1caf5653..df093a5f 100644 --- a/Source/MonoGame.Extended.Entities/ComponentManager.cs +++ b/Source/MonoGame.Extended.Entities/ComponentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using Microsoft.Xna.Framework; using MonoGame.Extended.Collections; @@ -6,23 +7,27 @@ using MonoGame.Extended.Entities.Systems; namespace MonoGame.Extended.Entities { - public class ComponentManager : UpdateSystem + public interface IComponentMapperService + { + ComponentMapper GetMapper() where T : class; + } + + public class ComponentManager : UpdateSystem, IComponentMapperService { public ComponentManager() - : base(Aspect.All()) { - _mappers = new Bag(); + _componentMappers = new Bag(); _componentTypes = new Dictionary(); } - private readonly Bag _mappers; + private readonly Bag _componentMappers; private readonly Dictionary _componentTypes; private ComponentMapper CreateMapperForType(int id) where T : class { var mapper = new ComponentMapper(id); - _mappers[id] = mapper; + _componentMappers[id] = mapper; return mapper; } @@ -31,8 +36,8 @@ namespace MonoGame.Extended.Entities { var id = GetComponentTypeId(typeof(T)); - if (_mappers[id] != null) - return _mappers[id] as ComponentMapper; + if (_componentMappers[id] != null) + return _componentMappers[id] as ComponentMapper; return CreateMapperForType(id); } @@ -47,9 +52,17 @@ namespace MonoGame.Extended.Entities return id; } - public override void Initialize(ComponentManager componentManager) + public BitArray GetComponentBits(int entityId) { - // TODO : Okay this is weird. + var bits = new BitArray(16); + + for (var componentId = 0; componentId < _componentMappers.Count; componentId++) + { + if (_componentMappers[componentId].Has(entityId)) + bits[componentId] = true; + } + + return bits; } public override void Update(GameTime gameTime) diff --git a/Source/MonoGame.Extended.Entities/ComponentMapper.cs b/Source/MonoGame.Extended.Entities/ComponentMapper.cs index caa371ff..6e69d99d 100644 --- a/Source/MonoGame.Extended.Entities/ComponentMapper.cs +++ b/Source/MonoGame.Extended.Entities/ComponentMapper.cs @@ -13,6 +13,7 @@ namespace MonoGame.Extended.Entities public int Id { get; } public Type ComponentType { get; } + public abstract bool Has(int entityId); } public class ComponentMapper : ComponentMapper @@ -41,8 +42,11 @@ namespace MonoGame.Extended.Entities return Components[entityId]; } - public bool Has(int entityId) + public override bool Has(int entityId) { + if (entityId >= Components.Count) + return false; + return Components[entityId] != null; } diff --git a/Source/MonoGame.Extended.Entities/Entity.cs b/Source/MonoGame.Extended.Entities/Entity.cs index 748fc18e..6f26b765 100644 --- a/Source/MonoGame.Extended.Entities/Entity.cs +++ b/Source/MonoGame.Extended.Entities/Entity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; namespace MonoGame.Extended.Entities { @@ -15,6 +16,7 @@ namespace MonoGame.Extended.Entities } public int Id { get; } + public BitArray ComponentBits => _componentManager.GetComponentBits(Id); public void Attach(T component) where T : class diff --git a/Source/MonoGame.Extended.Entities/EntityManager.cs b/Source/MonoGame.Extended.Entities/EntityManager.cs index 51abbccc..d6912f2b 100644 --- a/Source/MonoGame.Extended.Entities/EntityManager.cs +++ b/Source/MonoGame.Extended.Entities/EntityManager.cs @@ -8,7 +8,6 @@ namespace MonoGame.Extended.Entities public class EntityManager : UpdateSystem { public EntityManager(ComponentManager componentManager) - : base(Aspect.All()) { _componentManager = componentManager; Entities = new Bag(128); @@ -19,17 +18,22 @@ namespace MonoGame.Extended.Entities public Bag Entities { get; } + public event EventHandler EntityAdded; + public event EventHandler EntityRemoved; + public Entity CreateEntity() { // TODO: Recycle dead entites var id = _nextId++; var entity = new Entity(id, this, _componentManager); Entities[id] = entity; + EntityAdded?.Invoke(this, entity); return entity; } public void DestroyEntity(int entityId) { + EntityRemoved?.Invoke(this, entityId); throw new NotImplementedException(); //Entities[entityId] = null; } @@ -42,9 +46,5 @@ namespace MonoGame.Extended.Entities public override void Update(GameTime gameTime) { } - - public override void Initialize(ComponentManager componentManager) - { - } } } \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/EntitySubscription.cs b/Source/MonoGame.Extended.Entities/EntitySubscription.cs new file mode 100644 index 00000000..2ca76230 --- /dev/null +++ b/Source/MonoGame.Extended.Entities/EntitySubscription.cs @@ -0,0 +1,58 @@ +using System; +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities +{ + public class EntitySubscription : IDisposable + { + private readonly Bag _activeEntities; + private readonly EntityManager _entityManager; + private readonly Aspect _aspect; + private readonly bool _rebuildActives; + + public EntitySubscription(EntityManager entityManager, Aspect aspect) + { + _entityManager = entityManager; + _aspect = aspect; + _activeEntities = new Bag(entityManager.Entities.Capacity); + _rebuildActives = true; + + _entityManager.EntityRemoved += OnEntityRemoved; + _entityManager.EntityAdded += OnEntityAdded; + } + + // TODO: It's a bit heavy handed to rebuild the actives every time one entity is added or removed. + private void OnEntityAdded(object sender, Entity e) => RebuildActives(); + private void OnEntityRemoved(object sender, int e) => RebuildActives(); + + public void Dispose() + { + } + + public Bag ActiveEntities + { + get + { + if (_rebuildActives) + RebuildActives(); + + return _activeEntities; + } + } + + private void RebuildActives() + { + var count = 0; + _activeEntities.Clear(); + + foreach (var entity in _entityManager.Entities) + { + if (_aspect.IsInterested(entity.ComponentBits)) + { + _activeEntities[count] = entity.Id; + count++; + } + } + } + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/EntityWorld.cs b/Source/MonoGame.Extended.Entities/EntityWorld.cs index 81966a9f..cd2ae4af 100644 --- a/Source/MonoGame.Extended.Entities/EntityWorld.cs +++ b/Source/MonoGame.Extended.Entities/EntityWorld.cs @@ -6,12 +6,8 @@ namespace MonoGame.Extended.Entities { public class EntityWorld : SimpleDrawableGameComponent { - // TODO: Make these private again - public EntityManager EntityManager { get; } - public ComponentManager ComponentManager { get; } - private readonly Bag _updateSystems; - private readonly Bag _drawSystems; + private readonly Bag _drawSystems; public EntityWorld() { @@ -23,15 +19,20 @@ namespace MonoGame.Extended.Entities ComponentManager, EntityManager }; - _drawSystems = new Bag(); + _drawSystems = new Bag(); } + internal EntityManager EntityManager { get; } + internal ComponentManager ComponentManager { get; } + + public Bag AllEntities => EntityManager.Entities; + // TODO: Move this to world configuration - public void RegisterSystem(BaseSystem system) + public void RegisterSystem(UpdateSystem system) { switch (system) { - case DrawSystem drawSystem: + case EntityDrawSystem drawSystem: _drawSystems.Add(drawSystem); break; case UpdateSystem updateSystem: @@ -39,8 +40,16 @@ namespace MonoGame.Extended.Entities break; } - system.World = this; - system.Initialize(ComponentManager); + if (system is EntityUpdateSystem entitySystem) + { + entitySystem.World = this; + entitySystem.Initialize(ComponentManager); + } + } + + public Entity GetEntity(int entityId) + { + return AllEntities[entityId]; } public Entity CreateEntity() diff --git a/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs b/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs deleted file mode 100644 index 30d8f295..00000000 --- a/Source/MonoGame.Extended.Entities/Systems/BaseSystem.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index f164a47c..00000000 --- a/Source/MonoGame.Extended.Entities/Systems/DrawSystem.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace MonoGame.Extended.Entities.Systems -{ - public abstract class DrawSystem : BaseSystem - { - protected DrawSystem(Aspect.Builder aspect) - : base(aspect) - { - } - - public abstract void Draw(GameTime gameTime); - } -} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs b/Source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs new file mode 100644 index 00000000..5fca617e --- /dev/null +++ b/Source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs @@ -0,0 +1,19 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityDrawSystem : EntityUpdateSystem + { + protected EntityDrawSystem(Aspect.Builder aspect) + : base(aspect) + { + } + + public sealed override void Update(GameTime gameTime) + { + Draw(gameTime); + } + + public abstract void Draw(GameTime gameTime); + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs b/Source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs new file mode 100644 index 00000000..716648dc --- /dev/null +++ b/Source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityProcessingSystem : EntityUpdateSystem + { + protected EntityProcessingSystem(Aspect.Builder aspectBuilder) + : base(aspectBuilder) + { + } + + public override void Update(GameTime gameTime) + { + Begin(); + + foreach (var entityId in ActiveEntities) + Process(entityId); + + End(); + } + + public virtual void Begin() { } + public abstract void Process(int entityId); + public virtual void End() { } + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs b/Source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs new file mode 100644 index 00000000..d42c8acd --- /dev/null +++ b/Source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs @@ -0,0 +1,30 @@ +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityUpdateSystem : UpdateSystem + { + protected EntityUpdateSystem(Aspect.Builder aspectBuilder) + { + _aspectBuilder = aspectBuilder; + } + + private readonly Aspect.Builder _aspectBuilder; + private EntitySubscription _subscription; + + private EntityWorld _world; + public EntityWorld World + { + get => _world; + internal set + { + _world = value; + _subscription = new EntitySubscription(_world.EntityManager, _aspectBuilder.Build(_world.ComponentManager)); + } + } + + public Bag ActiveEntities => _subscription.ActiveEntities; + + public abstract void Initialize(IComponentMapperService mapperService); + } +} \ No newline at end of file diff --git a/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs b/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs index f4c18d38..5fa8ac8d 100644 --- a/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs +++ b/Source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs @@ -2,10 +2,9 @@ namespace MonoGame.Extended.Entities.Systems { - public abstract class UpdateSystem : BaseSystem + public abstract class UpdateSystem { - protected UpdateSystem(Aspect.Builder aspect) - : base(aspect) + protected UpdateSystem() { } diff --git a/Source/MonoGame.Extended/Collections/Bag.cs b/Source/MonoGame.Extended/Collections/Bag.cs index 64e03ba3..b8473782 100644 --- a/Source/MonoGame.Extended/Collections/Bag.cs +++ b/Source/MonoGame.Extended/Collections/Bag.cs @@ -43,6 +43,7 @@ namespace MonoGame.Extended.Collections public class Bag : IEnumerable { private T[] _items; + private readonly bool _isPrimitive; public int Capacity => _items.Length; public bool IsEmpty => Count == 0; @@ -50,6 +51,7 @@ namespace MonoGame.Extended.Collections public Bag(int capacity = 16) { + _isPrimitive = typeof(T).IsPrimitive; _items = new T[capacity]; } @@ -80,15 +82,20 @@ namespace MonoGame.Extended.Collections public void Clear() { - Array.Clear(_items, 0, Count); Count = 0; + + // non-primitive types are cleared so the garbage collector can release them + if (!_isPrimitive) + Array.Clear(_items, 0, Count); } public bool Contains(T element) { for (var index = Count - 1; index >= 0; --index) + { if (element.Equals(_items[index])) return true; + } return false; } @@ -105,6 +112,7 @@ namespace MonoGame.Extended.Collections public bool Remove(T element) { for (var index = Count - 1; index >= 0; --index) + { if (element.Equals(_items[index])) { --Count; @@ -113,6 +121,7 @@ namespace MonoGame.Extended.Collections return true; } + } return false; } @@ -120,9 +129,13 @@ namespace MonoGame.Extended.Collections public bool RemoveAll(Bag bag) { var isResult = false; + for (var index = bag.Count - 1; index >= 0; --index) + { if (Remove(bag[index])) isResult = true; + } + return isResult; } diff --git a/Source/Tests/MonoGame.Extended.Entities.Tests/EntitySubscriptionTests.cs b/Source/Tests/MonoGame.Extended.Entities.Tests/EntitySubscriptionTests.cs new file mode 100644 index 00000000..1a17d333 --- /dev/null +++ b/Source/Tests/MonoGame.Extended.Entities.Tests/EntitySubscriptionTests.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace MonoGame.Extended.Entities.Tests +{ + public class EntitySubscriptionTests + { + [Fact] + public void Fact() + { + var subscription = new EntitySubscription(); + + } + } +} \ No newline at end of file