5

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; }
}
  • 1
    I cannot fully answer but I see one mistake you are doing. In some places, you are using `typeof(TComponent)` and on the second `component.GetType()`. These are not equivalent keys! – Karel Kral Jan 13 '19 at 11:05

1 Answers1

3

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.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Hey, thanks a lot for your help :) I updated my post to give an overview about the full Code. I edited my Code as you wanted me to do. These are my changes https://pastebin.com/AiUGWQmM When debugging I get an invalid cast exception at `AddComponentToEntity`. Would you mind checking it? –  Jan 13 '19 at 20:28
  • There's a `new new Dictionary` which should be `object`. I recommend that you learn to use the debugger to find simple bugs like this. An experienced developer needs about 3 seconds to find this bug with a debugger but maybe minutes by reading the code. @totalBeginner – usr Jan 13 '19 at 20:38
  • I'm sorry I didn't get your explanation. I tried to improve my Code and I think I will split the code into multiple classes but currently I have this https://pastebin.com/k16BQJ63 I used the debugger and saw that the error appears when calling `AddComponentToEntity` but I'm still not able to fix the exception :/ Could you please help? –  Jan 13 '19 at 22:03
  • I think I was confused myself :) I will answer you tomorrow @totalBeginner – usr Jan 13 '19 at 22:14
  • No problem. This would be great :) –  Jan 14 '19 at 05:26
  • @totalBeginner you need to consistently use `Dictionary` (not `Dictionary`) everywhere. This means that a few methods must be generic. For example, `GetComponentCollectionByComponentType` must be `TComponent GetComponentCollectionByComponentType()`. Also `AddComponentToEntity` must have a type parameter `TComponent`. All callers of your component API must then specify a concrete component type. – usr Jan 14 '19 at 12:39
  • thanks for your reply. I tried to edit the code as you told me to do https://pastebin.com/5WazJvFi but I have one question left. Please have a look at `AddComponentToEntity`. When it comes to the `Add` method I am not able to pass in an initial component. It is just expecting a type, am I missing something? An example call would be `entityManager.AddComponentToEntity(playerId, new Position() { X = 4, Y = 13 });` And can I make sure that `TComponent` and the `IComponent` parameter are the same type? otherwise I could run into something like `(new MovementSpeed())` –  Jan 14 '19 at 21:14
  • @usr I would like to reward you the bounty. Could you please try to answer the question to finish this thread? – Question3r Jan 17 '19 at 06:04
  • @usr I'm sorry I didn't see I just had to change `IComponent` to `TComponent` at the parameter list.. thank you –  Jan 19 '19 at 11:56
  • @Question3r I was AFK for a few days, I apologize. Just right this second I was about to type the answer. – usr Jan 19 '19 at 11:57
  • @usr really no problem. I changed `public void AddComponentToEntity(Guid entityId, IComponent component) where TComponent : IComponent { GetTypedComponentPoolByType().Add(entityId, component); }` to `public void AddComponentToEntity(Guid entityId, TComponent component) where TComponent : IComponent { GetTypedComponentPoolByType().Add(entityId, component); }` this should be fine I think no? –  Jan 19 '19 at 12:52
  • I think so. ____ – usr Jan 19 '19 at 12:59