[OLD THREAD] DOTS Polymorphic Components!

Update: a newer/better version of this tool is available here:

Original post

Links:
Sample project
Package download


What does it do:

This tool gives you a DOTS equivalent to calling interface functions on “objects” without knowing what the type of the object is in advance, even from inside bursted jobs. It generates a struct (either a IComponentData or a IBufferElement) that can assume the role of any struct in the project that implements a given interface.

For example:

  • you create a IMyPolyComponent interface that has a “DoSomething()” function
  • you create structs A, B, C that implement that interface, and each do different things in DoSomething()
  • you press a button to generate a “MyPolyComponent” component that can assume the role of either A, B, or C, but they are all considered as the same component type in the eyes of the ECS.

This allows you to iterate on a set of MyPolyComponent components and call DoSomething() on them, and they’ll each do their own respective implementation of DoSomething(), depending on which of the sub-types (A, B, or C) they have been assigned

The generated struct can either be a “union struct” (will have a size equivalent to the max size among all of its forms), or not a union struct (will have a size equivalent to the total size of all of its forms, but with the advantage of being able to store the data of each of its forms)


When to use:

As you can see in this performance comparison , polymorphic components can be a much better solution than anything involving structural changes if those changes happen relatively frequently. They also have the advantage of making the code much simpler.

They can also be useful in more specific cases like these:


How to use:

How to use

1. Create the interface that defines your polymorphic component
you create an interface with a special attribute on it: this defines a all the functions that the various types within your polymorphic component can have

[PolymorphicComponentDefinition("MyPolyComponent", "_Samples/PolymorphicTest/_GENERATED")]
public interface IMyPolyComp
{
    void Update(float deltaTime, ref Translation translation, ref Rotation rotation);
}

2. Create the specific structs that are part of the polymorphic component
you create several structs that implement the interface from point 1: this defines the specific implementations of all the various types that your polymorphic component can assume

[Serializable]
public struct CompA : IMyPolyComp
{
    public float MoveSpeed;
    public float MoveAmplitude;

    [HideInInspector]
    public float TimeCounter;

    public void Update(float deltaTime, ref Translation translation, ref Rotation rotation)
    {
        TimeCounter += deltaTime;

        translation.Value.y = math.sin(TimeCounter * MoveSpeed) * MoveAmplitude;
    }
}

[Serializable]
public struct CompB : IMyPolyComp
{
    public float RotationSpeed;
    public float3 RotationAxis;

    public void Update(float deltaTime, ref Translation translation, ref Rotation rotation)
    {
        rotation.Value = math.mul(rotation.Value, quaternion.AxisAngle(math.normalizesafe(RotationAxis), RotationSpeed * deltaTime));
    }
}

3. Generate the polymorphic component code
you press a button to codegen a polymorphic component based on the interface you created in point 1, and the structs you defined in point 2. The generated component has all the methods of the interface from point 1, but it will automatically call them on whatever specific struct type was assigned to your polymorphic component by the authoring component

[Serializable]
[StructLayout(LayoutKind.Explicit, Size = 20)]
public struct MyPolyComponent : IComponentData
{
    public enum TypeId
    {
        CompA,
        CompB,
    }

    [FieldOffset(0)]
    public CompA CompA;
    [FieldOffset(0)]
    public CompB CompB;

    [FieldOffset(16)]
    public readonly TypeId CurrentTypeId;

    public MyPolyComponent(in CompA c)
    {
        CompB = default;
        CompA = c;
        CurrentTypeId = TypeId.CompA;
    }

    public MyPolyComponent(in CompB c)
    {
        CompA = default;
        CompB = c;
        CurrentTypeId = TypeId.CompB;
    }


    public void Update(Single deltaTime, ref Translation translation, ref Rotation rotation)
    {
        switch (CurrentTypeId)
        {
            case TypeId.CompA:
                CompA.Update(deltaTime, ref translation, ref rotation);
                break;
            case TypeId.CompB:
                CompB.Update(deltaTime, ref translation, ref rotation);
                break;
        }
    }
}

4. Call polymorphic functions from a system!

public class TestSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;

        Entities.ForEach((Entity entity, ref MyPolyComponent polyComp, ref Translation translation, ref Rotation rotation) =>
        {
            polyComp.Update(deltaTime, ref translation, ref rotation);
        }).Schedule();
    }
}
18 Likes

Thanks for sharing!
What did you end up using this for? Where was it useful?

In general, I’d say this would be useful for situations where you have references to entities that you know will have a specific kind of function, but could have different implementations of that function (and different data).

Examples:

  • An “ability system” where each ability lives on its own entity, and you want to call “abilityFromEntity[equippedAbility].Launch()” without knowing in advance what specific type that ability is
  • A state machine where states are Entities
  • Event systems

The kind of use case that makes this better than current alternatives (like adding events to an event buffer on the target entity and waiting for another job to pick up the event later in the frame) is when instant changes would be preferable. Or when you don’t want to create “1 job per type of ability/state” because it would create too many jobs to schedule or too much tedious code to write.

