Here is a sketch of how you can implement an archetype in C#:
class Archetype {
System.Type[] _componentTypes;
System.Array[] _components;
int _capacity;
public int Count { get; private set; }
public Archetype(int capacity, params System.Type[] componentTypes) {
_capacity = capacity;
_componentTypes = componentTypes;
_components = new System.Array[componentTypes.Length];
for( int i = 0; i < componentTypes.Length; i++) {
_components[i] = System.Array.CreateInstance(componentTypes[i], _capacity);
}
}
public T[] GetComponentsArray<T>() {
int index = System.Array.IndexOf(_componentTypes, typeof(T));
if (index < 0) return null;
return (T[])_components[index];
}
}
Note that adding and deleting entities or disabling them is elided from the sketch above - the chat thread I wrote it for has more discussion of the pros and cons of different strategies to use there.
So if I want to create an archetype for entities that have a Transform
, SpriteRenderer
, and AudioSource
component, with room for 200 such entities at a time, I'd write:
var noisySprites = new Archetype(
200,
typeof(Transform),
typeof(SpriteRenderer),
typeof(AudioSource)
);
And it will create the necessary internal arrays. Here we're representing your elements in SoA or Structure of Arrays layout, so there's no one patch of memory that corresponds to the entity as a whole. Instead, each entity is an index. Entity #0 has its Transform
component at noisySprites.GetComponentsArray<Transform>()[0]
, and its SpriteRenderer
component at noisySprites.GetComponentsArray<SpriteRenderer>()[0]
, etc.
This might look strange if you're used to AoS or Array of Structures layout, but this approach can actually give you significant performance wins. If your components are all struct
types, then the instances of each component type will be laid out contiguously in memory. So if you have a system that cares about only 2 of the 3 components in a given archetype, it can iterate over a densely-packed array of each of those types, without having to skip over every third item. Every bit of data that's pulled into cache gets used, so you make optimal use of the caching and prefetching mechanisms to hide memory latency.
A system in this form might look something like this:
public class SpriteRenderingSystem : System {
// On start-up, query for the kinds of archetypes we need to work on.
// This way any search/indexing can be done up-front and cached.
public void Initialize(ArchetypeCollection collection) {
_signature = collection.GetComponentSignature(
typeof(Transform),
typeof(SpriteRenderer)
);
}
public void Update(ArchetypeCollection collection) {
// Iterate over each archetype matching the
// Transform + SpriteRenderer component signature.
foreach (var archetype in collection.GetArchetypesWithSignature(_signature)) {
// Get the packed arrays of components we care about.
Transform[] transforms = archetype.GetComponentsArray<Transform>();
SpriteRenderer[] sprites = archetype.GetComponentsArray<Sprite>();
// Iterate over all active entity indices and do our work.
for (int entity = 0; entity < archetype.Count; entity++) {
RenderSprite(transforms[entity], sprites[entity]);
}
}
}
}
Note that we pay any search costs and cache misses for getting the right arrays just once per archetype that we want to iterate over, and then all memory access for each entity is linear and contiguous from there (again, assuming your components are struct
types), so you get efficient iteration for the things you're doing hundreds or thousands of times, and the overhead in the archetype handling is more manageable since you likely have just dozens of those, or a small handful that any one system needs to act on.