Merge pull request 'Goodies' (#2) from new_stuff into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

pull/5/head 0.20.0
cosmonaut 2020-03-18 21:24:17 +00:00
commit 72a7f8bfbf
14 changed files with 367 additions and 89 deletions

View File

@ -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+)?$/

24
.drone.yml Normal file
View File

@ -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/*.*.*

4
TODO
View File

@ -3,9 +3,5 @@
- method to remove all components of a type without destroying Entities - method to remove all components of a type without destroying Entities
- method to remove a component of a type without destroying entity - 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 - look at test coverage
- docs - docs

View File

@ -33,7 +33,6 @@ namespace Encompass
private TypedComponentStore<TComponent> Lookup<TComponent>() where TComponent : struct, IComponent private TypedComponentStore<TComponent> Lookup<TComponent>() where TComponent : struct, IComponent
{ {
//RegisterComponentType<TComponent>();
return Stores[typeof(TComponent)] as TypedComponentStore<TComponent>; return Stores[typeof(TComponent)] as TypedComponentStore<TComponent>;
} }

View File

@ -53,6 +53,11 @@ namespace Encompass
return Lookup<TMessage>().WithEntity(entityID); return Lookup<TMessage>().WithEntity(entityID);
} }
public bool SomeWithEntity<TMessage>(int entityID) where TMessage : struct, IMessage, IHasEntity
{
return Lookup<TMessage>().SomeWithEntity(entityID);
}
public void ProcessDelayedMessages(double dilatedDelta, double realtimeDelta) public void ProcessDelayedMessages(double dilatedDelta, double realtimeDelta)
{ {
foreach (var store in Stores.Values) foreach (var store in Stores.Values)

View File

@ -93,6 +93,11 @@ namespace Encompass
return entityToMessage.ContainsKey(entityID) ? entityToMessage[entityID] : System.Linq.Enumerable.Empty<TMessage>(); return entityToMessage.ContainsKey(entityID) ? entityToMessage[entityID] : System.Linq.Enumerable.Empty<TMessage>();
} }
public bool SomeWithEntity(int entityID)
{
return entityToMessage.ContainsKey(entityID) && entityToMessage[entityID].Count > 0;
}
public override void Clear() public override void Clear()
{ {
store.Clear(); store.Clear();

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MoonTools.FastCollections;
namespace Encompass namespace Encompass
{ {
@ -73,7 +74,14 @@ namespace Encompass
return false; return false;
} }
public bool UpdateComponent<TComponent>(int entityID, TComponent component, int priority) where TComponent : struct, IComponent internal void AddImmediateComponent<TComponent>(int entityID, TComponent component) where TComponent : struct, IComponent
{
immediateComponentStore.Set(entityID, component);
replayStore.Set(entityID, component);
upToDateComponentStore.Set(entityID, component);
}
internal bool UpdateComponent<TComponent>(int entityID, TComponent component, int priority) where TComponent : struct, IComponent
{ {
var result = upToDateComponentStore.Set(entityID, component, priority); var result = upToDateComponentStore.Set(entityID, component, priority);
if (result) if (result)
@ -83,6 +91,12 @@ namespace Encompass
return result; return result;
} }
internal void AddComponent<TComponent>(int entityID, TComponent component) where TComponent : struct, IComponent
{
upToDateComponentStore.Set(entityID, component);
replayStore.Set(entityID, component);
}
// existing or immediate reads // existing or immediate reads
internal IEnumerable<(TComponent, int)> ReadExistingAndImmediateComponentsByType<TComponent>() where TComponent : struct, IComponent internal IEnumerable<(TComponent, int)> ReadExistingAndImmediateComponentsByType<TComponent>() where TComponent : struct, IComponent
@ -249,5 +263,10 @@ namespace Encompass
drawLayerManager.UnRegisterComponentWithLayer<TComponent>(entityID); drawLayerManager.UnRegisterComponentWithLayer<TComponent>(entityID);
} }
} }
public bool UpToDateEntityIsEmpty(int entityID)
{
return upToDateComponentStore.EntityBitArray(entityID).AllFalse();
}
} }
} }

View File

@ -53,6 +53,8 @@ namespace Encompass
} }
} }
private HashSet<int> _newlyCreatedEntities = new HashSet<int>();
protected Engine() protected Engine()
{ {
ID = Guid.NewGuid(); ID = Guid.NewGuid();
@ -158,6 +160,24 @@ namespace Encompass
this.trackingManager = trackingManager; this.trackingManager = trackingManager;
} }
internal void CheckMessageRead<TMessage>() 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();
}
/// <summary> /// <summary>
/// Runs once per World update with the calculated delta-time. /// Runs once per World update with the calculated delta-time.
/// </summary> /// </summary>
@ -169,7 +189,9 @@ namespace Encompass
/// </summary> /// </summary>
protected Entity CreateEntity() protected Entity CreateEntity()
{ {
return entityManager.CreateEntity(); var entity = entityManager.CreateEntity();
_newlyCreatedEntities.Add(entity.ID);
return entity;
} }
/// <summary> /// <summary>
@ -180,6 +202,14 @@ namespace Encompass
return entityManager.EntityExists(entity.ID); return entityManager.EntityExists(entity.ID);
} }
/// <summary>
/// Returns true if an Entity with the specified ID exists.
/// </summary>
protected bool EntityExists(int entityID)
{
return entityManager.EntityExists(entityID);
}
/// <summary> /// <summary>
/// Returns an Entity containing the specified Component type. /// Returns an Entity containing the specified Component type.
/// </summary> /// </summary>
@ -448,6 +478,37 @@ namespace Encompass
} }
} }
/// <summary>
/// An alternative to SetComponent that can be used for new Entities and does not require setting write priority.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalWriteException">
/// Thrown when the Engine does not declare that it Writes the given Component Type.
/// </exception>
protected void AddComponent<TComponent>(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);
}
}
/// <summary> /// <summary>
/// Sends a Message. /// Sends a Message.
/// </summary> /// </summary>
@ -490,11 +551,7 @@ namespace Encompass
/// </exception> /// </exception>
protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage
{ {
if (!receiveTypes.Contains(typeof(TMessage))) CheckMessageRead<TMessage>();
{
throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
}
return messageManager.GetMessagesByType<TMessage>(); return messageManager.GetMessagesByType<TMessage>();
} }
@ -506,6 +563,7 @@ namespace Encompass
/// </exception> /// </exception>
protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage
{ {
CheckMessageRead<TMessage>();
return messageManager.First<TMessage>(); return messageManager.First<TMessage>();
} }
@ -517,11 +575,7 @@ namespace Encompass
/// </exception> /// </exception>
protected bool SomeMessage<TMessage>() where TMessage : struct, IMessage protected bool SomeMessage<TMessage>() where TMessage : struct, IMessage
{ {
if (!receiveTypes.Contains(typeof(TMessage))) CheckMessageRead<TMessage>();
{
throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", GetType().Name, typeof(TMessage).Name);
}
return messageManager.Any<TMessage>(); return messageManager.Any<TMessage>();
} }
@ -648,16 +702,36 @@ namespace Encompass
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <typeparam name="TMessage">The Message subtype.</typeparam> /// <typeparam name="TMessage">The Message subtype.</typeparam>
/// <param name="entity">The entity that all messages in the IEnumerable refer to.</param> /// <param name="entity">The entity that all messages in the IEnumerable refer to.</param>
/// <returns></returns> /// <returns></returns>
protected IEnumerable<TMessage> ReadMessagesWithEntity<TMessage>(Entity entity) where TMessage : struct, IMessage, IHasEntity protected IEnumerable<TMessage> ReadMessagesWithEntity<TMessage>(Entity entity) where TMessage : struct, IMessage, IHasEntity
{ {
CheckMessageRead<TMessage>();
return messageManager.WithEntity<TMessage>(entity.ID); return messageManager.WithEntity<TMessage>(entity.ID);
} }
/// <summary>
/// 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.
/// </summary>
protected TMessage ReadMessageWithEntity<TMessage>(Entity entity) where TMessage : struct, IMessage, IHasEntity
{
CheckMessageRead<TMessage>();
return messageManager.WithEntitySingular<TMessage>(entity.ID);
}
/// <summary>
/// Efficiently checks if any Message of a given type referencing a given Entity exists.
/// </summary>
protected bool SomeMessageWithEntity<TMessage>(Entity entity) where TMessage : struct, IMessage, IHasEntity
{
CheckMessageRead<TMessage>();
return messageManager.SomeWithEntity<TMessage>(entity.ID);
}
internal void CheckAndUpdateTracking(int entityID) internal void CheckAndUpdateTracking(int entityID)
{ {
if (_trackedEntities.Contains(entityID) && !entityQuery.CheckEntity(entityID, componentManager.ExistingBits)) if (_trackedEntities.Contains(entityID) && !entityQuery.CheckEntity(entityID, componentManager.ExistingBits))

View File

@ -76,5 +76,17 @@ namespace Encompass
entitiesMarkedForDestroy.Clear(); entitiesMarkedForDestroy.Clear();
} }
// NOTE: this is very suboptimal
public void PruneEmptyEntities()
{
foreach (var id in EntityIDs)
{
if (componentManager.UpToDateEntityIsEmpty(id))
{
MarkForDestroy(id);
}
}
}
} }
} }

View File

@ -56,5 +56,17 @@ namespace Encompass
{ {
return messageStore.WithEntity<TMessage>(entityID); return messageStore.WithEntity<TMessage>(entityID);
} }
internal TMessage WithEntitySingular<TMessage>(int entityID) where TMessage : struct, IMessage, IHasEntity
{
var enumerator = messageStore.WithEntity<TMessage>(entityID).GetEnumerator();
enumerator.MoveNext();
return enumerator.Current;
}
internal bool SomeWithEntity<TMessage>(int entityID) where TMessage : struct, IMessage, IHasEntity
{
return messageStore.SomeWithEntity<TMessage>(entityID);
}
} }
} }

View File

@ -54,9 +54,12 @@ namespace Encompass
{ {
engine.Update(dt); engine.Update(dt);
} }
engine.ClearNewlyCreatedEntities();
} }
messageManager.ClearMessages(); messageManager.ClearMessages();
entityManager.PruneEmptyEntities();
entityManager.DestroyMarkedEntities(enginesInOrder); entityManager.DestroyMarkedEntities(enginesInOrder);
componentManager.RemoveMarkedComponents(); componentManager.RemoveMarkedComponents();

View File

@ -36,7 +36,7 @@ namespace Encompass
private readonly HashSet<Engine> senders = new HashSet<Engine>(); private readonly HashSet<Engine> senders = new HashSet<Engine>();
private readonly HashSet<Type> componentTypesToRegister = new HashSet<Type>(); private readonly HashSet<Type> componentTypesToPreload = new HashSet<Type>();
private readonly HashSet<Type> messageTypes = new HashSet<Type>(); private readonly HashSet<Type> messageTypes = new HashSet<Type>();
@ -87,8 +87,6 @@ namespace Encompass
public void SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent public void SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent
{ {
RegisterComponentType<TComponent>(); RegisterComponentType<TComponent>();
componentTypesToRegister.Add(typeof(TComponent));
startingExistingComponentStore.Set(entity.ID, component); startingExistingComponentStore.Set(entity.ID, component);
startingUpToDateComponentStore.Set(entity.ID, component); startingUpToDateComponentStore.Set(entity.ID, component);
@ -104,6 +102,7 @@ namespace Encompass
if (!typeToIndex.ContainsKey(typeof(TComponent))) if (!typeToIndex.ContainsKey(typeof(TComponent)))
{ {
typeToIndex.Add(typeof(TComponent), typeToIndex.Count); typeToIndex.Add(typeof(TComponent), typeToIndex.Count);
componentTypesToPreload.Add(typeof(TComponent));
componentManager.RegisterComponentType<TComponent>(); componentManager.RegisterComponentType<TComponent>();
startingExistingComponentStore.RegisterComponentType<TComponent>(); startingExistingComponentStore.RegisterComponentType<TComponent>();
startingUpToDateComponentStore.RegisterComponentType<TComponent>(); startingUpToDateComponentStore.RegisterComponentType<TComponent>();
@ -115,11 +114,6 @@ namespace Encompass
messageTypes.UnionWith(types); messageTypes.UnionWith(types);
} }
internal void AddComponentTypeToRegister(Type componentType)
{
componentTypesToRegister.Add(componentType);
}
/// <summary> /// <summary>
/// Adds the specified Engine to the World. /// Adds the specified Engine to the World.
/// </summary> /// </summary>
@ -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)) foreach (var receiveType in engine.receiveTypes.Union(engine.readImmediateTypes))
{ {
if (!typeToReaders.ContainsKey(receiveType)) if (!typeToReaders.ContainsKey(receiveType))
@ -196,6 +185,7 @@ namespace Encompass
/// </summary> /// </summary>
public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawableComponent public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawableComponent
{ {
RegisterComponentType<TComponent>();
renderer.AssignEntityManager(entityManager); renderer.AssignEntityManager(entityManager);
renderer.AssignComponentManager(componentManager); renderer.AssignComponentManager(componentManager);
renderManager.RegisterOrderedRenderer<TComponent>(renderer.InternalRender); renderManager.RegisterOrderedRenderer<TComponent>(renderer.InternalRender);
@ -364,16 +354,23 @@ namespace Encompass
throw new EngineWriteConflictException(errorString); throw new EngineWriteConflictException(errorString);
} }
var engineOrder = new List<Engine>(); // doing reflection to grab all component types, because not all writes need to be declared
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
foreach (var registeredComponentType in componentTypesToRegister) {
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 method = typeof(WorldBuilder).GetMethod("RegisterComponentType", BindingFlags.NonPublic | BindingFlags.Instance);
var generic = method.MakeGenericMethod(registeredComponentType); var generic = method.MakeGenericMethod(componentType);
generic.Invoke(this, null); generic.Invoke(this, null);
} }
}
}
PreloadJIT(componentTypesToRegister, messageTypes); PreloadJIT(componentTypesToPreload, messageTypes);
var engineOrder = new List<Engine>();
foreach (var engine in engineGraph.TopologicalSort()) foreach (var engine in engineGraph.TopologicalSort())
{ {

View File

@ -3,7 +3,7 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Encompass</RootNamespace> <RootNamespace>Encompass</RootNamespace>
<PackageId>EncompassECS.Framework</PackageId> <PackageId>EncompassECS.Framework</PackageId>
<Version>0.19.0</Version> <Version>0.20.0</Version>
<Authors>Evan Hemsley</Authors> <Authors>Evan Hemsley</Authors>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Company>Moonside Games</Company> <Company>Moonside Games</Company>

View File

@ -497,6 +497,58 @@ namespace Tests
entityMessageResults.Should().BeEmpty(); 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<EntityMessage>(_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 class SomeComponentTestEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -1240,6 +1292,132 @@ namespace Tests
undilatedDeltaTime.Should().Be(0.5); 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<MockComponent>();
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<IllegalWriteException>(() => 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<MockComponentB>();
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 public class QueryTests
{ {
struct MockComponentB : IComponent { } struct MockComponentB : IComponent { }