The simplest way to create a First Person Shooter! (Part 2)

In just a few steps, learn how to make your very own First Person Shooter with multiple Guns, with different shooting ranges and ammunition types in Unity. Practical explanation + code included.

Seemanta Debdas
Eincode
14 min readFeb 22, 2022

--

This is the second part of a three-part series covering the Simplest Way to make a First-Person Shooter. It's highly recommended that you go through the first part before continuing with this one.

Check out the first part!

In this section, we're going to cover the following topics:

1. Adding different weapons and setting them up!
2. Changing weapons based on Player Input: Scrolling and Key Press!
3. Weapon Zooming system: With Scope and Without Scope!

With that out of the way, let's get started!

Resources

Full Course: https://academy.eincode.com/courses/the-complete-unity-guide-3d-beginner-to-rpg-game-dev-in-c

GitHub Repository: Blog-FirstPersonShooter

1. Adding Different Weapons!

In this section, we will add two more Weapon Prefabs under the main "Weapon" Game Object.

  • The Weapon Prefabs will be positioned and rotated similar to the first Weapon.
  • Weapon(Script) will be added to each of the Weapon Prefabs.
  • Add a Muzzle Flash Particle System as a child of each Weapon Prefab and position and rotate them so that they sit right at the muzzle of each Weapon.
  • Adding reference to the serialized fields of the Weapon(Script) sitting on each Weapon Prefab: Bullet Hole(Game Object), Muzzle Flash(Particle System), Bullet Hole Position Offset, Hittable Layer(Layer), Weapon Range and Fire Rate.

Positioning the Weapons

Like the first Weapon Prefab, drag in another Weapon Prefab and make it a child of the "Weapon" Game Object. Position and rotate it to face the Crosshair(center of the screen).

Similarly, go ahead and add as many Weapon Prefabs as you want!

Add Weapon Script

The Weapon Script is responsible for the behavior of our Weapons. To add it to the Weapon Prefabs,
Select the Prefab in the Hierarchy Tab → In Inspector Panel, click on Add Component → Type "Weapon" → Once the script appears in the search result drop-down panel, hit Enter.

NOTE: If some of the serialized fields are not available in your script, don't worry. We'll be covering them shortly after.

Repeat the steps for all the other Weapon Prefabs.

Adding Muzzle Flash

Like what we did for the First Weapon Prefab, drag your Muzzle Flash Prefab under the Weapon(Make it a child) and change its transform to sit right in front of the Muzzle the Weapon.

Make sure that the Play On Awake and Looping checkbox is unchecked in the particle system component. Also, set Stop Action to None.

NOTE: Handling of the Particle system, including code, has been covered in detail in the First Part.

Do the same for the rest of the Weapon Prefabs.

Filing up the empty Serialized Field in Weapon Script

→ Bullet Hole(Game Object): Drag in the Bullet Hole Prefab that we created from a Quad in the first part into this slot.

→Muzzle Flash(Particle System): Reference the Muzzle Flash child game object from each Weapon Prefabs in this slot.

→Bullet Hole Position Offset: Set something like 0.05. This is a slight offset value that gets added to the position of the Bullet Hole Game Object when it's instantiated—more about it in the first part.

→ Hittable Layer(Layer): This is the layer that the Weapon can Hit. From the drop-down list, select the custom layer called Hittable. All Objects that the Weapon can hit are assigned to the layer "Hittable."

→ Weapon Range: The range up to which a weapon can Shoot. Play with the value to find the proper Weapon Range for each Weapon.

→ Fire Rate: It is the rate at which the Weapon can shoot. The lower the Fire Rate, the faster the Weapon will shoot. Play with the value to find the proper Weapon Range for each Weapon.

After setting up these values for each Weapon, let's move on to implementing Weapon Switching!

2. Switching Weapons!

We'll be changing Weapons using a custom script called WeaponSwitcher. This script will sit on the parent "Weapons" Game Object since it has all the Weapon Prefabs as its child.

So, go ahead and make a script by:
Right Click on the Project Panel → Create → C# Script → Name it WeaponSwitcher.

Select the parent "Weapons" Game Object → Add Component → WeaponSwitcher.

