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
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
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
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
ManagedArrayStorage , in that
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.
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.
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
Defining “archetype” again?
From ECS document you might assume that an archetype is a combination of
ISharedComponentData types, but in reality, it is a set of
The ECS data type
ComponentType is not limited to
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
object will be added to
ManagedArrayStorage with this strategy
- Search for
nullhole, if found we allocate new
- If not found, we x2 the length of
ManagedArrayStoragethen surely a new
nullhole will appear.
ManagedArrayStoragestarts from length 1.
object for how large? Enough for each entity to get one!
Supposed that, we got 3 class type :
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
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
EntityManager one is for single
Entity. For the whole query (remember you can put
Component derived type into the
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.
Imagine this scenario
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.
SharedComponentDataProxy<B> which go into
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?
MonoBehaviour which go into
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