Usage Patterns
May 3, 2025 ยท View on GitHub
Table of Contents
- Polymorphic structs as a way to minimize structural changes or job counts
- Polymorphic structs as a way to minimize buffer lookups
- Polymorphic structs as a way to minimize archetype sizes
- Polymorphic structs as a way to handle type-independent ordering
- Polymorphic structs as a graph solving tool
- Minimizing data size in Polymorphic Structs
- Entity and Blob fields restriction workaround
Polymorphic structs as a way to minimize structural changes or job counts
Consider a use case where a single entity can change behaviour very often. Two typical ways of handling this is ECS would be:
- Use structural changes to remove the current behaviour component and add a new one on the entity.
- Une enableable components to enable the new behaviour component and disable the previous one.
There are cases where these will be the best solution for the job, but there are also cases where they won't. There is a certain point where the structural changes will be too costly (for the first approach), or at which the jobs overhead will be too costly (for the second approach). In these cases, polymorphic structs can be a better approach.
You would simply create a polymorphic MyBehaviour component, that can take many forms. When you want to change the behaviour, simply assign a new polymorphic struct sub-type to it, and the behaviour will change. This avoids both the cost of structural changes and the overhead of many jobs iterating enableable components.
Polymorphic structs as a way to minimize buffer lookups
Consider a use case where you need to access 3 dynamic buffers on an entity via BufferLookups. In a case like this, Polymorphic structs can offer a way to reduce this to 1 lookup (if it makes sense for the use case).
For example, let's say an entity must support 3 different types of events (stored in dynamic buffers on that entity and processed by a system): DynamicBuffer<EventA>, DynamicBuffer<EventB>, DynamicBuffer<EventC>. If something needs to add all 3 types of events on that entity, it will need to do 3 different buffer lookups in order to get all the event buffers. With Polymorphic structs, EventA, EventB, EventC could be turned into one single polymorphic Event type, stored in only one dynamic buffer. With this change, only one buffer lookup would be required for adding any amount of different types of events.
Polymorphic structs as a way to minimize archetype sizes
Imagine an AI entity that can have many different kinds of "behaviour modules" assigned to it. There are 20 different types of behaviour modules, and there can be several of each type of module. One way to implement this would be to add 20 dynamic buffers to your entity, one for each module type. However, even with an internal capacity of zero, this will bloat your archetype size due to the buffer header of each dynamic buffer, which must be stored in the chunk. With polymorphic structs, you could instead have just one dynamic buffer of modules, and add all modules of different types to this buffer. You're only paying the archetype size cost of one buffer instead of 20.
Polymorphic structs as a way to handle type-independent ordering
Imagine you need to implement an ordered events system, where events of many different types must be executed in the exact order they were added: EventTypeA, then EventTypeB, then EventTypeA, EventTypeC, then EventTypeB, etc.... Polymorphic structs can solve this problem easily and efficiently. Simply turn all your events into a polymorphic struct type, have a single buffer of those events, and add the polymorphic events in order.
Polymorphic structs as a graph solving tool
Polymorphic structs can be used as a general graph-solving tool. Consider this graph:

Let's say we need node A to receive a float input, and then this float value must receive a series of transformations while it flows through the other nodes, outputting different final values at nodes C, D, and E. This could be implemented as follows:
- Create a
INodepolymorphic interface type representing your nodes, with aNodeUpdate()method.- For each type of node input your nodes will need, add a property to the
INodeinterface for setting that input. Ex:InputFloat {get;set;},InputEntity {get;set;}, etc....
- For each type of node input your nodes will need, add a property to the
- For each node you'll have in the graph, create a poly struct implementing
INode. Each node can have fields to remember the indexes of its next nodes to pass outputs to (you can have aFixedListof output nodes as well, if you need a variable amount of outputs). - With this, the whole graph transformation of our
floatinput value would happen like this:- On node A, call
nodaA.InputFloat = 5f;, thennodeA.NodeUpdate();. - In node A's
NodeUpdate(), it does some transformation to the value of theInputFloatproperty, then for each output node index that it stored in its fields, callsoutputNode.InputFloat = transformedInput;, andoutputNode.NodeUpdate(). - Each node in the graph will continue this chain reaction of transformation of inputs, until we reach nodes that have no output nodes (nodes C, D, and E in this example). At this point we can get the output values from these nodes, perhaps with a new
GetOutput()polymorphic method for the nodes.
- On node A, call
Minimizing data size in Polymorphic Structs
It is generally a good thing to try to minimize the amount of data contained as fields of polymorphic structs, for performance reasons. Especially if most polymorphic structs under the same polymorphic interface are small, except a few that are very large. Because in those cases, all structs will take on the max size of all structs, which will affect performance.
For that reason, for larger structs, it can be a good idea to store some data outside of the struct itself, such as in another component/buffer on the entity, in some global HashMap, or even on another dedicated Entity just for that struct. Whether or not it's worth it will depend on how much cache space is wasted due to the size of the largest of the polymorphic structs of a certain type.
Entity and Blob fields restriction workaround
If not using the Merged Fields type of polymorphic structs, Entities and blob fields are not allowed in polymorphic structs. This is due to how ECS has special treatment for certain types of fields, and the union struct approach is incompatible with this special treatment. There are, however, alternatives to storing Entities in polymorphic structs.
Encompassing struct
The simplest alternative is to simply create a regular struct that encompasses the generated polymorphic struct. This encompassing struct will hold all the Entity/Blob fields you may need:
public struct MyEncompassingStruct // This is the regular "encompassing struct"
{
public Entity EntityA;
public BlobString BlobStringA;
public PolyMyStruct PolyStruct; // this is our generated polymorphic struct
}
Then, our polymorphic struct has a polymorphic method that takes the Entities/Blobs as parameters:
public void DoSomething(Entity entityA, in BlobString blobStringA, int val)
{
// TODO: polymorphic method that does something with entities and blobs
}
So from our encompassing struct, we can invoke our polymorphic method like this:
myEncompassingStruct.PolyStruct.DoSomething(myEncompassingStruct.EntityA, in myEncompassingStruct.BlobStringA, val);
External storage
Another alternative is to store Entities/Blobs in a DynamicBuffer alongside wherever your polymorphic struct is stored. You could, for example, have a buffer of several Entities, and your polymorphic structs simply store an index to an entity in that buffer. The buffer is then passed as parameter to the polymorphic functions, so the entities can be gotten from there.