Unity First-Person Parkour Controller

PHOTO EMBED

Fri Mar 18 2022 15:51:33 GMT+0000 (Coordinated Universal Time)

Saved by @BryanJ22

using UnityEngine;
//include the DoTween namespace for all of my tweening needs
using DG.Tweening;

//We require that any object with this script also has a character controller component attached
//if an object with this script does not have a character controller, unity will add it when the game runs
[RequireComponent(typeof(CharacterController))]

public class PlayerController : MonoBehaviour
{
    //Enum for each parkour state the player can be in
    public enum motorStates
    {
        Falling,
        Jumping,
        GettingHurt,
        Rolling,
        Sliding,
        Grounded
    }

    [Header("Connections")]
    public Camera PlayerCamera;
    public GameObject PlayerCol;
    public GameObject HangPoint;

    [Header("Physics Properties")]
    public float DecelerationMultipler = 1.5f;
    public float BaseSpeed = 4.0f;
    public float JumpSpeed = 8.5f;
    public float Gravity = 20.0f;
    public float MaxSpeed = 23;
    public float SpeedUpRate = 8;
    public float RunDecreaseRate = 25;
    public float SlideDecreaseRate = 35;
    public float SlideSpeedBoost = 1.5f;
    public float MaxTimeInAir = 1.5f;

    private CharacterController controller;
    private float capsuleRadius;
    private Vector3 moveDirection;
    private motorStates motorState = motorStates.Grounded;

    private float currentRatioCrouch = 1;
    private float currentSpeed = 0;
    private float timeInAir = 0;

    //bool for transition when hitting ground at certain height
    private bool highImpact = false;

    //distToGround should be half of the character controllers height, used for groundCheck raycast
    private float distToGround = 1;

    //bool that is set based on the groundCheck raycast result to check if we are on the ground
    private bool isGrounded = false;

    private bool sliding = false;
    private GameObject objectLastUnder;

    void Start()
    {
        //Get all the necesary components attached to the player
        controller = this.GetComponent<CharacterController>();
        capsuleRadius = this.GetComponent<CharacterController>().radius;
    }

    //NOTE: If using controller.isGrounded, the value will keep changing to true and false..
    //unless you set the min move distance to 0 in the controllers inspector 

    //I am raycasting below the player instead of using controller.isGrounded

    //Takes in a ray and returns a RaycastHit for our ground check
    RaycastHit groundCheck(Ray ray)
    {
        float length = distToGround + 0.1f;

        RaycastHit hit;
        Physics.Raycast(ray, out hit, length);
        return hit;
    }
    
    //If the game has not ended, constantly check for player inputs
    void Update()
    {
        if (!GameManager.Instance.GameEnded)
        {
            checkPlayerState();
        }
    }

    void checkPlayerState()
    {
        //Cast a ray down from the player
        //Set isGrounded bool based on if the hit.collider returns anything
        RaycastHit hit = groundCheck(new Ray(PlayerCol.transform.position, -transform.up));
        Debug.DrawRay(PlayerCol.transform.position, -transform.up);

        if (hit.collider != null)
        {
            isGrounded = true;
        }

        else
        {
            isGrounded = false;
        }


        //Check the current motor state and excecute the appropriate logic
        switch (motorState)
        {
            case (motorStates.Falling):
                updateFalling();
                break;
            case (motorStates.Jumping):
                updateJump();
                break;
            case (motorStates.GettingHurt):
                print("I was hurt");
                break;
            case (motorStates.Rolling):
                print("I rolled");
                break;
            case (motorStates.Sliding):
                updateSliding();
                break;
            case (motorStates.Grounded):

                //If a player walks off of a platform, the state will change to falling
                if (!isGrounded)
                {
                    motorState = motorStates.Falling;
                }

                updateGrounded();
                break;
        }

        //Update camera rotation
        PlayerCamera.GetComponent<SimpleCameraRotate>().CameraUpdate(sliding);

        //Update player movement constantly
        addGravityToPlayer();
    }

    //This function is used to check how long the player has been in the air after falling/jumping
    //If the player has been in the air for longer than a set time, the landing is considered a "High Impact" one
    //If a landing is a high impact one and the player doesn't roll on landing, they will get hurt and their speed will reset to 0
    void checkInAirTime()
    {
        if (timeInAir > MaxTimeInAir)
            highImpact = true;

        timeInAir = 0;
    }

    //Update player movement here with constant gravity
    void addGravityToPlayer()
    {
        moveDirection.y -= Gravity * Time.deltaTime;
        controller.Move(moveDirection * Time.deltaTime);
    }

