How to make a first person character controller - Unity

A first-person character controller script is the starting point of any fps shooter game. Even new Unity game developers tend to write their own fps controller script before learning anything else as it is challenging and very exciting. In this article, we gonna create two simple fps controllers from scratch and will guide you step by step on how it works.


fps-controller-unity


First, let's understand the basic concept of an fps controller. You should have a rotating camera that rotates in the x-axis to look up and down and the character should rotate on the y-axis to look around. There are two ways to create an fps controller in the Unity Engine. 
  • Using Character controller component
  • Using the RigidBody component
Both have different advantages and disadvantages when using them. It depends on your game to select the best fps controller according to your needs. 


Character controller based fps controller

Pros:
  • Is grounded check is built-in
  • Has its own methods to move the character
  • Slope angles and step offsets are built-in
Cons :
  • Gravity is not built-in


RigidBody component based fps controller

Pros :
  • Interacts with objects using the physics engine
  • Gravity is built-in

Cons :
  • Slope angles and step offsets should be configured
  • Has to implement a way to check if grounded





Fps Controller using Character controller


Create a capsule and attach a 'CharacterController' component to it. Make sure to remove the capsule collider.
Then set the main camera as a child of the capsule we created, reset the position and move the camera a little bit up to the eye view. Create two scripts for the camera controller and one for the movement controller. Attach the 'CharacterMovementController' to the capsule and the 'CharacterCameraController' to the camera.

CharacterMovementController.cs

using UnityEngine;

public class CharacterMovementController : MonoBehaviour
{
    public CharacterController characterController;

    public float moveSpeed;

    public float sprintSpeedMultiplier = 2f;

    public float jumpHeight = 3f;

    private float _gravity = -10f;

    private float _yAxisVelocity;

    private void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        if (Input.GetKey(KeyCode.LeftShift))
            vertical *= sprintSpeedMultiplier;
        
        Vector3 movement = horizontal * moveSpeed * Time.deltaTime * transform.right +
                           vertical * moveSpeed * Time.deltaTime * transform.forward;

        if (characterController.isGrounded)
            _yAxisVelocity = -0.5f;


        if (Input.GetKeyDown(KeyCode.Space))
            _yAxisVelocity = Mathf.Sqrt(jumpHeight * -2f * _gravity);

        _yAxisVelocity += _gravity * Time.deltaTime;
        movement.y = _yAxisVelocity * Time.deltaTime;
        
        characterController.Move(movement);
    }
}



fps-controller-unity



CharacterCameraController.cs


using UnityEngine;

public class CharacterCameraController : MonoBehaviour
{
    public Transform characterBase;
    public float lookSpeed = 10f;

    private float _camRotation;

    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    private void Update()
    {
        float y = Input.GetAxis("Mouse Y");
        float x = Input.GetAxis("Mouse X");
        
        _camRotation -= y * lookSpeed * Time.deltaTime * 10f;
        _camRotation = Mathf.Clamp(_camRotation, -90f, 90f);
        
        transform.localRotation = Quaternion.Euler(_camRotation , 0f ,0f);
        
        characterBase.Rotate(x * lookSpeed * Time.deltaTime * 10f * Vector3.up);
        
        if(Input.GetMouseButton(0))
            Cursor.lockState = CursorLockMode.Locked;
        
        if(Input.GetKeyDown(KeyCode.Escape))
            Cursor.lockState = CursorLockMode.Confined;
    }
}


fps-controller-unity



Make sure to attach all the required components on the inspector window. 'CharacterCameraController' gets the mouse inputs and rotates the camera on the x-axis and rotates the player base capsule on the y-axis. 'lookSpeed' variable acts as the mouse sensitivity and increasing the value results in increased rotation speed. The camera x-axis is clamped between-90 and 90 to make sure that the player is not going to flip the camera upside down. 

The cursor is locked at the start. Pressing the escape key unlocks it and pressing the left mouse button will lock it again.




We have simulated gravity using a variable called '_yAxisVelocity' and we increase it each frame if the player is not grounded. The left shift key multiplies the moving speed by the variable called 'sprintSpeedMultiplier'.

