Next-Level ECS Advanced Job Systems, Blob Assets Management and Aspect - DOTS Masterclass

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hello everyone, today we are going to dive into more advanced topic about ECS. So I hope you followed along the series and you did try to implement some of the features we will do today on your own because it's by doing it yourself that you will learn the best. So if you have not yet, tried yourself, you can, go back to the previous episode if you want and try it yourself. And if you have followed along the series, we have covered the basics of ECS, we can dive into more advanced topics such as the blob assets, aspects, and the different types of jobs that ECS offers. And at the end of the video, we'll even have a small teasers of the next episode about operating ECS. and MonoBehaviour's GameObjects at the same time to have a VFX in this episode and in the next episode, animations. So let's dive into it usual, you can as usual get the full documented code in the GitHub repository linked in the description below. And, today. We have the sixth folder shoot to kill, where we will implement, logic to spawn projectile from our towers and kill our enemies. So we'll implement three types of project tile. The first one, the simple one that just killed the first enemy. The second one which is a piercing projectile that can hit several enemies. And the last one a guided projectile that is able to follow the enemy. As we go further into the series, there are some stuff that we will, need to implement again and, using the features we've, covered in the previous episode, so I will skip over those and only highlight the new features that we will implement or the new tools that we will use to implement those features. But you can see everything in the github repository linked in the description below like I said. First up we can open in the scene the shoot to kill scene and we have the shoot to kill sub scene which contains our three towers so we can see here the three towers and we actually have one two three four and five enemies and the fifth enemy is in a sphere, is encapsulated within a sphere because the sphere needs to move. So I'm using the pathfollow we implemented in the second episode, to make the, little slime monster move. And then we have our two way points, that our slime can navigate between the two. And that's it. If we look at the tower entity, we have the canon authoring, which is the main authoring component we will view today. Actually, I have two other authoring components, but they are very simple . For the Canon authoring, we have a new feature, which are the blob assets. So, until now, what we have seen is, to store data about our entity, we store them in, component, IComponentData or IBufferElementData and, these data is replicated for every entity. So, it's great for data that is specific to that entity and that , can change at run time, like the health of the entity, the position of the entity, and so on. But for our towers, we will have some data that is just to configure the tower, like the fire rate, the range, and so on. So this kind of data doesn't have to change at runtime. It's a static data, static configuration data. So it would be a waste of memory to have to replicate that data for every tower that we spawn into our scene. So what we will use for that is blob assets. So as you might have guessed, the blob asset purpose is to be able to share some data between different entities. And that data needs to be read only. So, for our tower, we will have some data that will be dynamic, meaning that we will need to update it at time, and some data that will be purely, static data, like, the range of the entity, the total time between, each, fire or each shot. So, as usual, the first thing we do is to get the baking entity, and in that case we just need it to be renderable, meaning there is a position in space but the tower itself does not move. We need to have some, physics collision filter, so this will be used to, Detect our enemies. So we will perform as we will see in one of the system. We will perform an overlap sphere like we did in a previous episode, and we will, find the entities that are flagged as enemies for the tower to target. So this is part of the static data for the tower, and actually before we go into the dynamic data, let me move it at the bottom. We are dealing right now with the static data. So the static data or non dynamic data, will be stored in the blob asset. And the blob asset cannot be stored directly into a component. So we will need also to create a blob asset reference that will hold the blob asset. And the blob asset reference will be put into a component. So let's first, we declare a blob asset reference. Of type CannonConfig and the CannonConfig is just a struct that contains the data that we will need for configuring our tower. So, fire rate, so, how many times per second we will, shoot a projectile. The collision filter that we will use to detect our enemies. How far can we detect an enemy and offset to be able to spawn the projectile at an offset based on the tower position so that it spawned at the top of the tower and not from the tower , The middle door or within the tower. This is just a struct and we need to populate it, allocate some memory space that will be shared between entities. So to do that, we create first a blob builder. We can use the allocator temp because it'll just be used within the context of the baker. It doesn't need to be persisted. And we create through the block builder. Construct root of configuration data type. We basically here are allocating the necessary memory to be able to store the information about our Canon configuration. Note here that we are using the ref keyword, so we are dealing with structs, and we want to actually manipulate the memory, of the allocation we just did. That's why we are using the ref keyword here to get a reference to the struct, so the memory allocation of that struct. Otherwise, if we didn't have that, we would actually be, modifying a copy, so another allocation of, of that, struct. We can allocate all the data we need for that, and here I'm doing an offset based on the position of the tower. And a position of a GameObject, so if I look back at my tower, I have the tower itself, and I have the projectiles spawn point, which is another GameObject, an empty GameObject, that is placed relative to the tower. So I just compute the difference in position to get the offset between the two transform. And last thing we need to do is from the blob builder to create actually a blob asset reference. That will create a reference to our blob asset and that we will be able to put into our entities. or into component for our entity. Note here that we are using the persistent allocator because we need this memory allocation for the Canon config, to be, alive for the duration of our game. So for as long as our game is running, we will need to be, perssisting this allocation or persisting this data. So here, what we are doing is creating an allocation of memory, setting some data in to that allocation and preparing a reference that we can keep into our component attached to our entities. If we do that for every entity that have this authoring component, we will still be duplicating the data for every entity. To avoid the duplication of the data, there is a concept that is provided by Unity ECS, which is the Blob Asset Store. We need to add a reference to our configuration data into that Blob Asset Store. And we do that by calling the AddBlobAsset method, passing in a reference to our BlobAsset reference. And as an output, we get an hash, so basically, a unique identification of that blob asset. If we had a different, another config, canon config with a different fire rate, for instance, it would be a different value for the hash, so it would not share the same memory for, those two configurations. And then the last thing we do is to create a component. So that's a IComponentData that we have here. And that stores our blob asset reference of canon configuration into a simple struct component that we can attach to our entity. Here I've added in between another way to do the same thing. Because here what we are doing is to build the blob asset reference for every entity that creates, or that has the authoring component. So if you have complicated global assets, to create and to instantiate, it may take some times in the baking workflow and it may be, not very efficient. So in that case, what you can do is, Find a way to pre-compute your hash or to use a asset, GUID. For instance, you could find a way to use, the asset GUID for a scriptable object for instance. And in that case, instead of just adding or creating the global self reference and adding it to the global asset store, you can first try to get a blob asset reference based on the hash. So you could use the GUID of the scriptable object, for instance. And if you manage to get the, BlobAssetReference, fine, you can use it in your adding, on your component for the entity. And if you didn't find it, you can, create your BlobAsset, so do the same thing as here, and then add it with a custom hash, so using, The GUID that you are looking for into the blob asset store. And then the last thing we do is to add the dynamic data. So the non static data for our entity. And here we can see that we are reusing the file rate. So we have the fire rate into the static configuration and the fire rate that is used, but this one is used as a timer, so we will see in another system that we will basically decrease that timer and we will reset it once we have shot to the value from our blob asset. So we have two parts, we have the dynamic value that is changing throughout the duration of the game, and the static configuration that allows us to restart the cooldown , And the second dynamlic data so I, I will say dynamic in quotes because it's not really dynamic, it's the projectile. So we are getting an entity from the prefab projectile. It's a dynamic because it needs to be able to move. but You could tell me that this entity, does not change during runtime. , the tower will always . Fire the same projectile, so it could be considered a configuration or static data. The issue is that the blob assets, so what we built here on the top, does not support entity remapping, so that means that the entity reference we get here is only valid during the baking of the autoing component. it will not be valid if we store this reference directly into the blob asset. It will not be valid in the runtime. You can have a more detailed explanation about that on my other baking videos that I've already referenced several times throughout the series. And I actually already touch this point in a previous episode. So when you do a get entity, you get an entity reference that is only valid through the baking workflow and is not valid in the runtime. The reason why you can do it into a component data on iBufferElementData is that Unity supports entity remapping for these two component types. So as part of the component definition, if there is an entity field, Unity will detect that, and it will be able to remap the entity, In runtime to change the entity reference within that component for it to be the correct entity. So keep in mind if you want to get an entity reference associated to an entity, you need to store it into an iComponentData or iBufferElementData. You cannot store that into a blob asset So now let's see how we can use that blob asset to actually spawn our projectile. If we go to the projectile spawn system, here I'm doing an iSystem as usual. I'm requesting some component lookups and some singleton data to update my system. Same as last episode, I'm getting the singleton data, updating my lookup, and I'm doing a system API query using a for each to be able to Gather the canon data so this one is the dynamic data that I'm using, with the time to next spawn and the prefab I need to spawn, the local to world component, so the position of the tower itself. And The last thing I'm getting is the tower config blob asset, which is actually a component that contains the blob asset reference to my canon config. The first thing I will do in the system is to decrease the time to next shot, so I'm using the to data here. So ref RW Canon data. decreasing it by the system data time, and if the time to shoot has not come, we just continue the loop and update the next entity. If we have reached, below zero, the time to shoot our projectile has come, we will need to reset the next time to shoot, and we will also need to find the target and spawn the projectile , and pointing it towards the target. To get the reference to the blob asset, we will need to get the tower config value, readonly config value, and we need in that case to use also the reference keyword. We don't use the reference keyword, it will not work. Next we will need to check if we have an enemy to target because if we don't have an enemy to target and we won't shoot, we won't reset the timer. The first thing we do is to declare a closest hit collector. This will allow us to get the closest entity to our tower. And to find the closest entity to our tower, we need to perform an overlap sphere operation using the tower position the range of the entity, the collector we just declared and the filters we have in the, tower config blob asset. If we didn't find a hit, we continue to the next entity. If we did find a hit, it means that we have an enemy. So we can reset our timer to the firing rate. So it was a time to the next. shot And we can use the EntityCommandBuffer to instantiate our prefab, set its position based on the tower position plus the offset we have in the tower config, and last to, orient it towards the our enemy using the position of the entity we just spawned and the position lookup to find the position of our closest enemy. So here we compute the values and here we set the values through the command buffer. So with that we have our tower firing into the direction of our enemies. And we can look at the next system that will take in charge moving our entity. Here, for this system, I will be using one of the jobs provided by ECS. So this system will allow us to move our projectile in parallel or in a multithreaded manner. The first thing we'll need to do is to get the position as read only because we will need to have the position of our target in the case of the guided projectile. We will update that lookup as part of the update of the system and we then schedule our job like we did for the trigger event job. In that case we don't need the physics simulation so we just have as parameter the state dependency that we reassign after to the state dependency So this dependency, again, you can have a look at my other video about the job system in general to understand how the system of dependency works. But basically it's just there to make sure that two systems that work on the same component and same data don't, overlap each other and don't run in parallel. If one is in read. Only and one is in read and write, we would not want to have a race condition. So that's why the dependency system or dependency management takes care of it for us. And here, what I'm using is the IJob entity and the IJob entity is an interface to create a job as part of the ECS framework and Here, I am using the delta time, the position, very similar to what we had for the trigger event job, and here, we have an execute method, which take in a projectile aspect. So we'll see in a minute what this is, and actually we could take in other parameters. If it was a read only parameter, we would put in and the type of data. So we have, for instance, Canon data. If it's a read and write, we would use the ref keyword and put in, why not, the Canon config. Component And if it was a dynamic buffer, like the way points with same thing could put in dynamic buffer, for the read only for the dynamic buffer of way points. And I need a name or, arrive if we want, if we wanted to read and write from that. But here, what I'm using is something a bit different. It's an Aspect, and an Aspect is another type of struct that implements the IAspect interface. And it's a wrapper for a set of components that can be manipulated in a system or that have a coherence between them. And it allows the developer, so you, in that case, to provide high level method or to hide some of the complexity of, the component management. So here, what I'm doing is I just provided a method to say, okay, there is a projectile and that projectile can move. So to move the projectile, I need two things, the time and the positions of, or the component lookup positions. To perform that move behavior, I actually need four components. I need the guided component, which is actually a component that I can enable or disable. I didn't use the IEnableable component, but I could have for this one, for instance the speed of the projectile, The target of the projectile, so if it's a guided projectile, I will have the target of that projectile to be able to follow it. And actually that is something that I've set in the projectile spawn system. And, the position of the project tile itself. So here techno that I'm using the local transform of the project tile, but I'm using the local to world component lookup. If we needed the same type of local or the same type of component, I would need to remove it from, , there and use the read and write version of the component lookup instead. But here I'm working around it by not using the same type of components. And then I'm performing my move logic if it's a guided projectile and there is still a position. So here what I'm checking basically is to make sure that my target entity is still valid. If that's the case, I'm Rotating the projectile towards the target, and then I am moving the entity forward. Basically, if the guided projectile has a valid target, it will turn towards that target, and then move forward towards that target. If the projectile no longer has a valid target, meaning the target has already been destroyed, it will just keep moving forward. Here in the aspect, there are several, things you can use. So here I'm just using the read only and ref read write, but I've actually put everything that, you can use within the aspect. So. You could also have an entity field that would be the entity you are iterating on for that aspect. So the entity that is associated to that particular, guided, speed, target, or local transform. That can be useful for using in two command buffers, for instance. Or to use as part of the component lookup read write if you are using a component lookup. The component lookup cannot be itself referenced here. It needs to be passed in as a parameter of the method you expose. You can also have so ref read write and ref read only. You can also have the enabled buffer. One particularity for dynamic buffers, you don't again put in a ref read write or ref read only. Just like for the system API queries. So you need to access directly the dynamic buffer. So that covers how we can make our entity move. And actually, if we go back to Unity and enter play mode, you can see that our entity, our projectile here are moving toward our target. And once the target is destroyed, they keep going. So, if we didn't have any other system, they would keep going for infinity, and that is not good. If we look a bit closer, for this one, the non guided projectile, if they miss the target, they keep going, and at some point, they are destroyed. This is because I've implemented also a limited lifetime component and I'm doing here something very simple, I'm just having a Limit to the time. So if I look at the prefab and take the project tile, this project tile can only leave for five seconds. If it, if it was spawned more than five seconds ago, it'll be automatically destroyed. And I'm doing that within a system. again And this system is the limited lifetime system. And for this one, I'm using another type of job. And actually it's not really another type of job because the job we just saw a minute ago, the IJobEntity will actually be code generated. So there is a code generation step, with this one, and it will be transformed into the job that we will be using here, which is an IJobChunk. For the IJobChunk, we need to have the list of entities, or the list of components we want to manipulate through the job. And we did that with the execute method of the IJobEntity. But for the iJobChunk, we need to do a little bit more work, and we need to use an entity query. So we create an entity query using the System API Query Builder, saying we want all the entities that have the component , limited lifetime, And we get several type handles, so this is a reference to the component type of the type LimitedLifetime, and we need also the entity handle to be able to use the IJobChunk. We provide a few requirements, so we need to at least have one entity that matches the query and we also need the, command buffer singleton to be able to create our singleton. We create our command buffer, we update again our type handle, in the same way we update the component lookups, we need to update type handles, and we declare our job. So the declaration doesn't differ much from previous jobs. The only thing we need to pass in here is the query itself, and we will schedule the job using the query and the state dependency. As part of our data that will be manipulated through the job, we need to pass in the delta time to be able to decrease the time to live of our entity. We need to pass in the entity command buffer, and in that case we are using an entity command in a Parallel job multi threaded job. So we are using the entity command buffer and passing it as a parallel writer. So that's the key takeaway you need to have. If you want to use an entity command buffer in a parallel context, you need to pass it in as a parallel writer in and we pass in the component type handle and entity type handle, , Now, for the job itself,, I agree it's a bit more verbose there is more stuff to write, but I think it's important for you to understand what actually happens behind the IJob entity, which is kind of a sugarcoating, , , all this code. The IJob chunk, as the name implies, will iterate over chunk of entities. So, the entities are stored in chunk of memory, I have another episode, another video about, that if you want to have a look and have a better understanding on how the memory layout of ECS works. Basically is there is a small bag of entities that can go up to 128 entities and the job will run one bag after another, and, and iterate over one bag of entities after another. We get the EntityTypeHandle, we get the ComponentTypeHandle for the limited lifetime, the ParallelBuffer, and the DeltaTime. The execute method doesn't have parameters related to the component themselves. What we get instead is the archetype chunk, the unfiltered chunk index, the use enabled mask and chunk enabled mask. So these are used for the high enabled components, which we don't have here, and this one will be used as we will see for our command buffer the first thing we need to do is, from our chunk, we need to get the array, so the list of components that match our type handle, and we give it the reference to the type handle and get the native array of limited lifetime component. We do the same thing for the entity type handle to get the list of entities that are part of that chunk, that bag of entities that the job is processing. And then we iterate through a simple for loop over all the entities that are managed by the, that, chunk. So within the bag of entity managed by the job, we can get the. Lifetime, decrease it, and reassign it. So here, same thing, when I'm doing this, I'm actually getting a copy of the data, because it's a struct, it's not a reference. I'm decreasing the value and reassign it back to the component within the array. And if the time is below zero, meaning that the entity has exceeded its lifetime, I simply use the EntityCommandBuffer to destroy the entity. And since I'm using it as a parallel writer, here, because I'm in a parallel context, I need to be able to order deterministically. My commands, and for that I'm using the unfiltered chunk index as an additional sorting key. So, that means that if I have several, chunks, or the job runs in parallels on different chunks at the same time, it will still be, the command buffer will still be able to order and do the command in a destinistic manner, so whatever the order of the commands are queued in, it will still execute in the same or predetermined order based on that unfiltered chunk index. And that is our limited lifetime system. One system we didn't have a look at is the kill system, where I will be using again an IJobEntity, but I will also be using a command buffer to destroy my enemy entity. And that allows me to show you, so same thing, we have the usual stuff of scheduling, declaring the job, getting the singleton for the EntityCommandBuffer, again, using the ParallelWriter here, so I have the explanation here, and this time, within the IJobEntity, for us to be able to have an Entity, or a sorting key for our EntityCommandBuffer, we will use this syntax, The chunkIndexInQuery, attribute on the parameter named chunkIndexInQuery. And we will also, put in the entity because we need to know which entity we want to destroy. So that's how you can use also the, parallel command buffers within an iJob entity and not have to do all this, kind of, , work That can be a bit too, too much to write for simple cases. And now we can check out. The last System, which, if you take a look at the prefabs, and look at the projectile, you will see, in fact, that the projectile itself contains a physics particle, so VFX, , particle systems, and same thing for the explosion, actually, as part of the projectile, I have another prefab that is the impact, and this impact also has a particle system. This is a very very short and very simple introduction to our way we can communicate between ECS and GameObjects. Because, as you may know, ECS is not yet full featured, so there are things that we cannot do with pure ECS, like visual effects, like sounds, and like animation, that we will see in the next episode. And the way we do that is to have an association or a link between an entity and a game object. And in fact, in some cases, Unity does it by default, like for the VFX or particule system that works also for VFX graph, and it will automatically, if there is a particle system, create a companion GameObject, that's what they call it, and it will spawn the GameObject at the same time it spawns the entity. It works for some, MonoBehaviours . It does not work for all of them. So in the next episode, what we will see is how we can make it work for every single component, even the one we defined ourselves. So we can have a way to work with both entities and GameObject to actually, manage a full featured game and not just a game with no sound, no VFX, no animation, which can be done but may not be as fun I know there was a lot of stuff to cover today. Hopefully you did manage to enjoy the video. If you're still there, hopefully that's the case. Thank you for watching again. Don't forget to like and share the video if you find it interesting. . And if you want to support the channel and support me to make more tutorials like that, you can support me directly on Ko fi through a single donation or monthly subscription, or you can also use the Asset Store link in the description, which is an affiliate link, and we'll have Help me, to support the channel, there is actually plenty of sales, don't forget about the publisher of the week also, follow me on Twitter if you want to be reminded of that, I post a tweet every week about that, and, see you in the next video for animation and making ECS and GameObjects work together! See you in the next video!
Info
Channel: WAYN Games
Views: 442
Rating: undefined out of 5
Keywords: Entity component system, DOTS Physics, ECS Physics, unity dots 2024, unity dots tutorial 2024, Unity DOTS, Unity data oriented technology stack, how to make a game with unity dots, Unity dots tutorial 2022, ecs baking, unity dots 1.0, ECS vs Mono, Unity dots tutorial, Unity job system, game dev, unity data oriented design, unity dot, unity dots, unity ecs, unity ecs 1.0, unity ecs tutorial, unity esc, WAYN Games, unity physics, unity ecs blob
Id: FJdthVNK5dA
Channel Id: undefined
Length: 37min 1sec (2221 seconds)
Published: Sun Mar 24 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.