Search code examples
c#genericsstatic-membersentity-component-system

Component Implementation in a ECS with structs


Goal:

I am trying to implement an Entity Component System and I'm stuck with my component implementation. I intend to have a generic component type which contains a static Bit for each component there is, e.g. components of type Position have Bit=0, components of type Sprite have Bit=1, etc. A component should only consist of the values it holds therefore I started with structs. Following is my attempt on this (not working code):

    struct Position : Component<Position> //this does not work in c#
    {
        public int x, y;
    }

    internal struct ComponentBit
    {
        public static int bitCounter = 0;

    };

    public struct Component<T>
    {
        public static readonly int bit;

        static Component()
        {
            bit = ComponentBit.bitCounter++;
        }

        public int GetBit()
        {
            return bit;
        }
    }

After that I found out that structs cannot be inherited from so I tried to change the structs to classes which worked.

Question:

Is there any way to implement these features with structs as it would be possible in C++ (templates)? I'd like to keep them as value types instead of reference types for implementation/performance reasons later on.

Edit1:

The intended usage I'd like to have is:

Position pos = new Position(x, y);
int bit1 = pos.GetBit(); // or
int bit2 = Position.bit; // both should be possible

Solution

  • My suggestion is to not try to use inheritance.

    Any struct is a possible valid component, for example:

    struct Position
    {
        public int X, Y;
    
        public Position(int x, int y)
        {
            X = x;
            Y = y;
        }
    }
    

    Now, you can still have Component<T> to work as a type to int dictionary (Dictionary<Type, int>). Although, I'm going with least changes to make this work:

    internal static class ComponentBit
    {
        public static int bitCounter = 0;
    
    };
    
    public static class Component<T>
    {
        public static readonly int bit; // This is static, it has a value per type T
    
        static Component()
        {
            bit = ComponentBit.bitCounter++;
        }
    
        public static int GetBit()
        {
            // This method only accesses static members, it can be static
            // Thus, no instance of T is needed
            return bit;
        }
    }
    

    And you would use it like this:

    Position pos = new Position(x, y);
    int bit1 = Component<Position>.GetBit();
    int bit2 = Component<Position>.bit;
    

    No idea why you want two ways of doing this, yet there you go.


    Alright, now I will take the freedom to do bigger changes... with that in mind, if you want type inference you can do this:

    public static class Component
    {
        internal static int bitCounter = 0;
    
        public static int GetBit<T>()
            where T : struct // We only accept structs here
        {
            return Component<T>.bit;
        }
    
        public static int GetBit<T>(this ref T value)
            where T : struct // We only accept structs here
        {
            // This is an extension method.
            // It will appear as a method on any valid T (which is all structs)
            // The type T will be infered from the instance.
            // Passing the struct as a reference to avoid a copy
            _ = value; // discard value
            return GetBit<T>();
        }
    };
    
    internal static class Component<T>
        where T : struct // We only accept structs here
    {
        internal static readonly int bit;
    
        static Component()
        {
            bit = Component.bitCounter++;
        }
    }
    

    Usage:

    var pos = new Position(x, y);
    var bit1 = Component.GetBit<Position>();
    var bit2 = pos.GetBit();
    

    Yes, the above code compiles and runs. See it in SharpLab.

    Note: I would consider nesting Component<T> in Component.