The first person perspective has generally become the norm for the newest and hottest releases, such as Cyberpunk 2077 and Call of Duty, as well as cult classics and favorites like Minecraft and Skyrim. The first person perspective is a flexible foundation for games, being an effective control method for not just shooters, but also RPGs, puzzle games, horror and strategy. This tutorial delves into setting the foundation for a first person control scheme, putting together motion and camera controls.
For this tutorial, I will be using the Unity engine to develop this, and I will assume understanding of C# programming for the scripts related to controls, although I do not assume prior experience with Unity. It it also assumed that anybody reading this is targeting PC game development, as implied by my reliance on mouse controls.
This tutorial will use Unity 2020 1.6f1, although it can be assumed most versions of Unity will work similarly.
To create a new project to work off, open Unity Hub and press New, and make sure that the 3D option is selected. Name your project however you want to, and press create. Shortly after you create a game, you should be able to see a blank world, and in your Hierarchy, there should be two game objects: the main camera, and a directional light.
The main camera will control the player’s in-game vision, and its position in the game environment can be moved and rotated, also changing what the player is able to see, a characteristic that will be essential for mouse control over the camera.
As for the directional light, it is a game object that determines the lighting of the game environment. Without it, any visible game object in the game will not be colored and it will simply appear black. For the sake of this demonstration, this will not be important to us, although for future reference it should be noted that the directional light’s values can be modified in the Inspector tab to provide a certain desired ambiance for the game.
Now, in the GameObject menu, select the 3D Object option, and within the new drop down list, select Plane to create a new Plane object. Select the new Plane object in the Hierarchy, and through the Inspector that should appear as a result, recenter the plane at (0,0,0) and optionally expand the scale of the Plane if desired. For this project, I set the scale of the plane to be (2,1,2) to give myself more space to work with in case I want to place additional objects.
I do not believe it matters which shape you would like to use to represent the player, but for the sake of this demonstration I will use a capsule. Go to GameObject, select 3D Object, and select Capsule to get an object to represent the player. Recenter the object at (0,0,0) and then change the y value to a value in which the object does not appear like it is halfway through the floor. For reference, I will be using a y value of 2.
To create the first-person perspective we desire, select the move tool to move the main camera into a position that hovers over or overlaps at the top of the cylinder. (The move tool should be the four arrows pointing up, down, left, and right)
Select the Capsule through the Hierarchy, and for the sake of organization, rename the Capsule as Player.
Once selected, under the Tag drop down menu, select the Player tag. While this is not immediately important and will have no bearing on how our controls work, this will be important for later reference as the tag can be how to single out the Player game object if needed to be referred to through scripts.
Afterwards, scroll to the bottom of the menus, where an Add Component button exists. Through Add Component, add a Character Controller to the Player. This will be important as it is the means that we will control the player. You do not need to modify anything in Character Controller.
Now, in the Assets folder in the Project tab, create two scripts, PlayerControls and MouseLook.
Then, go to the Player object and add the PlayerControls script to the Player through Add Component.
Then, drag and drop the Main Camera object into the Player object, and make the camera a child of the Player object. Now, within the Main Camera object. add the MouseLook script.
When you open up the scripts, this how the scripts should look like by default, when first created:
The Start and Update functions are both called automatically by the Unity Engine, with the Start function being called when the game starts, and Update being called every tick. The Class in these scripts inherit from MonoBehavior so the engine will be able to identify the script and execute the Start and Update functions, as well as other functions such as collision detection(this will not yet be used in our project).
This will be how we want to fill out PlayerControls, the script for movement throughout the game:
The fields declared at the top of the script identify what data will be important to us while running the script. The controller field represents the character controller through which we want to send commands for how we want the player to move. The public float fields are public and declared immediately because we want these values to be modifiable through the Inspector, and these fields represent constraints as to how the player’s motion may be affected by factors such as speed and gravity.
The Vector3 fields input and moveDirection represent the value of vectors, and for this project are used to store the values of the player’s inputs and the Vector the player should move to, respectively, at any given moment. The CharacterController field represents the means in which we are going to move the player object, and is initialized in the Start method.
In the Update function, the float statements receive the value inputted by the player through Input.GetAxis, and the magnitude of this vector formed by is clamped by 1 through Vector3.ClampMagnitude to prevent strafing from being faster. Additionally, in the following if-else statement, the game checks whether or not the player is grounded, and if so, the player is able to jump if desired. Elsewise, the player input will take on the current y value. Then, the moveDirection’s y value is affected by gravity before the moveDirection value is forwarded to the controller.
We use Time.deltaTime in the last two lines for our rates calculated assume that these are the values per second in game, so we want our player to move with an amount proportional to the time taken at the current tick.
While we could hardcode WASD controls as an alternative to using Input.GetAxis, since the horizontal and vertical axes are already provided we might as well use them.
Now, this is how we want MouseLook to look like:
The three fields in this script include a Transform component and two float values. The Transform field, initialized in the Start function, is a reference to the Position, Rotation and Scale of the player object, while sensitivity refers to the speed of the mouse motions(the value 45 is arbitrarily defined and can be modified through the Inspector). Pitch is to be modified throughout the game, and is meant to store how far up or how far down the camera is looking. The pitch is by default set to 0 as the player will be staring straight when the game begins.
The Start function initializes the Transform, and also disables the cursor’s visibility and locks the cursor from moving(to return to using the cursor, press Esc while the game is running).
The Update function, every time it’s called, first retrieves the values inputted from the Mouse’s x and y axes, and then multiplied based on the sensitivity and Time.deltaTime for the same reason we use deltaTime for PlayerControls.
Then, we rotate the player’s Transform(playerBody) by the moveX float, as this will rotate the player object including the camera around the up axis by the specified amount.
For the y axis, we will subtract the amount inputted from the current pitch, and clamp the pitch value to be between -85 and 85 because we do not want the player to be able to see from the back of their head, although the values do not necessarily need to be -85 and 85. Finally, apply the pitch to the player’s Transform through Quaternion.Euler, which converts a Euler angle represented by a Vector3 into a Quaternion. The reason we use Quaternions is because localRotation in Transform is a Quaternion, and the reason Unity uses Quaternions is to avoid Gimbal Lock, which can cause issues with how much the player is able to see.
Now, press the Play button, the right-pointing triangle above the Scene window. Upon starting, try inputting WASD controls, pressing Space to jump and moving the mouse. Congratulations! The foundations to a potential masterpiece has been set up.