Hello everyone, today we'll do the game
over system for our tower defense game, and we'll use the ITriggerEventsJob
that is exposed by the physics package, that will allow us to introduce the
notion of jobs and multithreading. For that job we'll also need to
introduce the concept of component lookups to allow us to access entities
that are not iterated through. And we'll also create another system
using entity queries and an entity command buffer and we'll dive a bit
deeper into the playback pol
icies for that entity command buffer. As usual, you can get the full code
for this tutorial in the repository linked in the description below. And you can import that repository into
your project and have something like this. Once you've imported the fifth
folder game over, system or game over folder You can see that you can open
in the scene the game over scene. And if we look at the scene
quickly, we have the spawner that we already implemented in
the third episode of the series. So if you hav
en't been there
yet, you can go back to watch the video for that particular feature. Then we have just a simple floor
which is nothing particular, just a simple mesh for decoration. And we have a player, and the player
is represented by this, wooden house. And we will have, on this player,
a sphere collider, and a simple authoring component, giving the number
of lives, the player will have. We can notice that the physics
collider is on the player layer, so if you don't see that player layer
her
e, it's because it's one of the custom, named layers that I created. And you can go to the add
layer to add them, like we did for the previous episode. So here I've added to the project
all the layers that we will use, so. 25 players, 26 projectiles, 27 enemies,
28 paths, and the three layers that we already defined in the previous episode. Okay, so if I enter play mode,
now what should happen is that I have the spawner that will spawn a
few enemies that we can see here. And once they have reach
ed the
player, they will be destroyed. And when the player has no longer any
life, I'm disabling every entity enemies and you can see that they are still
there, but they are just disabled. Basically, that's our game over, and I'm
also logging the game over in the console. Let's take a look at the scripts we have. So, We have first the player authoring,
very simple, we just have a life count of five in the editor and
we bake that into a simple player component, which stores the life count. Very
simple. Next, what we have also is the
component for the life that we just seen and a tag component to
indicate that the game is over. So we will spawn that component once the
life count of our player has reached zero. The prefabs, so, the prefab
we have here is the enemy. As you can see, we have the
components that we have already used in the previous episodes to make
it follow some path, and we have a sphere collider, and a rigidbody. So, here, the rigidbody is very important, Because if you r
emove the rigidbody and
enter playmode again, you will see that the event will no longer be triggered. So the trigger event job that we will see
in a few minutes will not be triggered. So my enemies are just bouncing back
because the behavior I've designed is for them to loop back through the path. And I'm no longer having
any gameover notification. And if I look at the entities
of the player, the life count is no longer decreasing. If you don't have at least one
rigidbody in your scene, all the
physics events will not be triggered
because the physics system assumes that nothing is moving, so there
shouldn't be any event to be triggered. You really need to keep the
rigidbody on the prefab component for the enemy that's moving. The scenes I will skip
over and the system. We have two systems. The first system is the entity
player collision system. So that's where we will
implement the ITriggerEventsJob. So let's take a look at it. This one, we will make it run
in the fixed update system.
And we will update it after
the physics system group. Because we need the trigger events. To be set by the physics system group. We need to run after that one. Actually, we could replace these
two, Attributes by a single attribute because there is a system that is
called after physics system group that basically does the same thing. So I could do exactly that and
it would work exactly the same. So let's leave it like this for now. Then we have component
lookups and buffer lookups. So these two.
Elements that I'm using in the system
allows the system to access any entity data, so it doesn't have to be part
of the query, I just need to have the entity reference and I can look up
the component, particular component for the given entity reference. So here I'm having one component lookup
for the player, and one buffer lookup, so this one looks up for the dynamic
buffer waypoints, so this allows me to know that it's an enemy, and this
allows me to know that it's a player. In the onCreate,
I'm still using the same
as before the require for update, because I require a few singletons, a simulation
for the physics, and two command buffers, one for initialization command buffer, and
one for begin simulation command buffer. We'll see what, which one
we use for what a bit later. Then this declaration above for the
buffer lookup, and component lookup are just declarations, so they are
variables that need to be assigned. I am as assigning them through
the system API get buffer lookup or
get component lookup. And in parameter we can pass in if we are
only wanting to read from that component lookup or if we actually want to write
to the component that we will look up. So for the waypoints, I'm setting it to
read only true, and for the player, since I will want to decrease the life counts,
I need to write to it, so it's false. And if you don't specify it by default,
as you can see, it's read only false. Meaning that by default, you
will access the component lookup in a read write
manner. So you, you need to be careful
about what you want to actually do. And I find it more explicit to
always specify the value so that it's clear that we are not in read
only and that we are in readonly Next, in the update, as for the
previous episodes, we get our singletons and create our command buffers. And we have this part where
we need to update the lookup. So the lookup is just a reference to the
set of entities that have that component. And since that set of entity can change
throug
h structural change, as we've seen in previous video, we need to update,
continuously that lookup before using it. So every time we do an update,
we need to call on the lookup the update method, passing in the
reference, to the state of the system. Next, we are, diving into the job system,
so if you are not familiar with the job system, all we did until now is single
threaded and main threaded, meaning that there is a main thread that, Execute the
player loop and all our code has been executed o
n that main player loop we
can look at the analysis and profiler. And as I've hinted in a previous video, we
can, in the profiler, see all of our jobs. So we have the main thread. So that's the execution
of our player loop. And then we have a job section, where we
can see all the workers we have access to. So basically, it's a job. A thread that we can execute code onto. And we can see here that some
of the code is already executed in a multithreaded fashion. For instance, this drawBinCollector,
which is a job that probably is defined by Unity itself, is executed on the
worker number 4, so on the fourth thread. And it's actually working over 30
threads, so it's multithreaded. It's not only threaded. So, when I will refer to main
thread, I will mean this thread. When I will refer to threaded, I will mean
that it's executing on one worker thread. And when I'm referring to multi
threaded, it means that it's executing on several worker threads. If we look again at the job, we
can see that
this job is scheduled. So that means that this will be
run on a single worker thread. So it will be threaded,
not multi threaded. The way we declare that is that
we create a Jobstruct, so it will implement different interface. So in this case, we will view the
ITriggerEventsJob interface, which is provided by the physics package. There are other kinds of jobs, and I
actually have a video for jobs that are not related to ECS, if you want
to have a look at it, and it goes into more detail about th
e dependency system
and safety system that goes with the job . system from Unity that makes
it simpler to do multi threaded code rather than the usual C sharp threading back to our system. So we declare a new struct that
is our job and we pass in all the information that the job will need. So as we can see here, we are passing in
the Lookup for the enemies, the lookup for the player, and our two, command buffers. Take note here that whatever you
pass in the job system is a value type and not a
reference type. That means that if you were to pass in
an int, for instance, an integer, and you want to increment that integer in
the job and log it back after the job, you will not get the result of the sum. Or the operation you did in the
job because it's a value type. There is a difference between a
value type and a reference type. All structs are value type, meaning that
when you pass it to a method, you are not actually passing the same area of
memory, but you are passing a copy of the dat
a that was in that area of memory. If it was a class, it's a reference type. And in that case, you are
basically copying a pointer to that particular area of memory. So you are using exactly the same,
data and same exact area of memory. That's why also we have native containers. Because even though Native containers
are value types, because they are structs. What they hold is actually a pointer,
so you are not actually having the data inside the struct, you have
the data inside the area of memo
ry somewhere, and that struct has the
pointer to that area of memory. So when you are passing in a native
container, you are copying the content of the native container, which is
the address or the pointer to the memory where your data actually lives. So that's why the native containers
can be used in jobs to modify data. Let's look at the job itself now. So as we've said, we are using the two
lookups and the two common buffers in the execute method for the ITriggerEventsJob. That's not the case
for every job. The execute method for the
ITriggerEventsJob gives us a TriggerEvent which is populated by the physics system. And this one contains a pair of entities,
so entity A and B, but it's not very descriptive of which entity is which,
so the first thing we need to actually do is to figure out which entity is the
enemies and which entity are the players. So that's what we will see
in this method a bit below. Once we have figured which is the
player and which is the enemy, We actually ne
ed to check if, we are
interested in that particular pair, because we could have, trigger that. Concerns a player and something
else, or an enemy and something else. In that case, we need to filter
out the event because it's not processed by this system, by this job. So, let's assume that we have an
interaction between a player and an enemy. And in that case, what I will do is
use the player's lookup here, just like an array, and say, okay, for
my player, in my player lookup, I'm looking for the
player component because
that's what is defined as the lookup component in the component lookup. And I'm looking for the
player component of the player entity that I have identified. This here is an entity. Then I'm simply decreasing the
life count by one and reassigning back through the lookup. The player component I just modified. Then I'm actually destroying my
enemy, and same thing here, the enemy is the entity Enemy that I've
identified through this method. Last thing I'm doing is
checking
the life count. So if my life count is
above zero, I'm fine. My player is still alive, but if my
player is dead, meaning there is no more life on the player component. I'm actually creating a new entity
with the ECB BIS initialization system. So I'm using a different
system than the destroy system. You can look back at the previous
videos to understand exactly why I'm doing this in two different systems. And, creating a new entity
with the component GameOver. So this will create a tag component
or
tag entity that allows me to know that the game is over, the player has lost. Let's look back at the IdentifyEntityPair
method which is defined here. So we get in the trigger event and
what we do is first we declare two entities, the player and the enemy. We declare them as entity null, meaning
it's not a valid entity, it's nothing. And we check on the player's
component lookup, which is declared as part of the job members. We check, does the entity A that is
provided in the trigger event, S
o this entity, does it have the component player? If it does, it means that's a player. Same thing with the entity B. If the entity B has the component
player, it means that it's a player. So we populate the player entity reference
to the entity that has a player component, and we do the same thing with the
enemies, with the enemies buffer lookup this time, and we check if it has that
buffer, then it means it's an enemy. And as a return of this method,
we give back the tuple player and enemy ent
ities that we've defined. So here, if none of the
entities are the player, the player will remain entity null. Same thing, if none of the
entities are the enemy, the enemy entity will remain null. And if we add an interaction between
two enemies, for instance, The player enemy would remain new and the enemy's
entity would be either entity A or entity B in that case entity B. So now we can, as a next step,
check if we should process this particular tuple of entities. We do that in this method sho
uld
process, and here we are checking that none of the entity are null because
if one of the two entity is null, it means that it's not the pair that we
care about based on this definition. So, very simple logic, but it's a
logic that we need to do for every ITriggerEventsJob to make sure that we
are processing the correct entity pairs. This will give us the logic to
decrease the player life once an enemy reaches the player collider. Now, we've seen that when the player
has 0 health, we declare
a component that is a game over component. And in the behavior we've seen
in the play mode, we see that we disabled all the entities and we
logged the game over in the console. This is done by another system, which
is called the game over cleanup system. And for this one, I'm using a
different syntax, let's say, and I'm using What we call an entity query. So up until now, what we've been using
is the system API query to get the component we want to iterate over. And that is fine, but in some cas
es
we need to have a different syntax and actually first build a query and then use
that query to get the component we want or to iterate over the list of entities. That matches that query. So here I'm using the system API
query builder to build a query. That says I want all entities that
match and have the, GameOver component, and then I'm just building the query. I can also make it so that the system
will only run if there is at least one entity that matches that query by using
the requireForU
pdate and passing in the query, kind of like we did here in the
onCreate saying, okay, I requireForUpdate that particular singleton. Here I'm just using a query syntax
or a query to do the same thing. And actually this one is a
leftover that does not exist. Next, in the onUpdate, what I want to
do is first log that the game is over. And this will be logged only if the
GameOver entity exists, because I have this RequireForUpdate query. So it will only update the system
and create that log if I've
reached the player with 0 health. And then in an entity command
buffer, so this one is created locally, so it's not using one of
the systems defined by, by unity. I'm just creating a local entity
command buffer and I will, Play it back manually also. So to create the command buffer,
I need to provide an allocator. like we've seen in previous episodes
for the native array or native list. we have the temp allocator, which
is the fastest allocator we can get. And it does not require to be
dispose
d of because it only lives for the time of the update. But you could use other allocator
and I can specify a playback policy. So in the playback policy, we
have two options, either single playback or multi playback. So my entity command buffer will
be , played back with the ecb. playback method and this method with a
single playback can only be called once. So if I were to duplicate that line and
do it again, it would throw an exception. But if I add the multiple playback,
it would be okay wit
h replaying the command buffer and, disabling again
the entity, because that's actually what I'm doing as part of this system. I'm iterating over all the entities
that have the waypoints, so that means all my enemies and actually my spawner
itself, so that's how I disable my spawner and avoid having more enemies. And, I'm using the WithEntity access
to get the entity corresponding to the one I'm iterating on. Then, in the EntityCommandBuffer,
I'm just adding a DisabledComponent, and the disabled
component is a Unity
component that, similar to the Prefab component, will make the entity
ignored by all of the query systems, meaning that an entity with a disabled
tag component will not be updated. Again, there are options, like
for the prefab, to also update if you want the disabled entities. And last thing, like I said, since I'm
using the temp allocator, I don't need to dispose of the entity command buffer,
but if you were to create one with the temp job, for instance, you would need
to
deallocate the entity command buffer. Okay, so, so far we have seen today the
job system and the component lookups. We have seen a bit of physics
overlap in the previous episode. We have seen how to spawn entities in
the third episode and we have seen in the second episode how to move an entity along
a path or to follow a target, a position. So now what I'm challenging you to do
for next week is to start implementing the next step of the tutorial which
will be to implement the damage system or
the shoot and kill mechanic. So basically, , implement, , the
towers so that they shoot a projectile towards an enemy and damage it. So if you can do that for next week,
it would be a good exercise for you to take in the knowledge I shared
with you, , in these few episodes. And in the next episode, I will
show you how I'm doing it using the things we've seen in the previous
episode, but also introducing. A blob assets that we can use
for managing a bit better our memory some, visual effects. I
will introduce also the notion of
aspects and we will use other kinds of jobs also to do the mechanics also
so we can introduce new, knowledge in addition to doing similar features
that we have already implemented. Thank you for watching. Don't forget to like and share
the video if you like the content. You can also support me on Ko fi. Or you can follow the link in the
description below to the Asset Store, which is an affiliate link. And if you find something that interests
you, like the list
of DOS related assets that I've compiled on the Asset Store,
you can purchase one of the assets that interest you and it will support the
channel at no additional cost to you. So, thank you for your support
again, and see you in the next video.
Comments