Limit Movement in the direction of Slope

Hello there lovely people.
im working on a character controller of mine.
and i cannot figure out how to limit movement on slopes.

My Character controller uses a rigidbody to move.
ive tried to modify the calculateDesiredMoveDirection function to limit the movement in the direction but that didnt work correctly. i could jump into a wall and the player would still accelerate up the slope.

Here would be the character controller script i wrote.
i hope you could somehow help me out. <3

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Events;

public class characterController : MonoBehaviour
{
    [Header("General data")]
    public Camera camera;

    [Header("Movement Parameters")]
    public float maxSpeed = 8f;
    public float acceleration = 200f;
    public AnimationCurve accelerationFactorFromDotCurve;
    public float deceleration = 200f;
    public float accelMod = 1f;
    public float decelMod = 1f;
    public float turnSpeed = 10f;
    public AnimationCurve rotationSpeedByMoveSpeed;
    public float maxGroundAngle = 45f;

    [Header("Jump Parameters")]
    public float targetJumpHeight = 5.0f;
    public float cyoteTime = 0.1f;
    public float bufferedJumpTime = 0.1f;
    public float deccendGravityMultiplier = 2f;

    [Header("Float Parameters")]
    public float rideHeight = 0.8f;
    public float maxRideHeight = 1f;
    public float groundedDistance = 1f;
    public float rideSpringStrength = 1f;
    public float rideSpringDamper = 1f;
    public LayerMask groundLayerMask;

    [Header("state info")]
    public state currentState = state.baseState;
    public bool isMoving;
    public bool isJumping = false;
    public bool cancelJump = false;
    public bool isGrounded = true;

    public enum state
    {
        baseState,
        slideSlope
    };

    public RaycastHit groundHit;

    //private fields
    private Rigidbody rb;

    private float goalSpeed;
    private Vector2 inputDir;
    private Vector3 moveDirection;
    private Vector3 lookDirection;

    private bool wasGrounded;

    private float lastGroundTime;
    private float lastJumpInputTime;


    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void Start()
    {
        lastJumpInputTime = -Mathf.Infinity;

        lookDirection = transform.forward;
    }

    private void FixedUpdate()
    {
        isGrounded = CheckGrounded(out groundHit);

        //state switcher
        switch (currentState)
        {
            case state.baseState:
                break;
        }

        //state logic
        switch (currentState)
        {
            case state.baseState:
                calculateDesiredMoveDirection();

                isMoving = moveAndRotatePlayer();

                if (Time.time - lastJumpInputTime <= bufferedJumpTime)
                {
                    doJump();
                }

                if (isGrounded)
                {
                    hoverAboveGround(groundHit);
                }
                else
                {
                    if (wasGrounded && !isGrounded)
                    {
                        rb.velocity = new Vector3(rb.velocity.x, 0, rb.velocity.z);
                    }

                    if (rb.velocity.y < 0 || cancelJump)
                    {
                        updateGravityMultiplier(deccendGravityMultiplier);
                    }
                }
                break;
        }

        if(moveDirection != Vector3.zero)
        {
            lookDirection = moveDirection;
        }

        wasGrounded = isGrounded;
    }

    public void getMovementInput(InputAction.CallbackContext context)
    {
        inputDir.x = context.ReadValue<Vector2>().x;
        inputDir.y = context.ReadValue<Vector2>().y;
    }

    public void calculateDesiredMoveDirection()
    {
        float moveHorizontal = inputDir.x;
        float moveVertical = inputDir.y;

        Vector3 camForward = camera.transform.forward;
        camForward.y = 0f;
        camForward.Normalize();

        Vector3 desiredMoveDirection = (camForward * moveVertical) + (camera.transform.right * moveHorizontal);
        moveDirection = desiredMoveDirection.normalized;

        if (Vector3.Angle(Vector3.up, groundHit.normal) > maxGroundAngle)
        {
            Vector3 slopeDir = Vector3.ProjectOnPlane(-getGroundSlope(), Vector3.up);

            moveDirection -= slopeDir.normalized;
        }

        moveDirection.Normalize();
    }


    private float calculateRotationSpeedFactor()
    {
        float currentSpeed = rb.velocity.magnitude;

        float normalizedSpeed = Mathf.Clamp01(currentSpeed / maxSpeed);

        return normalizedSpeed;
    }

