 Okay, so that's most everything about the job system. Now let's talk about ECS, and having talked about ECS, we'll then talk about using the job system and ECS together. But first, for ECS, you can read this text here, and I start out with a rationale of what it's about, and then go into the concrete details. But here in the video, I'm going to take the opposite approach. I'm going to start out with concrete examples and just work backwards. So firstly, we'll look at an example system. A system is just a class which inherits from this class called component system, which comes from the entity's namespace, unity.entities. And your system class should override this method called onUpdate, which is a method called every frame for every system you create. And then you can also override onCreateManager and onDestroyManager, which as you would expect are called upon creation of the system and its destruction. And the fact that the word manager is in the name, I think that's just because one of the base classes of component system is a thing called a system manager for some reason. It's a little bit of a strange name, but that's what that's from. And this parameter incapacity, I think is actually vestigial. I'm not sure it actually does anything anymore, at least that's what I've written in forms. So I've not seen a code example that actually uses this parameter. Anyway, this code, of course, will run upon creation of the system. Every frame we should see high printed out. And then if our system is destroyed, we'll see this message printed. So we'll see this in action real quick. Come here to this project, hit play. And as you can see, it printed out creation and then hi, hi, hi, hi, every frame. Now, what I didn't do is I didn't create any game objects nor attach this script to any object. In fact, I don't think I can do that if I try and do this. Yeah, it gets an error because it's not a mono behavior. It is not a component of the old style. It's a totally different thing. And what's happening here actually is by default, there's this bootstrapping process where for every system class in your project assets, it just automatically creates an instance of that system and then calls its create manager methods and then every frame calls the on updates of all those systems. And there actually is a way to manually instantiate your systems and control the bootstrapping if you want to. But by default, they'll just be instantiated for us. But just to illustrate real quick, here I'll add another system. We'll call this one other system and it'll print out creation to, hi to and destruction to. And so this also will be instantiated automatically for us. And every frame will see both hi and hi to print it out. But the question then is, well, if I have multiple systems, whose update is called first? Is there a way of controlling that? And the answer is yes. You can put an annotation on each system class. You can say update after, and then you write type of, and then the class of that other system, there you go. And so now we're saying that this other systems update should only run after my systems update. And there's also update before, which of course will mean that this system here, it's on update, will be executed before my systems update. So you create a bunch of systems, and for the cases where you do care about the respective execution order, you add these attributes. And you can use multiple, like if I had some other system, I said, I don't know. Another systems, now we're saying that other systems should be updated before my system and another system. And so you don't have to necessarily though, fully specify the execution order. You could, if you wanted to, you could explicitly say, this one always executes before A, before B, before C, before D. You could do that. You don't have to explicitly specify the relative execution order fully. So in cases where it is left unspecified, whether A should run before B or not, Unity will just decide for itself. And I'm pretty sure, let's see, if I try to give this the annotation of my system should run before other system, well, that would of course be a contradiction. So I think I'll get an error here. Yeah, we're getting an exception at the start. It's saying, is a chain of circular dependencies. So it's just ignoring those dependencies. Yeah, this is a contradiction. My system can't run before other systems, if other systems trying to run before my system. So I'll just give you these annotations. And you're probably wondering, well, what are these systems about? They have an unumptap method called every frame, but your regular components have those too. They don't seem very special and the answer is, well, yes, by themselves systems, there's not much to talk about. They're basically just a bit of setup code. If you want to define on create manager, you can also specify how I think it's teared down, but the core of it is there's some update done every frame and for the systems whose relative update order you care about, you can specify this one needs to run before that one or this one needs to run after that one. That's really all they are. Having introduced systems, now let's bring entities into the picture. Antities are basically analogous to the old game objects. In Unity's traditional model, you have these game objects, which are actual C-sharp class instances, and those game objects contain components, which are other kinds of objects, but in ECS, our entities, which are the analogues of the game objects, they're not actual objects. They're just ID numbers. There's this thing called the entity manager, which stores a bunch of entities and their associated components. And when you create an entity, you're just allocating a unused ID number. And when you destroy an entity, you're getting rid of it. And so the entity manager is responsible for keeping track of which entities exist and which don't. So the question is, how do I get an entity manager? Well, an entity manager belongs to something called a world. And a world is something which contains an entity manager and a set of systems. We talked about how there's an automatic bootstrapping process that automatically creates instances of your system classes. Well, those system classes are added to the default world as a world that just exists by default. And so here with the static property active, we're getting the default world. And then from that world, we're using get or create manager to get its entity manager, and that's been assigned to EM here. And then we can call create entity, which creates a new entity and it's returning an entity struct value, which just has two properties, index and version, because an entity ID, for reasons we'll explain, is made up both of a unique index number and its associated version. So having created an entity, we then print out this index and this version, and what we see in unity when we play the game is every frame we see index, which is being incremented every frame, but the version is always one. When we create entities and an index is being used for the first time, the version number is going to be one. But if I come back here and we say em.destroy entity and pass in the same entity ID represented by this entity value, having created an entity and then destroying it, the next entity that's created will have the same index number, but we'll have a version number that's been incremented. So here if I play the game, we'll see index 0000 being reused repeatedly, but the version number is what's being incremented. So what's going on here is again, the entity manager is keeping track of which entities exist and which don't. So it knows which index numbers are currently representing a living index number or in which don't. And when you create a new entity, it just gives you the first index that's currently not already in use. And that might be an index which was used for a previous entity. So that's why the ID, this, this entity value, that's why it's made up of both an index and a version because every time an index is reused, the version is incremented. So we can distinguish between the idea of the old entity that's been destroyed and this new entity. That's how that distinction is made. When an entity is destroyed, that ID should be effectively invalidated and that's effective of what happens because the entity manager internally for every index currently associated with a living entity, it knows what the, the version number should be. So if you try and look up an entity and provide an index that's in use, but the version number is less than the current version number for that index. It knows that you're talking about an entity that's, that's been destroyed and no longer exists. Okay. So if entities are just IDs, well, good or they, well, you associate components with an entity. And we do so through the entity manager here, I'll get rid of this just around to the line and we'll add in these calls to add components. So these components are not the old component types. They're not instances of the unityengine.component class. They're struct types, which implement this new I component data interface. The interface itself doesn't actually require any methods and it doesn't have any extension methods either. It's just effectively marking a struct as being a component type of, of ECS. And we can give these structs fields and methods, but there is a restriction that these component types have to be so-called blittable types. Blittable in C sharp means it's a data type whose memory representation is the same in both managed code and native code, both in the garbage collected world and the non-garbage collected world, such that if you want to move a piece of data between the two worlds, all you have to do is a straight nem copy. You just copy the bytes. You don't have to fix references or, or anything like that. And so for a struct type to be blittable, all of its fields themselves must be blittable and that excludes any reference type. All the reference types are not blittable types. So all we can use our value types, like say float and int and other struct types, which are themselves also blittable. So these are the struct components, which we are going to associate with our entities and we do so through entity manager methods like add component, which takes an entity ID and then the type of the component it was to add, and then we can retrieve a component of an entity. This is a generic method where you specify which type you're getting, and then the entity ID, and now it's returning to my component value, whose num is zero, because when we added the component, it just defaulted to the default values for all the fields, in this case, zero for the float num. But of course, we want to be able to set the value for these components. So we call set component data, specify which type of component we're setting the entity, and then pass in the actual value for that component. And then we can also remove components by calling remove component, specifying the type and passing in the entity ID. And set component data will throw an exception if you don't already have a component of that type and likewise remove component will throw an exception if you don't have a component of that type. And I believe that component will also throw an exception if you were done with trying to add a component which it already has, I believe this is the case. Let me try this out. Yeah, it doesn't like it because you already have a component of that type, because that's a very important rule, you can only have one component of a particular type on any single entity, a single entity can have as many components as you like, but you can't have any duplicates of the same type on a single entity, that is not allowed. So this is different from our game objects and their components because their game object can have actually any number of components of the same type, if you don't choose, that is not allowed here. And that actually explains why when we call get component data and set component data and remove component, it's a generic method. And we're not specifying which my component component we're talking about, there can only be one on that entity. Now you can add and remove components from entities as you see fit. But of course, very commonly, we'll usually be creating many entities with the same set of components. And so we can define what's called an archetype, which is basically just a set of component types. And then when you create the entity you pass in that archetype, and it creates an entity with that set of components, rather than having to add them individually. For reasons we'll discuss very shortly, it's also more efficient to do it this way. And understand also this term archetype is used more broadly just to mean the set of component types which any entity has. So even if you don't use this entity archetype thing, your entities have an archetype. It's whatever set of component types that entity has, that is its archetype. So if you have an entity with types A, B, and C, and you add a component D, you are effectively changing its archetype, you're changing what set of component types it has. And there of course, there's no sense of order amongst the components on an entity. So A, B, C is the equivalent of C, B, A, or B, C, A, or any other ordering. We don't think in terms of order. Very much like in a relational database table, right? You don't think of the columns as having an order, they're just a set of columns. In this next example, I have added a third component type just called sum component. Doesn't really matter what's in it, it's just we need a third type. And I've moved the code that creates entities into the on create method rather than out of on update. So this is only done once when the system is created. And we're defining two archetypes, one which has two components and the second which has three components. And we're going to create 10 entities of the first type and 10 entities of the second. And then we're creating this new thing called a component group, which is being assigned to this field here of type component group. And the get component group method of your component system, you pass in some number of component types and you get back this group, which represents a set of components we want to iterate over. So whereas an archetype is defining a set of components, which an entity can have a component group is a set of components where you don't have necessarily any entities with that specific archetype. You just want to iterate over all entities which have those components. So in this example here through a component group with my component and other component, we can iterate over all entities which have those two components, regardless of whatever else they might have. So given entities of these two archetypes, we would actually be looping through all of them because they all meet the requirement of having both a my component and an other component. And then every frame and on update what we're going to do is create iterators which iterate over the components of the entities which match this component group. And so this get component data array method where we specify the type of my component, it gets an iterator of the my components and get component data array for other component gets us the other components. And then if we want to access the entity IDs themselves, we need a third iterator we need to call get entity array and that gets us an entity array iterator and understand that all these entities because they're from the same group, they will have the same length because the component group is selecting for all entities which have a my component and an other component, but we need two separate iterators to get at the two different components and we need a third iterator if we want to get at the entity IDs. So here in this loop, what we're doing is we're effectively visiting all of the entity IDs and components of these iterators in each iteration, we're getting an entity ID and the my component and other component value associated with that entity ID. And in this particular case, we're not doing anything with them, but we could print out these values or do whatever we want, right? And in fact, we could mutate these values. So here I can assign to this iterator at a certain index a new other component value. So these iterators are not just for reading entity data, you can also mutate the component values. But what we cannot do is while using these iterators is you might think, well, here I'll bring in an entity manager. And now while using these iterators, we will just say like destroy this entity. Normally, this would be an okay thing to do. But as long as we were dealing with these iterators, we can't destroy any of the entities they iterate through, nor can we remove or add components to those entities. If I ran this code right now, I get an error. And actually here, I need to this needs to be world dot active there. That's how we get the world. And it's entity manager. But because of this line here, we will get an error. So you know, just demonstrate that real quick, come into unity, hit play. And we get an exception every frame. Unfortunately, the air message is not all that helpful. It doesn't really exactly clue you in. But in this case, I happen to know yes, it's because we're trying to mess with the entities which are being iterated through. So if I get rid of that line, come back, play. And it's fine. We're not getting exceptions. So you probably wanted well, surely I sometimes would want to destroy entities as I iterate through them. Well, what we can do is we can use what's called an entity command buffer, which as the name implies, stores a buffer of commands of say like add component, remove component, destroy entity, those kinds of commands. And then at a later point, having queued up these commands, you can flush the buffer, which enacts all the commands. So if we had an entity command buffer here, we could queue up our destroy entity commands, and then have them done later when we're done with these iterators. And we don't need to actually create an entity command buffer, because one is provided for us, there's a field of our system called post update commands. And it has a method destroy entity. So we just queue up this command. And then post update commands is automatically flushed after on update returns. So if I save this now and go back to unity, I should be able to run my game with no errors. And yes, everything's fine. Now to understand entities and components and these component group iterators, it's really, really helpful to understand how they are actually stored in memory. Well, what happens is that the entity manager creates these things called chunks, where chunk is just a 16 kilobyte block of natively allocated memory. And each chunk can store entities of a particular archetype and only entities of that archetype. Say, for example, we have a chunk that stores entities with the archetype ABC. Well, then what would look like inside that this is the 16 kilobytes, you'd have first an array for all the entity IDs, then an array for all the A components and an array for the B components and then for the C components. And these are not necessarily the same size because, of course, the components themselves aren't necessarily the same size. So when the chunk is created, it just figures out well to maximize the number of entities we can store how large should these arrays be respectively. And because the chunks aren't necessarily full, also internally in the chunk we store the count of actual entities stored. And as we add and remove entities from a chunk, all the entities that are stored are kept at the front of these arrays. So you don't have any gaps in the arrays. For example, if we were to remove the entity, which is at index zero of these arrays, the first entity in these arrays, then that gap would get filled in by taking the last entity and moving it to that first slot to fill it in. Because we don't want any fragmentation in these chunks. We don't want it to be gaps. And the reason the arrays are stored in these parallel arrays, rather than a more obvious solution of having just the first entity and its ABC components first than the second entity and its ABC components, et cetera, you know, just all the components of each array grouped together. The reason for this is that it turns out generally to be more efficient for our component groups when they iterate through the components. If the components are split into separate parallel arrays, rather than being closer together for each entity, like say, for example, I have a component group, which iterates through all A and B components. And so what happens is that component group looks in the entity manager for all the chunks, which have an archetype that includes types A and B. And so this chunk here, for example, would be included because it has an A and B component. It also is a C, but we don't care about that. We just want to iterate through the A and Bs. But we would iterate through the A and Bs of this chunk. If instead of being stored in parallel arrays, the components were all grouped together on a per entity basis, then when we read entities in chunks where the archetype is say like ABCDEFG, you know, a bunch of other components other than the A and B that we care about for that particular component group, then we would wastefully be reading a bunch of bytes in memory that we don't actually care about. By splitting them into parallel arrays this way, our component groups that are only reading some selection of components from a chunk wouldn't waste memory bandwidth accessing components we don't care about for that particular component group. And likewise, the reason the arrays are kept tightly packed, the reason we don't allow gaps in these arrays is so as to avoid accessing bytes or useless data, we want to only read the bytes we need to. And so as we iterate through a component group, the memory we are accessing is tightly packed and entirely linear, except for the part where we're reading from parallel arrays, rather than just one array. And so in that sense, yes, our memory access pattern isn't strictly linear, we're reading through in conjunction, multiple parallel arrays. But on modern hardware, you can get away with reading from a few parallel arrays and still get all the benefits of linear memory access because you don't end up triggering any cache misses. I don't know exactly what those limits are. I'm sure it's different from system to system. But in general, I think you're okay as long as you're reading from like four parallel arrays, or maybe five or six, perhaps. But at some point, of course, if you read from too many parallel arrays at once, if like you're trying to access a component group of A, B, C, D, E, F, G, H, I, J, K, you know, you're reading from a bunch of parallel arrays at once, then yeah, effectively, you would be jumping around in memory. And you probably get cache misses. So again, I don't have a real sense of exactly what that limit is. It turns out though, in most systems for most purposes, when we do access data, we generally are just really accessing two or three, maybe components at a time. It's really quite rare, even for entities that maybe have a bunch of components on a single entity, it's still rare to to access all of those components at the same time. Turns out for most purposes looking at just two or three or maybe four. So it's this chunk memory layout that gets us the memory access pattern we that's most optimal, the linear memory layout with a small qualifier that it's, you know, parallel arrays rather than just necessarily one array. But that's close enough as long as we're not iterating through too many parallel arrays at once. There are cases, though, where you're going to want to access entities by looking up a specific entity by its ID, that's why entities have IDs in the first place. And so the entity manager has an entity data array, which is just one large array with all the entity IDs. And if you want to look at the entity whose ID index is say five, then look at slot five in the array, and you look at its entity data. And that tells you what the archetype is. It tells you where the chunk is, where it's stored, and it tells you where the index in that chunk the entity is stored. And it also is where we store the version number for that entity ID. And because entities can be destroyed, you're going to end up with gaps in this array where in between two existent entity IDs, you might have a slot that represents a dead, a non existent entity. And in those free slots, the chunk pointer and the archetype pointer are going to be null. And that's what indicates that this is a free slot. And then the index in chunk value is not actually going to represent an index into a chunk. It's just going to represent the index in the entity data array of where the next free slot is. So the entity manager keeps track of where the first free slot is in the array. And from there it's basically a skip list you can from the first free slot find the next free slot and then the next one. And when new entities are created, they're created by filling in these free slots. So again, this entity data array is what allows us to quickly look up entities by their index, you wouldn't have to want to search through every chunk, right? And it also is where we store the version. And the archetype pointer, I think, is not really strictly necessary because you could just look in the chunk to find the archetype of that entity. But for some operations, you want to directly look up the archetype in a more efficient way without having to go through two pointers. So that's why it's also stored here directly in the entity data array. So now having a sense of how this entity component data is stored, you should then have a sense of which operations are efficient and which are a bit less efficient. And as you can see, the whole system is optimized for the case of iterating through a selection of component sets. That is the number one priority of this whole system. But then there are certain operations which are surprisingly a bit less efficient. In particular, if you add or move components from an entity, well, a chunk, as we explained, can only store entities of a particular archetype. And so if you add or move components, you're changing that entity's archetype, so it can no longer be stored in that chunk, it needs to be moved to a different one. And when you move an entity to a different chunk, well, you have to remove it from that chunk. And that requires moving another entity into its place to fill that gap. And then you have to store it in another chunk, and you have to update these references, the whatever entities get moved within chunks, then you have to go into the entity data array and update the chunk pointer and the index and chunk value. So it could be quite costly. One way to mitigate this is that if you can queue up a bunch of entity command buffer commands, then the entity manager can be a bit smarter about how it does these operations, it can do certain things on mass in a way that's more efficient than if you do them individually. So that's one way to mitigate the inefficiencies that do arise. In general, though, it suggests that perhaps adding and removing components on a regular basis is something you should be careful about doing. You should maybe not try and add and remove too many components from existing entities too many times per frame that that's not going to be super optimal. Now, aside from post update commands, the other way to get an entity command buffer is to create what's called a barrier system, which is a system type like component system. But in a barrier, the unupdate method is sealed, we can't override it. And so when we create a barrier, we just leave it empty. So here I have my barrier class and the curly braces are just left empty. And then what I do is I inject this barrier into other systems, the inject attribute inject attribute here will go find the my barrier within the world and automatically assign an instance to this field. And then through that barrier instance, I can call create command buffer, which gets me an entity command buffer, which is going to be flushed automatically every time the barrier updates. So here instead of using post update commands, I'm using this buffer created here from the barrier system. And these commands are going to be flushed when my barrier updates. And when is my barrier going to be updated? Well, I've here through update before I've made sure that this my system is updated before the barrier. So typically when you create barriers, you're going to want to specify one way or another how systems which use that barrier are going to update relative to it either before or after. And of course, this example is very pointless because my barrier will update immediately after this update, and it'll have the same effect of if we just use post update commands. But in the useful scenario, we would have like say several systems that all inject the same barrier and all are executed before the barrier and they all create command buffers from that barrier. And then when the barrier updates, all of those buffers will get flushed at the same time. So barriers are used as the name sort of implies as marker execution points in our update cycles past that barrier, you would know that the work from several other systems, their entity changes have all taken effect.