The image is now wrong since SetComponentObject is open to public lately.

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[] deep 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 ManagedArrayStorage[] array for use because each World got its own EntityManager . How many object[] we get in a world? One per chunk.

(One other place, is a List<object> for housing ISharedComponentData . This is one linear, big, and long object list where anything goes. On the upside, we could represent any item of any kind with a single int as an index.)

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 ComponentDataProxy and SharedComponentDataProxy , to an Entity. How can this be? Isn’t a chunk could contain only blittable type? The answer is those MonoBehaviour  all goes into this object[] as a reference type.


You don't have to rely on that GameObjectEntity however, it is thanks to this method that we can add ANY object to Entity! (That could be qualified as a ComponentType) But how it is stored is not in a chunk, again, they are kept together in one of object[] in ManagedArrayStorage[] , in that ArchetypeManager .

Game object conversion

On using the conversion API, somehow you would also get the MonoBehaviour currently active on the GameObject together with the conversion product. It is again the work of storing those as reference type.

Sneaky int fields 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 gateways 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 also SharedComponentValueArray , 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! Actually, it is looking for anything derived from Component type. e.g. ComponentType.Create<Transform>() can be added to an Entity. And that's the start of "hybrid" ECS since ECS can now move normal Unity things via them.

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

  1. Search for null hole, if found we allocate new object[]
  2. If not found, we x2 the length of ManagedArrayStorage[] then surely a new null hole will appear.
  3. 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[30] 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

When entities are destroyed, there is a chance that a chunk will 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

You could use

- EntityManager -
public T GetComponentObject<T>(Entity entity)

- EntityQuery -
public static T[] ToComponentArray<T>(this EntityQuery group) where T : Component

The EntityManager one is for single Entity. For the whole query (remember you can put Component derived type into the EntityQuery query) ToComponentArray join forces with other To__ method, the noticable difference is that the returned array is really a normal C# array, not NativeArray. So the clean up job is left to the garbage collector as usual.

VS ISharedComponentData

Imagine this scenario

  1. GameObjectEntity + ComponentDataProxy<A> + SharedComponentDataProxy<B>
  2. GameObjectEntity + ComponentDataProxy<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 SharedComponentDataProxy<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 SharedComponentDataProxy 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 .