In this fps controller system, you can't detect collision using the normal collision detection methods. Instead, you have to use the 'OnControllerColliderHit' method. The ground check is done using the 'isGrounded' boolean in the 'CharacterController' component.


FPS Controller using RigidBody Component

This method is similar to the 'CharacterController' based fps controller but using the 'RigidBody' component. The game objects are similar to the above one. One capsule and the camera as a child an add a capsule collider and a 'RigidBody' component to the capsule. Create two scripts one for the capsule and one for the camera. 


RigidBodyMovementController.cs


using UnityEngine;

public class RigidBodyMovementController : MonoBehaviour
{
    public Rigidbody rb;

    public float moveSpeed = 35f;

    public float sprintSpeedMultiplier = 1.6f;

    public float jumpForce = 35f;

    public Transform groundCheckTransform;

    public LayerMask groundCheckLayerMask;
    
    private Vector3 _inputVector;

    private bool _isGrounded = true;

    private void Update()
    {
        _inputVector = new Vector3(Input.GetAxis("Horizontal") , 0f , Input.GetAxis("Vertical"));

        if (Input.GetKey(KeyCode.LeftShift))
            _inputVector.z *= sprintSpeedMultiplier;
        
        if (Input.GetKeyDown(KeyCode.Space) && _isGrounded)
        {
            rb.AddForce(jumpForce * 10 * transform.up, ForceMode.Acceleration);
            _isGrounded = false;
        }
    }

    private void FixedUpdate()
    {
        _isGrounded = Physics.CheckSphere(groundCheckTransform.position, 0.3f, groundCheckLayerMask);
        
        Vector3 movement = moveSpeed * 10f * _inputVector.z * Time.fixedDeltaTime * transform.forward +
                        moveSpeed * 10f * _inputVector.x * Time.fixedDeltaTime * transform.right;
        rb.MovePosition(rb.position + movement);
    }
}



fps-controller-unity



RigidBodyCameraController.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RigidBodyCameraController : MonoBehaviour
{
    public Rigidbody characterRb;
    public float lookSpeed = 10f;

    private float _camRotation;

    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    private void Update()
    {
        float y = Input.GetAxis("Mouse Y");
        float x = Input.GetAxis("Mouse X");
        
        _camRotation -= y * lookSpeed * Time.deltaTime * 10f;
        _camRotation = Mathf.Clamp(_camRotation, -90f, 90f);
        
        transform.localRotation = Quaternion.Euler(_camRotation , 0f ,0f);
        
        characterRb.rotation =  Quaternion.Euler(characterRb.rotation.eulerAngles + x * lookSpeed * Time.deltaTime * 10f * Vector3.up);
        
        if(Input.GetMouseButton(0))
            Cursor.lockState = CursorLockMode.Locked;
        
        if(Input.GetKeyDown(KeyCode.Escape))
            Cursor.lockState = CursorLockMode.Confined;
    }
}

fps-controller-unity


fps-controller-unity






The 'RigidBody' component based fps controller works in a similar way as the 'CharacterController' based fps controllers. Instead, we use the 'rigidbody.MovePosition()' method and pass the movement vector to it. The camera rotation is applied in the same way as the above fps controller.

make sure to attach the components on the inspector and turn on the 'RigidBody' constraints for rotation in every axis. Turn on interpolate and play around with the drag and jump force to get the perfect jump motion needed.

Ground check is implemented using the 'CheckSphere' method in the 'FixedUpdate' method and the input is recorded in the update method and applied in the 'FixedUpdate' method as applying physics in the 'Update' method is quite expensive. The 'groundCheckTransform'
is an empty game object positioned at the very bottom of the capsule.


Selecting what suits your game

It's totally up to you to decide which fps controller suits your game. Sometimes you might need the 'CharacterController' based fps controller in multiplayer games as its easy to sync and sometimes the 'RigidBody' based fps controller when you need your character to interact with every physics enabled object in the scene.


Related Articles




Comments