Unity First-Person Parkour Controller
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; } } }
Comments