Search Unity

Circle-to-circle collision with Collider.Cast

Discussion in 'Physics' started by SorneBachse, Mar 27, 2020.

  1. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Hey everyone!

    I'm trying to handle circle-to-circle collision with the use of collider.Cast and it got it working properly. But as soon as I have an offset to my collider (either with the transform component, or the offset values on the collider) it goes wrong.

    I've tried experimenting with lots of different things, but none seem to do the trick. I have a gif showing the issue here:



    The code for the circle displacement calculations is this:
    Code (CSharp):
    1.     public static Vector2 CalculateCircleDisplacement(CircleCollider2D displacementCircle, CircleCollider2D otherCircle)
    2.     {
    3.         float displacementPosX = displacementCircle.transform.position.x + displacementCircle.offset.x;
    4.         float displacementPosY = displacementCircle.transform.position.y + displacementCircle.offset.y;
    5.  
    6.         float otherPosX = otherCircle.transform.position.x + otherCircle.offset.x;
    7.         float otherPosY = otherCircle.transform.position.y + otherCircle.offset.y;
    8.  
    9.         float distanceX = displacementPosX - otherPosX;
    10.         float distanceY = displacementPosY - otherPosY;
    11.  
    12.         float sumRadius = displacementCircle.radius + otherCircle.radius;
    13.  
    14.         float length = Mathf.Sqrt(distanceX * distanceX + distanceY * distanceY);
    15.  
    16.         if (length == 0.0f)
    17.         {
    18.             length = 1.0f;
    19.         }
    20.  
    21.         float unitX = distanceX / length;
    22.         float unitY = distanceY / length;
    23.  
    24.         return new Vector2(otherPosX + (sumRadius) * unitX,
    25.             otherPosY + (sumRadius) * unitY);
    26.     }
    Here is the movement code:

    Code (CSharp):
    1.     private void InternalMove(Vector2 movement)
    2.     {
    3.         if (settings.DynamicObstacleCollision)
    4.         {
    5.             if (ColliderCast(movement * Time.fixedDeltaTime, dynamicObstacleFilter, out RaycastHit2D hit))
    6.             {
    7.                 Vector2 displacement = PhysicsUtils.CalculateCircleDisplacement(bodyCollider, hit.collider as CircleCollider2D);
    8.  
    9.                 if (showDebug)
    10.                 {
    11.                     Debug.DrawRay(bodyCollider.transform.position, displacement + movement * Time.fixedDeltaTime, Color.cyan);
    12.                 }
    13.  
    14.                 body.MovePosition(displacement + movement * Time.fixedDeltaTime);
    15.                 return;
    16.             }
    17.         }
    18.  
    19.         body.MovePosition(body.position + movement * Time.fixedDeltaTime);
    20.     }
    Also to note, the ColliderCast function works even when the collider is offset. So it has to be something inside the CalculateCircleDisplacement method I would say. But this is the ColliderCast code:

    Code (CSharp):
    1.     public bool ColliderCast(Vector2 direction, ContactFilter2D filter, out RaycastHit2D hit, Collider2D collider = null)
    2.     {
    3.         if (collider == null)
    4.         {
    5.             collider = bodyCollider;
    6.         }
    7.  
    8.         RaycastHit2D[] colliderCastHits = new RaycastHit2D[10];
    9.         int numHits = collider.Cast(direction, filter, colliderCastHits, direction.magnitude);
    10.  
    11.         if (numHits > 0)
    12.         {
    13.             hit = colliderCastHits[0];
    14.             return true;
    15.  
    16.             float dot = Vector2.Dot(direction.normalized, (hit.transform.position - bodyCollider.transform.position).normalized);
    17.             if (dot > 0.0f)
    18.             {
    19.                 return true;
    20.             }
    21.         }
    22.  
    23.         //No collision
    24.         hit = new RaycastHit2D();
    25.         return false;
    26.     }
    The movement code is of course run in FixedUpdate.
    Any help would gladly be appreciated!
     
    Last edited: Mar 27, 2020
  2. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    If your cast is swept over:

    movement * Time.fixedDeltaTime

    ... then the distance you need to move is simply:

    movement * Time.fixedDeltaTime * hit.fraction


    ... therefore you don't need the "PhysicsUtils.CalculateCircleDisplacement" call at all.

    Also, you should really store "displacement + movement * Time.fixedDeltaTime" and not keep recalculating it.

    Some other things to note:
    • Don't use Transform.position as the place where the body is. If you're using interpolation then that won't necessarily be the case. Queries use the current body positions i.e. the state of the physics sytem and NOT the state of the Transform system. Always use Rigidbody2D.position.
    • Every method in 2D physics that returns multiple results has an overload that allows you to specify a generic list i.e. List<T> so List<RaycastHit2D>. Create this list and reuse it. Queries will resize it as appropriate. If you use fixed arrays then do it once and reuse it. In your method you create it each call so it then gets left as garbage which will cause issues. If you only want a single result then use an array with a single element and reuse it. The results are always sorted before they are populated into the array so an array of length 1 will have the closest result.
    • Your dot-product doesn't need to use normalized vectors as you only seem interested in > 0.
    • Your dot-product might work for circles but they are wrong for other shapes or shapes not centered on the transform.position. Don't use Transform.position as anything meaningful all the time. I'm guessing your dot-product is to check you don't hit something behind you that you're just touching? If so, do the same but use the dot-product of the hit normal and the direction and look for opposite i.e. < 0.
    • There's RaycastHit2D.centroid which gives you the center of the shape when it hits. This is its position. Don't forget that this is offset by the collider offset so you'll need to subtract that if using this. This won't be the case if you use Rigidbody2D.Cast.

    Hope that helps.
     
  3. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Hey, thanks for the very thorough explanation! A lot of stuff I need to tweak and rework I can see.

    But, I tried replacing my line of code with yours like this:

    Code (CSharp):
    1.     private void InternalMove(Vector2 movement)
    2.     {
    3.         if (settings.DynamicObstacleCollision)
    4.         {
    5.             if (ColliderCast(movement * Time.fixedDeltaTime, dynamicObstacleFilter, out RaycastHit2D hit))
    6.             {
    7.                 body.MovePosition(body.position + movement * Time.fixedDeltaTime * hit.fraction);
    8.                 return;
    9.             }
    10.         }
    11.  
    12.         body.MovePosition(body.position + movement * Time.fixedDeltaTime);
    13.     }
    That unfortunately didn't work either. Now the "player" circle is just stuck whenever it interacts with the other one:


    In regards to this not working with other physics shapes is fine, because all of my units are gonna have circle colliders. The dot product check was mostly just to not collide when the movement is in the opposite direction of the colliders.

    Basically what I'm trying to accomplish is getting that "sliding" behaviour when I move directly into the circle.
     
    Last edited: Mar 27, 2020
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Because you're moving into touching the other circle. Next time you perform a cast in any direction, you're already contacting the circle at time=0. Because you're only looking at the first result you never move.

    You can dot the collision-normal and the movement (sweep) direction to ensure they're in opposite directions (<0). If they are not then check the next hit. You'd need to return at least two hits so make your array size two elements.

    You can also not move all the way but move most of the way minus a small buffer so you're not actually moving into absolute contact.

    Also, if it's all circles, you really only need to know if you contact. The sum radius of both the circles tells you where to move to as well. Lots of other strategies for this kind of movement.
     
  5. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    btw, I thought I'd let you know about the following query in-case you didn't know about it already. Given two colliders, you can ask about their distance, including overlap. It gives you points on both colliders, direction and distance.

    Physics2D.Distance (also note there's a Rigidbody2D.Distance & Collider2D.Distance too)

    It can be very useful as a simple overlap solver but also when you want to move colliders together.
     
  6. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Alright, thanks MelvMay! I'm gonna give those options a try. I got it working currently with the dot product check, but no sliding is occuring because of that. Gonna have to investigate on how to move the body correctly to get the sliding effect.
     
  7. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Got your covered on that. ;) So I have this GiHub repo that demonstrates a lot of 2D physics here.

    This scene, whilst demonstrating a Kinematic controller, does show how to use queries to produce a sliding effect. I specifically use this script to calculate the change of direction based upon a hit. It might provide some useful info for your use-case. It shows how to try to move a certain distance, culling away hits you encounter until you can no longer move or run out of iterations.

    Hope it helps.
     
    SorneBachse likes this.
  8. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Amazing! Thank you so much for your help! Gonna have a go through that :-D
     
    MelvMay likes this.
  9. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Hey MelvMay!

    Thanks for your code. I got a version of a kinematic character controller working in my game now which is awesome.

    Now, I'm trying to convert our old "Dodge Roll" code to this new code and I'm having trouble seeing if this is possible with how it's implemented. Previously, we just used the Rigidbody2D.MovePosition(dodgePos), and the dodge pos was fed into the function from an animation curve with a start and end position. This way we would have control over the whole dodge and the easing.

    But with this new way (movement logic is pretty much the same as your github example), if we just use MovePosition, we lose the collision logic. I'm having difficulty thinking of a way to have the same control over my dodge roll, but still have the collision as well.

    Movement Logic:
    Code (CSharp):
    1.     private void InternalMove(Vector2 movement)
    2.     {
    3.         if (movement.sqrMagnitude <= EPSILON)
    4.         {
    5.             return;
    6.         }
    7.  
    8.         Vector2 startPos = body.position;
    9.         Vector2 moveDirection = movement.normalized;
    10.  
    11.         float distanceRemaining = movement.magnitude * Time.fixedDeltaTime;
    12.         int currentIterations = settings.MaxIterations;
    13.  
    14.         while (currentIterations-- > 0 && distanceRemaining > EPSILON && moveDirection.sqrMagnitude > EPSILON)
    15.         {
    16.             float distance = distanceRemaining;
    17.  
    18.             int hitCount = bodyCollider.Cast(moveDirection, settings.MovementFilter, movementHits, distance);
    19.  
    20.             if (hitCount > 0)
    21.             {
    22.                 RaycastHit2D hit = movementHits[0];
    23.                 ColliderDistance2D colliderDistance = Physics2D.Distance(bodyCollider, hit.collider);
    24.  
    25.                 if (hit.distance > settings.ContactOffset)
    26.                 {
    27.                     distance = hit.distance - settings.ContactOffset;
    28.                     body.position += moveDirection * distance;
    29.                 }
    30.                 else if (colliderDistance.isOverlapped)
    31.                 {
    32.                     distance = colliderDistance.distance;
    33.                     body.position += colliderDistance.normal * colliderDistance.distance;
    34.                 }
    35.                 else
    36.                 {
    37.                     distance = 0.0f;
    38.                 }
    39.  
    40.                 moveDirection -= hit.normal * Vector2.Dot(moveDirection, hit.normal);
    41.             }
    42.             else
    43.             {
    44.                 body.position += moveDirection * distance;
    45.             }
    46.  
    47.             distanceRemaining -= distance;
    48.         }
    49.  
    50.         Vector2 targetPosition = body.position;
    51.         body.position = startPos;
    52.  
    53.         body.MovePosition(targetPosition);
    54.     }
    55.  
    56.     public void SetPosition(Vector2 position)
    57.     {
    58.         body.MovePosition(position);
    59.     }

    Dodge Logic:
    Code (CSharp):
    1.     private IEnumerator IEDash(Vector2 direction, DashSettings settings)
    2.     {
    3.         dashing = true;
    4.  
    5.         float timer = 0.0f;
    6.         float t;
    7.  
    8.         Vector2 startPos = body.GetPosition();
    9.         Vector2 endPos = startPos + direction.normalized * settings.Distance;
    10.  
    11.         IMDraw.Line3D(startPos, endPos, Color.yellow, settings.Duration);
    12.  
    13.         while (timer < settings.Duration)
    14.         {
    15.             t = settings.Curve.Evaluate(timer / settings.Duration);
    16.             Vector2 tPos = Vector2.Lerp(startPos, endPos, t);
    17.  
    18.             body.SetPosition(tPos);
    19.             IMDraw.WireBox3D(tPos, Vector3.one * 0.2f, Color.yellow);
    20.  
    21.             timer += Time.deltaTime;
    22.             yield return null;
    23.         }
    24.  
    25.         //while (timer < settings.Duration)
    26.         //{
    27.         //    t = settings.Curve.Evaluate(timer / settings.Duration);
    28.  
    29.         //    Vector2 tPos = Vector2.Lerp(startPos, endPos, t);
    30.         //    Vector2 moveDir = tPos - previousPos;
    31.  
    32.         //    body.Move(moveDir.normalized * 5.0f);
    33.  
    34.         //    previousPos = tPos;
    35.  
    36.         //    timer += Time.deltaTime;
    37.         //    yield return null;
    38.         //}
    39.  
    40.         dashing = false;
    41.         unit.Input.ActivateAllActions(true);
    42.         unit.StateMachineController.StateMachine.ChangeState(UnitStates.UnitState.Idle);
    43.     }
    As you can see, I've also tried with calling Move (which just sets the movement variable that the body uses) from the direction based on current and previous position, but with no luck.

    Any pointers on how to handle this?
     
  10. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Sorry, I don't really follow and it's not easy to visualise what's supposed to be going on. I have no idea what a "dodge roll" is or what's not working. I don't follow what you mean by "loosing collision logic".

    If you can perhaps ask a more specific question I might be able to advise you.
     
  11. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Sorry, I'll try to be more specific.

    So, this is a gif of how our dodge roll looks like in-game, working with the code from above:



    EDIT: gif dead for some reason? Here's the link: https://i.imgur.com/2SpN1LP.gif

    As you can see, no collision is happening because we are just calling the "body.SetPosition(tPos)" on line 18 in the IEDash() IEnumerator.

    Do you have any guidance on how we would achieve this same dodge roll, with our custom animation curve, and still have collisions?
    Because it seems to me, that when we want that control over our dodge roll, from an animation curve, we have to "break" the physics and set the position directly.

    I hope that helped clear up some of the confusion.
     
  12. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Okay so I can see the GIF but no idea what's happening. :(

    The part I'm not clear on is why can you not have collisions? You can get callbacks, perform queries, use GetContacts etc. I'm trying to figure out if you're saying you want to use the "InternalMove" stuff instead or there's some specific thing that's stopping you receiving or polling-for collisions.
     
  13. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Are you saying you were not using a Kinematic body before but now you are and you want to know what you're colliding with?

    If so, you can turn on Rigidbody2D.useFullKinematicContacts which forces kinematic/kinematic and kinematic/static contacts to be calculated (by default only Kinematic/Dynamic happen). They'll be both reported using the standard callbacks but they'll also be available using GetContacts. This doesn't result in a collision response because it's a Kinematic body.

    Note that you seem to be doing this stuff per-frame but unless you run the physics per-frame, you'll only get results from contacts after the simulation has run which by default is during the FixedUpdate which isn't necessarily per-frame.
     
  14. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62

    Haha, I guess I'm just very bad at explaining myself :p

    So, the problem is that the InternalMove method, where all the collision stuff is happening, is passed in a movement vector. A direction, basically.

    But when I'm using the IEDash IEnumerator, I'm settings the position directly, meaning I can't use the InternalMove where all the collision checks are.
    So I guess my question is just how I would go on about checking for collision, when I'm setting the position directly. The only thing I've thought of is comparing the new position passed in, to the body's current position and check for overlaps etc.

    EDIT: The rigidbody has the same settings as yours (bodyType is kinematic, useFullKinematicContacts).
    The InternalMove is called with FixedUpdate(), but you're correct the SetPosition() is called from a coroutine which runs in Update I guess.
     
    Last edited: Mar 30, 2020
  15. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Okay, so I tried implementing the same collision logic as in the InternalMove (just for testing) and that gave me this result:


    ( https://i.imgur.com/i4sfoOu.gif )

    With the code:

    Code (CSharp):
    1.     private IEnumerator IEDash(Vector2 direction, DashSettings settings)
    2.     {
    3.         dashing = true;
    4.  
    5.         float timer = 0.0f;
    6.         float t;
    7.  
    8.         Vector2 startPos = body.GetPosition();
    9.         Vector2 endPos = startPos + direction.normalized * settings.Distance;
    10.  
    11.         IMDraw.Line3D(startPos, endPos, Color.yellow, settings.Duration);
    12.  
    13.         while (timer < settings.Duration)
    14.         {
    15.             t = settings.Curve.Evaluate(timer / settings.Duration);
    16.             Vector2 tPos = Vector2.Lerp(startPos, endPos, t);
    17.  
    18.             body.SetPosition(tPos);
    19.             IMDraw.WireBox3D(tPos, Vector3.one * 0.2f, Color.yellow);
    20.  
    21.             timer += Time.deltaTime;
    22.             yield return null;
    23.         }
    24.  
    25.         dashing = false;
    26.     }
    Code (CSharp):
    1. public void SetPosition(Vector2 position)
    2.     {
    3.         Vector2 move = position - body.position;
    4.  
    5.         IMDraw.WireSphere3D(body.position, bodyCollider.radius, Color.yellow);
    6.  
    7.         Vector2 startPos = body.position;
    8.         Vector2 moveDirection = move.normalized;
    9.  
    10.         float distanceRemaining = move.magnitude;
    11.         int currentIterations = settings.MaxIterations;
    12.  
    13.         while (currentIterations-- > 0 && distanceRemaining > EPSILON && moveDirection.sqrMagnitude > EPSILON)
    14.         {
    15.             float distance = distanceRemaining;
    16.  
    17.             int hitCount = bodyCollider.Cast(moveDirection, settings.MovementFilter, movementHits, distance);
    18.  
    19.             if (hitCount > 0)
    20.             {
    21.                 RaycastHit2D hit = movementHits[0];
    22.                 ColliderDistance2D colliderDistance = Physics2D.Distance(bodyCollider, hit.collider);
    23.  
    24.                 if (hit.distance > settings.ContactOffset)
    25.                 {
    26.                     distance = hit.distance - settings.ContactOffset;
    27.                     body.position += moveDirection * distance;
    28.                 }
    29.                 else if (colliderDistance.isOverlapped)
    30.                 {
    31.                     distance = colliderDistance.distance;
    32.                     body.position += colliderDistance.normal * colliderDistance.distance;
    33.                 }
    34.                 else
    35.                 {
    36.                     distance = 0.0f;
    37.                 }
    38.  
    39.                 moveDirection -= hit.normal * Vector2.Dot(moveDirection, hit.normal);
    40.                 IMDraw.WireSphere3D(position, bodyCollider.radius, Color.red);
    41.  
    42.             }
    43.             else
    44.             {
    45.                 body.position += moveDirection * distance;
    46.                 IMDraw.WireSphere3D(position, bodyCollider.radius, Color.blue);
    47.             }
    48.  
    49.             distanceRemaining -= distance;
    50.         }
    51.  
    52.         Vector2 targetPosition = body.position;
    53.         body.position = startPos;
    54.  
    55.         body.MovePosition(targetPosition);
    56.     }
    As you can see, the character snaps around corners, because it's just trying to set the next position between the start and end of the dodge.

    I don't have much clue on how to handle this, while still maintaining the "slidyness" when collider with walls etc.
     
  16. SorneBachse

    SorneBachse

    Joined:
    Dec 27, 2012
    Posts:
    62
    Just wanted to say I got it fixed the way I wanted! :) I'm handling the dodge roll now with the same movement code as before, just with previous position and current position as actual movement.

    Thanks again MelvMay.
     
    MelvMay likes this.
  17. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,487
    Oh sorry, I forgot to get back to you. Glad you got it working!
     
    SorneBachse likes this.