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!