I found a very detailed Unity FPS Controller Unity Tutorial on YouTube and decided to rework the series.

First person camera movement

Following this tutorial, I modified some of the code.

public class PlayerController : MonoBehaviour
{
    // Whether it can be moved
    public bool CanMove { get; private set; } = true;
    
    // Parameters related to character movement
    [Header("Movement Parameters")]
    [SerializeField] private float walkSpeed = 3.0 f;
    [SerializeField] private float gravity = 9.81 f;
    [SerializeField] private bool isGrounded;
    
    // Camera Angle parameters
    [Header("Look Parameters")]
    // Mouse X axis speed, Y axis speed
    [SerializeField, Range(1, 100)] private float lookSpeedX = 2.0 f;
    [SerializeField, Range(1, 100)] private float lookSpeedY = 2.0 f;
    [SerializeField, Range(0, 90)] private float upperLookLimit = 89.0 f;
    [SerializeField, Range(0, 90)] private float lowerLookLimit = 89.0 f;
    
    // Camera and character controller variables
    private Camera playerCamera;
    private CharacterController characterController;

    // Store player movement speed
    private Vector3 moveDirection;
    // Store "Horizontal" and "Vertical" inputs
    private Vector2 moveInput;
    
    // Mouse down, the camera's X axis rotation Angle
    private float rotationX;
    // Save mouse input
    private float mouseY;
    private float mouseX;
    
    private void Awake()
    {
        // Camera is a child of player
        playerCamera = GetComponentInChildren<Camera>();
        characterController = GetComponent<CharacterController>();
        // Lock the computer cursor
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false; }}Copy the code

The tutorial is divided into moving input, mouse-viewing, and finally applying moving

void Update()
    {
        GroundCheck();
        if(CanMove) { HandleMovementInput(); HandleMouseLook(); ApplyFinalMovement(); }}Copy the code

Processing moving input

    private void HandleMovementInput()
    {
        // Get WASD input * speed
        moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * walkSpeed;
        // On the Y-axis, the vertical velocity is saved
        float moveDirectionY = moveDirection.y;
        // Get input in three directions
        moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
        moveDirection.y = moveDirectionY;
    }
Copy the code

Handling mouse input

private void HandleMouseLook()
    {
        mouseX = Input.GetAxis("Mouse Y") * lookSpeedY * Time.deltaTime;
        mouseY = Input.GetAxis("Mouse X") * lookSpeedX * Time.deltaTime;
        rotationX -= mouseX;
        // Hold your left hand in a thumbs up gesture with your thumb pointing in the positive direction and your fingers curling in the positive direction
        // So the minimum value is up, negative
        rotationX = Mathf.Clamp(rotationX, -upperLookLimit, lowerLookLimit);
        playerCamera.transform.localRotation = Quaternion.Euler(rotationX, 0.0);
        // We can only use multiplication because of coordinate changes.
        transform.rotation *= Quaternion.Euler(0, mouseY, 0);
        // This will do
        // transform.Rotate(transform.up * mouseY);
    }
Copy the code

Handling movement, gravity implementation as described in the previous article.

private void ApplyFinalMovement()
    {
        if(! isGrounded) { moveDirection.y += gravity * Time.deltaTime; } characterController.Move(moveDirection * Time.deltaTime); }private void GroundCheck()
    {
        isGrounded = Physics.CheckSphere(checkGround.position, groundCheckRadius, groundLayer);
        if (isGrounded && moveDirection.y < 0.0 f)
        {
            moveDirection.y = 2.0 f; }}Copy the code

The sprint

add

public class PlayerController : MonoBehaviour
{
    public bool CanMove { get; private set; } = true;
    // You can sprint, and press the sprint button to enter the sprint state
    private bool IsSprinting => canSprint && Input.GetKey(sprintKey);

    [Header("Functional Options")]
    [SerializeField] private bool canSprint = true;

    [Header("Controls")]
    // You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
    [SerializeField] private KeyCode sprintKey = KeyCode.LeftShift;

    [Header("Movement Parameters")]
    [SerializeField] private float walkSpeed = 3.0 f;
    [SerializeField] private float sprintSpeed = 6.0 f;
}
Copy the code

float speed = IsSprinting ? sprintSpeed : walkSpeed;

private void HandleMovementInput()
    {
        float speed = IsSprinting ? sprintSpeed : walkSpeed;
        
        // Get WASD input * speed
        moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * speed;
    }
Copy the code

You can also add an Input, and input.getButton (“Run”);

jumping

private bool ShouldJump => canJump && Input.GetKeyDown(jumpKey); The jump function is enabled and the jump key is pressed to determine that the jump should be performed

