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 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

View File

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

View File

@ -53,6 +53,11 @@ namespace Encompass
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)
{
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>();
}
public bool SomeWithEntity(int entityID)
{
return entityToMessage.ContainsKey(entityID) && entityToMessage[entityID].Count > 0;
}
public override void Clear()
{
store.Clear();

View File

@ -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<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);
if (result)
@ -83,6 +91,12 @@ namespace Encompass
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
internal IEnumerable<(TComponent, int)> ReadExistingAndImmediateComponentsByType<TComponent>() where TComponent : struct, IComponent
@ -249,5 +263,10 @@ namespace Encompass
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()
{
ID = Guid.NewGuid();
@ -158,6 +160,24 @@ namespace Encompass
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>
/// Runs once per World update with the calculated delta-time.
/// </summary>
@ -169,7 +189,9 @@ namespace Encompass
/// </summary>
protected Entity CreateEntity()
{
return entityManager.CreateEntity();
var entity = entityManager.CreateEntity();
_newlyCreatedEntities.Add(entity.ID);
return entity;
}
/// <summary>
@ -180,6 +202,14 @@ namespace Encompass
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>
/// Returns an Entity containing the specified Component type.
/// </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>
/// Sends a Message.
/// </summary>
@ -490,11 +551,7 @@ namespace Encompass
/// </exception>
protected IEnumerable<TMessage> ReadMessages<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);
}
CheckMessageRead<TMessage>();
return messageManager.GetMessagesByType<TMessage>();
}
@ -506,6 +563,7 @@ namespace Encompass
/// </exception>
protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage
{
CheckMessageRead<TMessage>();
return messageManager.First<TMessage>();
}
@ -517,11 +575,7 @@ namespace Encompass
/// </exception>
protected bool SomeMessage<TMessage>() 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<TMessage>();
return messageManager.Any<TMessage>();
}
@ -648,16 +702,36 @@ namespace Encompass
}
/// <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>
/// <typeparam name="TMessage">The Message subtype.</typeparam>
/// <param name="entity">The entity that all messages in the IEnumerable refer to.</param>
/// <returns></returns>
protected IEnumerable<TMessage> ReadMessagesWithEntity<TMessage>(Entity entity) where TMessage : struct, IMessage, IHasEntity
{
CheckMessageRead<TMessage>();
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)
{
if (_trackedEntities.Contains(entityID) && !entityQuery.CheckEntity(entityID, componentManager.ExistingBits))

View File

@ -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);
}
}
}
}
}

View File

@ -56,5 +56,17 @@ namespace Encompass
{
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.ClearNewlyCreatedEntities();
}
messageManager.ClearMessages();
entityManager.PruneEmptyEntities();
entityManager.DestroyMarkedEntities(enginesInOrder);
componentManager.RemoveMarkedComponents();

View File

@ -36,7 +36,7 @@ namespace Encompass
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>();
@ -87,8 +87,6 @@ namespace Encompass
public void SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent
{
RegisterComponentType<TComponent>();
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<TComponent>();
startingExistingComponentStore.RegisterComponentType<TComponent>();
startingUpToDateComponentStore.RegisterComponentType<TComponent>();
@ -115,11 +114,6 @@ namespace Encompass
messageTypes.UnionWith(types);
}
internal void AddComponentTypeToRegister(Type componentType)
{
componentTypesToRegister.Add(componentType);
}
/// <summary>
/// Adds the specified Engine to the World.
/// </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))
{
if (!typeToReaders.ContainsKey(receiveType))
@ -196,6 +185,7 @@ namespace Encompass
/// </summary>
public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawableComponent
{
RegisterComponentType<TComponent>();
renderer.AssignEntityManager(entityManager);
renderer.AssignComponentManager(componentManager);
renderManager.RegisterOrderedRenderer<TComponent>(renderer.InternalRender);
@ -364,16 +354,23 @@ namespace Encompass
throw new EngineWriteConflictException(errorString);
}
var engineOrder = new List<Engine>();
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<Engine>();
foreach (var engine in engineGraph.TopologicalSort())
{

View File

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

View File

@ -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<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
{
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<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
{
struct MockComponentB : IComponent { }