From 85f99a565ce7d7164a4d5817bb50f225851089e4 Mon Sep 17 00:00:00 2001 From: Evan Hemsley Date: Thu, 24 Oct 2019 12:48:36 -0700 Subject: [PATCH] adds some convenience methods and doc comments --- encompass-cs/ComponentMessageManager.cs | 11 +- encompass-cs/Engine.cs | 162 ++++++++++++++++- encompass-cs/Engines/Spawner.cs | 4 + encompass-cs/Entity.cs | 4 + .../Exceptions/NoComponentOfTypeException.cs | 25 +++ encompass-cs/IComponent.cs | 3 + encompass-cs/IDrawComponent.cs | 3 + encompass-cs/IMessage.cs | 3 + encompass-cs/Renderers/GeneralRenderer.cs | 4 + encompass-cs/Renderers/OrderedRenderer.cs | 3 + encompass-cs/World.cs | 12 +- encompass-cs/WorldBuilder.cs | 42 +++++ test/EngineTest.cs | 171 ++++++++++++++++-- test/test.csproj | 2 +- 14 files changed, 429 insertions(+), 20 deletions(-) create mode 100644 encompass-cs/Exceptions/NoComponentOfTypeException.cs diff --git a/encompass-cs/ComponentMessageManager.cs b/encompass-cs/ComponentMessageManager.cs index d363e37..c42716b 100644 --- a/encompass-cs/ComponentMessageManager.cs +++ b/encompass-cs/ComponentMessageManager.cs @@ -12,7 +12,7 @@ namespace Encompass private readonly Dictionary componentIDToType = new Dictionary(); private readonly Dictionary componentIDToEntityID = new Dictionary(); - + private readonly Dictionary> componentMessageTypeToExistingComponentIDs = new Dictionary>(); private readonly Dictionary> componentMessageTypeToPendingComponentIDs = new Dictionary>(); private readonly Dictionary> componentMessageTypeToComponentIDs = new Dictionary>(); @@ -136,12 +136,12 @@ namespace Encompass } } - private void RegisterExistingOrPendingComponentMessage(Entity entity, Guid componentID, TComponent component) where TComponent: struct, IComponent + private void RegisterExistingOrPendingComponentMessage(Entity entity, Guid componentID, TComponent component) where TComponent : struct, IComponent { componentIDToComponent[componentID] = component; componentIDToEntityID[componentID] = entity.ID; componentIDToType[componentID] = typeof(TComponent); - + if (!componentMessageTypeToComponentIDs.ContainsKey(typeof(TComponent))) { componentMessageTypeToComponentIDs.Add(typeof(TComponent), new HashSet()); @@ -163,7 +163,7 @@ namespace Encompass return Enumerable.Empty<(Guid, TComponent)>(); } - internal IEnumerable<(Guid, TComponent)> ReadExistingComponentsByType() where TComponent: struct, IComponent + internal IEnumerable<(Guid, TComponent)> ReadExistingComponentsByType() where TComponent : struct, IComponent { if (componentMessageTypeToExistingComponentIDs.TryGetValue(typeof(TComponent), out HashSet idSet)) { @@ -187,16 +187,19 @@ namespace Encompass internal (Guid, TComponent) ReadFirstExistingOrPendingComponentByType() where TComponent : struct, IComponent { + if (!SomeExistingOrPendingComponent()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); } return ReadExistingAndPendingComponentsByType().First(); } internal (Guid, TComponent) ReadFirstExistingComponentByType() where TComponent : struct, IComponent { + if (!SomeExistingComponent()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); } return ReadExistingComponentsByType().First(); } internal (Guid, TComponent) ReadFirstPendingComponentByType() where TComponent : struct, IComponent { + if (!SomeExistingComponent()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); } return ReadPendingComponentsByType().First(); } diff --git a/encompass-cs/Engine.cs b/encompass-cs/Engine.cs index cca223e..17255c3 100644 --- a/encompass-cs/Engine.cs +++ b/encompass-cs/Engine.cs @@ -6,6 +6,11 @@ using Encompass.Exceptions; namespace Encompass { + /// + /// Engines are the Encompass notion of an ECS System. + /// They are responsible for reading the World state, reading messages, emitting messages, and creating or mutating Entities and Components. + /// Engines run once per World Update. + /// public abstract class Engine { internal readonly HashSet sendTypes = new HashSet(); @@ -77,23 +82,42 @@ namespace Encompass this.componentMessageManager = componentMessageManager; } + /// + /// Runs once per World update with the calculated delta-time. + /// + /// The time in seconds that has elapsed since the previous frame. public abstract void Update(double dt); + /// + /// Creates and returns a new empty Entity. + /// protected Entity CreateEntity() { return entityManager.CreateEntity(); } + /// + /// Returns true if an Entity with the specified ID exists. + /// protected bool EntityExists(Guid entityID) { return entityManager.EntityExists(entityID); } + /// + /// Returns the Entity with the specified ID. + /// + /// + /// Thrown when an Entity with the given ID does not exist. + /// protected Entity GetEntity(Guid entityID) { return entityManager.GetEntity(entityID); } + /// + /// Returns the Entity ID associated with the specified Component Type and ID. + /// protected Guid GetEntityIDByComponentID(Guid componentID) where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); @@ -107,11 +131,37 @@ namespace Encompass return componentMessageManager.GetEntityIDByComponentID(componentID); } + /// + /// Returns the Entity associated with the specified Component Type and ID. + /// protected Entity GetEntityByComponentID(Guid componentID) where TComponent : struct, IComponent { return GetEntity(GetEntityIDByComponentID(componentID)); } + /// + /// Returns an Entity containing the specified Component type. + /// + protected Entity ReadEntity() where TComponent : struct, IComponent + { + var (id, component) = ReadComponent(); + return GetEntityByComponentID(id); + } + + /// + /// Returns all Entities containing the specified Component type. + /// + protected IEnumerable ReadEntities() where TComponent : struct, IComponent + { + foreach (var (id, component) in ReadComponents()) + { + yield return GetEntityByComponentID(id); + } + } + + /// + /// Returns the Component struct with the specified Component Type and ID. + /// protected TComponent GetComponentByID(Guid componentID) where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); @@ -142,7 +192,10 @@ namespace Encompass return GetEntity(componentManager.GetEntityIDByComponentID(componentID)); } - protected IEnumerable> ReadComponents() where TComponent : struct, IComponent + /// + /// Returns all of the Components with the specified Component Type. + /// + protected IEnumerable<(Guid, TComponent)> ReadComponents() where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage)); @@ -164,7 +217,10 @@ namespace Encompass } } - protected ValueTuple ReadComponent() where TComponent : struct, IComponent + /// + /// Returns a Component with the specified Component Type and ID. If multiples exist, an arbitrary Component is returned. + /// + protected (Guid, TComponent) ReadComponent() where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage)); @@ -186,6 +242,9 @@ namespace Encompass } } + /// + /// Returns true if any Component with the specified Component Type exists. + /// protected bool SomeComponent() where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); @@ -208,7 +267,16 @@ namespace Encompass } } - protected ValueTuple GetComponent(Entity entity) where TComponent : struct, IComponent + /// + /// Returns a Component with the specified Type that exists on the Entity. + /// + /// + /// Thrown when the Entity does not have a Component of the specified Type + /// + /// + /// Thrown when the Engine does not declare that it reads the given Component Type. + /// + protected (Guid, TComponent) GetComponent(Entity entity) where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage)); @@ -241,6 +309,12 @@ namespace Encompass } } + /// + /// Returns true if the Entity has a Component of the given Type. + /// + /// + /// Thrown when the Engine does not declare that is Reads the given Component Type. + /// protected bool HasComponent(Entity entity) where TComponent : struct, IComponent { var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage)); @@ -264,6 +338,12 @@ namespace Encompass } } + /// + /// Sets Component data for the specified Component Type on the specified Entity. If Component data for this Type already existed on the Entity, the component data is overwritten. + /// + /// + /// Thrown when the Engine does not declare that it Writes the given Component Type. + /// protected Guid SetComponent(Entity entity, TComponent component) where TComponent : struct, IComponent { var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0; @@ -288,11 +368,22 @@ namespace Encompass return componentID; } + /// + /// Overwrites Component struct data associated with the specified Component ID. + /// protected Guid SetComponent(Guid componentID, TComponent component) where TComponent : struct, IComponent { return SetComponent(GetEntityByComponentID(componentID), component); } + /// + /// Sets Draw Component data for the specified Component Type on the specified Entity. + /// This method must be used for the Draw Component to be readable by an OrderedRenderer. + /// If Component data for this Type already existed on the Entity, the component data is overwritten. + /// + /// + /// Thrown when the Engine does not declare that it Writes the given Component Type. + /// protected Guid SetDrawComponent(Entity entity, TComponent component, int layer = 0) where TComponent : struct, IComponent, IDrawComponent { var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0; @@ -317,6 +408,12 @@ namespace Encompass return componentID; } + /// + /// Sends a Message. + /// + /// + /// Thrown when the Engine does not declare that it Sends the Message Type. + /// protected void SendMessage(TMessage message) where TMessage : struct, IMessage { if (!sendTypes.Contains(typeof(TMessage))) @@ -327,6 +424,10 @@ namespace Encompass messageManager.AddMessage(message); } + /// + /// Sends a message after the specified number of seconds. + /// + /// The time in seconds that will elapse before the message is sent. protected void SendMessageDelayed(TMessage message, double time) where TMessage : struct, IMessage { messageManager.AddMessageDelayed(message, time); @@ -355,6 +456,12 @@ namespace Encompass componentMessageManager.AddPendingComponentMessage(message); } + /// + /// Reads all messages of the specified Type. + /// + /// + /// Thrown when the Engine does not declare that it Receives the specified Message Type. + /// protected IEnumerable ReadMessages() where TMessage : struct, IMessage { if (!receiveTypes.Contains(typeof(TMessage))) @@ -365,11 +472,23 @@ namespace Encompass return messageManager.GetMessagesByType(); } + /// + /// Reads an arbitrary message of the specified Type. + /// + /// + /// Thrown when the Engine does not declare that it Receives the specified Message Type. + /// protected TMessage ReadMessage() where TMessage : struct, IMessage { return ReadMessages().First(); } + /// + /// Returns true if a Message of the specified Type has been sent this frame. + /// + /// + /// Thrown when the Engine does not declare that it Receives the specified Message Type. + /// protected bool SomeMessage() where TMessage : struct, IMessage { if (!receiveTypes.Contains(typeof(TMessage))) @@ -380,11 +499,48 @@ namespace Encompass return ReadMessages().Any(); } + /// + /// Destroys the Entity with the specified ID. This also removes all of the Components associated with the Entity. + /// Entity destruction takes place after all the Engines have been processed by World Update. + /// protected void Destroy(Guid entityID) { entityManager.MarkForDestroy(entityID); } + /// + /// Destroys the specified Entity. This also removes all of the Components associated with the Entity. + /// Entity destruction takes place after all the Engines have been processed by World Update. + /// + protected void Destroy(Entity entity) + { + entityManager.MarkForDestroy(entity.ID); + } + + /// + /// Destroys an arbitrary Entity containing a Component of the specified Type. + /// Entity destruction takes place after all the Engines have been processed by World Update. + /// + protected void DestroyWith() where TComponent : struct, IComponent + { + Destroy(ReadEntity()); + } + + /// + /// Destroys all Entities containing a Component of the specified Type. + /// Entity destruction takes place after all the Engines have been processed by World Update. + /// + protected void DestroyAllWith() where TComponent : struct, IComponent + { + foreach (var entity in ReadEntities()) + { + Destroy(entity); + } + } + + /// + /// Removes the Component with the specified ID from its Entity. + /// protected void RemoveComponent(Guid componentID) { componentManager.MarkForRemoval(componentID); diff --git a/encompass-cs/Engines/Spawner.cs b/encompass-cs/Engines/Spawner.cs index da359e7..99e35bb 100644 --- a/encompass-cs/Engines/Spawner.cs +++ b/encompass-cs/Engines/Spawner.cs @@ -2,6 +2,10 @@ namespace Encompass.Engines { + /// + /// A Spawner is a special type of Engine that runs a Spawn method in response to each Message it receives. + /// Spawners are useful for organizing the building of new Entities in your game. + /// public abstract class Spawner : Engine where TMessage : struct, IMessage { protected Spawner() : base() diff --git a/encompass-cs/Entity.cs b/encompass-cs/Entity.cs index 080697d..fa584e3 100644 --- a/encompass-cs/Entity.cs +++ b/encompass-cs/Entity.cs @@ -2,6 +2,10 @@ namespace Encompass { + /// + /// An Entity is a structure composed of a unique ID and a collection of Components. + /// An Entity may only have a single Component of any particular Type. + /// public struct Entity : IEquatable { public readonly Guid ID; diff --git a/encompass-cs/Exceptions/NoComponentOfTypeException.cs b/encompass-cs/Exceptions/NoComponentOfTypeException.cs new file mode 100644 index 0000000..2300615 --- /dev/null +++ b/encompass-cs/Exceptions/NoComponentOfTypeException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace Encompass.Exceptions +{ + [Serializable] + internal class NoComponentOfTypeException : Exception + { + public NoComponentOfTypeException() + { + } + + public NoComponentOfTypeException(string message) : base(message) + { + } + + public NoComponentOfTypeException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NoComponentOfTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/encompass-cs/IComponent.cs b/encompass-cs/IComponent.cs index d51b93c..4fcce29 100644 --- a/encompass-cs/IComponent.cs +++ b/encompass-cs/IComponent.cs @@ -1,4 +1,7 @@ namespace Encompass { + /// + /// Structs that implement IComponent are considered to be Components. + /// public interface IComponent { } } diff --git a/encompass-cs/IDrawComponent.cs b/encompass-cs/IDrawComponent.cs index 1d02a11..b77bf94 100644 --- a/encompass-cs/IDrawComponent.cs +++ b/encompass-cs/IDrawComponent.cs @@ -1,4 +1,7 @@ namespace Encompass { + /// + /// Structs that implement IDrawComponent are considered to be DrawComponents. + /// public interface IDrawComponent { } } diff --git a/encompass-cs/IMessage.cs b/encompass-cs/IMessage.cs index fecbfd3..6b76042 100644 --- a/encompass-cs/IMessage.cs +++ b/encompass-cs/IMessage.cs @@ -1,4 +1,7 @@ namespace Encompass { + /// + /// Structs that implement IMessage are considered to be Messages. + /// public interface IMessage { } } diff --git a/encompass-cs/Renderers/GeneralRenderer.cs b/encompass-cs/Renderers/GeneralRenderer.cs index e408d8f..ffface9 100644 --- a/encompass-cs/Renderers/GeneralRenderer.cs +++ b/encompass-cs/Renderers/GeneralRenderer.cs @@ -1,5 +1,9 @@ namespace Encompass { + /// + /// GeneralRenderer is a Renderer which generically reads the game state in order to draw elements to the screen. + /// GeneralRenderers have a layer specified when they are added to the World. + /// public abstract class GeneralRenderer : Renderer { public abstract void Render(); diff --git a/encompass-cs/Renderers/OrderedRenderer.cs b/encompass-cs/Renderers/OrderedRenderer.cs index 2813e90..011a6c2 100644 --- a/encompass-cs/Renderers/OrderedRenderer.cs +++ b/encompass-cs/Renderers/OrderedRenderer.cs @@ -2,6 +2,9 @@ namespace Encompass { + /// + /// OrdereredRenderer provides a structure for the common pattern of wishing to draw a specific DrawComponent at a specific layer. + /// public abstract class OrderedRenderer : Renderer where TComponent : struct, IComponent, IDrawComponent { public abstract void Render(Guid drawComponentID, TComponent drawComponent); diff --git a/encompass-cs/World.cs b/encompass-cs/World.cs index 576c51e..1d8768a 100644 --- a/encompass-cs/World.cs +++ b/encompass-cs/World.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; namespace Encompass { + /// + /// The World is a collection of Engines, Renderers, Entities, Components, and Messages that compose the simulation. + /// public class World { private readonly List enginesInOrder; @@ -28,10 +31,14 @@ namespace Encompass this.renderManager = renderManager; } + /// + /// Drives the simulation. Should be called from your game engine's update loop. + /// + /// The time in seconds that has passed since the previous frame. public void Update(double dt) { messageManager.ProcessDelayedMessages(dt); - + foreach (var engine in enginesInOrder) { engine.Update(dt); @@ -45,6 +52,9 @@ namespace Encompass componentManager.WriteComponents(); } + /// + /// Causes the Renderers to draw. + /// public void Draw() { renderManager.Draw(); diff --git a/encompass-cs/WorldBuilder.cs b/encompass-cs/WorldBuilder.cs index be5d854..f5c0a0a 100644 --- a/encompass-cs/WorldBuilder.cs +++ b/encompass-cs/WorldBuilder.cs @@ -7,6 +7,14 @@ using Encompass.Engines; namespace Encompass { + /// + /// WorldBuilder is used to construct a World from Engines, Renderers, and an initial state of Entities, Components, and Messages. + /// + /// + /// WorldBuilder enforces certain rules about Engine structure. It is forbidden to have messages create cycles between Engines, + /// and no Component may be written by more than one Engine. + /// The WorldBuilder uses Engines and their Message read/emit information to determine a valid ordering of the Engines, which is given to the World. + /// public class WorldBuilder { private readonly List engines = new List(); @@ -35,26 +43,42 @@ namespace Encompass renderManager = new RenderManager(componentManager, drawLayerManager); } + /// + /// Creates and returns a new empty Entity. + /// public Entity CreateEntity() { return entityManager.CreateEntity(); } + /// + /// Specifies that the given Message should be sent immediately on the first World Update. + /// public void SendMessage(TMessage message) where TMessage : struct, IMessage { messageManager.AddMessage(message); } + /// + /// Specifies that the given Message should be sent after the specified number of seconds after the first World Update. + /// public void SendMessageDelayed(TMessage message, double time) where TMessage : struct, IMessage { messageManager.AddMessageDelayed(message, time); } + /// + /// Sets Component data for the specified Component Type on the specified Entity. + /// public Guid SetComponent(Entity entity, TComponent component, int priority = 0) where TComponent : struct, IComponent { return componentManager.MarkComponentForWrite(entity, component, priority); } + /// + /// Sets Draw Component data for the specified Component Type on the specified Entity. + /// This method must be used for the Draw Component to be readable by an OrderedRenderer. + /// public Guid SetDrawComponent(Entity entity, TComponent component, int priority = 0, int layer = 0) where TComponent : struct, IComponent, IDrawComponent { return componentManager.MarkDrawComponentForWrite(entity, component, priority, layer); @@ -66,6 +90,10 @@ namespace Encompass AddEngine((Engine)Activator.CreateInstance(typeof(ComponentMessageEmitter<>).MakeGenericType(componentType))); } + /// + /// Adds the specified Engine to the World. + /// + /// An instance of an Engine. public Engine AddEngine(TEngine engine) where TEngine : Engine { engine.AssignEntityManager(entityManager); @@ -126,6 +154,9 @@ namespace Encompass return engine; } + /// + /// Adds the specified OrderedRenderer to the World. + /// public OrderedRenderer AddOrderedRenderer(OrderedRenderer renderer) where TComponent : struct, IComponent, IDrawComponent { renderer.AssignEntityManager(entityManager); @@ -134,6 +165,12 @@ namespace Encompass return renderer; } + /// + /// Adds the specified GeneralRenderer to the World at the specified layer. + /// Higher layer numbers draw on top of lower layer numbers. + /// + /// An instance of a GeneralRenderer. + /// The layer at which the GeneralRenderer should render. Higher numbers draw over lower numbers. public TRenderer AddGeneralRenderer(TRenderer renderer, int layer) where TRenderer : GeneralRenderer { renderer.AssignEntityManager(entityManager); @@ -164,6 +201,11 @@ namespace Encompass } } + /// + /// Builds the World out of the state specified on the WorldBuilder. + /// Validates and constructs an ordering of the given Engines. + /// + /// An instance of World. public World Build() { BuildEngineGraph(); diff --git a/test/EngineTest.cs b/test/EngineTest.cs index 9f313d2..7da75c0 100644 --- a/test/EngineTest.cs +++ b/test/EngineTest.cs @@ -487,6 +487,38 @@ namespace Tests Assert.That(results, Does.Contain((componentCID, mockComponent))); } + [Receives(typeof(DestroyComponentMessage))] + class DestroyEntityEngine : Engine + { + public override void Update(double dt) + { + foreach (var message in ReadMessages()) + { + Destroy(message.entity); + } + } + } + + [Test] + public void DestroyEntityWithoutID() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new AddComponentEngine()); + worldBuilder.AddEngine(new DestroyEntityEngine()); + worldBuilder.AddEngine(new ReaderEngine()); + + var mockComponent = new MockComponent { }; + var entity = worldBuilder.CreateEntity(); + var componentID = worldBuilder.SetComponent(entity, mockComponent); + worldBuilder.SendMessage(new DestroyComponentMessage { entity = entity }); + + var world = worldBuilder.Build(); + world.Update(0.01); + + Assert.DoesNotThrow(() => world.Update(0.01)); + Assert.That(results, Does.Not.Contain((componentID, mockComponent))); + } + [Reads(typeof(DestroyerComponent), typeof(MockComponent))] class DestroyAndAddComponentEngine : Engine { @@ -681,7 +713,7 @@ namespace Tests Assert.Throws(() => world.Update(0.01f)); } - struct EntityIDComponent : IComponent { public Guid entityID; } + struct EntityIDComponent : IComponent { public Guid entityID; } static bool hasEntity; [Reads(typeof(EntityIDComponent))] @@ -774,7 +806,7 @@ namespace Tests foreach (var (componentID, component) in ReadComponents()) { RemoveComponent(componentID); - SendMessageDelayed(new MockMessage {}, 1); + SendMessageDelayed(new MockMessage { }, 1); } } } @@ -787,9 +819,9 @@ namespace Tests var worldBuilder = new WorldBuilder(); worldBuilder.AddEngine(new DelayedMessageEngine()); worldBuilder.AddEngine(new MessageReadEngine()); - + var entity = worldBuilder.CreateEntity(); - worldBuilder.SetComponent(entity, new MockComponent {}); + worldBuilder.SetComponent(entity, new MockComponent { }); var world = worldBuilder.Build(); @@ -817,7 +849,7 @@ namespace Tests foreach (var message in ReadMessages()) { var entity = CreateEntity(); - SetComponent(entity, new MockComponent {}); + SetComponent(entity, new MockComponent { }); } } } @@ -841,7 +873,7 @@ namespace Tests worldBuilder.AddEngine(new ActivateComponentEngine()); worldBuilder.AddEngine(new RemoveComponentEngine()); - worldBuilder.SendMessage(new MockMessage {}); + worldBuilder.SendMessage(new MockMessage { }); var world = worldBuilder.Build(); @@ -858,13 +890,13 @@ namespace Tests { foreach (var (componentID, component) in ReadComponents()) { - SetComponent(componentID, new MockComponent {}); + SetComponent(componentID, new MockComponent { }); } } } [Receives(typeof(DestroyComponentMessage))] - class DestroyEntityEngine : Engine + class DestroyEntityByIDEngine : Engine { public override void Update(double dt) { @@ -880,16 +912,133 @@ namespace Tests { var worldBuilder = new WorldBuilder(); worldBuilder.AddEngine(new AddComponentEngine()); - worldBuilder.AddEngine(new DestroyEntityEngine()); + worldBuilder.AddEngine(new DestroyEntityByIDEngine()); var entity = worldBuilder.CreateEntity(); - worldBuilder.SetComponent(entity, new MockComponent {}); + worldBuilder.SetComponent(entity, new MockComponent { }); worldBuilder.SendMessage(new DestroyComponentMessage { entity = entity }); var world = worldBuilder.Build(); world.Update(0.01); - + Assert.DoesNotThrow(() => world.Update(0.01)); } + + static Entity readEntity; + + [Reads(typeof(MockComponent))] + class ReadEntityByComponentTypeEngine : Engine + { + public override void Update(double dt) + { + readEntity = ReadEntity(); + } + } + + [Test] + public void GetEntityByComponentType() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ReadEntityByComponentTypeEngine()); + + var entity = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entity, new MockComponent { }); + + var world = worldBuilder.Build(); + world.Update(0.01); + + entity.Should().BeEquivalentTo(readEntity); + } + + + + static Entity[] readEntities; + + [Reads(typeof(MockComponent))] + class ReadEntitiesWithComponentTypeEngine : Engine + { + public override void Update(double dt) + { + readEntities = ReadEntities().ToArray(); + } + } + + [Test] + public void ReadEntities() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine()); + worldBuilder.AddEngine(new DestroyAllWithEngine()); + + var entity = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entity, new MockComponent { }); + + var entityB = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entityB, new MockComponent { }); + + var world = worldBuilder.Build(); + world.Update(0.01); + + readEntities.Should().Contain(entity); + readEntities.Should().Contain(entityB); + } + + [Reads(typeof(MockComponent))] + class DestroyWithEngine : Engine + { + public override void Update(double dt) + { + if (SomeComponent()) + { + DestroyWith(); + } + } + } + + [Test] + public void DestroyWith() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine()); + worldBuilder.AddEngine(new DestroyWithEngine()); + + var entity = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entity, new MockComponent { }); + + var world = worldBuilder.Build(); + world.Update(0.01); + world.Update(0.01); // update twice so the read happens after destroy + + readEntities.Should().BeEmpty(); + } + + [Reads(typeof(MockComponent))] + class DestroyAllWithEngine : Engine + { + public override void Update(double dt) + { + DestroyAllWith(); + } + } + + [Test] + public void DestroyAllWith() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine()); + worldBuilder.AddEngine(new DestroyAllWithEngine()); + + var entity = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entity, new MockComponent { }); + + var entityB = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entityB, new MockComponent { }); + + var world = worldBuilder.Build(); + world.Update(0.01); + world.Update(0.01); // update twice so the read happens after destroy + + readEntities.Should().BeEmpty(); + } } } diff --git a/test/test.csproj b/test/test.csproj index f525fa9..5e554ea 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.0 false Tests EncompassECS.Framework.Tests