 This is going to be a brief rundown of some ECS components that aren't particularly well documented at the moment including object components, trunk components, shared components, system state components, and blob assets. Information about these things is currently a little sparse so I'm not entirely confident that everything I say will be 100% accurate, but I think it's mostly accurate. Anyway, firstly, object components are pretty much what they sound like. With version 0.2 of the entity's package, you can now have classes that implement the iComponentData interface, and this defines a component type which you add to your entities with a special method, AddComponentObject, and because these components are instances of classes, they are managed objects and so are not directly stored within the chunks. For each object component type, there's an array that holds all of the object component values of that type, and what gets actually stored in the entity is just an index into this array. And so here, for example, in this code, we're first getting in the default world, which now you have to access through this new property, DefaultGameObjectInjectionsWorld, rather than the much more brief active. They decided that active was too convenient, too brief, encouraging people to misuse it, and so now they have this much longer name. Anyway, we get the entity manager and create an instance of this component type, a dog which we call D. We create an entity and add that component object to it, then create a second entity and give it the same component object. How this actually gets stored is that both entities actually have a different index into the same array, but those two slots of the array store a reference to the same dog object. So these two entities, yes, are indirectly sharing the same object component. So when we then assign 7 to the X field and then get this object component from the two entities using the method GetComponentObject, for each entity we're getting back the same dog object, and so printing out D.X prints 7 in both cases. And then lastly here, when we remove the component from the first entity, that's affecting only the first entity. The second entity still has its reference to the same object. So with object components, our entities now can reference managed objects. However, there's still the general restriction that within jobs you don't touch managed objects. It can actually technically be done safely if you get a so-called GC handle to the managed object, but if a job is burst-compiled, then it shouldn't touch managed objects, period. So in practice, you really should just not touch managed objects in your jobs. Outside of jobs, however, and like regular component systems, you can still access these components. A chunk component is pretty much what it sounds like. It's a component that belongs to a chunk itself, not to the entities within a chunk. These chunk components are not defined as distinct kinds of components, they're just regular i-component data. But to add a chunk component to an entity, you use AddChunkComponentData rather than AddComponent. And be clear, when you add a chunk component to an entity, you're modifying its archetype and thereby determining which chunk it can belong to, where it can be stored. But for that individual entity, there is not a unique chunk component value. For the whole chunk it's stored in, there's only one chunk component value per chunk component type. So here, for example, we create two entities, E and E2, and we give them both a chunk component of type cat, but they also both get a regular component of type cat. So within a chunk that stores these entities, each entity has its own cat, but there's also a chunk component cat that belongs to the whole chunk, and there's just one of those per chunk. So within a single archetype, a component type can be used as a regular component, but also a chunk component. Then to access this chunk component, we get a reference to the chunk of entity E, and then we call SetChunkComponentData to set the cat's x value to be 3. To access a chunk component value, we can do so via the chunk, but also more conveniently we can do it through an entity. So here where we call GetChunkComponentData pass in an E2, we're getting the cat of the chunk in which E2 belongs. And here in this example, because our two entities both belong to the same archetype, and there's only two of them, then it's virtually guaranteed they're stored in the same chunk, and so when we print out the x of this cat from E2, it prints 3, because this is a cat shared by both of the same entities because they belong to the same chunk. Lastly here, when we remove a chunk component, because it's distinct from a regular component, there's a special method, RemoveChunkComponent. So the first method here is removing the chunk component cat, the second method is removing the regular cat component. Be clear though that this is just affecting the archetype of the entity, and so with both these calls, the entity is getting moved to a different chunk. This is in no way destroying the chunk component, only when the chunk itself is destroyed because it no longer has any entities, only then do the chunk components get destroyed. Also understand that perhaps unintuitively, when an entity is moved from one chunk to another, the chunk components of either chunk are not affected, they don't get modified. It's only when set chunk component data is called, only then does the chunk component value get modified. Again, these chunk components really belong to the chunks themselves, not the entities within that chunk. Now, how are these chunk components useful, why would you create them? That's a more complicated question, there aren't super obvious use cases. The most prominent use of them I've seen so far is in regards to culling. It's useful to store in a chunk a single bounding area that encompasses all of the bounding areas of the entities within it, and so there's a culling system that computes this bounding area from all the entities within each chunk. As long as none of the entities within a chunk are modified since the last time this bounding area was computed, then it's valid for the chunk. That's the only real use case I've seen so far for chunk components, but I'm sure there are many others. Next we have shared components, which are defined with their own special interface, I shared component data, and the idea of these shared components is that all entities within a chunk share the same value for a shared component type, but unlike with chunk components, these shared components logically do belong to the individual entities. So when you set shared component values, you set them on individual entities, and because an entity then will have a different value from all the other entities within its chunk, it has to move into a chunk that shares its new value, or if there is no such chunk, a new chunk has to be created. So say I have a thousand different entities of a particular archetype, and that archetype includes a shared component, well if the value of that shared component is different for each one of the thousand entities, then each entity will have to be stored in its own chunk. That situation of course is very wasteful, and in fact defeats really the whole purpose of ECS because then you would have each entity spread all throughout memory in different places. So generally when we use shared components, we do so in cases where for that component type, many entities will be sharing the same values. The idea being that for all entities which share the same shared component value, we only have to store the value once. Like object components, shared components are not stored directly within chunks. Instead, all the shared components of a particular type are stored externally in an array, and each chunk with that shared component just has an index into that array. Now to avoid storing the shared component values redundantly, and to also properly put entities into chunks that match their shared component values, our shared component types need to be testable for equality, and for the sake of faster quality tests, we also want to compute hash codes. So our shared component types must implement the IEquatable interface, which has the equals method, and we should override the GetHashCode method, which every type inherits. Rather than implement these manually though, we can have Visual Studio generate them for us, and these auto-generated methods are generally just fine for our purposes. In this example, we have a shared component type mouse with two fields, an int x, and a name field, which is a single class with a string field. We could of course represent the mouse's name directly as a string, but I made the name field its own class for demonstration purposes on the next slide. Anyway, note that I've generated the IEquatable interface, and the GetHashCode methods for both these types. For the name class, doing this is optional, but then when we compare our mouse type for equality or get its hash codes, the equality in hash codes would be based just on the identity of the name field, not its actual value. In some cases that might actually be what you want, but otherwise the name class should have these methods. So here we're creating two entities and giving them both a shared component of type mouse, and note that when we add a shared component we have to provide a value upon adding the component, and in this case we're giving the same mouse value to both entities. So because these entities have the same archetype, there's just two of them, and their shared component values match, then they're going to be in the same chunk. When we get the chunk of both E and E2 here and test them for equality, it returns true. When we then assign Ted to the string field of the name within the mouse, we're effectively modifying in place the existing shared component of both these entities, but if we then set the mouse component of the first entity with the same mouse struct, even though it equals the mouse as currently stored already in both components, Unity will think it's different because now it's competing a different hash, and so now it's going to move the first entity into a different chunk, even though both entities actually really are storing the same value. So in the next line when we get the chunks of both entities and test them for equality, we get false, they're not the same chunk. If though we call get shared component data on E2, we get back a mouse struct whose name value has the same string Ted. So here in a sense we've actually fooled Unity, we're probably doing something we shouldn't, but understand exactly what's going on. When you add or set a shared component, only then does it compute the hash code and test it for equality with other entities. If the shared component includes mutable managed objects, those can get mutated unbeknownst to the entity manager, and so you can end up with strange scenarios like this one. Of course you probably should just avoid these scenarios, and in fact generally you should avoid mutating any managed objects that are part of any existing shared components. I think this kind of scenario is probably why in the documentation they say that in the future version, they probably won't allow shared components to store managed objects. For now though they still can, and in fact one of the most prominent uses of shared components is that they're used in the hybrid dot rendering package for storing mesh data. The render mesh component is the shared component type that keeps a reference to a standard Unity mesh object. By making the render mesh a shared component type, well then firstly it can have a reference to managed objects, but then secondly when entities get rendered, they're effectively grouped into chunks, such that all entities within a chunk share the same material in mesh. Effectively then the entities are laid out in memory in a way that's optimal for rendering. Now this will have to change in the future once shared components can no longer reference managed objects. I believe the idea at that point is that instead they'll lean on object components and what are called blob assets, which we'll talk about in a minute. Lastly in regards to shared components, there are some scenarios like the rendering example I described where you want to iterate through all entities which share the same component value. So here in this example we create a query that just matches on any chunk that has the mouse shared component type, and then we pass a list of type mouse to get all unique shared component data which will populate the list with every unique mouse value. In the loop here then for every mouse in the mice list we're setting the mouse value as a filter on the query such that the query will now only match chunks which have that particular mouse value. In the rest of the loop we then get all the chunks matching this query and then for each chunk we can go through all of its entities and do our business. Just be clear that for all the entities in the inner loop they share the same mouse value so we only need to access the mouse value once per iteration of the outer loop. So again in the rendering example when our render mesh is using the same pattern for each unique render mesh we can efficiently batch together the rendering of all entities with the same render mesh. Next we have system state components. What these are concretely is very simple but understanding how to use them is a little trickier and I actually really have no idea why they're called system state components. The name is quite strange to me. Anyway the difference between a system state component and a regular component is that when you destroy an entity if it has any system state components all the non system state components get removed but the entity is not actually destroyed. You can no longer add new components to the entity but the entity is not actually destroyed until you remove all of the system state components. So here in this example we have a regular component bird and a system state component rat and when we create an entity and give it both of these components notice we just use the regular methods for system state components and when we call destroy entity the entity is not really actually destroyed but the bird component is removed though the shared component rat is not and so has component for bird will return false but has component for rat returns true. Only once we remove the rat component does the entity no longer exist. So how are these things useful? Well the primary use case as far as I understand it is sometimes when entities are destroyed you might want to do some cleanup work and so if you give entities of an archetype a system state component then we can detect when destroy entity has been called on those entities by querying for entities which have the system state component but don't have the other regular components and so you put any information you need to do the cleanup work in the system state component and in a system with the right query you can get the entities that need to be cleaned up and once you've done your cleanup work you can then actually destroy the entity by removing the system state components. Also note that we can create shared system state components so you can have shared components to stick around after death too. Lastly we have blob assets and again the name is a little strange to me because these are not assets in the usual Unity sense they're not files stored within your project as assets they're not stored in an asset database perhaps though that will change in the future I don't know so maybe in the future it will make more sense but anyway they're called blob assets where a blob is a binary large object. Each blob is a piece of data within a single native allocation the data within a blob is all immutable so blobs are safe to use from jobs without any concern for coordination no safety checks are required or anything because the data is only ever read and within the blob the data can contain internal pointers pointers that point to other parts of the same blob but these pointers are represented not as actual memory addresses but merely offsets so they're merely relative within the blob and so if you want to serialize these blobs it's extremely fast and trivial as possible because we can simply just copy all the bytes we don't have to worry about fixing up references or anything like that so we can very trivially then serialize blob assets out to files and then deserialize them back into memory with no special logic required whatsoever it's just a straight copy so what are blob assets useful for? well potentially all sorts of things anytime you need a large binary piece of data as long as that piece of data doesn't really need to change then you can represent it as a blob so for example you might want to represent say mesh data in the form of the blob now there are some problematic use cases like say textures I believe because with textures there are standard binary formats that may not conform to the blob representation so this is not necessarily a solution for every piece of binary data but there's a lot of stuff that can be represented as blobs just fine so now how do we create blobs? well to make a blob we start by creating a blob builder object and for each blob we create there is a so-called root which is a struct value, an ordinary struct so in this case our root is a house type which has three elements a cost of type int a door of another struct type called door and a name of type blob string the blob string value itself does not actually contain string data the blob string itself just contains the length of the string I believe both the logical length in terms of characters and also the length in bytes and it also contains an offset pointing to the other part within the blob where the characters of the string are actually stored so in this example the blob will start out with the root house the root always comes first in the blob the door struct which it contains is just another struct that's inline stored within the outer struct but then the blob string will have an offset that points to after the root house where all the characters of the string will be stored so this blob is actually just going to have two elements the root house and the string data that follows possibly the root house will be followed by little padding for the sake of alignment when we construct the root note that we're getting a reference to the house we want to ref to a house rather than just a house value because a non-ref local house variable would just be its own copy we want to modify the house actually stored in the blob and so here when we assign to the cost and the height we're actually setting the values that will be stored in the blob we call allocate string passing in the value we want for the string and the reference to the blob string that's going to point to the offset to where the actual string data is stored now the builder is just how we set up the data we want but we don't get our actual final blob until we call create blob asset reference this creates the actual allocation which will be our blob and copies all the data from the builder into the actual blob because when we make the blob we don't know how big it's going to be and we want it to be mutable and we don't know how everything's going to be arranged the allocation for the root and the string and any other elements are actually just staging that has to be copied into the actual blob and then once we have our blob we no longer need the builder so we dispose of it now that we have a blob asset reference we can read its data and we can store this reference in components the blob asset reference itself is a believable type and so you can store it in regular entity components now here's another example where within the blob we're using a blob pointer blob ptr and so it's the same structure as before except instead of the root house directly containing the door struct it contains a pointer to a door and a blob pointer is just an offset pointing to elsewhere in the blob so our blob this time is going to contain three elements the root house the character data of the name string and the door struct so we create our builder and construct its root like we did before but to set the door field passing a ref to the door field of our root house and we get back a reference to a door struct which we can then set up here we assign 9 to its height field and lastly we set up the blob string just like we did before so now when we call create blob asset reference the actual blob is allocated and the builder takes the stage data and copies it into the actual blob and sets the pointer offsets as needed and so now we can use our blob to get at the data within the blob so we access the root with the value property but absolutely make sure to get it by rough not because we want to mutate it but because when we access elements of the blob through pointers through the offsets they are relative from where that pointer is in memory if we got a local copy of the house the house would be on the stack and then when we access any elements through offsets we would be indexing into the call stack which is not at all what we want to do and in fact it's quite dangerous we want a reference to the actual struct so that when we get a rough to the doors value we get in the very same door that's actually stored in the blob, not a copy now come to think of it these blobs are supposed to be immutable but I don't think there's anything actually stopping you here from say modifying the height of the door so it's on you to avoid doing that doing so is not actually going to necessarily create any problems assuming your blob isn't currently being used in any jobs but again it's really not how blobs are intended to be used so you probably shouldn't do it now last thing in this example again there's not really any good reason to make a blob pointer here to a door instead of just directly storing the door in the root but in larger more complicated blobs you might want to create some kind of structure of elements pointing to each other like for example maybe you want a blob which is a graph of nodes and so you create these node elements that point to each other and a single node might be pointed to by multiple other nodes in the last example we're replacing the blob pointer to a door with a blob array of doors so our house can now have multiple doors the blob builder code looks very much the same except for the allocate method we pass a second argument the number 5 which means this will return an array of 5 doors and so allocate here is returning a blob builder array value the distinction between blob builder array and regular blob array is a blob builder arrays are mutable and so in the next line when we use the index operator on our blob builder we're getting a rough to the element of the array so when I assign 9 to the height of doors subscript 0 that's actually mutating the first door storing the array it's doing what we want it's not mutating a copy but then once we've built our blob to access this data we again have to be very careful to use rough because otherwise we might end up indexing it into the call stack which is very bad so here I'm getting a reference to the root house and then getting a reference to the doors within the house doors 2 is a blob array not a blob builder array so we can only read from it when we get the door of the first index we again get a rough in this case it's not strictly necessary if we've got a copy of the door well the door itself doesn't contain anything with pointers so there's no danger with the individual door of indexing to the call stack but for consistency and safety I recommend just always using rough even in cases where it's not strictly necessary just to make sure you understand what blobs look like here's a diagram from an official Unity presentation in this blob the root is of type node graph which is made up of a single blob array of nodes and each node itself is made up of an array of ints and a float 3 so in the diagram it's showing the memory as laid out left to right and top to bottom with each block representing 4 bytes the fact that the lines have different numbers of blocks is not significant the blocks are presented in a way that's just more orderly to look at so first in the blob that top line is our node graph the node graph is made up of just a single blob array and the blob array value is really just 2 int values the second int the value 5 is the length of the array and the first value 8 is the offset to where the array is actually stored because the offset is relative from the beginning of the blob array value itself in this case it's 8 bytes from the start of the node array to where the node array is actually stored and as you can see the next 5 lines are all the nodes that make up the array each node itself starts with a blob array which is again 2 values, first an offset and then a length and then the float 3 which is itself made up of 3 floats an x, a y, and a z the int array of the first node is located 100 bytes after where the blob array value is and it has a length of 2 the int array of the second node starts 88 bytes after that blob array value and has a length of 3 the int array of the third node starts 80 bytes after that blob array value and it has a length of 2 and so forth and if the pattern seems a little strange it's because the int arrays come in different sizes that's why the difference between the offsets is not uniform again be clear that the offset is relative from where the offset itself is stored to find the int array of the last node you're counting 64 bytes from the start of where the 64 is itself stored so hopefully now you understand what a blob asset looks like in memory thanks to the use of offsets rather than actual memory addresses we can copy a blob byte by byte without having to fix up any references