Adding FPS Functionality to the Player Controller in Unity 2021
In this article, we’ll be extending the functionality of the GameDevHQ provided character controller to implement first-person shooter mechanics.
Before we dive in, let’s briefly cover the provided assets.
This project uses the above GameDEV HQ assets.
Essentially, the player’s input and movement with a first-person point-of-view set of arms and weapon has been implemented.
You could use Unity’s free First-Person Character Controller to accomplish the same functionality, though you’ll need to find arms and a weapon to recreate the current player point-of-view.
As shown above, this gives us a good foundation to implement our own player script mechanics for shooting.
As you’d expect, we start by creating a new script and assigning it to the PlayerController game object.
Weapon Firing Cooldown Mechanics
I’ve created the basic logic and layout for my Player script to fire the player’s weapon.
Since the weapon will be on a cooldown, it made sense to implement that logic before the actual firing logic.
Note that this script makes use of the new Input System.
Import the Input System package unless you plan to use the legacy input system in your code.
Using Debug.Log() printouts to the console, I can verify that the cooldown system is working as desired before starting on the firing/bullet mechanic itself.
Changing the Player’s Reticule
Currently, the provided reticule is very generic.
I’ve imported the Target Reticule package from GameDevHQ, but you can use any source image with transparency you like.
I’m partial to “Target_31” in the asset pack, so I’ve assigned it to the Canvas Image used for the reticule.
I’ve also renamed “Image” to “imgReticule”.
On the Canvas game object settings, I’ve made the Render Mode “Screen Space — Overlay” and the UI Scale Mode as “Scale With Screen Size” with a standard 1080p Reference Resolution.
Updating RobotAI Script for Health & Damage
Up until now, our AI has been invincible.
Now we’ll implement mechanics for taking damage, reducing health, and dying.
First, we’ll need to add health and default health variables to the RobotAI class / script.
In the Start() method, we’ll check that _aiHealth has been assigned a value higher than 0 in the Unity Editor.
If not, we’ll set it to the default value of 100.
Then we’ll assign the value of _aiHealth to _defaultHealth.
This way if the Game Designer sets an alternative value in the Unity Editor we’ll make sure to capture it is the default value while we use _aiHealth as the current health value.
Next, we’ll add public method TakeDamage() with a damageAmount parameter that will reduce our health variable when hit.
It will also call the RunToCover() method so our AI is a little more realistic.
It must be public, so that classes / scripts outside of RobotAI can access it.
Once TakeDamage() is called and results in a value <= 0, we call the OnDeath() method.
The OnDeath() method animates the robot’s death and then calls the waitForDeathAnimation() coroutine so as to wait for the animation to end before resetting the values and disabling the Robot game object.
We also add the DetectNearMiss() method which will be called from the Player script in the future.
Looking at the Death Animation in the Inspector and dragging the time position all the way to the right, I can see that the length is “3:20”.
This translates to 3.2 seconds, so I’ve set the WaitForSeconds() value in waitForDeathAnimation() coroutine to be 3.3 seconds.
We also need to reset our _aiHealth variable with the _defaultHealth variable so that if the is game object is enabled again via object pooling, the health will be correct.
The Start() method will not run again to reset the health to 100, and even if it did, this would override any value in the Unity Editor that a Game Designer had chosen.
So, it is very important we are careful to reset the current health variable with a defaults health variable.
We could take a more physics based approached to the shooting mechanics, but for simplicity’s sake and just because I enjoy the feel of instant hit detection, we’ll use a raycast based mechanic.
Player Class / Script
We’ll use the MainCamera game object in our scene to set the middle of the screen as the raycast origin since our target reticule will always be there.
So naturally, we’ll need a reference to the main camera in our Player class / script.
The provided Player Control mechanics include its own Main Camera game object on the Player game object.
Because of this, the original Main Camera is disabled or removed.
Thus, we assign the active Main Camera to our Player script component as shown above.
We disable any Debug.Log code that was simulating the weapon being fired for testing purposes.
We then use Ray and RaycastHit in conjunction with _mainCamera.ViewPointToRay(center of screen) to grab the information of the game object hit.
By using hitInfo.collider instead of hitInfo.transform, I can differentiate between the child game objects I’ve added to contain the hit colliders.
Originally the Robot_1 game object had a collider directly on it.
By making child game objects of Robot_1 for the colliders, they are detected correctly with raycasting.
Now, as shown above, each Robot will have colliders in front and behind to detect missed shots.
Note that the collider game objects are tagged for use in the FireWeapon() method.
I’ve also captured the layer of the game object hit for the FireWeapon() method to easily detect if the game object was cover rather than scenery or the AI.
I could use this cover detection as a basis for adding exploding cover game objects in the future.
This also demonstrates that we can either detect the object type by tag or layer as needed.
With that information, I can call the public TakeDamage() method of the RobotAI script and pass in a damage amount value.
I can also call DetectNearMiss() to trigger the AI to take cover despite not being hit.
Now I can test that the functionality is working in play mode.
It’s a bit hard to see without enlarging the gif above, but the Player’s raycast detection, the AI’s TakeDamage() and OnDeath() methods are all working properly.
I can even examine the RobotAI script component on those I’ve hit without killing and see that their health is indeed reduced from 100.
There are several improvements from here to make the “feel” of this prototype much better.
We could add recoil to the weapon and a small amount of camera shake when the weapon is fired.
The robot’s dying animation plays out while the NavMesh system continues to move it forward.
Ideally there’d be some forward momentum on death, but not the long sliding we are seeing here.
We could also add alternative firing modes, weapons, arms, etc.
This is why Project Scope is so important.
We could keep adding and modifying this until we were competing with Call of Duty 12 (that will probably be out by the time somebody solo devs a competing game).
I may revisit this in the future and add the necessary polish, but for now I’m going to move on to the User Interface (UI) and adding Audio effects.