Unity ECS is not all about high performance subset of C# and struct works. Knowing about how it deals with reference types can help you assemble a complete system where you need it.
The forbidden land `object`
There is an object in a class called ArchetypeManager, which your EntityManager owns it. Also this object is not just one, but an array of object ( ManagedArrayStorage )
That means, each World get one unique ManagedArrayStoragearray for use because each Worldgot its own EntityManager . How many object we get in a world? One per chunk.
One other place, is a List<object> for housing ISharedComponentData .
GameObjectEntity’s magical “MonoBehaviour wrapping” ability
You might have observe this “magical” behaviour from GameObjectEntity, where instantly it seems to be able to “wrap” even MonoBehaviour together with ComponentDataWrapper and SharedComponentDataWrapper , to an Entity. How can this be? Isn’t a chunk should contain only blittable type? They all goes into this object
Hidden internal method `SetComponentObject` on `EntityManager`, the only way IN
It is thanks to this method that we can add ANY object (C# object) to ECS! But how it is stored is not in a chunk, it is kept together in one of object in ManagedArrayStorage , in a class called ArchetypeManager .
This method is internal , and the only user currently of this method is GameObjectEntity . So, this object ‘s sole purpose is to keep attachableMonoBehaviour and not just your random classes.
Without reflection hack you don’t have any other way to get your object in ECS, other than using ISharedComponentData which go to a different place with different association scheme with a chunk.
Sneaky int field in a Chunk
The ECS docs might give an idea that chunk is a tightly packed, well defined memory idea thanks to how the layout from IComponentData assembled into an archetype, is known beforehand.
But there are 2 gateway to the forbidden lands built-in, that is ManagedArrayIndex which maps to one object which is not really a data in this chunk. This index is just one int . It indexes into ManagedArrayStorage which holds many object . That object is for this one chunk.
And alsoSharedComponentValueArray , which maps to List<object> for each type of ISharedComponentData that happen to be associated with this chunk. It is not really a data in this chunk. The index is used directly as an index to that List<object> .
Defining “archetype” again?
From ECS document you might assume that an archetype is a combination of IComponentData and ISharedComponentData types, but in reality, it is a set of ComponentType.
The ECS data type ComponentType is not limited to IComponentData and ISharedComponentData derived type. It can be any type including your MonoBehaviour type! And probably your arbitrary class type I think. (e.g. ComponentType.Create<Transform>() can be added to an Entity, and that’s how GameObjectArray works)
That means, AddComponent , the public method, can accept ANY component type! But remember you are just adding the type, data not included. So it does not mean yet that you can “cheat” by adding any data to an entity. The only gateway is still that internal method GameObjectEntity can use.
How ManagedArrayStorage works
Being not a List<object> like the storage for ISharedComponentData , there must be some kind of strategy to getting the data out and expanding the storage that is not as simple as just adding and using fixed index.
Each chunk get its own `object`, legth equal to no. of CLASS type in chunk’s archetype x possible amount of entity
The object will be added to ManagedArrayStorage with this strategy
- Search for null hole, if found we allocate new object
- If not found, we x2 the length of ManagedArrayStorage then surely a new null hole will appear.
- ManagedArrayStorage starts from length 1.
Allocate object for how large? Enough for each entity to get one!
Supposed that, we got 3 class type : Transform , RigidBody , LineRenderer and several other IComponentData which take considerable space. Each entity is now sized 1.6kB. Because one chunk is currently fixed at 16kB, the chunk “capacity” is 10.
In that case, we allocate new object so that each Entity can hold one of each class type. So.. the space implication is pointer size * capacity * amount of class types.
Then it is not wise to blindly add so many class type component to an archetype without planning to use them.
Getting that data
Now it is simple, we just get the object of that chunk (each chunk got its own) then multiply jump according to entity index in this chunk, then addition move to the correct space for this class type. Each class type simply get its own offset 0, 1, 2, …
null as removal
If you use GameObjectEntity to send your MonoBehaviour to this array, if you change scene, losing all of the original object, the GameObjectEntity has an ability to destroy its Entityon OnDisable . When entity is destroyed, a chunk is likely become empty. When it does, its personal object array will be null . Then GC can collect the pointers, and also the allocation routine can find a new null hole. It is just that the x2'ed ManagedArrayStorage is never shrink down.
The way OUT
Then you can use these data from ComponentArray (to be deprecated with ComponentDataArray?), or from GameObjectArray, and lastly from extension method on EntityManager.GetComponentObject . All of them are part of the hybrid package.
In chunk iteration, the way out is likely the last one. Getting GetArchetypeChunkEntityType first, then ask EntityManager for a managed type for an Entity . We learned that the gateway to managed object array is there in the chunk (in the form of only one int, that maps to chunk’s personal object) but all other API are currently internal (Entites preview 19). It would be so much more direct if we could access ArchetypeManager methods when doing chunk iteration.
Imagine this scenario
- GameObjectEntity + ComponentDataWrapper<A> + SharedComponentDataWrapper<B>
- GameObjectEntity + ComponentDataWrapper<A> + C : MonoBehaviour
The point is I want an entity to hold A and some non blittable reference fields that could not go into A . B and C is equivalent in content. I have 100 of this kind of game object, content of B and C all unique.
Using SharedComponentDataWrapper<B> which go into List<object>
One chunk can hold one index to each type of shared component data. I mentioned that each B is different, so no entity can be together in the same chunk.
I got 100 chunks, each chunk containing only 1 Entity! All off these 100 chunks has an exactly same archetype but because of different shared index they are separated. I lose 16kB * 100 = 1.6MB memory instantly. Chunk iteration is slow as it jumps around for long distance per Entity.
Note that it looks bad because I am breaking the definition of ISharedComponentData , “shared”. Each of my SharedComponentDataWrapper is not really shared with anyone else, only for that one entity. And so this usage is not appropriate.
But I do need reference types to kind of go with my entity in some way. What could be the solution?
Using MonoBehaviour which go into object
In this case I get only 1 chunk with 100 entities happily staying together (if it is small enough). A chunk get its own object , which is made for each entity. Each entity then can be chunk iterated faster, then get its object as needed from EntityManager .
Strategies to avoid cache misses when working with object
Getting the object could break cache memory, but at least with some clever NativeArray works on chunk iteration you can do things like..
Ask EntityManager for C (MonoBehaviour) as much as you like first to get the required component to work with (cache miss all you want!). Cache them all, then in one go iterate NativeArray<A> .
As opposed to iterating through NativeArray<A> and in each loop ask for C from EntityManager that would make iteration slower.
You see a chunk iteration gives you very clear understanding in regarding to performance in a complicated case like this.
Hooking any managed object into an Entity without help from GameObjectEntity
Finally, I will show an example how to reflection-hack into that internal SetComponentObject