    void UpdatePlayerMovement(bool inAir)
    {
        //Update our speed based on our speedUpRate when the W key is held
        if (Input.GetKey(KeyCode.W))
        {
            //only increase speed if the player is not in the air (since the players movement is updated while jumping/falling)
            if (!inAir)
            {
                currentSpeed += Time.deltaTime * SpeedUpRate;
            }
        }

        //If we're not holding the W key, the player will gradually start to decelerate
        else
        {
            //Only decelerate if our speed is greater than 0
            if (currentSpeed > 0)
            {
                //If we are not holding the S key, we will decelerate based on our runDecreaseRate
                if (!Input.GetKey(KeyCode.S))
                {
                    currentSpeed -= Time.deltaTime * RunDecreaseRate;
                }

                //If we are holding the S key down while we decelerate, we will decelerate at an even fast rate
                //We multply out runDecreaseRate by our decelerationMultipler
                else
                {
                    currentSpeed -= Time.deltaTime * (RunDecreaseRate * DecelerationMultipler);
                }
            }
        }

        //Clamp our speed between 0 and maxSpeed and assign it to clampedSpeed
        float clampedSpeed = Mathf.Clamp(currentSpeed, 0, MaxSpeed);

        //Get our moveDirection depending on our speed
        moveDirection = getMoveDirection(clampedSpeed);

        //For our x direction, we multiply by a fixed BaseSpeed
        moveDirection.x *= BaseSpeed;
        //For our charactes z direction (the forward direction), we multiply by our fixed speed plus the clampedSpeed for acceleration
        moveDirection.z *= BaseSpeed + clampedSpeed;

        //We use the transforms right and forward so we move forward and right/left based on the players rotation
        //If we don't do this we will move in a fixed forward/left/right direction regardless of the player's rotation (Vector3.right/Vector3.forward)
        moveDirection = (this.transform.right * moveDirection.x) + (this.transform.up * moveDirection.y) + (this.transform.forward * moveDirection.z);
    }

    void updateJump()
    {
        //Update timeInAir var to see how long you've been in the air for
        //this will be used to take damage if you've been in the air for a long time and don't roll when you land
        timeInAir += Time.deltaTime;

        //movement in air code
        UpdatePlayerMovement(true);

        //If the moveDirection.y is < 0, this means the player is no longer jumping, and is instead starting to fall
        //we should therefore switch over to the falling state
        if (moveDirection.y < 0)
        {
            motorState = motorStates.Falling;
        }
    }

    void updateFalling()
    {
        //Update timeInAir var to see how long you've been in the air for
        //this will be used to take damage if you've been in the air for a long time and don't roll when you land
        timeInAir += Time.deltaTime;

        //Movement in air code
        UpdatePlayerMovement(true);
        //

        //If the player was falling and is now touching the ground,
        //check how long they have been in the air BEFORE changing the state back to grounded
        if (isGrounded)
        {
            //checks in air time
            checkInAirTime();

            //If the landing is not a high impact landing, change your state back to grounded like normal
            //If the landing IS a high impact one, we need to check for a roll and react accordingly
            if (!highImpact)
            {
                motorState = motorStates.Grounded;
            }

            else
            {
                doRoll(Input.GetKey(KeyCode.LeftShift));
            }
        }
    }


    //Takes in a ray and returns a RaycastHit for our crouch check
    RaycastHit doCrouchCheck(Ray upRay)
    {
        float length = (1 - this.transform.localScale.y);

        RaycastHit hit;

        //We do a SphereCast instead of a raycast to avoid phasing through objects
        //For example if we do a normal raycast up, it could see no object but the corner of our player could be under the object,
        //meaning we when the game thinks we can uncrouch we are now standing inside of an object
        Physics.SphereCast(upRay.origin, capsuleRadius + .2f, upRay.direction, out hit, length);

        return hit;
    }