The outcome of this step should look something like this.

Logic Behind Weapon Switching

Like array elements, we can access the children of a Game Object using a function called transform.GetChild(index of the child). Where the index of the first child is 0, the second child is 1, and so on.

We can also get the total number of children using the function transform.childCount.

We’ll be declaring two variables(initially zero): previousWeaponIdx and currentWeaponIdx. At the beginning of each frame, we'll set

previousWeaponIdx = currentWeaponIdx. At the end of each frame, if previousWeaponIdx currentWeaponIdx, we'll change the Weapon.

Flow Diagram of how we're handling Weapon Switching.

The Code:

HandleKeyPress()

In this function we’re checking whether or not the Player presses(using the function Input.GetButtonDown()) Alpha1, Alpha2 or Alpha3.

These are keycodes for 1,2, and 3 on the keyboard, respectively. According to that, we set the currentWeaponIdx.

Alpha1 → Activate first Weapon Prefab(child index = 0) → set currentWeaponIdx = 0
Alpha2 →
Activate first Weapon Prefab(child index = 1) → set currentWeaponIdx = 1
Alpha3 →
Activate first Weapon Prefab(child index = 2) → set currentWeaponIdx = 2

HandleScrollWheel()

In this function, we're checking if:

Player Scrolls Up:

Set the currentWeaponIdx as the index of the last Weapon Prefab child if the currentWeaponIdx is 0. Else, please set it to currentWeaponIdx -1, i.e., the index of the previous Weapon Prefab child.

Player Scrolls Down:

Set the currentWeaponIdx as the index of the first Weapon Prefab child if the currentWeaponIdx is equal to the index of the last Weapon Prefab child. Else, set it to currentWeaponIdx + 1, i.e., the index of the next Weapon Prefab child.

SwitchWeapon()

In this function, we traverse through all the Weapon Prefab Children in the "Weapons" Game Object and SetActive that Weapon Prefab Child whose Child Index matches that of the currentWeaponIdx. Others will remain disabled.

3. Zooming!

Zooming mechanism:

In this section, we're going to implement two types of zooming mechanisms:

  • Zooming without scope
  • Zooming with scope

To get started, first create a C# script:

Right Click on the Project Panel → Create → C# Script → Name it WeaponZoom. Select each Weapon Prefab → In the Inspector Panel, click on Add Component → type WeaponSwitcher, and hit Enter.

Let's specify whether individual Weapon Prefabs have the scope or not. To do this, open the WeaponZoom script and add a serialized Boolean field called "hasScope."

In the Inspector panel under Weapon Zoom script, check the hasScope serialized field if the Weapon Prefab has scope. Else, uncheck it.

Since this Weapon Prefab had a scope, I checked the hasScoped field.

Zooming without scope:

To make it seem like we're zooming in without scope, we're going to manipulate two values:

  1. Camera Field of View
  2. Vignette Intensity Value

Weapon scoping will be handled by a script called "Weapon Script." We'll handle zooming by changing the camera FOV based on the user's input(Right Mouse Button Hold)

Make two serialized fields: one for zoom in and one for zoom out. Lerp from zoomed out state to zoomed-in state and vice versa.

To add more to the zooming-in effect, we can intensify the vignette value when the Weapon is in a zoomed-in state.

Camera Field of View

By manipulating the Camera Field of View under the Camera Game Object's Camera component, we can achieve a zooming effect. The lesser the Field of View, the more zoomed in the view.

Normal State(Field of View = 60)
Zoomed-in State(Field of View = 45)

To implement this, first, serialize the two Field of View values:

  1. Normal FOV
  2. Zoomed FOV

Also, to smoothly transition between two values, we’ll be using Mathf.Lerp(). This function allows us to transition between one float value to another over a course of time.

In the Update function, check whether the Player is holding Right Mouse Button. If so, call the NormalZoom() function or return to the original FOV.

Vignette Intensity Value

Vignette is a part of Unity's Post Processing stack that we will use to intensify the zooming effect.

Unity provides many post-processing effects and full-screen effects that can significantly improve the appearance of your application with little setup time. You can use these effects to simulate a physical camera. The output is either drawn to the screen or captured as a texture.

