1
\$\begingroup\$

This question has been updated to better reflect my current issue, and some of the progress made while solving it.

I've been working on a 2D character controller that uses raycasting to detect collisions with objects. Since Physics2D.Raycast and Physics2D.BoxCast both return a RaycastHit2D object, I was hoping to convert my raycast detection to boxcast detection, but I've been having trouble understanding how to do it. Unity's scripting API is a little vague on how BoxCast actually works; specifically, I don't understand the difference in use between the size and distance parameters.

My current method works like this:

  • I get the bounds of my box collider, and fire a raycast with an origin point.y of skinWidth inside the collider (this ensures that collisions can still be detected when the character is flat on the ground). skinWidth is a very small number (like .015f) used to achieve this result.
  • If I'm moving down, the raycast is fired from the bottom center (center.x, min.y) of my collider, otherwise it's fired from the top center.
  • The length of the raycast this frame is determined by deltaMovement.y + skinWidth, which is a reference to my character's velocity, but represents the change in movement this frame; this is also a small number. skinWidth is added back to this value to compensate for the ray beginning inside the box.
  • If the raycast hits the ground, deltaMovement is updated to be equal to the (hit.distance - skinWidth) * velocityDir. This means our character will move another hit.distance - skinWidth more before colliding with an object. skinWidth is subtracted because I added it to the ray's length, and I multiply by velocityDir to maintain my direction because hit.distance is always a positive value, even when moving down.

Here's what this looks like in code (this works exactly as I'd like):

private void CollideVertically(ref Vector2 deltaMovement)
{
    float velocityDir = Mathf.Sign(deltaMovement.y);
    float raycastLength = Mathf.Abs(deltaMovement.y) + skinWidth;
    Vector2 origin = (velocityDir == -1) ? bottomCenter : topCenter;

    //this makes the raycast move with the character on the x axis
    origin.x += deltaMovement.x 

    RaycastHit2D hit = Physics2D.Raycast(origin, Vector2.up * velocityDir, raycastLength, verticalCollisionMask);

    if (hit)
    {
        deltaMovement.y = (hit.distance - skinWidth) * velocityDir;
        State.IsCollidingAbove = velocityDir == 1;
        State.IsCollidingBelow = velocityDir == -1;
    }
}

I thought adapting this to work with boxcasts would be easy; after all, Physics2D.BoxCast has many of the same parameters. I ran into some issues, though, but here's my code:

private void CollideVertically(ref Vector2 deltaMovement)
{
    float velocityDir = Mathf.Sign(deltaMovement.y);
    float raycastLength = Mathf.Abs(deltaMovement.y) + skinWidth;
    Vector2 origin = (velocityDir == -1) ? bottomCenter : topCenter;
    origin.x += deltaMovement.x;
    Vector2 size = new Vector2(boxCollider.size.x, .02f);

    RaycastHit2D hit = Physics2D.BoxCast(origin, size, 0, Vector2.up * directionY, raycastLength, verticalCollisionMask);

    if (hit)
    {
        deltaMovement.y = hit.distance * velocityDir;
        State.IsCollidingBelow = velocityDir == -1;
        State.IsCollidingAbove = velocityDir == 1;
    }
}

One of the issues with this is simply how my character "feels" when using this method vs my raycast method. The movement doesn't feel quite as tight. Similarly, I have to use the arbitrary value of .02f as the box's size.y in order to make my character be flat on the ground. I've tried other values like skinWidth, or something like .03f, but these put my collider slightly in the ground or above it. I had initially used raycastLength as both the box's size.y and the distance parameter of the boxcast method, but this was inconsistent, and I think it produced "too much" of a box when casting (I was frequently colliding when my character was still airborne).

I also don't understand why I don't have to take away skinWidth in the line deltaMovement.y = hit.distance * velocityDir. I had been subtracting it, but this was producing a lot of bouncing; the movement wasn't right at all.

Ultimately, this related question helps explain why I want to use boxcasting. Instead of using many rays, each of which has a gap between it and the next ray, I'd like to use a boxcast to simulate a solid "sheet" of rays that can determine my collisions. Imagine a scenario in which the character is on the peak of a mountain; raycasts (with the aforementioned gaps) might miss this sharp point, but a boxcast will hit it, and that hit point will determine the character's movement.

Here's a little visualization I made about the two approaches: https://imgur.com/a/1di12By Am I thinking about this correctly? Should I be using Physics2D.OverlapBox instead?

\$\endgroup\$
  • \$\begingroup\$ Rather than appending updates to the bottom of your post, try updating the post as a whole to reflect your current problem. That way there can't be any misunderstandings of how you've modified your code since the first example - you can show us the very latest code you're working with. \$\endgroup\$ – DMGregory Dec 15 '18 at 15:25
2
\$\begingroup\$

