So I need to have a functional FPS game by next tuesday, and I’ve been pretty busy working on other things. I’m a newbie in Unity, so I’ve been following tutorial for the most part. I built my character controller based on this amazing tutorial series by Comp-3 Interactive.
It works great, but I would really like to add a slide mechanic, which I assume would be pretty easy given the code for crouching. Thing is I have no idea how to do it, and I’ve used too much time trying to investigate how to do it. If anyone knows an easy way to add this function I would really appreciate it
Btw, when I say slide I don’t mean slope slide, I mean run and then slide
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PlayerControllerV2 : MonoBehaviour
{
public bool canMove { get; private set; } = true;
private bool isSpritning => canSprint && Input.GetKey(sprintKey);
private bool shouldJump => Input.GetKeyDown(jumpKey) && characterController.isGrounded;
private bool shouldCrouch => Input.GetKeyDown(crouchKey) || Input.GetKeyUp(crouchKey) && !duringCrouchAnimation && characterController.isGrounded;
[Header("Functional Options")]
[SerializeField] private bool canSprint = true;
[SerializeField] private bool canJump = true;
[SerializeField] private bool canCrouch = true;
[SerializeField] private bool canUseHeadbob = true;
[SerializeField] private bool willSlideOnSlopes = true;
[SerializeField] private bool canZoom = true;
[Header("Controls")]
[SerializeField] private KeyCode sprintKey = KeyCode.LeftShift;
[SerializeField] private KeyCode jumpKey = KeyCode.Space;
[SerializeField] private KeyCode crouchKey = KeyCode.C;
// [SerializeField] private KeyCode slideKey = KeyCode.LeftControl;
[SerializeField] private KeyCode zoomKey = KeyCode.Mouse1;
[Header("Movement Parameters")]
[SerializeField] private float walkSpeed = 3.0f;
[SerializeField] private float sprintSpeed = 6.0f;
[SerializeField] private float crouchSpeed = 1.5f;
[SerializeField] private float slopeSpeed = 8f;
[Header("Look Parameters")]
[SerializeField, Range(1, 10)] private float lookSpeedX = 2.0f;
[SerializeField, Range(1, 10)] private float lookSpeedY = 2.0f;
[SerializeField, Range(1, 100)] private float upperLookLimit = 80.0f;
[SerializeField, Range(1, 100)] private float lowerLookLimit = 80.0f;
[Header("Jumping Parameters")]
[SerializeField] private float jumpForce = 8.0f;
[SerializeField] private float gravity = 30.0f;
[Header("Crouch Parameters")]
[SerializeField] private float crouchHeight = 0.5f;
[SerializeField] private float standingHeight = 2f;
[SerializeField] private float timeToCrouch = 0.25f;
[SerializeField] private Vector3 crouchingCenter = new Vector3(0, 0.5f, 0);
[SerializeField] private Vector3 standingCenter = new Vector3(0, 0.5f, 0);
public bool isCrouching;
private bool duringCrouchAnimation;
[Header("Headbob Parameters")]
[SerializeField] private float walkBobSpeed = 14f;
[SerializeField] private float walkBobAmount = 0.05f;
[SerializeField] private float sprintBobSpeed = 18f;
[SerializeField] private float sprintBobAmount = 0.11f;
[SerializeField] private float crouchBobSpeed = 8f;
[SerializeField] private float crouchBobAmount = 0.025f;
private float defaultYPos = 0;
private float timer;
[Header("Zoom Parameters")]
[SerializeField] private float timeToZoom = 0.1f;
[SerializeField] private float zoomFOV = 54f;
private float defaultFOV;
private Coroutine zoomRoutine;
// Slope sliding parameters
private Vector3 hitPointNormal;
private bool isSliding
{
get
{
if(characterController.isGrounded && Physics.Raycast(transform.position, Vector3.down, out RaycastHit slopeHit, 2f))
{
hitPointNormal = slopeHit.normal;
return Vector3.Angle(hitPointNormal, Vector3.up) > characterController.slopeLimit;
}
else
{
return false;
}
}
}
private Camera playerCamera;
public CharacterController characterController;
private Vector3 moveDirection;
private Vector2 currentInput;
private float rotationX = 0;
private void Awake()
{
playerCamera = GetComponentInChildren<Camera>();
characterController = GetComponent<CharacterController>();
defaultYPos = playerCamera.transform.position.y;
defaultFOV = playerCamera.fieldOfView;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
if (canMove)
{
HandleMovementInput();
HandleMouseLook();
if (canJump)
{
HandleJump();
}
if (canCrouch)
{
HandleCrouch();
}
if (canUseHeadbob)
{
HandleHeadbob();
}
if (canZoom)
{
HandleZoom();
}
ApplyFinalMovements();
}
}
private void HandleMovementInput()
{
currentInput = new Vector2((isCrouching ? crouchSpeed : isSpritning ? sprintSpeed : walkSpeed) * Input.GetAxis("Vertical"),
(isCrouching ? crouchSpeed : isSpritning ? sprintSpeed : walkSpeed) * Input.GetAxis("Horizontal"));
float moveDirectionY = moveDirection.y;
moveDirection = (transform.TransformDirection(Vector3.forward) * currentInput.x) + (transform.TransformDirection(Vector3.right) * currentInput.y);
moveDirection.y = moveDirectionY;
}
private void HandleMouseLook()
{
rotationX -= Input.GetAxis("Mouse Y") * lookSpeedY;
rotationX = Mathf.Clamp(rotationX, -upperLookLimit, lowerLookLimit);
playerCamera.transform.localRotation = Quaternion.Euler(rotationX, 0, 0);
transform.rotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * lookSpeedX, 0);
}
private void HandleJump()
{
if(shouldJump)
{
moveDirection.y = jumpForce;
}
}
private void HandleCrouch()
{
if (shouldCrouch)
{
StartCoroutine(CrouchStand());
}
}
private IEnumerator CrouchStand()
{
if(isCrouching && Physics.Raycast(playerCamera.transform.position, Vector3.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;
isCrouching = !isCrouching;
while (timeElapsed < timeToCrouch)
{
characterController.height = Mathf.Lerp(currentHeight, targetHeight, timeElapsed / timeToCrouch);
characterController.center = Vector3.Lerp(currentCenter, targetCenter, timeElapsed / timeToCrouch);
timeElapsed += Time.deltaTime;
yield return null;
}
characterController.height = targetHeight;
characterController.center = targetCenter;
duringCrouchAnimation = false;
}
private void HandleHeadbob()
{
if (!characterController.isGrounded)
{
return;
}
if(Mathf.Abs(moveDirection.x) > 0.1f || Mathf.Abs(moveDirection.z) > 0.1f)
{
timer += Time.deltaTime * (isCrouching ? crouchBobSpeed : isSpritning ? sprintBobSpeed : walkBobSpeed);
playerCamera.transform.localPosition = new Vector3(
playerCamera.transform.localPosition.x,
defaultYPos + Mathf.Sin(timer) * (isCrouching ? crouchBobAmount : isSpritning ? sprintBobAmount : walkBobAmount),
playerCamera.transform.localPosition.z);
}
}
private void HandleZoom()
{
if(Input.GetKeyDown(zoomKey))
{
if(zoomRoutine != null)
{
StopCoroutine(zoomRoutine);
zoomRoutine = null;
}
zoomRoutine = StartCoroutine(ToggleZoom(true));
}
if(Input.GetKeyUp(zoomKey))
{
if(zoomRoutine != null)
{
StopCoroutine(zoomRoutine);
zoomRoutine = null;
}
zoomRoutine = StartCoroutine(ToggleZoom(false));
}
}
private IEnumerator ToggleZoom(bool isEnter)
{
float targetFOV = isEnter ? zoomFOV : defaultFOV;
float startingFOV = playerCamera.fieldOfView;
float timeElapsed = 0;
while(timeElapsed < timeToZoom)
{
playerCamera.fieldOfView = Mathf.LerpAngle(startingFOV, targetFOV, timeElapsed / timeToZoom);
timeElapsed += Time.deltaTime;
yield return null;
}
playerCamera.fieldOfView = targetFOV;
zoomRoutine = null;
}
private void ApplyFinalMovements()
{
if (!characterController.isGrounded)
{
moveDirection.y -= gravity * Time.deltaTime;
}
if (willSlideOnSlopes && isSliding)
{
moveDirection += new Vector3(hitPointNormal.x, -hitPointNormal.y, hitPointNormal.z) * slopeSpeed;
}
characterController.Move(moveDirection * Time.deltaTime);
}
}
I tried to follow some tutorials on sliding, but most of them are about slope sliding, which I already implemented. I also wouldn’t really like to use a Rigidbody since I’m pretty comfortable with the Character Controller component
David Garrote Vita is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.