The Vignette effect darkens the edges of an image, leaving the center of the image brighter.

To implement Post Processing, first, we need to enable Post Processing in the Camera Game Object's Camera component.

Now, to create a Post Processing Volume Profile:

Right Click in the Hierarchy Panel → Create Empty → In the Inspector Panel, Click Add Component → Search "Volume" and hit enter.

Click on the "New" Button beside Profile to add a new Post Processing Profile → Add Override → Post Processing → Vignette → Select All and then manipulate the values for the desired result.

To change vignette value while scoping, we'll perform something similar to what we did with Camera FOV.
First, add the namespace for manipulating Post Processing through our script:

Then, add the fields required:

The Inspector panel references the Post Processing Game Object in the postProcessVolume serialized field. Also, set the normalVignetteIntensity and zoomedVignetteIntensity as desired. My ones are shown below.

Ignore the extra fields in the script. They will be covered shortly after.
In the Awake() method, get access to the Vignette component of the Post Process Volume:

Finally, modify the script as follows to implement Vignette:

In this script:

While Zooming: We lerp from the current vignette intensity value to zoomedVignetteIntensity over zoomTime.
When Not Zooming: We lerp from the current vignette intensity value to normalVignetteIntensity over zoomTime.

Zooming with Scope!

In the WeaponZoom script of Weapon Prefabs containing a Scope, check the hasScope serialized field.

The steps to implement zooming with the scope are:

Create a Weapon Camera that only renders objects in the Weapon layer. This camera will be toggled off when the Weapon is scoped in to hide the Weapon from view. This also solves the issue of the Weapon clipping through other objects. More about that later.

→ Create an animation that brings the Weapon to the center of the screen when the right mouse button is pressed and back to its previous position when the right mouse button is released.

Make a Scope Image(UI) that gets toggled on when Weapon is zoomed in.

Create a Weapon Camera

A separate Weapon Camera is required for the following reasons:

  • Weapons can clip through other objects present in the scene.
  • When zoomed in, rather than toggling off the Weapon Prefab(which has the scripts that get disabled along with it), we can disable the Weapon Camera and re-enable it once we're zoomed out.

To set up a Weapon Camera,

  • Create a Camera under the Main Camera by:
    Right, Click on the Main Camera → Camera. Name it "Weapon Camera."
  • Under the Weapon Camera's Camera component, set the Render Type to Overlay.
  • Please create a new layer and call it Weapon.
  • Assign the "Weapons" Game Object to the Weapon Layer. In the Pop-up, select Yes, change children.
  • Under Rendering, in the Weapon Camera, set the Culling Mask to Weapon only.
  • Under Rendering, in the Culling Mask drop-down in the Main Camera, unselect Weapon.
  • Under Stack, click on the "+" button in the Main Camera and add the Weapon Camera.

NOTE: Make sure to remove Audio Listener from the newly created camera.

Creating Animations!

In this section, we'll make two animations, one for when we're scoped in and another for when we're not(Idle).

To make this, we're going to use the Animation Tab.

To open the Animation Tab, go to Windows → Animation → Animation at the top.

First, let's create an Idle Animation for the Weapons.

Click on the "Weapon" Game Object. Then in the Animation tab, click Create. Choose a folder to store the Animations and name them according to the animation (e.g., Weapon_Idle, Weapon_Scope)

Notice that an Animator gets added as a component to the "Weapon" Game Object.

The Animator component is used to assign animation to a Game Object in your scene.

Also, an Animator controller gets added in the location where you saved the animation.

The Animator component requires a reference to an Animator Controller which defines which animation clips to use, and controls when and how to blend and transition between them.

We'll be needing them later.

To make the Idle Animation:

  • While having the "Weapons" Game Object selected, press the record button beside the Preview button to enter recording mode in the Animation tab.
  • Jump to the 1-second mark and slightly change the y-axis(about -0.02). Then jump to the 2-second mark → Copy the first key point(Ctrl + c) and paste(Ctrl + v) it at the 2-second mark. Press play to preview the idle animation.

Now that we have our Idle Animation let's make one for the scoped-in state.