I would very highly recommend that you visualise (using Debug.Draw... methods) your collision hit points/contacts and normals to better understand what's going on - actually, I'd say output all the info that RaycastHit2D gives you after a BoxCast2D. The main catch with BoxCast2D is that the hit point won't necessarily be "on the edge" of your collider (as it's the case with Raycast2D) - it can very likely be inside of your collider. Here's an image of how your BoxCast2D might behave:

enter image description here

If what you're trying to achieve is a precise "push out of collision" behaviour, you'll need to use BoxCast2D in tandem with Physics2D.Distance.

1) You BoxCast2D in the direction of movement.

2) Move your BoxCollider2D to the position where BoxCast2D collision happened (hit.centroid is where the box of a BoxCast2D was positioned when the hit happened).

3) Use Physics2D.Distance to find out how much you should push out your BoxCollider2D out of collision.

\$\endgroup\$
  • \$\begingroup\$ Very helpful diagram. I was printing my collision results earlier, and noticed that the hit.distance of my raycast was equal to skinWidth, while the hit.distance of my boxcast was 0. I guess that means the box is hitting inside my collider, which could explain my inability to be completely flat on the ground. Why would the box’s collision point be inside the collider though? \$\endgroup\$ – Ginger and Lavender Dec 16 '18 at 7:51
  • \$\begingroup\$ I believe that the way BoxCast2D works is that it casts a box along a ray each fixed distance. In other words, if your cast distance is D and BoxCast2D's cast step distance is N, the box will be cast D/N times - each time moving the box along the ray by distance of N. So during one step of the cast there might be no collision, but during the next cast step the box may partially or fully move inside a collider. When a collision happens, it seems that the hit point is the center of where the collider shapes overlap. \$\endgroup\$ – mt_ Dec 16 '18 at 8:02
  • \$\begingroup\$ You can also think of it this way: if you do a BoxCast2D downwards and by luck it so happens that it touched the ground collider just slightly, what would be a sensible collision point? The center of the bottom edge of the cast box. Now what if you're less lucky, maybe some lag happened, and your box went fully into the ground collider - what's a sensible collision point now? The center of the box. For all the other cases it's just an interpolation between the covered edge cases. \$\endgroup\$ – mt_ Dec 16 '18 at 8:09
  • \$\begingroup\$ Based on your last example, I think it would make more sense to use something like OverlapBox and set the position directly based on collision. As mentioned in another answer, and from what I’ve found through testing (even with raycasting), trying to dictate my deltaMovement via collision presents some concrete problems. If I move first, and then resolve collisions, I should have fewer cases where lag or some other disruption allows me to clip inside a wall, or have an undesirable collision point. \$\endgroup\$ – Ginger and Lavender Dec 16 '18 at 8:15
  • 1
    \$\begingroup\$ OverlapBox won't give you any hit points at all. It's just a check if there's a collision, but gives no info as to how you should solve it. I recommend you read through docs.unity3d.com/ScriptReference/ColliderDistance2D.html and you'll see that even the documentation states that you'll want to use ColliderDistance2D (returned by Physics2D.Distance ) when solving collisions of 2D kinematic rigidbodies. \$\endgroup\$ – mt_ Dec 16 '18 at 8:20
2
\$\begingroup\$

I think it's useful to visualize the physics 'Cast' operations as an object being thrown in a straight line and stopping when it hits another object (collider).

Size is the size of the box being "thrown".

Distance is how long that box is "thrown" in said direction.


When dealing with game physics an approach generally taken is to update positions, then calculate collisions and correct overlaps. This is done because it's very robust and easy. Trying to prevent overlaps before they happen (which it seems like you are trying to) is much harder and more prone to errors.

Translating that to your example that would mean:

  1. Add user input to velocity

  2. Add gravity to velocity

  3. Move by velocity*Time.deltaTime (velocity is movement divided by time, so by multiplying with time you get movement)

  4. Detect collisions and fix overlap

Your code would be the part slotted into nr 4 in that list.

Here's what I'd suggest:

2 & 3.

//in Update/FixedUpdate in controller script (use fixedDeltaTime if in FixedUpdate)
velocity += gravity*Time.deltaTime; //Gravity is an acceleration
transform.Translate(velocity*Time.deltaTime);

4.

void CollideVertically(){
    float velocityDir = Mathf.Sign(velocity.y);
    Vector2 size = new Vector2(boxCollider.size.x, boxCollider.size.y*0.1f);
    float rayLength = boxCollider.size.y/2 - boxCollider.size.y*0.1f/2; //Subtract how much the box being cast overshoots length
    RaycastHit2D hit = Physics2D.BoxCast(transform.position, size, 0, Vector2.up*velocityDir, rayLength);
    if (hit){
        velocity.y = 0;
        var overlap = transform.position.y + boxCollider.size.y/2 * velocityDir - hit.point.y;
        transform.Translate(new Vector3(0, -overlap, 0));
    }
}

