 ECS is an architectural pattern consisting of entities, components, and systems. An entity is a logical ID associated with any number of components, which are simply data elements. A system, meanwhile, is a unit of code that's executed in the game loop. Unity's ECS may be used in conjunction with Unity's game objects, or it may be used as a full replacement. At this time, however, many of Unity's features have no ECS equivalents, so creating a full game today using only ECS is not generally practical. While there are no plans to duplicate game objects, game objects suffer from a few key problems that ECS means to address. This is covered at length in many other videos about Unity ECS, but very briefly, the game object pattern tends to structure data and code in a way that is very unfriendly to the CPU cache and so incurs large performance hits. Moreover, the game object pattern entangles data and code together, arguably making both less flexible and harder to reason about. In contrast, the entities and components of ECS lay out data in a sequentially contiguous way that is very cache friendly, and the systems of ECS keep the data and code logically separate. So an entity, again, is just a unique integer ID number, but to allow ID numbers to be reused for new entities once existing entities are destroyed, each entity also has a version number. Each time an ID number is reused, the version number is incremented. For example, the first entity created in a game will have the ID number 0 and version number 0. If we destroy this entity, an entity created later that reuses ID number 0 will have version number 1. An entity can have associated with it any number of components, but it cannot have multiple components of the same type. The set of component types associated with an entity is called its archetype, and like the columns of a relational table, there's no sense of order to the components. An archetype of component types A, B, and C is the same as an archetype of component types C, B, and A, or B, A, and C, etc. A component type is defined as a struct which implements interface iComponentData. This interface has no methods, it simply marks a struct as a component type. The struct must be blittable, which in short means that it cannot have any reference type fields, including strings. While there is no hard size limit, it's generally best to keep component types small, say under 100 bytes. So rather than bundle many fields into a single component type, it's usually better to split them into very small component types with just a few fields. Unlike mono behavior components, entity components have no event methods like update, and while we can give our component structs whatever methods we like, it's generally best to instead put most game logic in systems. Unlike game objects, entities do not have parents or children. However, components can have entity IDs as fields, so in this way entities can be logically related to each other through their components. A system is a class inheriting from component system. Like a mono behavior, systems have event methods called in the game loop. OnUpdate is called once per frame. OnCreate is called once upon the system's creation. OnDestroy is called when the system is destroyed. OnStartRunning is called before the first onUpdate, and again when the system resumes after being stopped or disabled. OnStopRunning is called when the system is stopped or disabled. The order of updates among all the systems is decided by Unity, but we can use attributes to ensure that a system updates before or after other particular systems. A system also keeps track of entity queries, as we'll discuss later, and when no entities match its queries, its update method is skipped. A world is a set of entities and systems. A world's entity manager keeps track of the entities, and the systems of a world normally only access the entities of their own world. The systems within a world are organized into a hierarchy of groups, and system updates can be ordered relative to these groups. While many games may have no need for more than one world, splitting entities into separate worlds can be useful for simulation and networking. A default world is automatically created on launch, and instances of all the component systems in your project get automatically added to this world. This default behavior can be disabled and worlds can be created explicitly, but the API for this is currently underdeveloped, so I won't cover it here. The key to understanding ECS is to understand how it stores the entities and their components. A chunk is a 16 kilobyte block of natively allocated memory that stores some number of entities with the same archetype. In this diagram, for example, we have a chunk storing entities with components of types A, B, and C. The chunk then is split into four parallel arrays, the first for storing the entity IDs, the second for storing the A components, the third for storing the B components, and the fourth for storing the C components. Because components come in different sizes, the number of entities that can be stored in a chunk varies. Also, these arrays that make up a single chunk may take up different amounts of space, but they all have the same number of slots. The first slot of each array stores the entity ID and components of the first entity, the second slot stores the entity ID and components of the second entity, etc. The chunk also has a header with information about the chunk, including the count of entities currently stored in it. When a single chunk is not enough to hold all the entities of a particular archetype, additional chunks are created. In our code, we use a world's entity manager to create and destroy entities and to add, remove, and set their components, but it's the entity manager that handles the details of creating and managing the chunks. Adding or removing components of an entity changes its archetype, and because a chunk can only store entities of one archetype, the entity must be moved to a different chunk with a matching archetype. The entities in a chunk are meant to be stored contiguously with no gaps, so when an entity is removed from a chunk, the last entity in the chunk is moved to fill in the gap. In addition to the chunks, the entity manager tracks existing entities in an entity data array. Each slot index corresponds to an entity ID, and each slot stores the entity's version number, a pointer to its chunk, the index of the entity within the chunk, and a pointer to its archetype. This array starts out small and grows as needed, and free slots of the array are tracked in another structure. So that's how ECS stores its data. What this structure optimizes for is scenarios where we want to loop through all entities with a particular set of components. For example, if we want to do something with all entities having an A and B component, then we loop over all chunks which include those component types, and for each chunk, we can loop over the entity IDs and A and B components of the entities in that chunk. Ideally, this data would all be laid out in memory sequentially in one big array, but the chunk structure of ECS is very close to this ideal. Assuming our query matches multiple chunks, yes, we do have to jump between chunks, and the data within the chunks is split across separate arrays, but as long as we access only a few components in the loop, we still get near optimal memory access. Unlike with the game object structure, our data is not scattered everywhere throughout memory, and we waste very little memory band with accessing data we don't actually need in our loop. So now, looking at the actual API, how do we create and destroy entities? Well, we need a world's entity manager, and here we'll use the default active world. Note also that we're writing the code in a mono behavior for now, but we'll later use a system instead. With the entity manager here, the first create entity call here creates a new entity with no components, returning an entity struct, which simply contains two integers, the ID number and the version number of the newly created entity. We then call add component twice to give this entity two components. The compA type is defined above to have a single int field x, and the compB type is defined to have two float fields x and y. Calling remove component removes the compA component, and lastly, we destroy the entity by calling destroy entity. Be clear that each time we add or remove a component, we're changing the entity's archetype, and so it gets moved to a different chunk each time. Consequently, it's most efficient to create an entity with a set of component types specified up front. We can do this by passing component types to create entity, or by calling instantiate, which creates a new entity with the same component types as an existing entity and copies its component values. Once we've created entities and given them components, we can get and set individual components with the get component data and set component data methods. When components are first added, they have default values, which is zero for number fields, so the x field of the compA struct here has the value zero when it's printed out. We then set its value to five and set the component with this new value. When we create many entities, it's generally more performant and more convenient to first create archetypes and then pass these archetypes to create entities. Here at the top, we've defined an archetype with types compA and compB, and then we create an entity with this archetype. When we pass this array of 10 entity structs to create entity, it creates 10 entities with this archetype and populates the array with the new entity's IDs. We can also do likewise with instantiate to create multiple copies of an existing entity. Passing an array of entity structs to destroy entity destroys multiple entities in one call. We can also create entities explicitly in new chunks with createChunk by passing in an array of archetype chunks. This call here creates 200 new entities, all stored in new chunks, and the archetype chunk values representing those chunks are stored in the array. Just remember that having created these native arrays, we're responsible for disposing of them when we no longer need them. As a special case, if a component type has no fields, it takes up no stored space in the chunks. We call these component types tags because they can be used to tag enemies. For example, if we want only some objects to be affected by gravity, we might give only those entities a gravity tag. Then in our code, when we loop through the entities to apply gravity, we query only for entities with this gravity tag. Even though tag components take up no space, they are still part of an entity's archetype, so when you add or remove a tag component, the entity must be moved to another chunk, just like for any other change to its archetype. Though generally useful only for debugging purposes, we can get all the entities or all of the chunks from an entity manager. They get all entities and get all chunks methods, return native arrays, which we must dispose of when we no longer need them. We can get the chunk an entity is stored in by calling getChunk, and then we can iterate through all of its entity IDs and components. We call archetypeChunkComponentType to get a value representing that component type. Passing true instead of false would indicate we want read only access, in this case though we want write access. We also call archetypeChunkEntity to get a value representing the entity field of the array. We then call the chunks getNativeArray method, passing in these type values to get the arrays that make up the chunk. Be clear that these are not newly created arrays, but rather the actual underlying arrays that make up the chunk, and so we should not dispose of them. These arrays should only be disposed by the entity manager when the chunk itself is disposed, and that too is under the control of the entity manager. Anyway, now that we have these arrays, we can loop through them in parallel. We can mutate the component values, but be clear that the square brackets operator returns a struct copy, not the struct itself stored in the array. Consequently, we must assign the mutated struct value back to the same index to actually mutate the component. Here we retrieve the compA component, send its x field to four, and then assign the mutated component back to the same index. If we want to find all chunks matching certain criteria, we can create an entity query. The createEntityQuery method takes component types as arguments and returns an entity query object from which we can retrieve a native array of chunks that match the query criteria by calling createArchetypeChunkArray. We must specify which allocator to use, and we are responsible for disposing of this chunk when we no longer need it. Once we have the array of chunks, we can iterate through every chunk and also iterate through their entities like we did in the previous example. In this example, our query matches all chunks that include components of type compA and compB, and when we loop over the entities of each chunk, we are mutating the compB values. Notice that for the query, we declared compB to be read only, but when we called getArchetypeChunkComponentType, we specified false, meaning not read only. In this context, it's just the latter that governs whether we can mutate the array, and so it's okay that we mutate the compB values here. Later though, when we access components and jobs, the query's readOnly constraint will have significance. Rather than access the chunks directly, we could also copy from a query's matching chunks all of the entity IDs and component values into new arrays. Here, the query'sToEntityArray method returns an array with all of the entity values, and the query'sToComponentArray method returns an array with all of the compB values. In the loop, when we modify the compB values in the array, we are not modifying the actual chunk data, only they copy in this array. Once we're done with these arrays, we must dispose of them. Also note that because these methods internally copy the data in jobs, we cannot use the temp allocator, which is not allowed in jobs. We can only use the temp job or persistent allocators. A query can also be created from an entity queryDesk, desk is in description, which has three arrays of component types. The allArray specifies component types which matching chunks must have, the anyArray specifies component types which matching chunks must have at least one of, and the noneArray specifies component types which matching chunks must not have. In this example, the query matches chunks which have component types compA and compB, but chunks which also have at least one of compC or compD, or both, and which do not have either compE or compF. When we access a chunk, we can use the has method to test whether it includes any component type, thus in effect allowing us to access optional components of our query when they are present. Because all entities in a chunk have the same archetype, we need only check for particular optional component type once per chunk, not per entity. In the next video, we'll look at systems, dynamic buffer components, and how to use the job system with ECS.