adds some convenience methods and doc comments

pull/5/head
Evan Hemsley 2019-10-24 12:48:36 -07:00
parent 27c3fa1058
commit 85f99a565c
14 changed files with 429 additions and 20 deletions

View File

@ -12,7 +12,7 @@ namespace Encompass
private readonly Dictionary<Guid, Type> componentIDToType = new Dictionary<Guid, Type>(); private readonly Dictionary<Guid, Type> componentIDToType = new Dictionary<Guid, Type>();
private readonly Dictionary<Guid, Guid> componentIDToEntityID = new Dictionary<Guid, Guid>(); private readonly Dictionary<Guid, Guid> componentIDToEntityID = new Dictionary<Guid, Guid>();
private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToExistingComponentIDs = new Dictionary<Type, HashSet<Guid>>(); private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToExistingComponentIDs = new Dictionary<Type, HashSet<Guid>>();
private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToPendingComponentIDs = new Dictionary<Type, HashSet<Guid>>(); private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToPendingComponentIDs = new Dictionary<Type, HashSet<Guid>>();
private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToComponentIDs = new Dictionary<Type, HashSet<Guid>>(); private readonly Dictionary<Type, HashSet<Guid>> componentMessageTypeToComponentIDs = new Dictionary<Type, HashSet<Guid>>();
@ -136,12 +136,12 @@ namespace Encompass
} }
} }
private void RegisterExistingOrPendingComponentMessage<TComponent>(Entity entity, Guid componentID, TComponent component) where TComponent: struct, IComponent private void RegisterExistingOrPendingComponentMessage<TComponent>(Entity entity, Guid componentID, TComponent component) where TComponent : struct, IComponent
{ {
componentIDToComponent[componentID] = component; componentIDToComponent[componentID] = component;
componentIDToEntityID[componentID] = entity.ID; componentIDToEntityID[componentID] = entity.ID;
componentIDToType[componentID] = typeof(TComponent); componentIDToType[componentID] = typeof(TComponent);
if (!componentMessageTypeToComponentIDs.ContainsKey(typeof(TComponent))) if (!componentMessageTypeToComponentIDs.ContainsKey(typeof(TComponent)))
{ {
componentMessageTypeToComponentIDs.Add(typeof(TComponent), new HashSet<Guid>()); componentMessageTypeToComponentIDs.Add(typeof(TComponent), new HashSet<Guid>());
@ -163,7 +163,7 @@ namespace Encompass
return Enumerable.Empty<(Guid, TComponent)>(); return Enumerable.Empty<(Guid, TComponent)>();
} }
internal IEnumerable<(Guid, TComponent)> ReadExistingComponentsByType<TComponent>() where TComponent: struct, IComponent internal IEnumerable<(Guid, TComponent)> ReadExistingComponentsByType<TComponent>() where TComponent : struct, IComponent
{ {
if (componentMessageTypeToExistingComponentIDs.TryGetValue(typeof(TComponent), out HashSet<Guid> idSet)) if (componentMessageTypeToExistingComponentIDs.TryGetValue(typeof(TComponent), out HashSet<Guid> idSet))
{ {
@ -187,16 +187,19 @@ namespace Encompass
internal (Guid, TComponent) ReadFirstExistingOrPendingComponentByType<TComponent>() where TComponent : struct, IComponent internal (Guid, TComponent) ReadFirstExistingOrPendingComponentByType<TComponent>() where TComponent : struct, IComponent
{ {
if (!SomeExistingOrPendingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadExistingAndPendingComponentsByType<TComponent>().First(); return ReadExistingAndPendingComponentsByType<TComponent>().First();
} }
internal (Guid, TComponent) ReadFirstExistingComponentByType<TComponent>() where TComponent : struct, IComponent internal (Guid, TComponent) ReadFirstExistingComponentByType<TComponent>() where TComponent : struct, IComponent
{ {
if (!SomeExistingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadExistingComponentsByType<TComponent>().First(); return ReadExistingComponentsByType<TComponent>().First();
} }
internal (Guid, TComponent) ReadFirstPendingComponentByType<TComponent>() where TComponent : struct, IComponent internal (Guid, TComponent) ReadFirstPendingComponentByType<TComponent>() where TComponent : struct, IComponent
{ {
if (!SomeExistingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadPendingComponentsByType<TComponent>().First(); return ReadPendingComponentsByType<TComponent>().First();
} }

View File

@ -6,6 +6,11 @@ using Encompass.Exceptions;
namespace Encompass namespace Encompass
{ {
/// <summary>
/// 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.
/// </summary>
public abstract class Engine public abstract class Engine
{ {
internal readonly HashSet<Type> sendTypes = new HashSet<Type>(); internal readonly HashSet<Type> sendTypes = new HashSet<Type>();
@ -77,23 +82,42 @@ namespace Encompass
this.componentMessageManager = componentMessageManager; this.componentMessageManager = componentMessageManager;
} }
/// <summary>
/// Runs once per World update with the calculated delta-time.
/// </summary>
/// <param name="dt">The time in seconds that has elapsed since the previous frame.</param>
public abstract void Update(double dt); public abstract void Update(double dt);
/// <summary>
/// Creates and returns a new empty Entity.
/// </summary>
protected Entity CreateEntity() protected Entity CreateEntity()
{ {
return entityManager.CreateEntity(); return entityManager.CreateEntity();
} }
/// <summary>
/// Returns true if an Entity with the specified ID exists.
/// </summary>
protected bool EntityExists(Guid entityID) protected bool EntityExists(Guid entityID)
{ {
return entityManager.EntityExists(entityID); return entityManager.EntityExists(entityID);
} }
/// <summary>
/// Returns the Entity with the specified ID.
/// </summary>
/// <exception cref="Encompass.Exceptions.EntityNotFoundException">
/// Thrown when an Entity with the given ID does not exist.
/// </exception>
protected Entity GetEntity(Guid entityID) protected Entity GetEntity(Guid entityID)
{ {
return entityManager.GetEntity(entityID); return entityManager.GetEntity(entityID);
} }
/// <summary>
/// Returns the Entity ID associated with the specified Component Type and ID.
/// </summary>
protected Guid GetEntityIDByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent protected Guid GetEntityIDByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -107,11 +131,37 @@ namespace Encompass
return componentMessageManager.GetEntityIDByComponentID(componentID); return componentMessageManager.GetEntityIDByComponentID(componentID);
} }
/// <summary>
/// Returns the Entity associated with the specified Component Type and ID.
/// </summary>
protected Entity GetEntityByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent protected Entity GetEntityByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{ {
return GetEntity(GetEntityIDByComponentID<TComponent>(componentID)); return GetEntity(GetEntityIDByComponentID<TComponent>(componentID));
} }
/// <summary>
/// Returns an Entity containing the specified Component type.
/// </summary>
protected Entity ReadEntity<TComponent>() where TComponent : struct, IComponent
{
var (id, component) = ReadComponent<TComponent>();
return GetEntityByComponentID<TComponent>(id);
}
/// <summary>
/// Returns all Entities containing the specified Component type.
/// </summary>
protected IEnumerable<Entity> ReadEntities<TComponent>() where TComponent : struct, IComponent
{
foreach (var (id, component) in ReadComponents<TComponent>())
{
yield return GetEntityByComponentID<TComponent>(id);
}
}
/// <summary>
/// Returns the Component struct with the specified Component Type and ID.
/// </summary>
protected TComponent GetComponentByID<TComponent>(Guid componentID) where TComponent : struct, IComponent protected TComponent GetComponentByID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -142,7 +192,10 @@ namespace Encompass
return GetEntity(componentManager.GetEntityIDByComponentID(componentID)); return GetEntity(componentManager.GetEntityIDByComponentID(componentID));
} }
protected IEnumerable<ValueTuple<Guid, TComponent>> ReadComponents<TComponent>() where TComponent : struct, IComponent /// <summary>
/// Returns all of the Components with the specified Component Type.
/// </summary>
protected IEnumerable<(Guid, TComponent)> ReadComponents<TComponent>() where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -164,7 +217,10 @@ namespace Encompass
} }
} }
protected ValueTuple<Guid, TComponent> ReadComponent<TComponent>() where TComponent : struct, IComponent /// <summary>
/// Returns a Component with the specified Component Type and ID. If multiples exist, an arbitrary Component is returned.
/// </summary>
protected (Guid, TComponent) ReadComponent<TComponent>() where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -186,6 +242,9 @@ namespace Encompass
} }
} }
/// <summary>
/// Returns true if any Component with the specified Component Type exists.
/// </summary>
protected bool SomeComponent<TComponent>() where TComponent : struct, IComponent protected bool SomeComponent<TComponent>() where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -208,7 +267,16 @@ namespace Encompass
} }
} }
protected ValueTuple<Guid, TComponent> GetComponent<TComponent>(Entity entity) where TComponent : struct, IComponent /// <summary>
/// Returns a Component with the specified Type that exists on the Entity.
/// </summary>
/// <exception cref="Encompass.Exceptions.NoComponentOfTypeOnEntityException">
/// Thrown when the Entity does not have a Component of the specified Type
/// </exception>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it reads the given Component Type.
/// </exception>
protected (Guid, TComponent) GetComponent<TComponent>(Entity entity) where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>)); var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -241,6 +309,12 @@ namespace Encompass
} }
} }
/// <summary>
/// Returns true if the Entity has a Component of the given Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that is Reads the given Component Type.
/// </exception>
protected bool HasComponent<TComponent>(Entity entity) where TComponent : struct, IComponent protected bool HasComponent<TComponent>(Entity entity) where TComponent : struct, IComponent
{ {
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>)); var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -264,6 +338,12 @@ namespace Encompass
} }
} }
/// <summary>
/// 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.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalWriteException">
/// Thrown when the Engine does not declare that it Writes the given Component Type.
/// </exception>
protected Guid SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent protected Guid SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent
{ {
var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0; var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0;
@ -288,11 +368,22 @@ namespace Encompass
return componentID; return componentID;
} }
/// <summary>
/// Overwrites Component struct data associated with the specified Component ID.
/// </summary>
protected Guid SetComponent<TComponent>(Guid componentID, TComponent component) where TComponent : struct, IComponent protected Guid SetComponent<TComponent>(Guid componentID, TComponent component) where TComponent : struct, IComponent
{ {
return SetComponent(GetEntityByComponentID<TComponent>(componentID), component); return SetComponent(GetEntityByComponentID<TComponent>(componentID), component);
} }
/// <summary>
/// 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.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalWriteException">
/// Thrown when the Engine does not declare that it Writes the given Component Type.
/// </exception>
protected Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int layer = 0) where TComponent : struct, IComponent, IDrawComponent protected Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int layer = 0) where TComponent : struct, IComponent, IDrawComponent
{ {
var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0; var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0;
@ -317,6 +408,12 @@ namespace Encompass
return componentID; return componentID;
} }
/// <summary>
/// Sends a Message.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalSendException">
/// Thrown when the Engine does not declare that it Sends the Message Type.
/// </exception>
protected void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage protected void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage
{ {
if (!sendTypes.Contains(typeof(TMessage))) if (!sendTypes.Contains(typeof(TMessage)))
@ -327,6 +424,10 @@ namespace Encompass
messageManager.AddMessage(message); messageManager.AddMessage(message);
} }
/// <summary>
/// Sends a message after the specified number of seconds.
/// </summary>
/// <param name="time">The time in seconds that will elapse before the message is sent.</param>
protected void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage protected void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage
{ {
messageManager.AddMessageDelayed(message, time); messageManager.AddMessageDelayed(message, time);
@ -355,6 +456,12 @@ namespace Encompass
componentMessageManager.AddPendingComponentMessage(message); componentMessageManager.AddPendingComponentMessage(message);
} }
/// <summary>
/// Reads all messages of the specified Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage
{ {
if (!receiveTypes.Contains(typeof(TMessage))) if (!receiveTypes.Contains(typeof(TMessage)))
@ -365,11 +472,23 @@ namespace Encompass
return messageManager.GetMessagesByType<TMessage>(); return messageManager.GetMessagesByType<TMessage>();
} }
/// <summary>
/// Reads an arbitrary message of the specified Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage
{ {
return ReadMessages<TMessage>().First(); return ReadMessages<TMessage>().First();
} }
/// <summary>
/// Returns true if a Message of the specified Type has been sent this frame.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected bool SomeMessage<TMessage>() where TMessage : struct, IMessage protected bool SomeMessage<TMessage>() where TMessage : struct, IMessage
{ {
if (!receiveTypes.Contains(typeof(TMessage))) if (!receiveTypes.Contains(typeof(TMessage)))
@ -380,11 +499,48 @@ namespace Encompass
return ReadMessages<TMessage>().Any(); return ReadMessages<TMessage>().Any();
} }
/// <summary>
/// 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.
/// </summary>
protected void Destroy(Guid entityID) protected void Destroy(Guid entityID)
{ {
entityManager.MarkForDestroy(entityID); entityManager.MarkForDestroy(entityID);
} }
/// <summary>
/// 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.
/// </summary>
protected void Destroy(Entity entity)
{
entityManager.MarkForDestroy(entity.ID);
}
/// <summary>
/// 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.
/// </summary>
protected void DestroyWith<TComponent>() where TComponent : struct, IComponent
{
Destroy(ReadEntity<TComponent>());
}
/// <summary>
/// Destroys all Entities containing a Component of the specified Type.
/// Entity destruction takes place after all the Engines have been processed by World Update.
/// </summary>
protected void DestroyAllWith<TComponent>() where TComponent : struct, IComponent
{
foreach (var entity in ReadEntities<TComponent>())
{
Destroy(entity);
}
}
/// <summary>
/// Removes the Component with the specified ID from its Entity.
/// </summary>
protected void RemoveComponent(Guid componentID) protected void RemoveComponent(Guid componentID)
{ {
componentManager.MarkForRemoval(componentID); componentManager.MarkForRemoval(componentID);

View File

@ -2,6 +2,10 @@
namespace Encompass.Engines namespace Encompass.Engines
{ {
/// <summary>
/// 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.
/// </summary>
public abstract class Spawner<TMessage> : Engine where TMessage : struct, IMessage public abstract class Spawner<TMessage> : Engine where TMessage : struct, IMessage
{ {
protected Spawner() : base() protected Spawner() : base()

View File

@ -2,6 +2,10 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// 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.
/// </summary>
public struct Entity : IEquatable<Entity> public struct Entity : IEquatable<Entity>
{ {
public readonly Guid ID; public readonly Guid ID;

View File

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

View File

@ -1,4 +1,7 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// Structs that implement IComponent are considered to be Components.
/// </summary>
public interface IComponent { } public interface IComponent { }
} }

View File

@ -1,4 +1,7 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// Structs that implement IDrawComponent are considered to be DrawComponents.
/// </summary>
public interface IDrawComponent { } public interface IDrawComponent { }
} }

View File

@ -1,4 +1,7 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// Structs that implement IMessage are considered to be Messages.
/// </summary>
public interface IMessage { } public interface IMessage { }
} }

View File

@ -1,5 +1,9 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// 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.
/// </summary>
public abstract class GeneralRenderer : Renderer public abstract class GeneralRenderer : Renderer
{ {
public abstract void Render(); public abstract void Render();

View File

@ -2,6 +2,9 @@
namespace Encompass namespace Encompass
{ {
/// <summary>
/// OrdereredRenderer provides a structure for the common pattern of wishing to draw a specific DrawComponent at a specific layer.
/// </summary>
public abstract class OrderedRenderer<TComponent> : Renderer where TComponent : struct, IComponent, IDrawComponent public abstract class OrderedRenderer<TComponent> : Renderer where TComponent : struct, IComponent, IDrawComponent
{ {
public abstract void Render(Guid drawComponentID, TComponent drawComponent); public abstract void Render(Guid drawComponentID, TComponent drawComponent);

View File

@ -2,6 +2,9 @@ using System.Collections.Generic;
namespace Encompass namespace Encompass
{ {
/// <summary>
/// The World is a collection of Engines, Renderers, Entities, Components, and Messages that compose the simulation.
/// </summary>
public class World public class World
{ {
private readonly List<Engine> enginesInOrder; private readonly List<Engine> enginesInOrder;
@ -28,10 +31,14 @@ namespace Encompass
this.renderManager = renderManager; this.renderManager = renderManager;
} }
/// <summary>
/// Drives the simulation. Should be called from your game engine's update loop.
/// </summary>
/// <param name="dt">The time in seconds that has passed since the previous frame.</param>
public void Update(double dt) public void Update(double dt)
{ {
messageManager.ProcessDelayedMessages(dt); messageManager.ProcessDelayedMessages(dt);
foreach (var engine in enginesInOrder) foreach (var engine in enginesInOrder)
{ {
engine.Update(dt); engine.Update(dt);
@ -45,6 +52,9 @@ namespace Encompass
componentManager.WriteComponents(); componentManager.WriteComponents();
} }
/// <summary>
/// Causes the Renderers to draw.
/// </summary>
public void Draw() public void Draw()
{ {
renderManager.Draw(); renderManager.Draw();

View File

@ -7,6 +7,14 @@ using Encompass.Engines;
namespace Encompass namespace Encompass
{ {
/// <summary>
/// WorldBuilder is used to construct a World from Engines, Renderers, and an initial state of Entities, Components, and Messages.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class WorldBuilder public class WorldBuilder
{ {
private readonly List<Engine> engines = new List<Engine>(); private readonly List<Engine> engines = new List<Engine>();
@ -35,26 +43,42 @@ namespace Encompass
renderManager = new RenderManager(componentManager, drawLayerManager); renderManager = new RenderManager(componentManager, drawLayerManager);
} }
/// <summary>
/// Creates and returns a new empty Entity.
/// </summary>
public Entity CreateEntity() public Entity CreateEntity()
{ {
return entityManager.CreateEntity(); return entityManager.CreateEntity();
} }
/// <summary>
/// Specifies that the given Message should be sent immediately on the first World Update.
/// </summary>
public void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage public void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage
{ {
messageManager.AddMessage(message); messageManager.AddMessage(message);
} }
/// <summary>
/// Specifies that the given Message should be sent after the specified number of seconds after the first World Update.
/// </summary>
public void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage public void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage
{ {
messageManager.AddMessageDelayed(message, time); messageManager.AddMessageDelayed(message, time);
} }
/// <summary>
/// Sets Component data for the specified Component Type on the specified Entity.
/// </summary>
public Guid SetComponent<TComponent>(Entity entity, TComponent component, int priority = 0) where TComponent : struct, IComponent public Guid SetComponent<TComponent>(Entity entity, TComponent component, int priority = 0) where TComponent : struct, IComponent
{ {
return componentManager.MarkComponentForWrite(entity, component, priority); return componentManager.MarkComponentForWrite(entity, component, priority);
} }
/// <summary>
/// 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.
/// </summary>
public Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int priority = 0, int layer = 0) where TComponent : struct, IComponent, IDrawComponent public Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int priority = 0, int layer = 0) where TComponent : struct, IComponent, IDrawComponent
{ {
return componentManager.MarkDrawComponentForWrite(entity, component, priority, layer); return componentManager.MarkDrawComponentForWrite(entity, component, priority, layer);
@ -66,6 +90,10 @@ namespace Encompass
AddEngine((Engine)Activator.CreateInstance(typeof(ComponentMessageEmitter<>).MakeGenericType(componentType))); AddEngine((Engine)Activator.CreateInstance(typeof(ComponentMessageEmitter<>).MakeGenericType(componentType)));
} }
/// <summary>
/// Adds the specified Engine to the World.
/// </summary>
/// <param name="engine">An instance of an Engine.</param>
public Engine AddEngine<TEngine>(TEngine engine) where TEngine : Engine public Engine AddEngine<TEngine>(TEngine engine) where TEngine : Engine
{ {
engine.AssignEntityManager(entityManager); engine.AssignEntityManager(entityManager);
@ -126,6 +154,9 @@ namespace Encompass
return engine; return engine;
} }
/// <summary>
/// Adds the specified OrderedRenderer to the World.
/// </summary>
public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawComponent public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawComponent
{ {
renderer.AssignEntityManager(entityManager); renderer.AssignEntityManager(entityManager);
@ -134,6 +165,12 @@ namespace Encompass
return renderer; return renderer;
} }
/// <summary>
/// Adds the specified GeneralRenderer to the World at the specified layer.
/// Higher layer numbers draw on top of lower layer numbers.
/// </summary>
/// <param name="renderer">An instance of a GeneralRenderer.</param>
/// <param name="layer">The layer at which the GeneralRenderer should render. Higher numbers draw over lower numbers.</param>
public TRenderer AddGeneralRenderer<TRenderer>(TRenderer renderer, int layer) where TRenderer : GeneralRenderer public TRenderer AddGeneralRenderer<TRenderer>(TRenderer renderer, int layer) where TRenderer : GeneralRenderer
{ {
renderer.AssignEntityManager(entityManager); renderer.AssignEntityManager(entityManager);
@ -164,6 +201,11 @@ namespace Encompass
} }
} }
/// <summary>
/// Builds the World out of the state specified on the WorldBuilder.
/// Validates and constructs an ordering of the given Engines.
/// </summary>
/// <returns>An instance of World.</returns>
public World Build() public World Build()
{ {
BuildEngineGraph(); BuildEngineGraph();

View File

@ -487,6 +487,38 @@ namespace Tests
Assert.That(results, Does.Contain((componentCID, mockComponent))); Assert.That(results, Does.Contain((componentCID, mockComponent)));
} }
[Receives(typeof(DestroyComponentMessage))]
class DestroyEntityEngine : Engine
{
public override void Update(double dt)
{
foreach (var message in ReadMessages<DestroyComponentMessage>())
{
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))] [Reads(typeof(DestroyerComponent), typeof(MockComponent))]
class DestroyAndAddComponentEngine : Engine class DestroyAndAddComponentEngine : Engine
{ {
@ -681,7 +713,7 @@ namespace Tests
Assert.Throws<ComponentTypeMismatchException>(() => world.Update(0.01f)); Assert.Throws<ComponentTypeMismatchException>(() => world.Update(0.01f));
} }
struct EntityIDComponent : IComponent { public Guid entityID; } struct EntityIDComponent : IComponent { public Guid entityID; }
static bool hasEntity; static bool hasEntity;
[Reads(typeof(EntityIDComponent))] [Reads(typeof(EntityIDComponent))]
@ -774,7 +806,7 @@ namespace Tests
foreach (var (componentID, component) in ReadComponents<MockComponent>()) foreach (var (componentID, component) in ReadComponents<MockComponent>())
{ {
RemoveComponent(componentID); RemoveComponent(componentID);
SendMessageDelayed(new MockMessage {}, 1); SendMessageDelayed(new MockMessage { }, 1);
} }
} }
} }
@ -787,9 +819,9 @@ namespace Tests
var worldBuilder = new WorldBuilder(); var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new DelayedMessageEngine()); worldBuilder.AddEngine(new DelayedMessageEngine());
worldBuilder.AddEngine(new MessageReadEngine()); worldBuilder.AddEngine(new MessageReadEngine());
var entity = worldBuilder.CreateEntity(); var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent {}); worldBuilder.SetComponent(entity, new MockComponent { });
var world = worldBuilder.Build(); var world = worldBuilder.Build();
@ -817,7 +849,7 @@ namespace Tests
foreach (var message in ReadMessages<MockMessage>()) foreach (var message in ReadMessages<MockMessage>())
{ {
var entity = CreateEntity(); var entity = CreateEntity();
SetComponent(entity, new MockComponent {}); SetComponent(entity, new MockComponent { });
} }
} }
} }
@ -841,7 +873,7 @@ namespace Tests
worldBuilder.AddEngine(new ActivateComponentEngine()); worldBuilder.AddEngine(new ActivateComponentEngine());
worldBuilder.AddEngine(new RemoveComponentEngine()); worldBuilder.AddEngine(new RemoveComponentEngine());
worldBuilder.SendMessage(new MockMessage {}); worldBuilder.SendMessage(new MockMessage { });
var world = worldBuilder.Build(); var world = worldBuilder.Build();
@ -858,13 +890,13 @@ namespace Tests
{ {
foreach (var (componentID, component) in ReadComponents<MockComponent>()) foreach (var (componentID, component) in ReadComponents<MockComponent>())
{ {
SetComponent(componentID, new MockComponent {}); SetComponent(componentID, new MockComponent { });
} }
} }
} }
[Receives(typeof(DestroyComponentMessage))] [Receives(typeof(DestroyComponentMessage))]
class DestroyEntityEngine : Engine class DestroyEntityByIDEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
{ {
@ -880,16 +912,133 @@ namespace Tests
{ {
var worldBuilder = new WorldBuilder(); var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new AddComponentEngine()); worldBuilder.AddEngine(new AddComponentEngine());
worldBuilder.AddEngine(new DestroyEntityEngine()); worldBuilder.AddEngine(new DestroyEntityByIDEngine());
var entity = worldBuilder.CreateEntity(); var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent {}); worldBuilder.SetComponent(entity, new MockComponent { });
worldBuilder.SendMessage(new DestroyComponentMessage { entity = entity }); worldBuilder.SendMessage(new DestroyComponentMessage { entity = entity });
var world = worldBuilder.Build(); var world = worldBuilder.Build();
world.Update(0.01); world.Update(0.01);
Assert.DoesNotThrow(() => 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<MockComponent>();
}
}
[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<MockComponent>().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<MockComponent>())
{
DestroyWith<MockComponent>();
}
}
}
[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<MockComponent>();
}
}
[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();
}
} }
} }

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<RootNamespace>Tests</RootNamespace> <RootNamespace>Tests</RootNamespace>
<AssemblyName>EncompassECS.Framework.Tests</AssemblyName> <AssemblyName>EncompassECS.Framework.Tests</AssemblyName>