world builder structure

pull/5/head
Evan Hemsley 2019-06-16 17:56:36 -07:00
parent c1aa6a07c9
commit cfc5ca178f
10 changed files with 399 additions and 8 deletions

View File

@ -6,9 +6,9 @@ namespace Encompass
{
public abstract class Engine
{
public readonly List<Type> mutateComponentTypes = new List<Type>();
public readonly List<Type> emitMessageTypes = new List<Type>();
public readonly List<Type> readMessageTypes = new List<Type>();
private readonly List<Type> mutateComponentTypes = new List<Type>();
private readonly List<Type> emitMessageTypes = new List<Type>();
private readonly List<Type> readMessageTypes = new List<Type>();
private EntityManager entityManager;
private ComponentManager componentManager;

View File

@ -4,19 +4,19 @@ namespace Encompass
{
public class World
{
private List<Engine> engines;
private List<Engine> enginesInOrder;
private EntityManager entityManager;
private ComponentManager componentManager;
private MessageManager messageManager;
internal World(
List<Engine> engines,
List<Engine> enginesInOrder,
EntityManager entityManager,
ComponentManager componentManager,
MessageManager messageManager
)
{
this.engines = engines;
this.enginesInOrder = enginesInOrder;
this.entityManager = entityManager;
this.componentManager = componentManager;
this.messageManager = messageManager;
@ -24,7 +24,7 @@ namespace Encompass
public void Update(float dt)
{
foreach (var engine in engines)
foreach (var engine in enginesInOrder)
{
engine.Update(dt);
}

View File

@ -1,15 +1,22 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
namespace Encompass
{
public class WorldBuilder
{
private List<Engine> engines = new List<Engine>();
private DirectedGraph<Engine> engineGraph = new DirectedGraph<Engine>();
private ComponentManager componentManager;
private EntityManager entityManager;
private MessageManager messageManager;
private Dictionary<Type, HashSet<Engine>> messageTypeToEmitters = new Dictionary<Type, HashSet<Engine>>();
private Dictionary<Type, HashSet<Engine>> messageTypeToReaders = new Dictionary<Type, HashSet<Engine>>();
public WorldBuilder()
{
componentManager = new ComponentManager();
@ -32,13 +39,122 @@ namespace Encompass
engines.Add(engine);
engineGraph.AddVertex(engine);
var emitMessageAttribute = engine.GetType().GetCustomAttribute<Emits>(false);
if (emitMessageAttribute != null)
{
foreach (var emitMessageType in engine.GetType().GetCustomAttribute<Emits>(false).emitMessageTypes)
{
if (!messageTypeToEmitters.ContainsKey(emitMessageType))
{
messageTypeToEmitters.Add(emitMessageType, new HashSet<Engine>());
}
messageTypeToEmitters[emitMessageType].Add(engine);
if (messageTypeToReaders.ContainsKey(emitMessageType))
{
foreach (var reader in messageTypeToReaders[emitMessageType])
{
engineGraph.AddEdge(engine, reader);
}
}
}
}
var readMessageAttribute = engine.GetType().GetCustomAttribute<Reads>(false);
if (readMessageAttribute != null)
{
foreach (var readMessageType in engine.GetType().GetCustomAttribute<Reads>(false).readMessageTypes)
{
if (!messageTypeToReaders.ContainsKey(readMessageType))
{
messageTypeToReaders.Add(readMessageType, new HashSet<Engine>());
}
messageTypeToReaders[readMessageType].Add(engine);
if (messageTypeToEmitters.ContainsKey(readMessageType))
{
foreach (var emitter in messageTypeToEmitters[readMessageType])
{
engineGraph.AddEdge(emitter, engine);
}
}
}
}
return engine;
}
public World Build()
{
if (engineGraph.Cyclic())
{
var cycles = engineGraph.SimpleCycles();
var errorString = "Cycle(s) found in Engines: ";
foreach (var cycle in cycles)
{
errorString += "\n" +
string.Join(" - > ", cycle.Select((engine) => engine.GetType().Name)) +
" -> " +
cycle.First().GetType().Name;
}
throw new EngineCycleException(errorString);
}
var mutatedComponentTypes = new HashSet<Type>();
var duplicateMutations = new List<Type>();
var componentToEngines = new Dictionary<Type, List<Engine>>();
foreach (var engine in engines)
{
var mutateAttribute = engine.GetType().GetCustomAttribute<Mutates>(false);
if (mutateAttribute != null)
{
foreach (var mutateComponentType in engine.GetType().GetCustomAttribute<Mutates>(false).mutateComponentTypes)
{
if (mutatedComponentTypes.Contains(mutateComponentType))
{
duplicateMutations.Add(mutateComponentType);
}
else
{
mutatedComponentTypes.Add(mutateComponentType);
}
if (!componentToEngines.ContainsKey(mutateComponentType))
{
componentToEngines[mutateComponentType] = new List<Engine>();
}
componentToEngines[mutateComponentType].Add(engine);
}
}
}
if (duplicateMutations.Count > 0)
{
var errorString = "Multiple Engines mutate the same Component: ";
foreach (var componentType in duplicateMutations)
{
errorString += "\n" +
componentType.Name + " mutated by: " +
string.Join(", ", componentToEngines[componentType].Select((engine) => engine.GetType().Name));
}
throw new EngineMutationConflictException(errorString);
}
var engineOrder = new List<Engine>();
foreach (var engine in engineGraph.TopologicalSort())
{
engineOrder.Add(engine);
}
var world = new World(
this.engines,
engineOrder,
this.entityManager,
this.componentManager,
this.messageManager

View File

@ -21,5 +21,8 @@
<Content Include="attributes\Emits.cs" />
<Content Include="exceptions\IllegalMessageEmitException.cs" />
<Content Include="exceptions\IllegalMessageReadException.cs" />
<Content Include="exceptions\EngineCycleException.cs" />
<Content Include="exceptions\EngineMutationConflictException.cs" />
<Content Include="attributes\Reads.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
using System;
namespace Encompass
{
public class EngineCycleException : Exception
{
public EngineCycleException(
string format,
params object[] args
) : base(string.Format(format, args)) { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace Encompass
{
public class EngineMutationConflictException : Exception
{
public EngineMutationConflictException(
string format,
params object[] args
) : base(string.Format(format, args)) { }
}
}

View File

@ -159,6 +159,11 @@ namespace Encompass
return output;
}
public bool Cyclic()
{
return StronglyConnectedComponents().Any((scc) => scc.Count() > 1);
}
public IEnumerable<T> TopologicalSort()
{
var dfs = NodeDFS();

View File

@ -341,5 +341,34 @@ namespace Tests
result.Should().ContainEquivalentOf(cycleE);
result.Should().HaveCount(5);
}
[Test]
public void Cyclic()
{
var myGraph = new DirectedGraph<int>();
myGraph.AddVertices(1, 2, 3, 4);
myGraph.AddEdges(
Tuple.Create(1, 2),
Tuple.Create(2, 3),
Tuple.Create(3, 1),
Tuple.Create(3, 4)
);
Assert.That(myGraph.Cyclic(), Is.True);
}
[Test]
public void Acyclic()
{
var myGraph = new DirectedGraph<int>();
myGraph.AddVertices(1, 2, 3, 4);
myGraph.AddEdges(
Tuple.Create(1, 2),
Tuple.Create(2, 3),
Tuple.Create(3, 4)
);
Assert.That(myGraph.Cyclic(), Is.False);
}
}
}

212
test/WorldBuilderTest.cs Normal file
View File

@ -0,0 +1,212 @@
using NUnit.Framework;
using Encompass;
using System.Collections.Generic;
namespace Tests
{
public class WorldBuilderTest
{
public class EngineCycleSimple
{
struct AMessage : IMessage { }
struct BMessage : IMessage { }
[Reads(typeof(AMessage))]
[Emits(typeof(BMessage))]
class AEngine : Engine
{
public override void Update(float dt)
{
BMessage message;
this.EmitMessage(message);
}
}
[Reads(typeof(BMessage))]
[Emits(typeof(AMessage))]
class BEngine : Engine
{
public override void Update(float dt)
{
AMessage message;
this.EmitMessage(message);
}
}
[Test]
public void EngineCycle()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine<AEngine>();
worldBuilder.AddEngine<BEngine>();
Assert.Throws<EngineCycleException>(() => worldBuilder.Build());
}
}
public class EngineCycleComplex
{
struct AMessage : IMessage { }
struct BMessage : IMessage { }
struct CMessage : IMessage { }
struct DMessage : IMessage { }
[Reads(typeof(AMessage))]
[Emits(typeof(BMessage))]
class AEngine : Engine
{
public override void Update(float dt)
{
BMessage message;
this.EmitMessage(message);
}
}
[Reads(typeof(BMessage))]
[Emits(typeof(CMessage))]
class BEngine : Engine
{
public override void Update(float dt)
{
CMessage message;
this.EmitMessage(message);
}
}
[Reads(typeof(CMessage))]
[Emits(typeof(DMessage))]
class CEngine : Engine
{
public override void Update(float dt)
{
DMessage message;
this.EmitMessage(message);
}
}
[Reads(typeof(DMessage))]
[Emits(typeof(AMessage))]
class DEngine : Engine
{
public override void Update(float dt)
{
AMessage message;
this.EmitMessage(message);
}
}
[Test]
public void EngineCycle()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine<AEngine>();
worldBuilder.AddEngine<BEngine>();
worldBuilder.AddEngine<CEngine>();
worldBuilder.AddEngine<DEngine>();
Assert.Throws<EngineCycleException>(() => worldBuilder.Build());
}
}
public class MutationConflict
{
struct AComponent : IComponent { }
[Mutates(typeof(AComponent))]
class AEngine : Engine
{
public override void Update(float dt) { }
}
[Mutates(typeof(AComponent))]
class BEngine : Engine
{
public override void Update(float dt) { }
}
[Test]
public void MutationConflictException()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine<AEngine>();
worldBuilder.AddEngine<BEngine>();
Assert.Throws<EngineMutationConflictException>(() => worldBuilder.Build());
}
}
public class LegalEngines
{
static List<Engine> order = new List<Engine>();
struct AComponent : IComponent { }
struct BComponent : IComponent { }
struct AMessage : IMessage { }
struct BMessage : IMessage { }
struct CMessage : IMessage { }
struct DMessage : IMessage { }
[Mutates(typeof(AComponent))]
[Emits(typeof(AMessage))]
class AEngine : Engine
{
public override void Update(float dt)
{
order.Add(this);
}
}
[Mutates(typeof(BComponent))]
[Emits(typeof(BMessage))]
class BEngine : Engine
{
public override void Update(float dt)
{
order.Add(this);
}
}
[Reads(typeof(AMessage), typeof(BMessage))]
[Emits(typeof(DMessage))]
class CEngine : Engine
{
public override void Update(float dt)
{
order.Add(this);
}
}
[Reads(typeof(DMessage))]
class DEngine : Engine
{
public override void Update(float dt)
{
order.Add(this);
}
}
[Test]
public void EngineOrder()
{
var worldBuilder = new WorldBuilder();
var engineA = worldBuilder.AddEngine<AEngine>();
var engineB = worldBuilder.AddEngine<BEngine>();
var engineC = worldBuilder.AddEngine<CEngine>();
var engineD = worldBuilder.AddEngine<DEngine>();
Assert.DoesNotThrow(() => worldBuilder.Build());
var world = worldBuilder.Build();
world.Update(0.01f);
Assert.That(order.IndexOf(engineA), Is.LessThan(order.IndexOf(engineC)));
Assert.That(order.IndexOf(engineB), Is.LessThan(order.IndexOf(engineC)));
Assert.That(order.IndexOf(engineC), Is.LessThan(order.IndexOf(engineD)));
}
}
}
}

View File

@ -15,5 +15,6 @@
<ProjectReference Include="..\src\encompass-cs.csproj" />
<Content Include="EngineTest.cs" />
<Content Include="DirectedGraphTest.cs" />
<Content Include="WorldBuilderTest.cs" />
</ItemGroup>
</Project>