Welcome to part 7 of this Unity tutorial series where we’re creating a survival shooter
game using Unity’s implementation of the Entity-Component-System pattern, or Unity
ECS for short.
In part 6 we continued our work on player shooting by implementing rate of fire, which
is kind of a weird thing to do considering we haven’t even begun generating bullets
yet.
But don’t worry because in this video that’s exactly what we’ll be doing, and we’ll
be wrapping up player shooting once and for all.
So, without further ado, let’s get right into the video.
So far, we’ve create two systems for player shooting: PlayerShootingSystem and CleanupFiringSystem.
At a high level, the player shooting system is responsible for tagging the weapon entity
with a Firing component whenever the player presses the “Fire 1” button.
And the cleanup firing system is responsible for untagging the weapon entity after a set
period of time, which for now is just hard coded to be half a second.
But all of this is happening in memory, so to speak.
No bullets are actually being generated in the scene.
It’s time to fix that.
First, we’ll need to create a new system.
Let’s call it “FiringSystem.”
This system will be responsible for generating bullets, and a lot of them, so it’ll need
to leverage the C# job system.
We can do this by having it extend the JobComponentSystem class.
Bullet generation means it’ll need to be able to create new entities, so we’ll need
to inject a barrier.
For the uninitiated, barriers are used to queue up actions that we want to perform on
entities, from creation to modification to destruction.
Barriers live on the main thread where it’s safe to perform these actions, as opposed
to inside of our jobs which are run on separate threads in parallel.
And of course, we’ll need to inject it.
If you’ve been watching this series, then this should all look pretty familiar.
But we’re going to try out a new type of job this time, so this next step will be a
little different.
Let’s create a job called “FiringJob” that derives from IJobProcessComponentData.
Wow that’s a mouthful.
Basically, this type of job processes entities based on their components.
We tell it which components to look for through it’s generic parameters.
In this case, we’re looking for entities that have a firing, position, and rotation
component.
The firing component acts like a notification.
When an entity is tagged with it, the job knows it’s time to generate an actual bullet.
The position and rotation components instruct the job where to generate the bullet in the
scene and in what direction it should travel.
Let’s implement the job’s Execute method, where all of the action will happen.
And I’ll just quickly clean up the argument names while I’m at it.
Next we’ll add an EntityCommandBuffer field to the job.
The EntityCommandBuffer will be responsible for queuing up the creation of our bullet
entities.
Lastly we’ll need to override JobComponentSystem’s OnUpdate method and schedule the FiringJob.
IJobProcessComponentData’s Schedule method is a little different than the ones we’ve
seen before.
It’s specialized for ECS so all it takes is a reference to the system and a JobHandle
object.
Alright, now it’s time to generate bullets…
Right after we do one more thing.
We need a way to configure the mesh and material of our bullets.
To this end, we’re going to create a MonoBehaviour that exposes a MeshInstanceRenderer through
a static field.
The common naming convention here is “Bootstrap” or the name of your game followed by the word
“Bootstrap”.
For now I’ll just stick to “Bootstrap.”
Now we can add a static MeshInstanceRenderer property.
I’ll call it “BulletRenderer”.
MeshInstanceRenderers are components that are used by the MeshInstanceRenererSystem.
The MeshInstanceRendererSystem is responsible for rendering 3D models in the scene.
It’s one of the handful of systems that comes bundled in with the Entities package.
We’ll actually be using another one of these bundled systems very shortly, but let’s
not get ahead of ourselves.
I’d like to be able to configure this variable from the editor, so let’s create a serialized
field with the same name.
Then implement the Awake method and use it to initialize the static renderer.
Now we can switch back to the firing system and use the static bullet renderer to generate
bullet entities.
First we’ll use the EntityCommandBuffer to queue up the creation of a new entity from
the Execute method of the FiringJob.
Then we can add the bullet renderer component to it, which is a shared component.
But this isn’t quite enough.
The MeshInstanceRendererSystem only operates on entities that have both a MeshInstanceRenderer
component and a TransformMatrix component.
So let’s add that, as well.
Now we can use that other built-in system that I mentioned earlier.
One called the MoveForwardSystem.
I actually made a video recently on automated testing where I explain this system in more
detail and demonstrate how you’d write unit tests for it.
Definitely check that one out.
But basically, the MoveForwardSystem operates on entities that have a MoveForward, MoveSpeed,
Position, and Rotation component.
It propels these entities forward in the scene based on the data contained within those components.
MoveFoward is a tag the operates just like the Firing component.
It just tells the MoveForwardSystem that this particular entity needs to be acted on.
The MoveSpeed component simply tells the system how fast the entity should move.
And the Position and Rotation components are used to determine where and in what direction
the entity is moving at any given time.
We’ll grab those from the entity with the Firing component, which, in our case, is the
weapon entity.
Alright!
Now we can switch to the editor and see this baby in action!
First we’ll need to add the Bootstrap script to the scene so we can configure the bullet
renderer.
We’ll also need to create a material that has GPU instancing enabled.
You can find the checkbox for GPU instancing underneath “Advanced Options.”
Now we can setup the bullet renderer.
I’m just going to use the built in sphere mesh for now.
Now we can start the scene and….
Hey what gives?
Let’s take a look at the entity manager.
Uh oh.
Our system seems to be generating a series of bullets instead of just the one that we
were expecting.
Why is that?
Let’s take another look at the Firing System.
The firing system generates a single bullet every frame for each entity that it finds
with a Firing, Position, and Rotation component attached to it.
We tag the weapon entity with a firing component when the player presses the “Fire 1” button,
but we don’t untag the entity until after half a second has passed.
So the firing system will generate a bullet every frame for an entire half of a second.
How do we fix this?
Are we going to need to rewrite our entire player shooting logic?
Luckily for us, the answer is “no.”
We can turn the firing system into a sort of reactive system that’ll operate on the
entities we want based on changes made to their components.
In this case, we’ll have it operate on entities that have a firing, position, and rotation
component, but only during the frame that the firing component was added.
To do this we’ll need to employ a different strategy for gathering entities.
We’ll use something called a component group.
Which we’ll initialize inside of the base class’s OnCreateManager method using GetComponentGroup,
which is also a part of the base class.
GetComponetGroup expects a list of component types.
It’s using the same concept that we use when we create Data structs.
We’re defining which components we want this system to operate on.
Now we need to call the component group’s SetFilterChanged method and pass in the type
for the Firing component.
This is how we tell the firing system to react only to changes that are made to the entities
it operates on.
In this case it’ll react to the addition of the Firing component.
Reacting to other changes, such as removals, is not yet supported.
Now, we need to refactor the firing job so it extends IJobParallel for.
We’ll need to pass in the Position and Rotation components, as well.
Now we an use ComponentGroup’s CalculateLength and ComponentDataArray methods to finish scheduling
the job.
And that’s all there is to it!
Let’s switch back to the editor and see this in action.
Beautiful!
This is exactly the behaviour we’re looking for!
And with that player shooting is complete!
If you’d like to download the project files for this video and support my content at the
same time, please consider becoming a tier-2 patron today.
You can find a download link in the description and comments below.
Well we finally did.
We completed player shooting.
I haven’t quite decided what to work on next, so I thought I’d ask you guys.
Leave a comment letting me know what you’d like to see in part 8.
Or join us on Discord where you can ask your question and then just hang out with us.
If you found this video helpful, please leave a like and a comment letting me know what
you thought.
And for more Unity tutorials just like this one don’t forget to subscribe with notifications
on.
I’ll catch you guys in the next video.
Thank you to all of my patrons.
And a special shoutout to Thomas, Chance McDonald, Glaswyll Entertainment, Richard Stan, and
Yakov.