When you’re not operating on massive amounts of entities, I think it definitely helps making your code much simpler and it possibly even improves performance compared to scheduling tons of jobs. And when dealing with thousands of entities, performance should still be pretty good; it just won’t be “the best possible thing”

3 Likes

You have a pretty cool way of thinking about code structure.
I’ve never thought of putting Update logic in components but it makes sense in many ways. Makes some of the code in systems way more readable. I’ve put a lot of code from systems ForEach in methods and the code file gets long. Scrolling and maintaining such systems is not that great.

1 Like

Really cool stuff, but it is definitely an OOP design. Per the DOTS Best Practices, “Combining data with methods that operate on that data encapsulated into a single structure (or class) is an OOP concept. DOD is based around having structures of data and systems to operate on that data. This means that as a general rule, there should be no methods in components and no data in systems. […] Methods on components aren’t necessarily harmful, but they indicate a DOD anti-pattern: the “do things one by one” anti-pattern.”

That being said, I could see it being useful in situations where you don’t have very many entities being processed (e.g. Camera state machine, Objective / level / mission flow control, character state in a classic FPS/TPS game).

2 Likes

I like to think that the true definition of DoD is “Understand how the CPU works, so that you can make better programming decisions”, and not something like “always do a by-the-book ECS implementation”, which is what tends to be implied when we hear about DoD

Or at least, regardless of the true definition, I think the former is a more useful mindset to have than the latter. If the best solution to a specific problem resembles OOP, then that solution would count as “DoD”, because it is the best solution to the problem. And that “best solution” must take into consideration usability and maintainability too

There definitely are caveats to having methods directly in components though. For example; it can change data inside of the component without you being aware of it, and so it becomes easy to forget writing that data back to the ECS. Perhaps I could modify the codegen so that it generates static functions that take the polymorphic component by ref instead. At least that way it would be clearer


Performance-wise, I wouldn’t be too surprised if this “polymorphic” approach performed better than a “each different thing has its own job” approach in several cases. If you have 100 different kinds of states/abilities in your game, that would mean scheduling up to 100 jobs with the latter approach. If you have let’s say under 100 actors that need these states/abilities in your game, I’d be ready to bet that the cost of scheduling up to 100 jobs could be greater than the cost of random data access

9 Likes

This seems pretty cool, and I can’t wait for something like this to be integrated into the compilation pipeline with C# source generators.

One thing I noticed briefly scanning the code is that you don’t have any protections for Entity fields or BlobAssetReference fields. If these do not receive unaliased memory (or if aliased, aliased by the same type), the memory region they occupy will be clobbered by the remappers.

2 Likes

oh, that’s something I didn’t know / think about. Thanks for pointing it out
Do you think adding explicit [FieldOffset] attributes over entity fields when declaring your structs would solve the problem? I’m not very familiar with those concepts

I doubt that would work. A component could have more than one entity reference, so all structs would have to avoid field offsets for the max amount of entity references for any component implementation.

The only way I can think of solving this is to codegen code that manually packs the union and exposes conversion operations. The manual packing is then aware of Entity and BlobAssetReference fields. That gets especially tricky with nested structs. You can see this code as well as the neighboring .gen.cs how I implemented this manually for circumventing a single BlobAssetReference. https://github.com/Dreaming381/Latios-Framework/blob/v0.3.3/PsyshockPhysics/Physics/Components/ColliderPsyshock.cs

3 Likes

I agree 100% with this. Instead of having a job for each ability state, I spread a state / ability across different entities (1 entity per state / ability) and have different, common components on each state / ability entity. States / abilities are activated using tags.

Jobs are based on component combinations in bite sized chunks. That way you minimize jobs, maximize code reuse. This limits the number of abilities being processed. This comes with the disadvantage of having to sync some data between owner and active ability (delta position, delta rotation, etc.), but this data is typically small and not an issue.

Somethings can be collapsed into a single job and selected for with an enum (e.g. weapon state enum of idle, firing, meleeing, reloading, casting, etc) to minimize job overhead. Your polymorphic component could even be a good candidate for updating common attributes related to weapon state in this example.

1 Like

Hi,

Interesting approach, I myself use some method in helper struct but never on components.

I have 2 questions remarks.

Wouldn’t the use of a switch statement in the polymorphic update cause branching and therefore hurt performance ? Or is that optimized by burst ?

Regarding the ability/skill exemple, you are worried about scheduling lots of jobs for each ability. As you may now I’m working on such a system and my logic is how many stuff can a skill really do ? You can have hundreds of different skill in your game but at their core they all have the same kind of effects (deal damage/ heal / play sound / vfx , spawn something,…) I feel like that really limits the number of systems you have to make. I would be interested in your thoughts on that ?

1 Like

In a project I’m working on we just used generic systems and put virtual methods inside components (so a component type is an argument of generic system). Works well enough.

I know compilers often optimize switch statements into a jump table past a certain number of cases, but I haven’t verified if that’s the case with burst (i’m not sure I’d know how to decipher the IL / machine code either)

Regardless, right now I don’t really have better ideas on how to handle this. I thought about something involving a table of function pointers, but the doc seems to suggest that there are some pretty big limitations with what you can pass as params to function pointers, so I chose to avoid them