    //Using this function to visualize the bounds of the SphereCast
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Debug.DrawLine(PlayerCol.transform.position, PlayerCol.transform.position + PlayerCol.transform.up);
        Gizmos.DrawWireSphere(PlayerCol.transform.position + PlayerCol.transform.up, capsuleRadius + .2f);
    }
    //

    void updateSliding()
    {
        //for uncrouching check
        RaycastHit hit = doCrouchCheck(new Ray(PlayerCol.transform.position, transform.up));
        //Debug.DrawRay(startPos, transform.up);

        //If I crawled out or slid out from object but I'm no longer holding shift, I should start uncrouching
        if (hit.collider == null && objectLastUnder != null)
        {
            if (sliding)
                sliding = false;

            currentRatioCrouch = 1;
            ///scale here
            scalePlayerForSlide();
            objectLastUnder = null;
        }
        //

        //do slide slow down
        if (currentSpeed > 0)
        {
            if (!Input.GetKey(KeyCode.S))
            {
                currentSpeed -= Time.deltaTime * SlideDecreaseRate;
            }

            else
            {
                currentSpeed -= Time.deltaTime * (SlideDecreaseRate * DecelerationMultipler);
            }
        }

        //create var clamp speed to get speed limited between 0-maxSpeed
        float clampedSpeed = Mathf.Clamp(currentSpeed, 0, MaxSpeed);

        //set direction based on speed
        moveDirection = getMoveDirection(clampedSpeed);

        //If your current speed is less than the speed to start sliding, this means you are only crouching
        //We therefore want the player to move at a normal fixed speed
        if (clampedSpeed < 6)
        {
            moveDirection.x *= BaseSpeed;
            moveDirection.z *= BaseSpeed;
            sliding = false;
        }

        //If the speed is greater than the speed to start sliding, this means we're currently sliding
        //move the player by the current speed multiplied by the slide speed boost value
        else
        {
            moveDirection.x *= BaseSpeed;
            moveDirection.z *= (clampedSpeed * SlideSpeedBoost);
            sliding = true;
        }

        //We use the transforms right and forward so we move forward and right/left based on the players rotation
        //If we don't do this we will move in a fixed forward/left/right direction regardless of player rotation (Vector3.right/Vector3.forward)
        moveDirection = (this.transform.right * moveDirection.x) + (this.transform.up * moveDirection.y) + (this.transform.forward * moveDirection.z);

        //When pressing Left Shift, get pre Slide info like if we are going fast enough to slide or if we will crawl
        if (Input.GetKeyDown(KeyCode.LeftShift))
        {
            currentRatioCrouch = .5f;
            ///scale here
            scalePlayerForSlide();
        }

        //When letting go of the shift key to stand up, we need to first check if we can
        else if (Input.GetKeyUp(KeyCode.LeftShift))
        {
            //If we're not under an object, we can stand up at any time,
            //regardless if we're sliding or crouching
            if (hit.collider == null)
            {
                if (sliding)
                    sliding = false;

                currentRatioCrouch = 1;
                ///scale here
                scalePlayerForSlide();
            }

            //Ff we're under an object, assigned the object we're under to object last under
            //If we were under an object last but are not anymore, we will uncrouch automatically
            else
            {
                objectLastUnder = hit.collider.gameObject;
            }
        }

        //If you're not pressing the shift key (uncrouching), then we switch back to the grounded motor state after the players scale is almost back to 1
        if (!Input.GetKey(KeyCode.LeftShift))
        {
            //Change state to grounded when do tween is almost complete
            if (this.transform.localScale.y >= .9f)
            {
                motorState = motorStates.Grounded;
            }
        }

        //If player wants to jump out of a slide, they can
        //First we need to set the scale value to 1 since we will tween back to full size while in the air
        //We start tweening the scale back to 1 BEFORE we change the state since we only tween the scaling in the sliding motor state/when first pressing shift from grounded
        if (Input.GetButtonDown("Jump"))
        {
            if (sliding)
                sliding = false;

            currentRatioCrouch = 1;
            moveDirection.y = JumpSpeed;

            scalePlayerForSlide();
            motorState = motorStates.Jumping;
        }
    }

    void scalePlayerForSlide()
    {
        //Use do tween to scale the player on the y axis based on the CurrentRatioCrouch var
        this.transform.DOScaleY(currentRatioCrouch, 1);
    }

    void updateGrounded()
    {
        //Change state to sliding if we press shift
        if (Input.GetKey(KeyCode.LeftShift))
        {
            currentRatioCrouch = .5f;
            ///scale here
            scalePlayerForSlide();
            motorState = motorStates.Sliding;
        }

        UpdatePlayerMovement(false);

        //If the player jumps, set the move Direction.y to the jumpspeed so force is applied upward when the player is moved.
        //We also switch the motor state to jumping
        if (Input.GetButton("Jump"))
        {
            moveDirection.y = JumpSpeed;
            motorState = motorStates.Jumping;
        }
    }

    //Takes in the clamped speed and returns vector 3 that is used to show the direction we move in
    Vector3 getMoveDirection(float cSpeed)
    {
        Vector3 dir = Vector3.zero;

        //If our clamped speed is above 0, it means we are still moving, and therefore we want our z vector to be 1
        //This is so we continue to move forward in the z direction as long as our speed is above 0
        if (cSpeed > 0)
        {
            dir = new Vector3(Input.GetAxisRaw("Horizontal"), moveDirection.y, 1);
        }

        //If our clamped speed is 0, we're not moving forward in the z direction, but our vector should be set to receive input for backwards movement
        else
        {
            dir = new Vector3(Input.GetAxisRaw("Horizontal"), moveDirection.y, Input.GetAxisRaw("Vertical"));
        }

        return dir;
    }

    //Check for roll landing or hurt landing here, and change the motorstate accordingly
    void doRoll(bool roll)
    {
        //If the player rolls, they keep some of their momentum
        if (roll)
        {
            currentSpeed = currentSpeed * .5f;
            motorState = motorStates.Rolling;
        }

        //If the player does not roll after a high impact landing, they are hurt, and their speed is reset.
        else
        {
            currentSpeed = 0;
            motorState = motorStates.GettingHurt;
        }
    }
}
content_copyCOPY