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;
}
}
}
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter