How to make my character not walk up steep slopes. and not pass through walls.

Hello everyone.
I've been trying to make 3D movement using RigidBody but I'm facing some issues implementing some checks.
I was trying to implement slope movement to prevent the character from bouncing down slopes and that worked fine.
Then I tried to implement a max slope angle, where the character can't climb steep slopes, while doing so I noticed that Rigidbody.Move() makes my character sometimes pass through walls, and worse pass through the ground.

Tried to do some checks to fix both issues but I can't adjust the movement Vector properly.

I need the movement Vector to be adjusted as to prevent movement in the direction of the slope/wall but can move along side it.
Here's the movement script that works in FixedUpdate()

private void FixedUpdate() {
        HandleMovement();
    }

    void HandleMovement() {
        Vector2 inputVector = gameInput.GetMovementVectorNormalized();

        float moveDistance = moveSpeed * Time.deltaTime;
        if (inputVector != Vector2.zero) {
            // calculate rotation angle from input vector
            float targetRotation = Mathf.Atan2(inputVector.x, inputVector.y) * Mathf.Rad2Deg + Camera.main.transform.eulerAngles.y;
            // Rotate character
            transform.eulerAngles = Vector3.up * Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation, ref rotationSpeed, 0.1f);
            Vector3 moveDir = Quaternion.Euler(0f, targetRotation, 0f) * Vector3.forward;

            moveDir = AdjustVelocityToSlope(moveDir, transform);
            moveDir = CheckFrontCollision(moveDir, transform);

            // move character in direction
            rbody.MovePosition(transform.position + moveDir * moveDistance);
        }

        isWalking = inputVector != Vector2.zero;
    }

and here's both the function that checks wall collision and slope angle

public Vector3 CheckFrontCollision(Vector3 velocity, Transform transform) {
        float stepHeight = .3f;
        float playerHeight = 1f;
        float playerRadius = 1f;
        float checkDistance = .2f;
        // Capsule Cast to check wall collision
        bool canMove = !Physics.CapsuleCast(
            transform.position + Vector3.up * stepHeight,
            transform.position + Vector3.up * playerHeight,
            playerRadius,
            velocity,
            checkDistance
        );
        print(canMove);
        // if hitting an object stop forward direction
        if(!canMove) velocity.z = 0;    // I believe this is the issue
        return velocity;
    }

    public Vector3 AdjustVelocityToSlope(Vector3 velocity, Transform transform) {
        Ray ray = new Ray(transform.position, Vector3.down);
        if (Physics.Raycast(ray, out RaycastHit hit, 0.2f)) {
            // calculate slope angle
            float slopeAngle = Vector3.Angle(hit.normal, transform.up);
            // calculate whether we are going up the ramp or down the ramp
            float slopeDirection = Vector3.Angle(hit.normal, transform.forward) - 90;
            print(slopeAngle);

            // if higher than certain angle block movement forward.
            if (slopeAngle > 35 && slopeDirection > 0) {
                velocity.z = 0;     // I believe this is the issue
                return velocity;
            }

            // align move vector with the ramp
            Quaternion slopeRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
            Vector3 adjustedVeclocity = slopeRotation * velocity;
            if (adjustedVeclocity.y < 0) return adjustedVeclocity;
        }
        return velocity;
    }

basically it works fine as long as my wall and slop isn't rotated around Y axis.
I guess the issue is that I set the velocity.z to 0 and this makes the character not walk in the global forward direction.

Here's some images to clarify.

8922446--1222349--Screenshot 2023-04-02 204517.png

8922446--1222352--Screenshot 2023-04-02 204632.png

Needless to say if I am walking towards the wall head on then the character should not walk in any direction

You may want to give some thought to using the CharacterController component instead of doing everything by hand. It has all of the features you're describing. Here's how to use that instead:
1. First off, remove your Rigidbody component from your player. Also the collider - CharacterController comes with a collider.
2. With your character selected, click Component->Physics->Character Controller. This will add a CharacterController component to your player.
3. When it comes time to move your player, use something like the following:

[SerializeField]
private CharacterController controller;

// alternatively, use GetComponent<CharacterController>() if you know
// the CharacterController will always be on the player character

private void Update() {
    Vector3 movement = whateverMethodYouUseForThat();
    controller.Move(movement);
    // insert whatever code you want for rotating - that part doesn't
    // work any differently for a CharacterController.
}

Using CharacterController.Move() respects colliders and slope limits. You will also want to apply gravity to the character. CharacterController doesn't do that for you. I do it by tracking an internal "vertical momentum" variable, which I set to zero when in contact with the ground or adding gravity to it otherwise, and apply it to the Y-axis when calling Move(). That's also useful when you implement jumping and fall damage.

1 Like

I did actually change to a characterController yesterday and everything works perfectly now.
I even made it so he can't jump when on high slopes.
Now I have 2 small issues that is kinda just a quality of life:
1) When I hit an inclined wall with an angle it stops movement completely instead of walking along side the wall

2) when I'm on a high slope instead of sliding down the slope, I just remain on the same height.
I want the character to slide when it's standing on inclined surface that is > slopeLimit

Aside from these 2 issues, everything is perfect now :)

How did you resolve this in the end? I’ve just spent two days of coding trying to find a robust way to fix this very problem and no luck so far.

using UnityEngine;

public class SlippyController : MonoBehaviour
{
    CharacterController cc;
    Vector3 playerVelocity;
    RaycastHit hit;

    float gravity=20f;
    float slippy=10f; // How slippery are the slopes?

    void Start()
    {
        cc=GetComponent<CharacterController>();
    }

    void Update()
    {
        Vector3 moveDirection=new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"));
        if (cc.isGrounded)
        {
            // slide down slopes:
            if (Physics.SphereCast(transform.position,0.5f,Vector3.down,out hit,5f)) // raycast to get the hit normal
            {
                Vector3 dir=Vector3.ProjectOnPlane(Vector3.down,hit.normal); // slope direction
                playerVelocity+=dir*Vector3.Dot(Vector3.down,dir)*gravity*slippy*Time.deltaTime;
            }
            playerVelocity+=moveDirection;
            playerVelocity*=0.95f;   // basic friction
        }
        else
            playerVelocity.y-=gravity*Time.deltaTime;

        cc.Move(playerVelocity*Time.deltaTime);
    }
}

I had to implement my own logic for sliding down slopes that's bigger than the limit using Vector3.ProjectOnPlane.
It's a way similar to what zulo3d did above.

private void Update() {
        isSliding = OnSlope();
        isGrounded = Physics.CheckSphere(feet.position, checkSphereRadius, groundLayer);

        HandleMovement();
        HandleGravity();
        HandleSlopeSliding();
}

private void HandleSlopeSliding() {
        if (!isSliding) return;

        slidingDirection = Vector3.ProjectOnPlane(Vector3.down, hitInfo.normal);
        controller.Move(slidingDirection * slidingSpeed * Time.deltaTime);
    }

private bool OnSlope() {
        if (Physics.SphereCast(body.position, checkSphereRadius, Vector3.down, out hitInfo, 1f, groundLayer)) {
            // Slope Angle
            float angle = Vector3.Angle(hitInfo.normal, Vector3.up);
            if (angle > controller.slopeLimit) {
                return true;
            }
        }

        return false;
}