In the Animation tab, click on the list of Animations under the record button and select Create New Clip… Save the newly created animation in the same folder. I'm naming mine: "WeaponScoped."

Hit the Record button and adjust the scoped Weapon Prefab is centered on the screen, and the scope is as close to the screen as possible.

Now that we have our Animations let's configure the Animator Controller.

Animator Controller will help us transition from one animation to another based on specific parameters controlled from the script.

To edit the Animator Controller, we use the Animator tab.
To open the Animator tab, at the top, go to Windows → Animation → Animator.

Notice that in the Animator tab, one of the animations is Orange, which indicates that it's the default state. To change the default animation, Right Click on any Animation → Select "Set as Layer Default State."

We can also create parameters that can be used to transition from one animation state to another.

Setting Up Transitions

To make a transition,
Right-click on "WeaponIdle" and select "Make Transition" → Click on "WeaponScoped."

Similarly, make a transition from "WeaponScoped" to "WeaponIdle."

Click on each transition arrow and uncheck the Has Exit Time checkbox. This is because we will handle transitioning using parameters.

Also, set the Transition Duration of both transitions to something like 0.15. This tells us how long it will take to transition from one animation state to other. The lower the Transition Duration, the faster the transition will happen.

Adding a Parameter

  • On the left side of the Panel, Click the "+" sign and add a Boolean parameter. Call it "IsScoped."
  • Click on the transition arrow from "WeaponIdle" to "WeaponScoped." In the Inspector, under "Conditions," click on the "+" button, → add the "IsScoped" parameter, and set its value to true.
  • Click on the transition arrow going from "WeaponScoped "to "WeaponIdle." In the Inspector, under "Conditions," click on the "+" button, → add the "IsScoped" parameter, and set its value to false.

Adding Scope Image

The scope image will be overlaid on the whole screen when the Weapon is zoomed in, and the Weapon Camera is disabled. You can use any image you want. I am going to go with something like this:

To set this up,

  • Right Click on the "UI" Canvas(one that has the "Crosshair" Image) → UI →Image → Name it "Scope."
  • Select the newly created image and under the Anchor Presets section, select the bottom right option while holding the Alt key. This will scale the whole image to fill the screen.
  • Then, click on the selector button under the Image component and select the scope image.

Setup everything up in the code.

First, let's Serialize some references.

In the Awake method, set a reference to the Animator that sits on the parent. So we’ll use the method GetComponentInParent<Animator>()

Update the Awake method as follows:

In the Update method, set a condition whether the Weapon Prefab has scope or not.

If it has scope,

  • Call Coroutine, which waits for 0.15 seconds(the time for the Weapon to transition from Idle state to Scoped State), and then it zooms in and then sets the scope image as true and Weapon camera as false.
  • Also, set the IsScoped parameter to true to trigger the scoping animations.

If it doesn't have a scope, then,

  • Call NormalZoom()

Else,

  • Set the IsScoped parameter to false.
  • Enable Weapon Camera.
  • Disable Scope Image.
  • Set Field Of View back to normal.
  • Set Vignette Intensity Value back to normal.

Coroutines are ways to write code that says “wait at this line for a little.” To be more precise, WaitForSeconds and WaitForEndOfFrame (yield return null) are the commands that make code wait at one spot. They can only be used inside Coroutines.

Modify the Update method like this:

Set up Scoped Zoom like this:

The whole script:

Inspector view of each Weapon Prefab

Conclusion

In the following parts, we're going to take it to cover the following topics:

→ Basic Ammo Functionality!
→ Different Ammo types!
→ Simple reloading functionality!

If this simple First Person Shooter piqued your curiosity, then you should consider opting for The Complete Unity Guide 3D- Beginner to RPG Game Dev in C# offered by Eincode. This course features among the most immersive and practical resources out there.

This course is curated by experienced software engineer and freelance developer Filip Jerga. This course starts with the fundamentals. Then, it progresses gradually to eventually take its subscribers through the journey of developing their own RPG game by using Unity 2020 and C#!

Cheers!

Debdas

--

--

Seemanta Debdas
Eincode
Writer for

Game Dev enthusiast contributing to the Gaming Industry!