public class PlayerController : MonoBehaviour
{
    private bool ShouldJump => canJump && Input.GetKeyDown(jumpKey);
    // Function switch
    [Header("Functional Options")]
    [SerializeField] private bool canJump = true;
    
    [Header("Controls")]
    [SerializeField] private KeyCode jumpKey = KeyCode.Space;
    
    [Header("Jumping Parameters")]
    // Maximum number of jumps
    [SerializeField] private int maxJumpCount = 3;
    // Current number of jumps
    [SerializeField] private int jumpCount = 0;
    // Last jump time
    [SerializeField] private float lastJumpTime;
    // Jump count resets the cooldown
    [SerializeField] private float jumpCoolDownTime = 0.1 f;
    // Jump height
    [SerializeField] private float jumpHeight = 5.0 f;
    [SerializeField] private float gravity = 9.81 f;
    }
Copy the code
    void Update()
    {
        GroundCheck();
        if (CanMove)
        {
            HandleMovementInput();
            HandleMouseLook();
            // If jump is enabled
            if (canJump)
            {
                // Perform the jumpHandleJump(); } ApplyFinalMovement(); }}Copy the code

Because there is a small amount of time off the ground, it will still be judged on the ground, so add a small amount of time to prevent the first jump off the ground, not counting the use of the jump

    private void HandleJump()
    {
        
        if (ShouldJump && jumpCount < maxJumpCount)
        {
            moveDirection.y = Mathf.Sqrt(-gravity * 2f * jumpHeight);
            jumpCount++;
            lastJumpTime = Time.time;
        }

        if (Time.time - lastJumpTime > jumpCoolDownTime && isGrounded) jumpCount = 0;
    }
Copy the code

She lay down

Switch squats

Most tutorials on the Internet either don’t teach you how to do the crouch directly, or simply change the ‘Height’ of ‘CharacterControllerd’ by pressing the button and adding a smooth overtone, but it’s hard to find a tutorial where you can hold down the crouch and release the stand automatically. Here, press “C” to switch between standing and squatting.

public class PlayerController : MonoBehaviour
{
    // When the function is enabled and the "C" key is pressed, it is not already squatting/standing up, and it is on the ground (this can be determined according to requirements)
    private boolShouldCrouch => canSwitchCrouch && Input.GetKeyDown(switchCrouchKey) && ! duringCrouchAnimation && isGrounded; [Header("Functional Options")]
    // Whether to enable the switch squat function
    [SerializeField] private bool canSwitchCrouch = false;

    [Header("Controls")]
    // You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
    [SerializeField] private KeyCode switchCrouchKey = KeyCode.C;
    
    [Header("Movement Parameters")]
    // The speed of movement while crouching
    [SerializeField] private float crouchSpeed = 1.0 f;
    
    [Header("Crouch Parameters")]
    // Squat height and standing height
    [SerializeField] private float crouchHeight = 1.35 f;
    [SerializeField] private float standingHeight = 2f;

    [SerializeField] private float timeToCrouch = 0.25 f;
    [SerializeField] private bool isCrouching;
    [SerializeField] private bool duringCrouchAnimation;
Copy the code
    void Update()
    {
        GroundCheck();
        if (CanMove)
        {
            HandleMovementInput();
            HandleMouseLook();
            if (canJump)
                HandleJump();
                // If the switch squatting process is enabled, perform squatting
            if(canSwitchCrouch) HandleCrouch(); ApplyFinalMovement(); }}Copy the code

Press squat, should squat or stand up, perform a coroutine

    private void HandleCrouch()
    {
        if (ShouldCrouch)
            StartCoroutine(CrouchStand());
    }
Copy the code

The coroutine does these things:

(1) Judge if it is in the state of squatting, then it is necessary to perform standing below, judge whether there are obstacles on the top of the head, and do not perform standing if there are obstacles

If “isCrouching” is “True”, it indicates that the crouching state is in the crouching state, and the “targetHeight” is the “standingHeight”

(3) Get the current height, and “timeElapsed” is a timer that Lerp will use to smooth the standing squats

< span style = “font-size: 10.5pt; color: RGB (0, 0, 0)

⑤ After completing the height change, the height will be set as the target height because there will be some errors

⑥ The state of the squat changes, the end of the squat animation

private IEnumerator CrouchStand()
    {
        if (isCrouching && Physics.Raycast(playerCamera.transform.position, transform.up, 1f)) yield break;

        duringCrouchAnimation = true;

        float timeElapsed = 0;
        float targetHeight = isCrouching ? standingHeight : crouchHeight;
        float currentHeight = characterController.height;

        while (timeElapsed < timeToCrouch)
        {
            float chrouchPercentage = timeElapsed / timeToCrouch;
            characterController.height = Mathf.Lerp(currentHeight, targetHeight, chrouchPercentage);
            timeElapsed += Time.deltaTime;
            yield return null; } characterController.height = targetHeight; isCrouching = ! isCrouching; duringCrouchAnimation =false;
    }
Copy the code

Up to this point, the height can be smooth and uniform for a set timeToCrouch time. However, the camera height will not change, so it is necessary to change the position of “Center” to move the collider up.

    [Header("Crouch Parameters")]
    [SerializeField] private float crouchHeight = 1.35 f;
    [SerializeField] private float standingHeight = 2f;
    // While squatting, the center position of the collider changes
    [SerializeField] private float timeToCrouch = 0.25 f;
    [SerializeField] private Vector3 crouchingCenter = Vector3.zero;
    [SerializeField] private Vector3 standingCenter = Vector3.zero;
    [SerializeField] private bool isCrouching;
    [SerializeField] private bool duringCrouchAnimation;
    // Ground detects object and camera position changes
    // changedLength = (standingHeight - crouchHeight) / 2
    [SerializeField] private float changedDistanceY;
Copy the code

The crouchingCenter height can be set, which I calculate here in Start().

void Start()
    {
        changedDistanceY = (standingHeight - crouchHeight) / 2f;
        crouchingCenter.Set(standingCenter.x, standingCenter.y + changedDistanceY, standingCenter.z);
changedDistanceY + crouchingCenter.y, 0);
    }
Copy the code

In coroutines, it’s the same operation as height.

private IEnumerator CrouchStand()
    {
        if (isCrouching && Physics.Raycast(playerCamera.transform.position, transform.up, 1f)) yield break;

        duringCrouchAnimation = true;

        float timeElapsed = 0;
        float targetHeight = isCrouching ? standingHeight : crouchHeight;
        float currentHeight = characterController.height;
        
        Vector3 targetCenter = isCrouching ? standingCenter : crouchingCenter;
        Vector3 currentCenter = characterController.center;

        while (timeElapsed < timeToCrouch)
        {
            float chrouchPercentage = timeElapsed / timeToCrouch;
            characterController.height = Mathf.Lerp(currentHeight, targetHeight, chrouchPercentage);
            characterController.center = Vector3.Lerp(currentCenter, targetCenter, chrouchPercentage);

            timeElapsed += Time.deltaTime;
            yield return null; } characterController.height = targetHeight; characterController.center = targetCenter; isCrouching = ! isCrouching; duringCrouchAnimation =false;
    }
Copy the code

When I tested it here, if I set it to center down, it might go through the wall when I stand up.

The position of groundCheck moves with it, the same operation.

    [SerializeField] private Vector3 groundCheckStandingPosition;
    [SerializeField] private Vector3 groundCheckCrouchPosition;
       void Start()
    {
        // Squat related data
        changedDistanceY = (standingHeight - crouchHeight) / 2f;
        crouchingCenter.Set(standingCenter.x, standingCenter.y + changedDistanceY, standingCenter.z);
        groundCheckStandingPosition = new Vector3(0, groundCheck.localPosition.y, 0);
        groundCheckCrouchPosition = new Vector3(0, groundCheck.localPosition.y + changedDistanceY + crouchingCenter.y, 0);
    }
        private IEnumerator CrouchStand()
    {
        Vector3 targetGroundCheckPosition = isCrouching ? groundCheckStandingPosition : groundCheckCrouchPosition;
        Vector3 currentGroundCheckPosition = groundCheck.localPosition;

        while (timeElapsed < timeToCrouch)
        {
            groundCheck.localPosition = Vector3.Lerp(currentGroundCheckPosition, targetGroundCheckPosition, chrouchPercentage);

            timeElapsed += Time.deltaTime;
            yield return null; } groundCheck.localPosition = targetGroundCheckPosition; isCrouching = ! isCrouching; duringCrouchAnimation =false;
    }
Copy the code

Press “LeftCtrl” to squat

The second phase has a better solution: (2) the first person perspective controller – open lens, head swing, slope slide

This is really can not find a good tutorial, feel how many have a bit of a problem, the following is my own writing, it is not too big a problem.

public class PlayerController : MonoBehaviour
{
    // Function switch
    [Header("Functional Options")]
    [SerializeField] private bool canSwitchCrouch = false;
    // Press the squat switch
    [SerializeField] private bool canHoldToCrouch = true;
    [Header("Controls")]
    // You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
    [SerializeField] private KeyCode holdToCrouchKey = KeyCode.LeftControl;
    [SerializeField] private KeyCode switchCrouchKey = KeyCode.C;
    
    [Header("Crouch Parameters")]
    // Squat speed
    [SerializeField, Range(50f, 100f)]  private float crouchStandSpeed = 50f;
    / / squat lock
    [SerializeField] private bool crouchLock;
}   
Copy the code

I divided into two kinds of switches, one is the top, press C to switch squat and stand. One is to hold down squat, can also achieve squatting and standing at the same time.

    void Update()
    {
        GroundCheck();
        if (CanMove)
        {
            HandleMovementInput();
            HandleMouseLook();
            if (canJump)
                HandleJump();
            if(canSwitchCrouch || canHoldToCrouch) HandleCrouch(); ApplyFinalMovement(); }}Copy the code

① If you press “C” to switch the squatting position at the beginning of the squatting position lock, the squatting position lock will be switched

② If the squat lock is open, but the “leftControl” is pressed, the squat lock is closed

③ isCrouching is determined by whether the crouching lock is opened or the crouching lock is pressed

private void HandleCrouch()
    {
        // If press and hold squat is enabled, it has priority to press and hold squat
        if (canHoldToCrouch)
        {
            // If squat is pressed to switch, the lock switch is switched
            if(Input.GetKeyDown(switchCrouchKey)) crouchLock = ! crouchLock;// If the hold crouch key is pressed in the lock state, the lock is closed
            if (crouchLock && Input.GetKeyDown(holdToCrouchKey)) crouchLock = false;
            // Press and hold the Hold squat button, or lock it to crouch, to display the crouch state
            isCrouching = Input.GetKey(holdToCrouchKey) || crouchLock;
            // Adjust height
            AdjustHeight();
        }

        // Otherwise, squat via coroutine
        else if (ShouldCrouch)
            StartCoroutine(CrouchStand());

    }
Copy the code

In update, adjust height in real time.

① If the target height is already the current height, return directly.

② If the error between the current height and the target height is small, the target height is directly set

③ If it is other cases, it is necessary to adjust the height

Because it was implemented in update, the timer wasn’t as good as it should have been, and the “leftControl” was released in the middle of the squat.

I tried timers, or mathf.Smoothdamp (), and nothing worked.

The third time of the race, using fixed parameters or timers, or height ratios, has a stalling or other troublesome situation.

Test down, so far set a squat speed, and then multiply Time. DeltaTime is still relatively good.

Mathf.Lerp(currentHeight, targetHeight, crouchStandSpeed * Time.deltaTime);

Want to know if there is a more mature implementation method, online search is in is unable to find.

Check this out for details on Lerp and SmoothDamp.

Lerp is Mathf.Lerp(a, b, t(0.5)). If t is 0.5, then in this frame, we take the (b – a) segment and multiply it by 0.5, which means we cut it in half, and then the new position is a + half of the length, and then, if we take the coordinates of a + half of the length, As a new starting point, I’m going to continue to take half, which is 3/4 of the original position. So it’s going to get closer and closer to B, but it’s going to get slower and slower.

But if you take time, you’re going to be able to have constant velocity in your coroutine, instead of slowing down like this.

A SmoothDamp, on the other hand, is similar to the feeling of a car accelerating to start up, then slowing down, and finally stopping.

private void AdjustHeight()
    {
        float targetHeight = isCrouching ? crouchHeight : standingHeight;
        float currentHeight = characterController.height;

        // If yes, return
        if (targetHeight == currentHeight) return;


        Vector3 targetCenter = isCrouching ? crouchingCenter : standingCenter;
        Vector3 currentCenter = characterController.center;

        Vector3 targetGroundCheckPosition = isCrouching ? groundCheckCrouchPosition : groundCheckStandingPosition;
        Vector3 currentGroundCheckPosition = groundCheck.localPosition;

        if (Mathf.Abs(targetHeight - currentHeight) < 0.01 f)
        {
            characterController.height = targetHeight;
            characterController.center = targetCenter;
            groundCheck.localPosition = targetGroundCheckPosition;
        }
        else
        {
            // Squat -> Standing, if there is a block on the head, can not change the height
            if (Physics.Raycast(playerCamera.transform.position, transform.up, 1f) && !isCrouching) return; characterController.height = Mathf.Lerp(currentHeight, targetHeight, crouchStandSpeed * Time.deltaTime); characterController.center = Vector3.Lerp(currentCenter, targetCenter, crouchStandSpeed * Time.deltaTime); groundCheck.localPosition = Vector3.Lerp(currentGroundCheckPosition, targetGroundCheckPosition, crouchStandSpeed * Time.deltaTime); }}Copy the code

And the speed of movement while crouching, changed here.

private void HandleMovementInput()
    {
        float speed = isCrouching ? crouchSpeed : IsSprinting ? sprintSpeed : walkSpeed;
        // Debug.Log("speed: " + speed);
        // Get WASD input * speed
        moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * speed;
        // On the Y-axis, the vertical velocity is saved
        float moveDirectionY = moveDirection.y;
        // Get input in three directions
        moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
        moveDirection.y = moveDirectionY;

    }
Copy the code