2
\$\begingroup\$

As C# is a strongly-typed language, I'm having great difficulties associating all my data types together so that I can have a clean, if not performant, game loop.

Example #1: Keeping Components Together

Keeping components together means I need an interface or base class to store them in an array, which would require keeping track of each type separately so that it can be converted back to the derived type before being consumed. The constant type conversion seems like a bad idea in an ECS?

To compound the problem, I would prefer to use structs instead of classes for my components, which when cast to an interface will cause boxing. In some simple stop watch tests I've run it seems the casting/boxing is 16x slower than just looping over the raw type. (Pseudo code below)

IComponent sprite = new SpriteComponentStruct() // boxing

for(int i=0; i<iComponents.Length; i++)
{
    var type = GetComponentTypeByIndex(i);
    var system = GetSystemWithType(type);
    SpriteComponent sprite = Convert.ChangeType(...);
    system.Execute(sprite);
}

Example #2: Keeping Components Separate

To keep components separately in their own classes/managers/systems I would likely need a generic class to cut down on the code duplication. The problem I run into here is that looping over each class will require a base class or interface, which at the very least would force me to cast the component types again. (Pseudo code below)

ComponentManager<T>() where T : struct
{
    T[] components;

    // can't use with non-generic interface to put manager into a list
    public void AddComponent(T component) {...} 

    // won't compile; type "T" will not cast to type "T"
    public void AddComponent<T>(T Component) {...}

    // requires casting and/or boxing, which defeats
    // the purpose of keeping components separate
    public void AddComponent(object component) {...}

}

It seems every which way I try to implement a ECS in C# ends with hacky/non-performant code, that's assuming I can get it to compile in the first place. From studying other ECS systems around the net, it seems some side-step these issues by using void/int pointers or storing component bytes directly in memory (like I believe the new ECS in Unity 2018 does). Either way those are unsafe operations in what's supposed to be a managed memory language.

So is C# the wrong tool for the job? Am I approaching this incorrectly? I'd love to hear some opinions as this just seem impossible...

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Your problem is likely where T : struct. Structs are...not classes. They're packets of data, like a Vector3 is just 3 floats wrapped up and given a fancy name. Additionally you're trying to say that ANY struct can be a valid component (is a Vector3 a component too?). Its also possible that you need to use the .NET 4.6 experimental version of .NET (in the Unity settings somewhere) in order to have the covariance and contravariance necessary for the kinds of generics you want to use to compile. \$\endgroup\$ Commented Oct 28, 2018 at 1:42
  • \$\begingroup\$ With regards to the new ECS implementation in Unity, keep in mind that most of it (especially in conjunction with Jobs and Burst) is designed to be unmanaged under the hood to prevent GC running at all which would be a massive performance loss. This is a reason why you are forced to work with sequential data, NativeArrays and slices that you have manage and dispose yourself. \$\endgroup\$ Commented Oct 28, 2018 at 11:44
  • \$\begingroup\$ @Draco18s You're right, that constraint should be an interface that my structs implement and not accept any old structs. \$\endgroup\$ Commented Oct 29, 2018 at 0:54
  • \$\begingroup\$ @ChristianIvicevic Indeed, that certainly made the source code much more difficult to understand when I first went through it! Now that I understand it better I actually kind of like how it's implemented, though as I don't have much experience managing memory myself (sounds fun though) I will likely avoid going that kind of route. \$\endgroup\$ Commented Oct 29, 2018 at 1:24
  • \$\begingroup\$ I am writing something similar, using struct components, and I went with static instances of generic component managers/pools. They handle all logic using generic constraints to avoid boxing, and they keep track each entity's components. EntityIndices[id] gives an index into the pool. I have no need to iterate over the managers/pools themselves for anything but save and load and such, and reduced efficiency is acceptable there. In all other cases, I know which types I need at compile time, allowing me to access the generic static instance. Component enumeration via ref return enumerator. \$\endgroup\$
    – dpaz
    Commented Mar 16, 2019 at 21:00

2 Answers 2

3
\$\begingroup\$

So a solution to this, as I've just successfully implemented, is to keep and "consume" all data types separately, thus avoiding casting/boxing operations. Every component type has it's own unique array and type-specific methods (a generic helps here, but the generic really needs to be accessed separately and not part of a bigger collection). The key to keeping everything separate seems to be using indexes to link it all back together. I've included example code below.

*Note that using "structs" as I mentioned in my question has an additional requirement of using the REF keyword (a lot), or finding a way to re-assign any updated components.

class Game
{
    // entities store indexes of type and component together
    Entity[] entities;

    // type index for identifying components
    Dictionary<Type,int> componentTypes;

    PositionComponent[] positionComponents;
    SpriteComponent[] spriteComponents;
    HealthComponent[] healthComponents;

    PlayerSystem playerSystem;

    void UpdateLoop()
    {

        // for each entity... 

        this.playerSystem.Execute(
            GetPositionComponent(entity),
            GetSpriteComponent(entity),
            GetHealthComponent(entity));

    }

    PositionComponent GetPositionComponent(Entity entity)
    {
        int typeIndex = this.componentTypes(typeof(PositionComponent);
        int componentIndex = entity.GetComponentIndex(typeIndex);
        return this.positionComponents[componentIndex]
    }

    ...
}
class entity
{
   int[] componentTypes;
   int[] componentData;
}
\$\endgroup\$
0
\$\begingroup\$

If you haven’t already checked it out, Artemis is an excellent ECS framework. Originally written in Java, there is a C# port available here:

https://thelinuxlich.github.io/artemis_CSharp/

I’ve successfully implemented it in a couple of SharpDX projects, most recently with a threaded message bus infrastructure. The GitHub repo is here:

https://github.com/thelinuxlich/artemis_CSharp

\$\endgroup\$
0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .