diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c922620..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: 2.1 - -defaults: &defaults - working_directory: ~/repo - docker: - - image: mcr.microsoft.com/dotnet/core/sdk:3.0 - environment: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - -jobs: - test: - <<: *defaults - steps: - - checkout - - run: dotnet restore - - run: dotnet build -c Release - - run: dotnet test -c Release - - persist_to_workspace: - root: . - paths: ./encompass-cs/bin - - deploy: - <<: *defaults - steps: - - checkout - - attach_workspace: - at: . - - run: dotnet nuget push ./encompass-cs/bin/Release/EncompassECS.Framework.*.nupkg -k $API_KEY -s $NUGET_SOURCE - -workflows: - version: 2 - test_and_deploy: - jobs: - - test: - filters: - tags: - only: /.*/ - - deploy: - requires: - - test - filters: - branches: - ignore: /.*/ - tags: - only: /^\d+\.\d+\.\d+(-preview\d+)?$/ diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..7d02a31 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,24 @@ + kind: pipeline + type: docker + name: default + + workspace: + path: /build + + steps: + - name: test + image: mcr.microsoft.com/dotnet/core/sdk:3.1 + commands: + - dotnet build -c Release + - dotnet test -c Release + + - name: deploy + image: mcr.microsoft.com/dotnet/core/sdk:3.1 + environment: + API_KEY: + from_secret: API_KEY + commands: + - dotnet nuget push /build/encompass-cs/bin/Release/EncompassECS.Framework.*.nupkg -s https://api.nuget.org/v3/index.json -k $API_KEY + when: + ref: + - refs/tags/*.*.* diff --git a/TODO b/TODO index 2822a8e..d96f6e9 100644 --- a/TODO +++ b/TODO @@ -3,9 +3,5 @@ - method to remove all components of a type without destroying Entities - method to remove a component of a type without destroying entity -- auto destroy entities that no longer have components - -- fast lookup for messages that contain entity references instead of `Where` loop? - - look at test coverage - docs diff --git a/encompass-cs/Collections/ComponentStore.cs b/encompass-cs/Collections/ComponentStore.cs index 0645510..aee17e8 100644 --- a/encompass-cs/Collections/ComponentStore.cs +++ b/encompass-cs/Collections/ComponentStore.cs @@ -33,7 +33,6 @@ namespace Encompass private TypedComponentStore Lookup() where TComponent : struct, IComponent { - //RegisterComponentType(); return Stores[typeof(TComponent)] as TypedComponentStore; } diff --git a/encompass-cs/Collections/MessageStore.cs b/encompass-cs/Collections/MessageStore.cs index 9fb586a..5e7186f 100644 --- a/encompass-cs/Collections/MessageStore.cs +++ b/encompass-cs/Collections/MessageStore.cs @@ -53,6 +53,11 @@ namespace Encompass return Lookup().WithEntity(entityID); } + public bool SomeWithEntity(int entityID) where TMessage : struct, IMessage, IHasEntity + { + return Lookup().SomeWithEntity(entityID); + } + public void ProcessDelayedMessages(double dilatedDelta, double realtimeDelta) { foreach (var store in Stores.Values) diff --git a/encompass-cs/Collections/TypedMessageStore.cs b/encompass-cs/Collections/TypedMessageStore.cs index 54aebe6..bfa7a8b 100644 --- a/encompass-cs/Collections/TypedMessageStore.cs +++ b/encompass-cs/Collections/TypedMessageStore.cs @@ -93,6 +93,11 @@ namespace Encompass return entityToMessage.ContainsKey(entityID) ? entityToMessage[entityID] : System.Linq.Enumerable.Empty(); } + public bool SomeWithEntity(int entityID) + { + return entityToMessage.ContainsKey(entityID) && entityToMessage[entityID].Count > 0; + } + public override void Clear() { store.Clear(); diff --git a/encompass-cs/ComponentManager.cs b/encompass-cs/ComponentManager.cs index ded956e..046704e 100644 --- a/encompass-cs/ComponentManager.cs +++ b/encompass-cs/ComponentManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MoonTools.FastCollections; namespace Encompass { @@ -73,7 +74,14 @@ namespace Encompass return false; } - public bool UpdateComponent(int entityID, TComponent component, int priority) where TComponent : struct, IComponent + internal void AddImmediateComponent(int entityID, TComponent component) where TComponent : struct, IComponent + { + immediateComponentStore.Set(entityID, component); + replayStore.Set(entityID, component); + upToDateComponentStore.Set(entityID, component); + } + + internal bool UpdateComponent(int entityID, TComponent component, int priority) where TComponent : struct, IComponent { var result = upToDateComponentStore.Set(entityID, component, priority); if (result) @@ -83,6 +91,12 @@ namespace Encompass return result; } + internal void AddComponent(int entityID, TComponent component) where TComponent : struct, IComponent + { + upToDateComponentStore.Set(entityID, component); + replayStore.Set(entityID, component); + } + // existing or immediate reads internal IEnumerable<(TComponent, int)> ReadExistingAndImmediateComponentsByType() where TComponent : struct, IComponent @@ -249,5 +263,10 @@ namespace Encompass drawLayerManager.UnRegisterComponentWithLayer(entityID); } } + + public bool UpToDateEntityIsEmpty(int entityID) + { + return upToDateComponentStore.EntityBitArray(entityID).AllFalse(); + } } } diff --git a/encompass-cs/Engine.cs b/encompass-cs/Engine.cs index aca9b61..8a3cdf6 100644 --- a/encompass-cs/Engine.cs +++ b/encompass-cs/Engine.cs @@ -53,6 +53,8 @@ namespace Encompass } } + private HashSet _newlyCreatedEntities = new HashSet(); + protected Engine() { ID = Guid.NewGuid(); @@ -158,6 +160,24 @@ namespace Encompass this.trackingManager = trackingManager; } + internal void CheckMessageRead() where TMessage : struct, IMessage + { + if (!receiveTypes.Contains(typeof(TMessage))) + { + throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name); + } + } + + private bool EntityCreatedThisFrame(int entityID) + { + return _newlyCreatedEntities.Contains(entityID); + } + + internal void ClearNewlyCreatedEntities() + { + _newlyCreatedEntities.Clear(); + } + /// /// Runs once per World update with the calculated delta-time. /// @@ -169,7 +189,9 @@ namespace Encompass /// protected Entity CreateEntity() { - return entityManager.CreateEntity(); + var entity = entityManager.CreateEntity(); + _newlyCreatedEntities.Add(entity.ID); + return entity; } /// @@ -180,6 +202,14 @@ namespace Encompass return entityManager.EntityExists(entity.ID); } + /// + /// Returns true if an Entity with the specified ID exists. + /// + protected bool EntityExists(int entityID) + { + return entityManager.EntityExists(entityID); + } + /// /// Returns an Entity containing the specified Component type. /// @@ -448,6 +478,37 @@ namespace Encompass } } + /// + /// An alternative to SetComponent that can be used for new Entities and does not require setting write priority. + /// + /// + /// Thrown when the Engine does not declare that it Writes the given Component Type. + /// + protected void AddComponent(Entity entity, TComponent component) where TComponent : struct, IComponent + { + if (!EntityCreatedThisFrame(entity.ID)) + { + throw new IllegalWriteException("AddComponent used on Entity that was not created in this context. Use SetComponent instead."); + } + + if (writeImmediateTypes.Contains(typeof(TComponent))) + { + componentManager.AddImmediateComponent(entity.ID, component); + trackingManager.ImmediateUpdateTracking(entity.ID, typeof(TComponent)); + } + else + { + componentManager.AddComponent(entity.ID, component); + } + + trackingManager.RegisterAddition(entity.ID, typeof(TComponent)); + + if (component is IDrawableComponent drawableComponent) + { + componentManager.RegisterDrawableComponent(entity.ID, component, drawableComponent.Layer); + } + } + /// /// Sends a Message. /// @@ -490,11 +551,7 @@ namespace Encompass /// protected IEnumerable ReadMessages() where TMessage : struct, IMessage { - if (!receiveTypes.Contains(typeof(TMessage))) - { - throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name); - } - + CheckMessageRead(); return messageManager.GetMessagesByType(); } @@ -506,6 +563,7 @@ namespace Encompass /// protected TMessage ReadMessage() where TMessage : struct, IMessage { + CheckMessageRead(); return messageManager.First(); } @@ -517,11 +575,7 @@ namespace Encompass /// protected bool SomeMessage() where TMessage : struct, IMessage { - if (!receiveTypes.Contains(typeof(TMessage))) - { - throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", GetType().Name, typeof(TMessage).Name); - } - + CheckMessageRead(); return messageManager.Any(); } @@ -648,16 +702,36 @@ namespace Encompass } /// - /// Efficiently reads Messages of a given type that all reference the same Entity. + /// Efficiently reads Messages of a given type that all reference the given Entity. /// /// The Message subtype. /// The entity that all messages in the IEnumerable refer to. /// protected IEnumerable ReadMessagesWithEntity(Entity entity) where TMessage : struct, IMessage, IHasEntity { + CheckMessageRead(); return messageManager.WithEntity(entity.ID); } + /// + /// Efficiently reads a single Message of a given type that references a given Entity. + /// It is recommended to use this method in conjunction with SomeMessageWithEntity to prevent errors. + /// + protected TMessage ReadMessageWithEntity(Entity entity) where TMessage : struct, IMessage, IHasEntity + { + CheckMessageRead(); + return messageManager.WithEntitySingular(entity.ID); + } + + /// + /// Efficiently checks if any Message of a given type referencing a given Entity exists. + /// + protected bool SomeMessageWithEntity(Entity entity) where TMessage : struct, IMessage, IHasEntity + { + CheckMessageRead(); + return messageManager.SomeWithEntity(entity.ID); + } + internal void CheckAndUpdateTracking(int entityID) { if (_trackedEntities.Contains(entityID) && !entityQuery.CheckEntity(entityID, componentManager.ExistingBits)) diff --git a/encompass-cs/EntityManager.cs b/encompass-cs/EntityManager.cs index d9e9337..c800281 100644 --- a/encompass-cs/EntityManager.cs +++ b/encompass-cs/EntityManager.cs @@ -8,7 +8,7 @@ namespace Encompass private readonly int entityCapacity; private readonly IDManager idManager = new IDManager(); private readonly HashSet IDs = new HashSet(); - + private readonly HashSet entitiesMarkedForDestroy = new HashSet(); private readonly ComponentManager componentManager; @@ -76,5 +76,17 @@ namespace Encompass entitiesMarkedForDestroy.Clear(); } + + // NOTE: this is very suboptimal + public void PruneEmptyEntities() + { + foreach (var id in EntityIDs) + { + if (componentManager.UpToDateEntityIsEmpty(id)) + { + MarkForDestroy(id); + } + } + } } } diff --git a/encompass-cs/MessageManager.cs b/encompass-cs/MessageManager.cs index 1256946..c9e53ae 100644 --- a/encompass-cs/MessageManager.cs +++ b/encompass-cs/MessageManager.cs @@ -56,5 +56,17 @@ namespace Encompass { return messageStore.WithEntity(entityID); } + + internal TMessage WithEntitySingular(int entityID) where TMessage : struct, IMessage, IHasEntity + { + var enumerator = messageStore.WithEntity(entityID).GetEnumerator(); + enumerator.MoveNext(); + return enumerator.Current; + } + + internal bool SomeWithEntity(int entityID) where TMessage : struct, IMessage, IHasEntity + { + return messageStore.SomeWithEntity(entityID); + } } } diff --git a/encompass-cs/World.cs b/encompass-cs/World.cs index 677bf8f..b611d96 100644 --- a/encompass-cs/World.cs +++ b/encompass-cs/World.cs @@ -54,9 +54,12 @@ namespace Encompass { engine.Update(dt); } + + engine.ClearNewlyCreatedEntities(); } messageManager.ClearMessages(); + entityManager.PruneEmptyEntities(); entityManager.DestroyMarkedEntities(enginesInOrder); componentManager.RemoveMarkedComponents(); diff --git a/encompass-cs/WorldBuilder.cs b/encompass-cs/WorldBuilder.cs index 368d694..3145626 100644 --- a/encompass-cs/WorldBuilder.cs +++ b/encompass-cs/WorldBuilder.cs @@ -36,7 +36,7 @@ namespace Encompass private readonly HashSet senders = new HashSet(); - private readonly HashSet componentTypesToRegister = new HashSet(); + private readonly HashSet componentTypesToPreload = new HashSet(); private readonly HashSet messageTypes = new HashSet(); @@ -87,8 +87,6 @@ namespace Encompass public void SetComponent(Entity entity, TComponent component) where TComponent : struct, IComponent { RegisterComponentType(); - componentTypesToRegister.Add(typeof(TComponent)); - startingExistingComponentStore.Set(entity.ID, component); startingUpToDateComponentStore.Set(entity.ID, component); @@ -104,6 +102,7 @@ namespace Encompass if (!typeToIndex.ContainsKey(typeof(TComponent))) { typeToIndex.Add(typeof(TComponent), typeToIndex.Count); + componentTypesToPreload.Add(typeof(TComponent)); componentManager.RegisterComponentType(); startingExistingComponentStore.RegisterComponentType(); startingUpToDateComponentStore.RegisterComponentType(); @@ -115,11 +114,6 @@ namespace Encompass messageTypes.UnionWith(types); } - internal void AddComponentTypeToRegister(Type componentType) - { - componentTypesToRegister.Add(componentType); - } - /// /// Adds the specified Engine to the World. /// @@ -164,11 +158,6 @@ namespace Encompass } } - foreach (var componentType in engine.readTypes.Union(engine.writeTypes).Union(engine.readImmediateTypes)) - { - AddComponentTypeToRegister(componentType); - } - foreach (var receiveType in engine.receiveTypes.Union(engine.readImmediateTypes)) { if (!typeToReaders.ContainsKey(receiveType)) @@ -196,6 +185,7 @@ namespace Encompass /// public OrderedRenderer AddOrderedRenderer(OrderedRenderer renderer) where TComponent : struct, IComponent, IDrawableComponent { + RegisterComponentType(); renderer.AssignEntityManager(entityManager); renderer.AssignComponentManager(componentManager); renderManager.RegisterOrderedRenderer(renderer.InternalRender); @@ -364,16 +354,23 @@ namespace Encompass throw new EngineWriteConflictException(errorString); } - var engineOrder = new List(); - - foreach (var registeredComponentType in componentTypesToRegister) + // doing reflection to grab all component types, because not all writes need to be declared + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - var method = typeof(WorldBuilder).GetMethod("RegisterComponentType", BindingFlags.NonPublic | BindingFlags.Instance); - var generic = method.MakeGenericMethod(registeredComponentType); - generic.Invoke(this, null); + foreach (var componentType in assembly.GetTypes()) + { + if (typeof(IComponent).IsAssignableFrom(componentType) && componentType.IsValueType && !componentType.IsEnum && !componentType.IsPrimitive) + { + var method = typeof(WorldBuilder).GetMethod("RegisterComponentType", BindingFlags.NonPublic | BindingFlags.Instance); + var generic = method.MakeGenericMethod(componentType); + generic.Invoke(this, null); + } + } } - PreloadJIT(componentTypesToRegister, messageTypes); + PreloadJIT(componentTypesToPreload, messageTypes); + + var engineOrder = new List(); foreach (var engine in engineGraph.TopologicalSort()) { diff --git a/encompass-cs/encompass-cs.csproj b/encompass-cs/encompass-cs.csproj index f2c9458..9fda87b 100644 --- a/encompass-cs/encompass-cs.csproj +++ b/encompass-cs/encompass-cs.csproj @@ -3,7 +3,7 @@ netstandard2.0 Encompass EncompassECS.Framework - 0.19.0 + 0.20.0 Evan Hemsley true Moonside Games @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/test/EngineTest.cs b/test/EngineTest.cs index de4d956..77ceeb1 100644 --- a/test/EngineTest.cs +++ b/test/EngineTest.cs @@ -415,8 +415,8 @@ namespace Tests Assert.Throws(() => world.Update(0.01f)); } - struct EntityMessage : IMessage, IHasEntity - { + struct EntityMessage : IMessage, IHasEntity + { public EntityMessage(Entity entity, int myInt) { Entity = entity; @@ -497,6 +497,58 @@ namespace Tests entityMessageResults.Should().BeEmpty(); } + [Sends(typeof(EntityMessage), typeof(MockMessage))] + class EntityMessageSingularEmitterEngine : Engine + { + private Entity _entity; + + public EntityMessageSingularEmitterEngine(Entity entity) + { + _entity = entity; + } + + public override void Update(double dt) + { + SendMessage(new EntityMessage(_entity, 2)); + SendMessage(new MockMessage()); + } + } + + static EntityMessage entityMessageResult; + + [Receives(typeof(EntityMessage))] + class SingularMessageWithEntityEngine : Engine + { + private Entity _entity; + + public SingularMessageWithEntityEngine(Entity entity) + { + _entity = entity; + } + + public override void Update(double dt) + { + entityMessageResult = ReadMessageWithEntity(_entity); + } + } + + [Test] + public void MessageWithEntity() + { + var worldBuilder = new WorldBuilder(); + + var entity = worldBuilder.CreateEntity(); + + worldBuilder.AddEngine(new EntityMessageSingularEmitterEngine(entity)); + worldBuilder.AddEngine(new SingularMessageWithEntityEngine(entity)); + + var world = worldBuilder.Build(); + + world.Update(0.01); + + entityMessageResult.Should().Be(new EntityMessage(entity, 2)); + } + class SomeComponentTestEngine : Engine { public override void Update(double dt) @@ -1240,6 +1292,132 @@ namespace Tests undilatedDeltaTime.Should().Be(0.5); } + class AddComponentWithoutPriorityEngine : Engine + { + public override void Update(double dt) + { + var entity = CreateEntity(); + AddComponent(entity, new MockComponent()); + + var entityB = CreateEntity(); + AddComponent(entityB, new MockComponent()); + } + } + + [Test] + public void AddComponent() + { + var worldBuilder = new WorldBuilder(); + + worldBuilder.AddEngine(new AddComponentWithoutPriorityEngine()); + worldBuilder.AddEngine(new ReadComponentsTestEngine()); + + var world = worldBuilder.Build(); + + world.Update(0.01); + world.Update(0.01); + + resultComponents.Should().HaveCount(2); + + world.Update(0.01); + + resultComponents.Should().HaveCount(4); + } + + [Reads(typeof(MockComponent))] + class AddComponentToPreviouslyExistingEntityEngine : Engine + { + public override void Update(double dt) + { + var (component, entity) = ReadComponentIncludingEntity(); + + AddComponent(entity, new MockComponent()); + } + } + + [Test] + public void AddComponentToPreviouslyExistingEntityTest() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new AddComponentToPreviouslyExistingEntityEngine()); + + var entity = worldBuilder.CreateEntity(); + worldBuilder.SetComponent(entity, new MockComponent()); + + var world = worldBuilder.Build(); + + Assert.Throws(() => world.Update(0.01)); + } + + [WritesImmediate(typeof(MockComponentB))] + class AddImmediateComponentEngine : Engine + { + public override void Update(double dt) + { + var entity = CreateEntity(); + AddComponent(entity, new MockComponentB(5)); + } + } + + [ReadsImmediate(typeof(MockComponentB))] + class ReadImmediateComponentEngine : Engine + { + public override void Update(double dt) + { + var (component, entity) = ReadComponentIncludingEntity(); + + getComponentResult = component; + } + } + + [Test] + public void AddImmediateComponentTest() + { + getComponentResult = default(MockComponentB); + + var worldBuilder = new WorldBuilder(); + + worldBuilder.AddEngine(new AddImmediateComponentEngine()); + worldBuilder.AddEngine(new ReadImmediateComponentEngine()); + + var world = worldBuilder.Build(); + + world.Update(0.01); + + getComponentResult.Should().Be(new MockComponentB(5)); + } + + static bool entityExistsResult; + + class EntityExistsEngine : Engine + { + private int _id; + + public EntityExistsEngine(int id) + { + _id = id; + } + + public override void Update(double dt) + { + entityExistsResult = EntityExists(_id); + } + } + + [Test] + public void PruneEmptyEntities() + { + var worldBuilder = new WorldBuilder(); + var entity = worldBuilder.CreateEntity(); + var id = entity.ID; + + var world = worldBuilder.Build(); + + world.Update(0.01); + + entityExistsResult.Should().BeFalse(); + } + public class QueryTests { struct MockComponentB : IComponent { }