 In this video, I'll give you a quick summary of the concepts of Unity's data-oriented technology stack, but the details, including the API, are covered in other videos of this playlist. Dots is comprised of these four elements. The job system enables us to utilize multiple cores by creating units of work called jobs, and the Brose compiler optionally can be used to compile the code of these jobs, generating highly optimized code. ECS is a new way of structuring data and code that avoids the performance and organization problems of Unity's conventional game objects. The E stands for entity, which is a logical ID. The C stands for component, which is a piece of data associated with an entity. And the S stands for system, which is a unit of code executed in the game loop. Lastly, Project Tiny is a new engine runtime that supports only dots, stripping out support for conventional Unity features, and thus stripping out unnecessary overhead in games that don't use them. So firstly, let's look at the job system. Without the job system, multi-threading requires us to create and manage threads ourselves, as well as to manually synchronize access to data that's shared between the threads. When using the job system, it creates and manages a pool of threads for us, usually one per CPU core. We then create units of work called jobs, and add them to the job system queue. The job system then farms these jobs from the queue to the pool of threads as the threads become available. We leave it up to the system to decide precisely which jobs run on which threads in what order, and once a job is running on a thread, it is never preempted by other jobs. While a job runs, its thread is occupied until the job finishes. For primarily this reason, the job system is not appropriate for doing IO work. If a job were to wait for IO, its thread would be wastefully idle during that time, thus thwarting our goal of maximizing utilization of the course. So jobs are intended only for doing computation on in-memory data. Now it may be the case that two or more concurrently scheduled jobs access the same data. If the shared data is only read, then there's no problem, but if one or more of the jobs mutate the data, we usually then want to ensure these jobs run in a certain order, and not overlap in execution. The job system lets us do this by specifying dependencies between jobs. If we tell the job system that job A is dependent upon job B, then the job system will make sure job B finishes executing before job A starts. A job can have multiple direct dependencies, and those dependencies can have their own dependencies, and a job will not start executing until all of its dependencies have finished. So the idea is that by splitting our workload into separate jobs and creating dependencies between them only where they're needed, we can maximize the use of the course. In this diagram, each box represents a job, and each arrow points in the direction of dependency. Job D, for example, must wait for B and C to finish before it starts executing, and B first must wait for A. Be clear, though, that because A and C have no dependencies, they need not wait for any other job. On the right side of D, jobs E, F, and G all must wait for D to finish, and in turn, H must wait for G, and I must wait for H. Again, be clear that though E, F, and G all must wait for D, they need not wait for each other, so in fact, G, H, and I might all finish before either E or F finish, or before they even start executing. Also note that job dependencies cannot be circular. We wouldn't want job X to depend on Y if Y also depends back on X, because then both jobs would wait for each other forever, neither ever executing. Fortunately, the job system API makes it impossible to schedule circular dependencies, so it's not really a problem. As mentioned, a job should not perform IO, but furthermore, it should not access any managed objects, and it should only access the data explicitly handed to it. Because it can't perform IO or touch managed objects, the only way a job can do useful work is by mutating one or more natively allocated data structures called native containers. For example, if the purpose of a job is to calculate a single number, we create a native array of length one and hand the array to the job before scheduling it. In the job, we store the calculated number in the single slot of the array, and after the job finishes, we can read the result from the array. It is these native containers that might be shared between jobs and so might cause scheduling conflicts. To avoid one job interfering with the work of another concurrently scheduled job that accesses the same native container, one job must depend upon the other directly or indirectly. Which job should be a dependency of the other is up to us, but we don't want to leave their order of execution up to the happenstance of scheduling. Very helpfully, the job system safety checks will give us an error if we try to schedule conflicting jobs with no dependency between them. However, these checks are fairly costly and so they are enabled only within the editor, not within standalone builds. The burst compiler, again, is an optimizing C sharp compiler that can aggressively utilize SIMD instructions, single instruction multiple data instructions. This burst compiler, though, only works on a subset of C sharp that Unity calls high-performance C sharp, and it only works on job code. Especially for computation heavy code, burst can often yield 2x to 10x performance gains, sometimes even more. While there are no plans to deprecate game objects, 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. 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. So an entity, again, is just a unique integer ID number, and an entity can have associated with it any number of components. For a single component type, however, a single entity can have only one instance of that type. The set of component types associated with an entity is called its archetype. Like the columns of a relational table, there's no sense of order to the components, so say an archetype consisting of components A, B, and C is the same as an archetype of components C, B, and A, or B, A, and C, etc. Order doesn't matter. 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 GameObject components, monobahaviors, entity components have no event methods like update. While we can give our components whatever methods we like, it's generally best to instead put most game logic in systems. Unlike GameObjects, entities do not have parents or children. However, components can have entity IDs as fields, so in this way, an entity can reference other entities through its components. The key to understanding ECS is to understand how it stores the entities and their components in memory. 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 A, B, and C. The chunk 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. Be clear though that while the arrays of a chunk may take up different amounts of space, they all have the same number of slots. The first slot of the arrays 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 the chunk. If a single chunk is not enough to hold all the entities of a particular archetype, additional chunks are created. Because a single chunk can only store entities of one particular archetype, when we change an entity's archetype by adding or removing components, the entity must be moved to a different chunk that matches the entity's new 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 up to fill in the gap. So that's how entities and their components are stored. 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, we loop over all chunks, which include those component types, and for each chunk we can loop over the entity ID 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 bandwidth accessing data which we don't actually need in the loop. Here, for example, say each box represents a chunk, and each letter denotes a component type of that chunk. The chunks themselves may be scattered all throughout memory, but the entity IDs and components within the chunks are stored tightly packed one after the other. If we want to loop through all entities with both B and C components, we touch these five chunks. In other cases, we might want to access entities with a certain set of components, but not access necessarily all of those components. Here, for example, we're still visiting every entity with both B and C components, but we're only accessing the C components. In still other cases, we might want to access entities with a certain set of components that excludes one or more component types. Here, if we want to access the chunks that include B and C, but exclude A, then we would access just these three chunks. A system, concretely, is a class inheriting from component system. Like a monob behavior, systems have event methods called in the game loop. On update, for example, is called once per frame. Unlike monob behaviors, we don't attach systems to any game objects. We can group systems and use attributes to determine their order of updates, but where unspecified, the order of updates is left up to Unity. What Unity calls a world is comprised of a set of systems and an entity manager, which keeps track of the world's entities. The systems of a world normally only access the entities of their own world. While many games may have no need for more than one world, splitting entities into separate worlds can be useful for various purposes, such as networking. We can access entities and their components in jobs using special job types, but two concurrently scheduled jobs will conflict if they access the same entity components. To fix such conflicts between two jobs, one job must be scheduled as a dependency of the other to ensure that one finishes executing before the other starts. Safety checks in the editor will catch these conflicts if we fail the set of needed dependencies. To help us chain the appropriate dependencies between jobs, we use a variant of component system called job component system. While we can read and mutate entity components within jobs, we cannot in a job make structural changes to the chunks, meaning we can't add or remove entities or components. Allowing such structural changes in jobs would create too many potential conflicts. To work around this limitation, a job can record changes it wants made with an entity command buffer, and after the job finishes, the recorded changes of the buffer can then be actually enacted. Rather than directly create entities and components in the Unity editor, we still in ECS create scenes composed of conventional game objects, but using special conversion systems, we can convert the game objects of sub-scenes into entities when a scene loads. After conversion, the game objects are then usually destroyed. A component with no data is called a tag because such components can be used to tag entities. For example, if we want only some objects to be affected by gravity, we might give only those entities a gravity tag. When we then apply gravity to entities in our code, we only query for entities with this gravity tag. Even though tag components take up no storage space, they are still part of an entity's archetype. So when we add or remove a tag component, the entity must be moved to another chunk, just like for any other change to the archetype. An object component is a special kind of component that is a managed object. These components are not stored in the chunks, but rather in external arrays, and chunks reference the objects by indexes. Because these objects are managed, they cannot be accessed in jobs. Well, sometimes convenient, object components are not cache friendly, and so using too many object components defeats the whole purpose of ECS. A chunk component is, as the name implies, a component that belongs to a chunk itself, not any individual entity. When entities are added into chunks or moved between chunks, no chunk component values are affected. Chunk components are useful in a few niche scenarios. For example, a colon system might store a chunk component, which is an access-aligned bounding box, encompassing the bounding boxes of all the entities within the chunk. This bounding box must be updated as the bounding boxes of the entities change, but keeping this chunk bounding box up to date can speed up colon calculations. Shared components, like chunk components, only have one value per chunk, not one value per entity. But these shared components logically belong to the individual entities, not the chunk. So when we change the shared component value of an entity, it no longer matches the value of other entities in the same chunk, and therefore, the entity must be moved to a different chunk that shares the same new value. If no such chunk already exists, a new chunk is created. Also, like object components, the shared components are not stored in the chunk, but rather in arrays outside of the chunks. The chunk itself, for each entity, simply stores an index into the arrays. The other special thing about these shared components is that unique shared component values are stored only once, so entities across all chunks that share a value all reference the very same value, not any copies. This means that for each unique shared component value, at least one chunk is required. If we have 100 different entities of the same archetype, each with a different shared component value, then each one will be stored in its own chunk. Consequently, shared components are generally only appropriate when we expect many entities to share the same values. Otherwise, our data will get scattered throughout memory, defeating the whole purpose of ECS. While regular components cannot contain arrays, we can give our entities dynamic buffer components. These dynamic buffers are expandable arrays. For each dynamic buffer component of each entity, the chunk stores a fixed-size array, but these arrays can be supplemented with additional storage outside the chunk. For example, an entity might have a dynamic buffer component with five fixed slots stored directly in the chunk, but we can expand the array to have, say, 12 slots, in which case, seven additional slots are allocated outside the chunk. System state components are like regular components except for one difference. When an entity with any system state components is destroyed, all non-system state components get removed, but the entity is not actually destroyed. Only once we remove all of the system state components does the entity actually get destroyed. The primary use case of system state components is that we might want to do some kind of cleanup work after destroying entities. If we put any data needed for cleanup in system state components of an entity, then we can find the entity for cleanup by querying for all entities with only those system state components. We then perform our cleanup and remove the system state components to actually destroy the entities. The term blob is an acronym for binary large object. A blob asset in ECS is not actually an asset in the usual Unity sense. Blob assets are not necessarily stored as files and currently they aren't imported into the asset database, though perhaps this might change in the future. Instead, blob assets are simply immutable blobs created in memory that can be referenced from components. Because they're immutable, they can be safely used in jobs without any concern for conflicts. While these blobs cannot contain any absolute pointers, i.e. memory addresses, they can contain relative pointers, i.e. offsets. Because these blobs contain no absolute pointers, the blobs can be trivially serialized and deserialized by simply copying the bytes verbatim. So these blobs are assets in the sense that we might store them on disk at build time and then load them from disk at runtime. Strings can't be stored directly in regular components, but one work around is to have components reference blobs which store the needed string data. Blob assets are useful for myriad other kinds of data, such as collision geometry or AI navigation graphs, or whatever else we can think of that fits the format. Understand, though, that not all binary data formats can be expressed as blob assets. For example, we could store textured data in a blob asset, but most standard texture formats have very particular binary formats that don't conform to the blob asset format, so we can't have the GPU directly load and render textures that are stored in blob assets. So that covers all the major elements of ECS, but again, note that for most games, ECS is not yet truly viable as a full replacement for game objects. Most of the core Unity Engine functionality of game object components, as of yet, has no ECS equivalent, and so if you use ECS today, you yourself would have to implement this functionality or use conventional game objects alongside ECS. The only major pieces in place right now are three packages, hybrid.rendering, Unity.transforms and Unity.physics, all of which are still currently under development. ECS feature parity with conventional game objects is at least a few years away. Lastly, Project Tiny, which we mentioned at the beginning, is a new runtime that strips out everything but support for dots. Because the runtime doesn't support game objects, all conversion of game objects to entities must be done in build time rather than runtime. Like ECS itself, Project Tiny is still currently under heavy development. The project's emphasis for now is on web and mobile games because they would most benefit from smaller download sizes and faster load times.