You'll need a CollideHorizontally as well, which needs to execute before the vertical one to ensure that you can't climb up walls. But that should be identical to the vertical one, only with reversed X and Y.

As for the input movement (step 1), that will vary wildly from game to game, so that's entirely up to you.

Hope that helps :)

\$\endgroup\$
  • \$\begingroup\$ I appreciate your reply. Your code still seems to produce the bouncing/teleporting I have been seeing, though the actual collision based on the box size and rayLength appears more accurate. I never really considered doing collision detection the way you presented (move first, then resolve); do you know where I might be able to read up on this approach? \$\endgroup\$ – Ginger and Lavender Dec 16 '18 at 2:30
  • \$\begingroup\$ Oh, ops. Forgot to divide the boxCollider.size.y by 2 in the overlap. I've updated the answer and tested the code to make sure it works (also fixed the minor syntax errors along the way :D). \$\endgroup\$ – Karl Ramstedt Dec 16 '18 at 13:41
  • 1
    \$\begingroup\$ @GingerandLavender as for the reading material, I learned that from lectures in a physics course at my university. It's probably in a bunch of books and other sources on the topic, but sadly I can't point you to one I know contains it. We did have a course-book called Game Physics Engine Development by Ian Millington which seemed solid from the parts I read. Might be worth giving a look. \$\endgroup\$ – Karl Ramstedt Dec 16 '18 at 13:59
0
\$\begingroup\$

Unity has collision detection and collison resolution built in. If you want to know what collider your character is standing on, use OnCollisionEnter. No need to reinvent the wheel using raycasts.

If you want to know about colliders nearby, i.e. colliders a fixed distance away from your character but not yet touching it, use a trigger volume and OnTriggerEnter.

However, if for some reason you really need to know about colliders a dynamic distance away from your character, e.g. for predicting future collisions, a box cast comes in handy. Note the difference between Physics.BoxCast and Physics.OverlapBox though, a box cast moves the box along a line.

The reason why your code is not working is because you need to offset the origin of the cast by the half-extents of the box so that the entire box starts within your character bounds. If you start the cast near the bottom of your character bounds then half of the box already overlaps with the outside world.

\$\endgroup\$
  • \$\begingroup\$ The problem with using Unity's built in collisions is that (as far as I'm aware) you need to use a Rigidbody for movement, which means you're at the mercy of Unity's built in physics. This is fine for some things, but I wanted tighter controls, something more SNES-like, and thus I'm trying to program my own collisions and physics. \$\endgroup\$ – Ginger and Lavender Dec 16 '18 at 7:00
0
\$\begingroup\$

So after trying some more things today, I've got this working about as well as I think I can. Here's the boxcast method:

private void CollideVertically(ref Vector2 deltaMovement)
{
    float velocityDir = Mathf.Sign(deltaMovement.y);
    float raycastLength = Mathf.Abs(deltaMovement.y);
    Vector2 origin = (velocityDir == -1) ? bottomCenter : topCenter;
    origin.x += deltaMovement.x;
    Vector2 size = new Vector2(boxCollider.size.x - skinWidth * 2, 2 * skinWidth);

    RaycastHit2D hit = Physics2D.BoxCast(origin, size, 0, Vector2.up * directionY, raycastLength, verticalCollisionMask);

    if (hit)
    {
        deltaMovement.y = hit.distance * velocityDir;
        State.IsCollidingBelow = velocityDir == -1;
        State.IsCollidingAbove = velocityDir == 1;
    }
}

The relatively simple changes that I made were to:

  1. Remove the addition of skinWidth to raycastLength
  2. Make the size.y of the boxcast 2 * skinWidth
  3. Remove the subtraction of skinWidth from deltaMovement.y
  4. Subtract 2 * skinWidth from the boxcast's size.x (not important for the issues in this question, but prevents clipping on walls/corners)

The reason points 1 and 2 are important is because the size.y of the boxcast makes up for the skinWidth needed in the raycast method. In the raycast method I was adding skinWidth to raycastLength so that the length would always reach outside of my collider (remember that the rays start from inside the collider). By removing this in the boxcast method, and making the boxcast have a size.y of skinWidth * 2, the bottom half of the boxcast acts as the "made up" length that I was using in the raycast method. In addition, since I'm no longer adding it to raycastLength, skinWidth does not need to be removed from deltaMovement.y = hit.distance * velocityDir.

With that being said, this method still doesn't produce a completely accurate collision when compared to the raycast method. For example, my position is still off by .005f using a boxcast, and my character is still ever so slightly landing off the floor (you have to zoom is quite far in the scene view to see this, but there is no visible gap using raycasts). While this is still very accurate, I'm wondering if this discrepancy would cause collision issues once more (complex) functionality is added to the controller.

@Karl Ramstedt presented an alternative in which collisions are resolved after moving the character, and this might be a better/more reliable approach. I would still appreciate some feedback if anybody has anything else to contribute.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.