Hello everyone! I’m working on implementing slope sliding for my character controller, but I’ve encountered an issue. Sometimes, the character won’t slide down slopes and gets stuck. The only way to get the character moving again is by jumping and moving away from slope(jump is for now enabled on slopes), and I can’t figure out why this is happening. (red circle is that sliding SphereCast and green line is normal) Also in the video I’ve attached u can notice at the end 2 edges between slopes and when Player goes over them sometimes he will slide down how can I prevent it? If anyone has suggestions or insights, I would really appreciate your help! Here is the video showcasing the issue, along with my code. https://youtu.be/QtjG_7_IKj4 (edited)
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class CharacterControllerFPS : MonoBehaviour
{
[Header("Player")]
[Tooltip("Move speed of the character in m/s")]
[SerializeField] public float playerSpeed;
public float SpeedChangeRate = 10.0f;
[Space(10)]
[Tooltip("The height the player can jump")]
public float JumpHeight = 1.2f;
[Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
public float Gravity = -9.81f;
public float gravityMultiplier = 3.0f;
private float _verticalVelocity;
[Space(10)]
[Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
public float JumpTimeout = 0.1f;
[Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
public float FallTimeout = 0.15f;
[Header("Movement Settings")]
public bool analogMovement;
[Header("Player Grounded")]
[Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
public bool Grounded = true;
[Tooltip("Useful for rough ground")]
public float GroundedOffset = -0.14f;
[Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
public float GroundedRadius = 0.5f;
[Tooltip("What layers the character uses as ground")]
public LayerMask GroundLayers;
[Space(10)]
//Sliding
[Header("Player Sliding")]
[SerializeField] bool WillSlideOnSlopes;
[SerializeField] float angle;
[SerializeField] Vector3 hitNormal;
[SerializeField] private float slopeSpeed = 8f;
Vector3 castOrigin;
private bool IsSliding
{
get
{
float sphereCastVerticalOffset = _controller.height / 2 - _controller.radius;
castOrigin = transform.position - new Vector3(0, sphereCastVerticalOffset, 0);
if (Grounded && Physics.SphereCast(castOrigin,_controller.radius - .01f, Vector3.down, out var hit, .1f, ~LayerMask.GetMask("Player"), QueryTriggerInteraction.Ignore))
{
hitNormal = hit.normal;
angle = Vector3.Angle(hitNormal, Vector3.up);
return angle > _controller.slopeLimit;
}
else
{
return false;
}
}
}
[Space(10)]
[Header("Player Crouch")]
[SerializeField] float crouchHeight = 1f;
[SerializeField] float crouchTransitionSpeed = 10f;
private float currentHeight;
private float standingHeight;
private bool isTryingToCrouch;
private Vector3 initialCameraPosition;
public GameObject playerCameraRoot;
[HideInInspector]
public float currentHorizontalSpeed;
//private variables
private Vector3 movementDirection;
private Vector2 inputDirection;
private bool isJumping = false;
private CharacterController _controller;
private Camera _camera;
private float _terminalVelocity = 53.0f;
//timeout deltatime
private float _jumpTimeoutDelta;
private float _fallTimeoutDelta;
private float _speed;
private void Awake()
{
_controller = GetComponent<CharacterController>();
_camera = Camera.main;
}
private void Start()
{
// reset our timeouts on start
_jumpTimeoutDelta = JumpTimeout;
_fallTimeoutDelta = FallTimeout;
isJumping = false;
standingHeight = currentHeight = _controller.height;
initialCameraPosition = playerCameraRoot.transform.localPosition;
}
public void OnMove(InputAction.CallbackContext context)
{
inputDirection = context.ReadValue<Vector2>();
}
public void OnJump(InputAction.CallbackContext context)
{
if (context.started)
{
isJumping = true;
}
else if (context.canceled)
{
isJumping = false;
}
}
public void OnCrouch(InputAction.CallbackContext context)
{
if (context.started)
{
isTryingToCrouch = true;
}
else if (context.canceled)
{
isTryingToCrouch = false;
}
}
private void Update()
{
ApplyGravity();
GroundedCheck();
Crouch();
Jump();
if (WillSlideOnSlopes && IsSliding)
{
Vector3 slopeDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z) * slopeSpeed;
_controller.Move(slopeDirection * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}
else
{
HandleMovement();
}
}
private void HandleMovement()
{
float targetSpeed = playerSpeed;
if (inputDirection == Vector2.zero)
{
targetSpeed = 0.0f;
}
currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;
float speedOffset = 0.1f;
float inputMagnitude = analogMovement ? inputDirection.magnitude : 1f;
//Accelerate or decelerate to the target speed
if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
{
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}
Vector3 forward = _camera.transform.forward;
Vector3 right = _camera.transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
movementDirection = (forward * inputDirection.y + right * inputDirection.x).normalized;
_controller.Move(movementDirection * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}
private void ApplyGravity()
{
if (Grounded && _verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}
else
{
_verticalVelocity += Gravity * gravityMultiplier * Time.deltaTime;
movementDirection.y = _verticalVelocity;
}
}
private void Jump()
{
if (Grounded)
{
if (isJumping )
{
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
isJumping = false;
}
}
}
private void Crouch()
{
float heightTarget = isTryingToCrouch ? crouchHeight : standingHeight;
float crouchDelta = Time.deltaTime * crouchTransitionSpeed;
currentHeight = Mathf.Lerp(currentHeight, heightTarget, crouchDelta);
Vector3 halfHeightDifference = new Vector3(0, (standingHeight - currentHeight) / 2, 0);
Vector3 newCameraPosition = initialCameraPosition - halfHeightDifference;
playerCameraRoot.transform.localPosition = newCameraPosition;
_controller.height = currentHeight;
}
private void GroundedCheck()
{
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
}
private void OnDrawGizmosSelected()
{
//Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
//Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);
//if (Grounded) Gizmos.color = transparentGreen;
//else Gizmos.color = transparentRed;
////when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
//Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(castOrigin, _controller.radius - .01f);
Gizmos.DrawLine(castOrigin, castOrigin + Vector3.down * 0.1f);
if (IsSliding)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(castOrigin, castOrigin + hitNormal * 0.5f);
}
}
}