How can I convert Physics2D.Raycast to Physics2D.BoxCast to yield the same results?

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, the reason I want to convert my approach is because I think boxcasting will be best for my purposes. 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: Imgur: The magic of the Internet Am I thinking about this correctly? Should I be using Physics2D.OverlapBox instead?

1 Answer

1

Think of BoxCast as moving a box along a ray. The box’s width and height are determined by the size parameter. It spawns with its center at the start of the ray (origin), and then moves the specified distance along the ray. If it hits a point, that point is returned as a RaycastHit2D, and the movement ends.

(This isn’t literally how the function is implemented, I assume, but it’s how it behaves.)


As it appears, you’re casting the box along the distance specified by the frame’s movement:
float raycastLength = Mathf.Abs(deltaMovement.y) + skinWidth;

That could cause some problems. The box would indeed end up having travelled distance units away from its starting point, but it would detect collisions size.y / 2 units farther than that point!
To compensate, try subtracting size.y / 2 from raycastLength before the cast happens. (Although it’d be more optimal if you could use 2*skinWidth for the box’s height, because then you could subtract skinWidth instead of size.y / 2. From what I hear, dividing is a tad inefficient compared to multiplying and adding, and is best to avoid in loops and performance-critical code…though, if you know better, please don’t hesitate to correct me!)


Also: have you experimented with the value of skinWidth? I would guess that it’s set to 0.01f, and that’s why 0.02f works as your arbitrary box height–because it’s 2 * 0.01f.

Thank you, you cleared up a lot of things that confused me.