Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

2D how to detect if player is grounded on slope [raycast]

Discussion in 'Physics' started by MitjaHD, Nov 4, 2018.

  1. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Player is a 2d circle sprite and I'm using raycast to check if player is on ground. Lenght of ray is 0.51f just enough to reach the collider on ground. But if I'm not on the flat surgace the ray is not long enough to go through collider so I can't jump. I could make the ray longer but then I could jump mulitple times because the ray would still be inside collider.
     
  2. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
  3. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    I lied earlier when i said i use raycasts and you should too, i use spherecast and you should find the 2D version of that(circlecast?) if melv didnt already said it
     
  4. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    I kept the raycast, but changed the distance to GetComponent<Collider2D>().bounds.extents.y + 0.1f and for some reasons it is possible to jump on slopes now as well. Previously I had the distance set to 0.51 which should be the same but it didn't work, i don't know.

    But now my jumping sometimes results in weird behaviours such as very low/very high jumps, especially on box gameobject.

    This occurs when holding down space (jump button) and moving around, noramlly it should jump around 4 units, which is the very left ray, others are either too small or too big.

    Code (CSharp):
    1. bool IsGrounded()
    2.     {
    3.         Vector2 position = transform.position;
    4.         Vector2 direction = Vector2.down;
    5.         Debug.DrawRay(position, direction*DistanceToTheGround, Color.yellow, 10f);
    6.         RaycastHit2D hit = Physics2D.Raycast(position, direction, DistanceToTheGround + 0.1f, groundLayer);
    7.         if (hit.collider != null)
    8.         {
    9.             return true;
    10.         }
    11.         return false;
    12.     }
    Code (CSharp):
    1. void Update()
    2.     {
    3.         if (Input.GetButton("Jump") && IsGrounded())
    4.         {
    5.             jump = true;
    6.         }
    7.     }
    Code (CSharp):
    1. if (jump)
    2.         {
    3.             rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
    4.             jump = false;
    5.         }
     
  5. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Alright I think I fixed my own problem. I added rb.velocity = new Vector2(rb.velocity.x, 0f); when you jump so the y velocity becomes 0 just before the jump and then adds force.

     
  6. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    1) this would work depending on your game, if for example you can pick up some speed and hit a ramp while you jump to get extra height you're cancelling it out, but if it don't matter it don't matter.

    2) use GetButtonDown instead of GetButton to fire the jump event only once

    (i'm not saying anything more about the IsGrounded because i have 0 experience with 2D)
     
  7. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Yeah there is CircleCast but it's confusing, the syntax is: RaycastHit2D[] hits = Physics2D.CircleCastAll(position, 1f, direction, DistanceToTheGround, groundLayer);

    So it's an array, but I don't know how to make it return either true or false, also I can't draw it to visiulize where it is colliding.
     
  8. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Oops, sorry I I guess I didn't realize it doesn't have to be an array: RaycastHit2D hit = Physics2D.CircleCast(position, 1f, direction, DistanceToTheGround, groundLayer);

    But still since I can't see it I can't decide where it should be, right now I can't jump before I touch the ground.
     
  9. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Alright so what I did is I added another bool isGroundedOnSlope and this simply returns true by comparing the tag of collider (and collider must have a tag "slope"), so basically simple oncollision enter. So each platform that will be angled will have to have a tag "slope" which for my game isn't that big of a deal because there won't be many, I know this isn't the best solution but I don't really understand others, raycast could work but that would be just another same circle collider with offset. The reason I can't use oncollisionenter for everything is because if I hit the roof or even a wall, this would detect a collision and I could jump.
    Thanks for help everyone, but I'm sure I will find some bugs again sooner or later :)
     
  10. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    I thought it might be worth mentioning that you can easily take any Collider2D and perform a Cast on it with Collider2D.Cast. You only need to provide a direction and an empty array of results it can fill in and it returns you how many results were returned. It'll automatically take all the collider shape(s) and cast them through the scene for you. This makes "visualisation" easier as you only have to visualise the direction/distance.
     
    Muhammad_Sannan likes this.
  11. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Okay but how would I replace a raycast with this collider2D cast to check if my player is grounded. It returns the number of results returned. If I used it it should return true when in contact with ground (some other collider) and false when it's not.
     
  12. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    The only difference between the two is the geometry used i.e. your collider geometry rather than a ray. Your "player" isn't a ray but an actual collider.

    Cast the collider in the direction/distance you want (presumably towards the ground) filtering by your "groundLayer". I am not sure what your criteria is but presumably any results means you're grounded.

    TBH, I'm still not sure why casting a distance beyond where you're actually touching is considered grounded. In reality, you're only grounded if you're actually touching something i.e. there are contacts. This is why I posted the comment above about the "IsTouching" which checks if there are any current contacts. This gives you a true/false result. You can ask, "are there any contacts below me on this specific layer".
     
    PGJ likes this.
  13. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Yeah, but what is "below" in a rotating circle? First I used simply oncollisionenter, but then I was grounded even if I was touching a wall or something above me (roof). I modfied my code to cast a "circle collider" below me.

    Code (CSharp):
    1.     bool IsGrounded()
    2.     {
    3.         int maxResultCount = 1;
    4.         var hitResults = new RaycastHit2D[maxResultCount];
    5.         myCollider2D.Cast(Vector2.down, hitResults,0.01f);
    6.         if (hitResults[0] == true)
    7.         {
    8.             return true;
    9.         }
    10.         return false;
    11.     }
    And it works fine, but 0.01f doesn't allow me to jump on angled surfaces, changhing it 0.02f however fixes it.
    It's funny that I wrote the code, but don't really understand it. hitResults is an array that holds information about object that was hit. myCollider2D (circle) casts a circle down for 0.01f and it also holds results. Then I check if it hit the first element and returns true.

    I don't know how to add a layerMask, simply adding a ", groundLayer" doesn't work because it says: Argument 4: cannot convert from 'UnityEngine.LayerMask' to 'bool'. I have set the groundLayer to be a public LayerMask.
     
  14. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    Sounds like you're struggling with the basics of the language here too. Let's step back a little.

    When you're in contact with anything then the contact defines some important properties. The first is the contact point or points, the second is the collision normal. If you look purely at a contact with the ground layer then as you've already seen, you could contact the ground layer above you if you were to jump up into it. To know if this is happening, particularly if you're only interested in contacts below your player, you need to consider the collision normal.

    If the contact is above your player then the collision normal will be pointing downwards. Likewise, if the contact is below you, it'll be pointing upwards. You can easily look at the collision normal and check this to decide if the contact is above, below, to the side or whatever inbetween.

    You can use the standard collision callbacks to know when contacts happen but this means you need to put code in the OnCollisionEnter2D and OnCollisionExit2D to track if you're in contact. By far the better way is to ask if contacts exist and then filter them by collision normal. Getting contacts however can seem complex if you're just starting so we also provide a few helpers like "IsTouching()" which is basically saying, give me a yes/no if I have a specific contact. The nice thing is, this method can even filter by the angle of the collision normal, the layer(s) etc.

    Here's an example script that looks to see if a Rigidbody2D is "touching" i.e. has any contacts. It will also filter using the ContactFilter2D which allows filtering only the contacts you're interested in:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class CheckGrounded : MonoBehaviour
    4. {
    5.     public ContactFilter2D Filter;
    6.     public bool IsGrounded;
    7.  
    8.     private Rigidbody2D rb;
    9.  
    10.     void Start()
    11.     {
    12.         rb = GetComponent<Rigidbody2D>();  
    13.     }
    14.  
    15.     void Update()
    16.     {
    17.         IsGrounded = rb.IsTouching(Filter);
    18.     }
    19. }
    20.  
    Here's how I configured it: https://oc.unity3d.com/index.php/s/5R7huodLSOgY3I6 but you can also configure it from script rather than using the inspector. Also, I'm using the "Rigidbody2D.IsTouching(filter)" which checks all the colliders on that Rigidbody2D but you can also use "Collider2D.IsTouching(filter)" to check a specific Collider2D.

    As you can see, I checked the "Use Layer Mask" then selected my "Ground" layer although you can check multiple layers if you wish. I also checked the "Use Normal Angle" and specified the "Min/Max Normal Angle" as "75" and "105". What this means is that I'm only interested in angles between 75 and 105 degrees. 0-degrees is right, 90-degrees is up, 180-degrees is left and 270-degrees is down.

    What this will do then is only use contacts it finds which are touching the "Ground" layer and whose collision normal is pointing upwards from 75-105 degrees. This means the collision was below.

    Note you can see these contacts and collision normals if you go into "Physics 2D settings > Gizmos > Show Collider Contacts".
     
  15. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Yes I'm new to C# and Unity, this is my first game. Thanks for explanation it is more clear now, but instead of isTouching I will continue using cast since it's working fine right now, I also added Filter (myCollider2D.Cast(Vector2.down, filter, hitResults,0.02f);) so I can now set layermask and other properties if necessery.
     
    Last edited: Nov 5, 2018
    MelvMay likes this.
  16. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    Cool. In the end, it's whatever works!
     
  17. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Hey again, I found a bug where I could jump if I was holding down space bar and hit the wall/edge, so I guess this casting below my player isn't that good. I tried your suggestion with isTouching and it works much better the only problem is that I have a surface that is angled 45 degrees so 75 and 105 doesn't work for me (I understand why), chaning it to about 30 works, but I still don't quite understand how are these pointing down, to me it looks going up, but I'm stupid.
     
  18. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    Look at the contact-points and the collision normals in a callback or look at the them via the gizmo option I mentioned above. The collision point is the point of contact and the collision normal is a unit vector that points away from the collider edge you hit. If you search for collision normal on Google you'll get lots of info and images, much better than I can described here. :)

    A nice way of thinking about it is that the contact point is where a force would be applied to move you away from the contact/overlap and the collision normal is the direction the force would be applied. If you jump up into a platform, the contact point would be above you and the collision normal point downwards; this is where and the direction the force was applied to stop you going into the platform. Same goes when you hit the ground below you, contact point below and collision normal pointing upwards.
     
  19. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Hello once more, I might be getting a little annoying already. I have one more question, I want to play a sound whenever I hit a surfce, so far I have 3 different surfaces (grass,wood,trampoline), each with unique tag but as far as I understand IsTouching only returns true/false so it' can't check tags. I could use raycast's collider to check for tag, but then it wouldn't be as precise as as IsTouching. In Filter settings there is layermask, but that doesn't help me much. Thanks for answer.
     
  20. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    The "IsTouching" method simply gets the contacts and filters them by whatever you specify. If there's at least as single contact that matches the filtering then it returns true, if there are none then it returns false. It throws away the contacts it fetches.

    I'm telling this you because it means that you can use "GetContacts()" to ask the exact same question as "IsTouching" does. If you get any contacts then it's touching and if not then it's not touching but this also means you have the contacts themselves which contain all the contact details. There's also an overload of "GetContacts()" that simply returns you the Colliders in contact and not the other details.

    It's worth checking out "GetContacts()".
     
  21. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Code (CSharp):
    1. Rigidbody2D rb;
    2.     public ContactFilter2D filter;
    3.     Collider2D[] colliders = new Collider2D[1];
    4.  
    5.     void Start ()
    6.     {
    7.         rb = GetComponent<Rigidbody2D>();
    8.     }
    9.    
    10.     void Update ()
    11.     {
    12.         rb.GetContacts(filter, colliders);
    13.     }
    14.  
    15.     void OnCollisionEnter2D(Collision2D collision)
    16.     {
    17.         if (colliders[0].CompareTag("Wood"))
    18.         {
    19.             Debug.Log("wood touched");
    20.         }
    21.     }
    The script is working, but I'm getting 1 error that on line 25(if(colliders[0].CompareTag("Wood"))), Object reference not set to an instance of an object. I must've done something wrong.
     
  22. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    "GetContacts()" returns how many contacts were returned, it might be zero and "colliders[0]" might be NULL or it could still be a valid collider from the last time you called it. Always best to store how many results you got during the last query.

    BTW, Why are you getting contacts during "Update()"? Contacts are only updated during the fixed update or when you perform a manual simulation. I'm also not following why you're checking for contacts in a OnCollisionEnter2D callback. The callbacks provide the contact information themselves i.e. "collision.GetContacts()" but this returns the contacts passed to that callback only.
     
  23. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Yes I completley messed up the code
    Code (CSharp):
    1. public ContactFilter2D filter;
    2.     Collider2D[] colliders = new Collider2D[1];
    3.  
    4.     void OnCollisionEnter2D(Collision2D collision)
    5.     {
    6.         collision.otherCollider.GetContacts(filter, colliders);
    7.  
    8.         if (colliders[0].CompareTag("Wood"))
    9.         {
    10.             Debug.Log("wood touched");
    11.         }
    12.     }
    I was confused for a second, because I used collider instead of otherCollider, the second one represents the Player (the one who hit into something) I guess.
     
  24. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    What I'm trying to get across though is that you're doing a general contact query in a callback that's already passing you the contacts. If the "OnCollisionEnter2D" is on the player then it already passes you the contacts. :)

    GetContacts is typicially when you're not using the callbacks but instead are polling the contacts yourself.
     
  25. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Would it better be to then check the contacts on rigidbody in FixedUpdate? I undersand that oncollisionenter2d gives you the contats and you can compare tag etc., but I think you can not use filter, I only want to play the sound when the ground is below me (same as IsTouching, but I don't need true/false, but the tag). If I could use filter to only execute code when collision is between set angles I would use collision.collider.commparetag, if that makes sense. I'm still in high school and I have learnt most of the programming by myself, thats probably why I suck.
     
  26. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    By the sound of what you want, yes. Simply grab the contacts you're interested in on the Rigidbody2D or specific Collider2D with a filter you want then do the logic with the results. The contacts will always be calculated anyway, asking for them doesn't add a lot of overhead and it's certainly quicker than using the collision callbacks.

    Hey don't put yourself down, you're trying and getting there step by step and that's how we all work in the end. Good luck.
     
  27. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Hey I have one more not-so-importnat question, the sound plays whenever I hit something below me, but sometimes I have terrain that has 2 colliders nex to each other so the sound plays 2 times If i go over them both (basically sound plays if I touch the collider no matter if I jumped before, which is normal becuse of my scritp). Maybe I could join these colliders to make them one though.



    Btw thansk for you free help, I really appriciate it.
     
  28. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328
    If you need to merge these two colliders together, use the CompositeCollider2D component. Note that the contacts will then be on the composite, not the individual colliders, same with the callbacks.

    Here's an older video when I was developing it which might help you visualise it:
     
  29. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Yes this is what I was looking for thanks. I have yet again one more minor problem with my sound.
    Code (CSharp):
    1.     public ContactFilter2D filter;
    2.     Collider2D[] colliders = new Collider2D[1];
    3.  
    4.     void OnCollisionEnter2D()
    5.     {
    6.         GetComponent<CircleCollider2D>().GetContacts(filter, colliders);        //pridobi komponento CircleCollider2D in njegov kontak (upoštevaj filter)
    7.         if (colliders[0].CompareTag("Wood"))                                    //če ima collider tag "Wood"
    8.         {
    9.             GameObject.Find("LandOnWood").GetComponent<AudioSource>().Play();   //predvajaj zvok LandOnWood
    10.         }
    11.         else if (colliders[0].CompareTag("Grass"))                              //če ima collider tag "Grass"
    12.         {
    13.             GameObject.Find("LandOnGrass").GetComponent<AudioSource>().Play();  //predvajaj zvok LandOnGrass
    14.            
    15.         }
    16.     }
    It all works fine, but if i hit a wall for example, the sound of what is underneath me plays. Because it detects collision and then gets the contact of collider below me. But I realized that even if I jump and touch the wall in air, it still plays the sound of grass even tho I'm not touching it at that point.
     
  30. MitjaHD

    MitjaHD

    Joined:
    Sep 30, 2018
    Posts:
    64
    Alright I think I solved my own problem

    Code (CSharp):
    1.     bool IsGrounded;
    2.     public ContactFilter2D filter;
    3.     Collider2D[] colliders = new Collider2D[1];
    4.     GameObject oldGO;
    5.     GameObject newGO;
    6.     Rigidbody2D rb;
    7.  
    8.     void Start()
    9.     {
    10.         oldGO = new GameObject();
    11.         rb = GetComponent<Rigidbody2D>();
    12.     }
    13.  
    14.     void FixedUpdate()
    15.     {
    16.         IsGrounded = rb.IsTouching(filter);
    17.         GetComponent<CircleCollider2D>().GetContacts(filter, colliders);
    18.         newGO = colliders[0].gameObject;
    19.  
    20.         if(IsGrounded && (newGO.tag!=oldGO.tag))
    21.         {
    22.             if (colliders[0].CompareTag("Wood"))                                    //če ima collider tag "Wood"
    23.             {        
    24.                 GameObject.Find("LandOnWood").GetComponent<AudioSource>().Play();   //predvajaj zvok LandOnWood
    25.             }
    26.             else if (colliders[0].CompareTag("Grass"))                              //če ima collider tag "Grass"
    27.             {
    28.                 GameObject.Find("LandOnGrass").GetComponent<AudioSource>().Play();  //predvajaj zvok LandOnGrass
    29.  
    30.             }
    31.             oldGO = newGO;
    32.         }
    33.         else if(!IsGrounded && rb.velocity.y > 5f)
    34.         {
    35.             oldGO = new GameObject();
    36.             newGO = new GameObject();
    37.         }
    38.     }
    I should've listened to you when you sait to use fixedupdate instead of collision enter. Now the sound plays only when I hit a surface with another tag, or when the velocity of player is more than 5f, so it's reasonable amout of fall/impact on the ground. This was important because when I had two same tag colliders together my player sometimes jumped just a little when it hit another one, even tho they were on same height.
     
    MelvMay likes this.
  31. ZionAtGSN

    ZionAtGSN

    Joined:
    Jan 10, 2018
    Posts:
    1
    Hi Melvin,

    How can a filter be set up for collisions to the left where the angle is less than 45 or greater than 315 for example?
     
  32. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,328