    private bool moveAndRotatePlayer()
    {
        bool isMoving = false;

        float idealSpeed = moveDirection.magnitude * maxSpeed;
        float speedDifference = idealSpeed - goalSpeed;

        Vector3 unitVel = rb.velocity.normalized;
        Vector3 unitGoal = moveDirection.normalized;
        float velDot = Vector3.Dot(unitGoal, unitVel);

        if (speedDifference > 0)
        {
            goalSpeed = Mathf.Min(goalSpeed + ((acceleration * accelMod) * accelerationFactorFromDotCurve.Evaluate(velDot)) * Time.deltaTime, idealSpeed);
        }
        else if(speedDifference < 0)
        {
            if (!isJumping)
            {
                goalSpeed = Mathf.Max(goalSpeed - (deceleration * decelMod) * Time.deltaTime, idealSpeed);
            }
        }

        Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
        float turnSpeedFactor = rotationSpeedByMoveSpeed.Evaluate(calculateRotationSpeedFactor());
        rb.rotation = Quaternion.Lerp(rb.rotation, targetRotation, Time.fixedDeltaTime * ((turnSpeed * accelMod) * turnSpeedFactor));

        if (goalSpeed > Mathf.Epsilon)
        {
            isMoving = true;

            Vector3 idealVel = transform.forward * goalSpeed;
            Vector3 neededAccel = (idealVel - rb.velocity) / Time.fixedDeltaTime;



            rb.AddForce(new Vector3(neededAccel.x, 0, neededAccel.z), ForceMode.Acceleration);
        }
        else
        {
            Vector3 velocity = rb.velocity;
            velocity.x = 0;
            velocity.z = 0;

            rb.velocity = velocity;
        }

        return isMoving;
    }

    private void hoverAboveGround(RaycastHit groundHit)
    {
        if (groundHit.distance <= maxRideHeight)
        {
            float distanceToGround = groundHit.distance;
            Vector3 upForce = Vector3.up * (rideHeight - distanceToGround) * rideSpringStrength;
            Vector3 dampingForce = new Vector3(0f, -rb.velocity.y, 0f) * rideSpringDamper;

            rb.AddForce(upForce + dampingForce);

            if(groundHit.rigidbody != null)
            {
                Vector3 groundVel = groundHit.rigidbody.velocity;
                groundVel.y = 0;


                Vector3 platformAngularVelocity = groundHit.rigidbody.angularVelocity;

                // Calculate player's position relative to rotation axis (assuming axis is centered)
                Vector3 relativePosition = transform.position - groundHit.rigidbody.position;

                // Calculate tangential velocity based on angular velocity and relative position
                Vector3 tangentialVelocity = Vector3.Cross(platformAngularVelocity, relativePosition);

                // Add tangential velocity to player's velocity, scaled by a factor
                rb.velocity += tangentialVelocity + groundVel;

                groundHit.rigidbody.AddForceAtPosition(-(upForce / 2) + dampingForce, groundHit.point);
            }
        }
    }

    public void triggerJump(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            lastJumpInputTime = Time.time;
        }
        else if (context.canceled)
        {
            if (isJumping)
            {
                cancelJump = true;
            }
        }
    }

    public void doJump()
    {
        if (!(isGrounded || Time.time - lastGroundTime <= cyoteTime) || isJumping)
        {
            return;
        }

        isGrounded = false;
        isJumping = true;

        float jumpForce = calculateJumpForce();

        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    }

    private float calculateJumpForce()
    {
        float initialVelocity = Mathf.Sqrt(2 * Mathf.Abs(Physics.gravity.y) * targetJumpHeight);
        return initialVelocity * rb.mass;
    }

    private bool CheckGrounded(out RaycastHit groundHit)
    {
        RaycastHit hit;

        if (Physics.Raycast(transform.position, Vector3.down, out hit, Mathf.Infinity, groundLayerMask))
        {
            groundHit = hit;

            if (isGrounded)
            {
                isJumping = false;
                cancelJump = false;

                return hit.distance <= maxRideHeight;
            }
            else
            {
                lastGroundTime = Time.time;

                return hit.distance <= groundedDistance;
            }
        }

        groundHit = hit;
        return false;
    }


    private Vector3 getGroundSlope()
    {
        Vector3 gravityDirection = Physics.gravity.normalized;
        Vector3 slopeDirection = Vector3.ProjectOnPlane(gravityDirection, groundHit.normal).normalized;

        return slopeDirection;
    }

    private void updateGravityMultiplier(float multiplier)
    {
        rb.AddForce(Physics.gravity * multiplier, ForceMode.Acceleration);
    }
}
using UnityEngine;
public class SimpleRigidbodyController : MonoBehaviour
{
    Rigidbody rb;