I think the least we could say is that the performance impact of the switch case won’t be a concern unless you start using this on thousands of entities, but it’ll give you the benefit of instant changes, simpler code, and fewer jobs to schedule (so potentially better performance at small scales)

I can definitely imagine that ability logic in many games could be done with only a few jobs, and in that case one-job-per-ability-type would be good. But there are also some games that can have a few dozen different ways that abilities can “operate”, and in those cases this tool would help. “Buff systems” are also a good candidate, because in the games where I’ve tried creating buff systems, I was often ending up with over 20 different kinds of buff jobs (for >20 different stats that can be buffed)

This polymorphic component tool really isn’t meant to be used everywhere, but there are a few cases where it could make your code much simpler. Here’s a better example than the ability system:

  • During your game, you accumulate an ordered list of “events” that must happen in that specific sequence
  • An event that gets processed can create new events that insert themselves in the list right after that event

If your events were processed with “1 job per type of event”, this would be a nightmare due to the fact that they must be processed in order instead of in batches of similar types. You’d have to create a SystemGroup that has all of your event-processing jobs, and run your entire group again for every single event you process, on by one, most likely with structural changes between each run.

With these polymorphic structs, you can just do: foreach(event in buffer) → event.Play(ref data). In this case, this gives you a massive performance win compared to the other approach

2 Likes

Burst is very aggressive about this. I’d link to an old post I made regarding this topic, but I am too lazy to dig it up.

Branches and jump tables can be suboptimal, but they are still typically an order of magnitude faster than a cache miss. So union components rather than sparse chunks is usually still a win.

3 Likes

Pushed an update:

  • option to allow generating the component as a IBufferElement instead of IComponentData

  • option to allow generating the component as NOT a union struct. This has the downside of making the struct size be the cumulative size of all of its possible forms (instead of the maximum size among all of its possible forms), but the upsides are:

  • it can safely have Entity/BlobAssetReference fields

  • it can store the data of all of its other forms even after it changes form (can be useful for certain kinds of state machine implementations & such)

  • Don’t allow Entity/BlobAssetReference fields in polymorphic components that are in union struct mode, because of this

1 Like

I presume you need to specify the TypeId when using the generated component but I don’t see it set as readonly.

Edit: Are there guarantees for say if I use TypeId = CompA but I’m setting values for CompB? Would there be errors in that case?

You’d create authoring components for your specific types like this (note that I just refactored the names “ComponentType” and “TypeId” to “TypeId” and “CurrentTypeId”):

[DisallowMultipleComponent]
public class CompAAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public CompA CompA;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new MyPolyComponent { CompA = CompA, CurrentTypeId = MyPolyComponent.TypeId.CompA });
    }
}

So at conversion time, it tells the polymorphic component what its type is. However, I want to keep the option open to change its type at runtime, which is why I’m not making it readonly

No guarantees. I suppose the solution to this would be to codegen all kinds of constructors for the poly component (one constructor for each type it can assume the form of) and make the TypeId private

1 Like

I see. IMO, I don’t think that it’s a good idea to allow changing type at runtime. I mean a regular C# class instance doesn’t change type at runtime. I’d rather have type safe constructor for each type. For example:

public readonly ComponentType TypeId;

public MyPolyComponent(in CompA component) {
    this.CompA = component;
    this.TypeId = ComponentType.CompA;
}

public MyPolyComponent(in CompB component) {
    this.CompB = component;
    this.TypeId = ComponentType.CompB;
}

The use case I have in mind for this is state machines

You’d make your state machine be a polymorphic component that can assume the form of each state, and then you’d manage state transitions by manually changing the TypeId & assigning state data. Especially useful if your poly component is not in union struct mode (as explained here )

EDIT: well now that I think about it, the constructors approach would still allow this sort of stuff. I think I’ll add it (only when the polyComponent is in union struct mode)

1 Like

Update pushed:

Constructors for setting up the polymorphic component with a specific type, and readonly TypeId when the component is in UnionStruct mode

[Serializable]
[StructLayout(LayoutKind.Explicit, Size = 20)]
public struct MyPolyComponent : IComponentData
{
    public enum TypeId
    {
        CompA,
        CompB,
    }

    [FieldOffset(0)]
    public CompA CompA;
    [FieldOffset(0)]
    public CompB CompB;

    [FieldOffset(16)]
    public readonly TypeId CurrentTypeId;

    public MyPolyComponent(in CompA c)
    {
        CompB = default;
        CompA = c;
        CurrentTypeId = TypeId.CompA;
    }

    public MyPolyComponent(in CompB c)
    {
        CompA = default;
        CompB = c;
        CurrentTypeId = TypeId.CompB;
    }


    public void Update(Single deltaTime, ref Translation translation, ref Rotation rotation)
    {
        switch (CurrentTypeId)
        {
            case TypeId.CompA:
                CompA.Update(deltaTime, ref translation, ref rotation);
                break;
            case TypeId.CompB:
                CompB.Update(deltaTime, ref translation, ref rotation);
                break;
        }
    }
}
2 Likes