I'm setting up a really small Entity-Component-System example and have some problems with the component pools.
Currently my entities are just IDs (GUIDs) which is fine.
Each system has to implement the ISystem
interface
internal interface ISystem
{
void Run();
}
and is stored in a KeyedByTypeCollection
. This collection makes sure, that each system is unique.
Each component has to implement the IComponent
interface.
internal interface IComponent
{
}
By doing so I can store all the different component types to their matching component pool. Each pool is a Dictionary<Guid, IComponent>
. The key represents the ID of the Entity and the value is the component of that entity. Each pool is stored in a KeyedByTypeCollection
to make sure the component pool is unique.
Currently my EntityManager
class contains the core logic. I don't know if it's required to have it static but currently my systems need to get some information from it.
The important core methods to deal with the component pools are:
internal static class EntityManager
{
public static List<Guid> activeEntities = new List<Guid>();
private static KeyedByTypeCollection<Dictionary<Guid, IComponent>> componentPools = new KeyedByTypeCollection<Dictionary<Guid, IComponent>>();
public static KeyedByTypeCollection<ISystem> systems = new KeyedByTypeCollection<ISystem>(); // Hold unique Systems
public static Guid CreateEntity() // Add a new GUID and return it
{
Guid entityId = Guid.NewGuid();
activeEntities.Add(entityId);
return entityId;
}
public static void DestroyEntity(Guid entityId) // Remove the entity from every component pool
{
activeEntities.Remove(entityId);
for (int i = 0; i < componentPools.Count; i++)
{
componentPools[i].Remove(entityId);
}
}
public static Dictionary<Guid, TComponent> GetComponentPool<TComponent>() where TComponent : IComponent // get a component pool by its component type
{
return componentPools[typeof(TComponent)];
}
public static void AddComponentPool<TComponent>() where TComponent : IComponent // add a new pool by its component type
{
componentPools.Add(new Dictionary<Guid, TComponent>());
}
public static void RemoveComponentPool<TComponent>() where TComponent : IComponent // remove a pool by its component type
{
componentPools.Remove(typeof(TComponent));
}
public static void AddComponentToEntity(Guid entityId, IComponent component) // add a component to an entity by the component type
{
componentPools[component.GetType()].Add(entityId, component);
}
public static void RemoveComponentFromEntity<TComponent>(Guid entityId) where TComponent : IComponent // remove a component from an entity by the component type
{
componentPools[typeof(TComponent)].Remove(entityId);
}
}
I created a small movement system for testing purposes:
internal class Movement : ISystem
{
public void Run()
{
for (int i = 0; i < EntityManager.activeEntities.Count; i++)
{
Guid entityId = EntityManager.activeEntities[i];
if (EntityManager.GetComponentPool<Position>().TryGetValue(entityId, out Position positionComponent)) // Get the position component
{
positionComponent.X++; // Move one to the right and one up ...
positionComponent.Y++;
}
}
}
}
Which should be fine because of the dictionary lookups. I am not sure if I can optimize it but I think I have to loop through all the entities first.
And here is the problem:
I am not able to use KeyedByTypeCollection<Dictionary<Guid, IComponent>>
because I need to have something like
KeyedByTypeCollection<Dictionary<Guid, Type where Type : IComponent>>
My GetComponentPool
and AddComponentPool
methods throw errors.
GetComponentPool: Cannot implicitly convert IComponent to TComponent
When calling GetComponentPool
I would have to cast the dictionary values from IComponent
to TComponent
.
AddComponentPool: Cannot convert from TComponent to IComponent
When calling AddComponentPool
I would have to cast TComponent
to IComponent
.
I don't think casting is an option because this seems to lower the performance.
Would someone mind helping me fix the type problem?
Update:
When debugging I use this Code to test the whole ECS
internal class Program
{
private static void Main(string[] args)
{
EntityManager.AddComponentPool<Position>();
EntityManager.AddComponentPool<MovementSpeed>();
EntityManager.Systems.Add(new Movement());
Guid playerId = EntityManager.CreateEntity();
EntityManager.AddComponentToEntity(playerId, new Position());
EntityManager.AddComponentToEntity(playerId, new MovementSpeed(1));
foreach (ISystem system in EntityManager.Systems)
{
system.Run();
}
}
}
I already showed the Movement
System, here are the missing components
internal class Position : IComponent
{
public Position(float x = 0, float y = 0)
{
X = x;
Y = y;
}
public float X { get; set; }
public float Y { get; set; }
}
internal class MovementSpeed : IComponent
{
public MovementSpeed(float value = 0)
{
Value = value;
}
public float Value { get; set; }
}
The heart of the problem is that Dictionary<Guid, TComponent>
and Dictionary<Guid, IComponent>
are entirely unrelated types. They are not convertible to one another. This is because the dictionary can be read and written. If you could convert a List<Giraffe>
to a List<Animal>
you could then store a Tiger
which would break type safety. If you could convert the other way around you could read Tiger
instances and treat them as Giraffe
which again would break type safety. You can search the web for .net covariance contravariance
to find more information.
A simple solution is to change from
KeyedByTypeCollection<Dictionary<Guid, IComponent>> componentPools
to
Dictionary<Type, object> componentPools
.
This enables you to store a Dictionary<Guid, TComponent>
in the collection which was impossible before. The downside is that you need to cast now. But you only need to cast the dictionary instance which is rather cheap. You do not need to convert the entire dictionary in O(N) time.
The accessors:
public static Dictionary<Guid, TComponent> GetComponentPool<TComponent>() where TComponent : IComponent // get a component pool by its component type
{
return (Dictionary<Guid, TComponent>)componentPools[typeof(TComponent)]; //cast inserted
}
public static void AddComponentPool<TComponent>() where TComponent : IComponent // add a new pool by its component type
{
componentPools.Add(typeof(TComponent), (object)new Dictionary<Guid, TComponent>()); //unneeded cast inserted for clarity
}
Maybe you don't like that we need to cast at all. I can see no way around that. Practical experience shows that when you become to elaborate with the type system (generics, variance) you obtain a worse solution. So don't worry about it. Do what works.