Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

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

Discussion in 'Physics' started by DarkenSoda, Apr 2, 2023.

  1. DarkenSoda

    DarkenSoda

    Joined:
    Apr 3, 2020
    Posts:
    12
    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()

    Code (CSharp):
    1. private void FixedUpdate() {
    2.         HandleMovement();
    3.     }
    4.  
    5.     void HandleMovement() {
    6.         Vector2 inputVector = gameInput.GetMovementVectorNormalized();
    7.  
    8.         float moveDistance = moveSpeed * Time.deltaTime;
    9.         if (inputVector != Vector2.zero) {
    10.             // calculate rotation angle from input vector
    11.             float targetRotation = Mathf.Atan2(inputVector.x, inputVector.y) * Mathf.Rad2Deg + Camera.main.transform.eulerAngles.y;
    12.             // Rotate character
    13.             transform.eulerAngles = Vector3.up * Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation, ref rotationSpeed, 0.1f);
    14.             Vector3 moveDir = Quaternion.Euler(0f, targetRotation, 0f) * Vector3.forward;
    15.            
    16.             moveDir = AdjustVelocityToSlope(moveDir, transform);
    17.             moveDir = CheckFrontCollision(moveDir, transform);
    18.            
    19.             // move character in direction
    20.             rbody.MovePosition(transform.position + moveDir * moveDistance);
    21.         }
    22.  
    23.         isWalking = inputVector != Vector2.zero;
    24.     }
    and here's both the function that checks wall collision and slope angle

    Code (CSharp):
    1. public Vector3 CheckFrontCollision(Vector3 velocity, Transform transform) {
    2.         float stepHeight = .3f;
    3.         float playerHeight = 1f;
    4.         float playerRadius = 1f;
    5.         float checkDistance = .2f;
    6.         // Capsule Cast to check wall collision
    7.         bool canMove = !Physics.CapsuleCast(
    8.             transform.position + Vector3.up * stepHeight,
    9.             transform.position + Vector3.up * playerHeight,
    10.             playerRadius,
    11.             velocity,
    12.             checkDistance
    13.         );
    14.         print(canMove);
    15.         // if hitting an object stop forward direction
    16.         if(!canMove) velocity.z = 0;    // I believe this is the issue
    17.         return velocity;
    18.     }
    19.  
    20.     public Vector3 AdjustVelocityToSlope(Vector3 velocity, Transform transform) {
    21.         Ray ray = new Ray(transform.position, Vector3.down);
    22.         if (Physics.Raycast(ray, out RaycastHit hit, 0.2f)) {
    23.             // calculate slope angle
    24.             float slopeAngle = Vector3.Angle(hit.normal, transform.up);
    25.             // calculate whether we are going up the ramp or down the ramp
    26.             float slopeDirection = Vector3.Angle(hit.normal, transform.forward) - 90;
    27.             print(slopeAngle);
    28.            
    29.             // if higher than certain angle block movement forward.
    30.             if (slopeAngle > 35 && slopeDirection > 0) {
    31.                 velocity.z = 0;     // I believe this is the issue
    32.                 return velocity;
    33.             }
    34.  
    35.             // align move vector with the ramp
    36.             Quaternion slopeRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
    37.             Vector3 adjustedVeclocity = slopeRotation * velocity;
    38.             if (adjustedVeclocity.y < 0) return adjustedVeclocity;
    39.         }
    40.         return velocity;
    41.     }
    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.

    Screenshot 2023-04-02 204517.png


    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
     
  2. SOICGeek

    SOICGeek

    Joined:
    Jun 25, 2019
    Posts:
    77
    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:
    Code (CSharp):
    1. [SerializeField]
    2. private CharacterController controller;
    3.  
    4. // alternatively, use GetComponent<CharacterController>() if you know
    5. // the CharacterController will always be on the player character
    6.  
    7. private void Update() {
    8.     Vector3 movement = whateverMethodYouUseForThat();
    9.     controller.Move(movement);
    10.     // insert whatever code you want for rotating - that part doesn't
    11.     // work any differently for a CharacterController.
    12. }
    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.
     
    DarkenSoda likes this.
  3. DarkenSoda

    DarkenSoda

    Joined:
    Apr 3, 2020
    Posts:
    12
    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 :)
     
  4. xeosD

    xeosD

    Joined:
    Apr 6, 2023
    Posts:
    8
    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.
     
  5. zulo3d

    zulo3d

    Joined:
    Feb 18, 2023
    Posts:
    757
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class SlippyController : MonoBehaviour
    4. {
    5.     CharacterController cc;
    6.     Vector3 playerVelocity;
    7.     RaycastHit hit;
    8.  
    9.     float gravity=20f;
    10.     float slippy=10f; // How slippery are the slopes?
    11.  
    12.     void Start()
    13.     {
    14.         cc=GetComponent<CharacterController>();
    15.     }
    16.  
    17.     void Update()
    18.     {
    19.         Vector3 moveDirection=new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"));
    20.         if (cc.isGrounded)
    21.         {
    22.             // slide down slopes:
    23.             if (Physics.SphereCast(transform.position,0.5f,Vector3.down,out hit,5f)) // raycast to get the hit normal
    24.             {
    25.                 Vector3 dir=Vector3.ProjectOnPlane(Vector3.down,hit.normal); // slope direction
    26.                 playerVelocity+=dir*Vector3.Dot(Vector3.down,dir)*gravity*slippy*Time.deltaTime;
    27.             }
    28.             playerVelocity+=moveDirection;
    29.             playerVelocity*=0.95f;   // basic friction
    30.         }
    31.         else
    32.             playerVelocity.y-=gravity*Time.deltaTime;
    33.        
    34.         cc.Move(playerVelocity*Time.deltaTime);
    35.     }
    36. }
     
  6. DarkenSoda

    DarkenSoda

    Joined:
    Apr 3, 2020
    Posts:
    12
    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.


    Code (CSharp):
    1.  
    2. private void Update() {
    3.         isSliding = OnSlope();
    4.         isGrounded = Physics.CheckSphere(feet.position, checkSphereRadius, groundLayer);
    5.  
    6.         HandleMovement();
    7.         HandleGravity();
    8.         HandleSlopeSliding();
    9. }
    10.  
    11. private void HandleSlopeSliding() {
    12.         if (!isSliding) return;
    13.  
    14.         slidingDirection = Vector3.ProjectOnPlane(Vector3.down, hitInfo.normal);
    15.         controller.Move(slidingDirection * slidingSpeed * Time.deltaTime);
    16.     }
    17.  
    18. private bool OnSlope() {
    19.         if (Physics.SphereCast(body.position, checkSphereRadius, Vector3.down, out hitInfo, 1f, groundLayer)) {
    20.             // Slope Angle
    21.             float angle = Vector3.Angle(hitInfo.normal, Vector3.up);
    22.             if (angle > controller.slopeLimit) {
    23.                 return true;
    24.             }
    25.         }
    26.  
    27.         return false;
    28. }
    29.