Search Unity

Walking and jumping on a sloped surface

Discussion in 'Scripting' started by Robster95, Feb 26, 2021.

  1. Robster95

    Robster95

    Joined:
    Jul 3, 2019
    Posts:
    154
    I'm having trouble with my 3d platformer game where I have flat surfaces and slopes in the scene. When the player is on a slope sometimes it registers that the player is actually on the slope or in the air (code below).
    I am trying to make this game in the style of Banjo Kazooie on the N64 but these problems with the player jumping on sloped surfaces are really confusing to me.

    I have separate code for the player being on the ground and being on the slope
    Code (csharp):
    1.  
    2.     public bool Grounded()
    3.     {
    4.         return Physics.CheckSphere(transform.position, checkRadius, groundLayer);
    5.     }
    6.  
    7.     public bool OnSlope()
    8.     {
    9.         RaycastHit hit;
    10.         bool raycast = Physics.Raycast(transform.position, Vector3.down, out hit, rayCastDist, groundLayer);
    11.  
    12.         // if it hits something
    13.         if(raycast)
    14.         {
    15.             // if what it hits is a slope
    16.             if (hit.normal != Vector3.up)
    17.             {
    18.                 return true;
    19.             }
    20.  
    21.             // if it's not a slope
    22.             else
    23.             {
    24.                 return false;
    25.             }
    26.         }
    27.  
    28.         // if it doesn't hit something
    29.         else
    30.         {
    31.             return false;
    32.         }
    33.     }
    34.  
    ^ is the code that registers whether the player is on the ground or on a sloped surface while:
    Code (csharp):
    1.  
    2.     private void AddGravity()
    3.     {
    4.         //on the ground
    5.         if (groundCheck.Grounded())
    6.         {
    7.             Debug.Log("On the ground");
    8.             doubleJump = true;
    9.             velocity.y = gravity;
    10.  
    11.             if (controller.stepOffset == 0)
    12.             {
    13.                 controller.stepOffset = .3f;
    14.             }
    15.  
    16.             if(playerAttack.doingGroundPoundAttack)
    17.             {
    18.                 playerAttack.doingGroundPoundAttack = false;
    19.             }
    20.         }
    21.  
    22.         //on a slope
    23.         else if (groundCheck.OnSlope())
    24.         {
    25.             doubleJump = true;
    26.  
    27.             velocity.y = gravity;
    28.  
    29.             Debug.Log("On a slope");
    30.  
    31.             if (controller.stepOffset == 0)
    32.             {
    33.                 controller.stepOffset = .3f;
    34.             }
    35.  
    36.             //transform.position = groundCheck.hit.point;
    37.  
    38.             if (playerAttack.doingGroundPoundAttack)
    39.             {
    40.                 playerAttack.doingGroundPoundAttack = false;
    41.             }
    42.         }
    43.  
    44.         //in the air
    45.         else
    46.         {
    47.             velocity.y += gravity * 1.5f * Time.deltaTime;
    48.  
    49.             Debug.Log("In Air");
    50.  
    51.             if (controller.stepOffset != 0)
    52.             {
    53.                 controller.stepOffset = 0;
    54.             }
    55.         }
    56.     }
    57.  
    ^ this is the code that registers how much gravity to apply to the player depending on the surface they're on.

    When the player is on a slope it registers they are standing on it a majority of the time but when I'm walking up the slop and try to jump (using a charactercontroller.move()) the slope check instantly recognizes the slope again and pulls the player back down or wont let me jump at all because it doesn't recognize the slope in every frame. But if I jump while walking down the slope I don't have this problem.

    I even have the jumping script turn off the groundcheck script for .5f so the player has enough time to get off the ground and jump.

    I have tried many different raycast distances on the slope check method and it doesn't seem to help much because I will continuously get the same problem of the script registering there is a jump.

    Please can someone help? I've watched multiple videos and looked up multiple coding sources / help but have been having trouble finding exactly what I need to fix this problem.
    Thank you
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    I think you way you are checking OnSlope() is problematic: even a perfectly flat plane might not have a normal that is PRECISELY up, so that's probably not a good thing to decide flat or not.

    Instead you probably only care when the rise-over-run is beyond what your character can climb, and you can get that just by seeing if the .y portion of the normal is below a certain amount. That up amount would be the cosine of the ground slope angle.

    For instance if you don't want them to climb more than 30 degrees, then when contemplating if you can move to a new location, if that location's normal vector y component is less than cosine(30) (which is 0.866), don't allow the player to move there.

    You can get trickier too and let him move there, but start pushing him downhill, but let's only solve one problem at a time. See simplicity below!

    Another improvement is to tilt your player input motion in the direction of the normal of what you're standing on that frame.

    This means whatever flat X/Z vector you are intending to move, rotate that vector first by the Quaternion.FromToRotation( Vector3.up, surfaceNormal), which will clean it up and not require you to fall so far, because essentially you'll be skimming the ground at each point, and collisions and your gravity faller should clean it up at the margins of each facet.

    As for falling off ledges, it can be useful to allow a few frames of "hey there's nothing under me" to detect that you're not on the ground, which will help transitions off small ledges.

    When you jump, it's useful to immediately remove your "is on ground" status, and to not even try to detect if you are on the ground if you are going upwards still in a jump arc.

    All in all this stuff is fiddly to get right, which is why there are so many ways to control a character in 3D space.

    Generally I find that refactoring it to have as few moving parts as possible is the big win. It seems that starting from a blank slate and layering the complexities on as sparingly as possible can really help to keep the solution clean.

    Remember, nobody makes a "final best ever wins the entire internet" movement solution. That doesn't exist.
     
    Robster95 and Joe-Censored like this.
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    You say that sometimes when on a slope it registers as being in the air. I'm sure this would occur if the distance to ground was actually greater than rayCastDist. I'm wondering if you've got that too short.

    Since you're using that in your AddGravity(), I'm also wondering if you're doing your movement before calling AddGravity(). If that were the case, and your movement is as usual multiplied by Time.deltaTime, you'd move farther in a single frame if the frame for some reason took much longer. If you were going down a slope, and you move further in the X/Z directions, you could end up with a larger distance between the character and ground that frame. That might make it farther than rayCastDist. (so every time framerate drops, you get reporting as in air instead of slope when going down the sloap) I'm doing a lot of speculation, because I don't see the rest of the movement code here.

    Alternatively, when going up the slope, I'm wondering if you're slightly clipping the slope, causing your raycast to originate slightly below the slope collider. That would cause it to completely miss the slope, and result in reporting that you're in air as well.

    @Kurt-Dekker 's answer is better than mine, just wanted to add my thoughts.
     
    Robster95 and Kurt-Dekker like this.
  4. Robster95

    Robster95

    Joined:
    Jul 3, 2019
    Posts:
    154
    Thank you for the very in depth response! I’m actually at work right now so I won’t be able to look into this with great detail and try to implement what you said into my code right now. However reading your response I did get a little confused. I was wondering will it be at all possible to message you outside of these threads? I’ve noticed you’ve responded to a few of my questions before and I appreciate all of your help and feedback it’s just hard to get the help I need through the chance of reading your response right after you sent it
     
  5. Robster95

    Robster95

    Joined:
    Jul 3, 2019
    Posts:
    154
    Thank you for your response as well. Something I noticed about the Ray cast is while going up the slope it looks like the Ray cast shoots into the floor a little more than it does when the player is just standing still and while the playing is running down the slope the Ray cast doesn’t seem to shoot far enough to register that the players on the slope. I’ve tried different distances but it seems the further it shoots the less it will allow the player to jump or let the play jump less often due the the Ray cast registering another hit while the player is trying to jump
     
  6. Robster95

    Robster95

    Joined:
    Jul 3, 2019
    Posts:
    154
    Hey guys just a quick update! I believe I was able to get a very simple solution for my problem. Probably not the best solution and may lead to some problems down the road but all I did was add a reference to my player movement script in the ground check and in the OnSlope() method I added an if statement that checks whether the players velocity.y is less than 0 then if it is check to see if the player is on a slope like this
    Code (csharp):
    1.  
    2. private PlayerMovement playerMovement;
    3.  
    4.     [SerializeField]
    5.     private float checkRadius;
    6.     [SerializeField]
    7.     private float rayCastDist;
    8.     [SerializeField]
    9.     private LayerMask groundLayer;
    10.  
    11.     private void Start()
    12.     {
    13.         playerMovement = GetComponentInParent<PlayerMovement>();
    14.     }
    15.  
    16.     public bool Grounded()
    17.     {
    18.         return Physics.CheckSphere(transform.position, checkRadius, groundLayer);
    19.     }
    20.  
    21.     public bool OnSlope()
    22.     {
    23.         if (playerMovement.velocity.y < 0)
    24.         {
    25.             RaycastHit hit;
    26.             bool raycast = Physics.Raycast(transform.position, Vector3.down, out hit, rayCastDist, groundLayer);
    27.  
    28.             // if it hits something
    29.             if (raycast)
    30.             {
    31.                 // if what it hits is a slope
    32.                 if (hit.normal != Vector3.up)
    33.                 {
    34.                     return true;
    35.                 }
    36.  
    37.                 // if it's not a slope
    38.                 else
    39.                 {
    40.                     return false;
    41.                 }
    42.             }
    43.  
    44.             // if it doesn't hit something
    45.             else
    46.             {
    47.                 return false;
    48.             }
    49.     }
    50.  
    51.         else
    52.         {
    53.             return false;
    54.         }
    55.     }
    56. }
    57.  
    ^ Very simple fix to the problem I had and now it works the way I intended!