    void Start()
    {
        rb=GetComponent<Rigidbody>();
    }

    void Update()
    {
        rb.MoveRotation(transform.rotation*Quaternion.AngleAxis(Input.GetAxis("Horizontal")*5, Vector3.up));
    }

    void FixedUpdate()
    {
        Vector3 force=((transform.right*Input.GetAxisRaw("Horizontal"))+(transform.forward*Input.GetAxisRaw("Vertical"))).normalized;
        if (Physics.SphereCast(transform.position,0.5f,Vector3.down,out RaycastHit hit,1.3f))  // are we grounded?
        {
            force=Vector3.ProjectOnPlane(force,hit.normal);
            rb.AddForce(force*20);
        }
    }
}

hey zulo3d.

your code seems to show a character controller wich will move along slopes.
my goal would be limiting movement on updwards slopes above a specified angle.

ive tried to use a sphere cast for ground detection but that results in an ugly offset caused by the radius of the sphere cast.

thanks for the repsonse

It isn’t really necessary to have a max angle when using a physics engine. Gravity and the character’s force should dictate the player’s climbing angle. Perhaps you’re adding too much force?.

But if you still want a max angle then you can limit the upwards force like this:

using UnityEngine;
public class SimpleRigidbodyController : MonoBehaviour
{
    Rigidbody rb;

    void Start()
    {
        rb=GetComponent<Rigidbody>();
    }

    void Update()
    {
        rb.MoveRotation(transform.rotation*Quaternion.AngleAxis(Input.GetAxis("Horizontal")*5, Vector3.up));
    }

    void FixedUpdate()
    {
        Vector3 force=((transform.right*Input.GetAxisRaw("Horizontal"))+(transform.forward*Input.GetAxisRaw("Vertical"))).normalized;
        if (Physics.SphereCast(transform.position,0.5f,Vector3.down,out RaycastHit hit,1.3f))  // are we grounded?
        {
            force=Vector3.ProjectOnPlane(force,hit.normal);
            if (force.y>0 && Vector3.Angle(hit.normal,Vector3.up)>45) // We're pushing up a slope with an angle greater than our max climb angle of 45 degrees?
                 force=Vector3.ProjectOnPlane(force,Vector3.Cross(Vector3.Cross(hit.normal,Vector3.up),hit.normal)); // deflect our upwards force

            rb.AddForce(force*20);
        }
    }
}

Hello There again :smile:

im using physics to hover my player above the ground, then im moving my player.
so gravity and friction dont limit my player going up slopes.

thanks fo your contributiuon again. i will be taking a closer look at this when im not in school later.

Here is some code I’ve used to smoothly move up/down slopes with my RB controllers.

public Vector3 GetPlayerMoveForce()
        {
            Vector3 playerMovement = _playerInput.MoveInput;

            if (_isGrounded)
            {
                playerMovement = new Vector3(playerMovement.x, 0f, playerMovement.y);

                Vector3 localGroundCheckHitNormal = _rigidbody.transform.InverseTransformDirection(_groundCheckHit.normal);
                float groundSlopeAngle = GetSlopeAngle(localGroundCheckHitNormal);

                if (groundSlopeAngle != 0f)
                {
                    Quaternion slopeAngleRotation = Quaternion.FromToRotation(_rigidbody.transform.up, localGroundCheckHitNormal);
                    playerMovement = slopeAngleRotation * playerMovement;
                }
            }

            return playerMovement;
        }


        private float GetSlopeAngle(Vector3 localGroundCheckNormal = new Vector3())
        {
            if (localGroundCheckNormal == Vector3.zero)
            {
                localGroundCheckNormal = _rigidbody.transform.InverseTransformDirection(_groundCheckHit.normal);
            }

            float groundSlopeAngle = Vector3.Angle(localGroundCheckNormal, _rigidbody.transform.up);
            _currentSlopeAngle = groundSlopeAngle;

            return groundSlopeAngle;
        }

Just disallow movement if the current slope is greater than your max slope.