2
votes

I have this very weird problem with a C# port of the Box2D physics engine.

I'm basically using the C# port called Box2DX (originally hosted here: https://code.google.com/archive/p/box2dx/ and exported to this GitHub repo: https://github.com/colgreen/box2dx)

Now, the thing is that raycasting works as expected ONLY if all dynamic bodies are in certain positions in the simulated physics world. It is hard to explain, so I created a test class in the TestBed project that comes with the engine in order to precisely re-create the problem.

I set up the world to contain 3 static bodies to represent the ground. I also added a large box to represent the "player" character and a small box. Both are dynamic bodies with a density of 1.

In this image you can see the setup with the raycasting not picking up any of the fixtures (ray represented by red line starting in midair and ending inside the large ground fixture). It should detect the box or at the very least the static ground fixture.

RayCast Not Working

Now, the weird part is, that the raycasting DOES work, the moment the "player" body is positioned "above" the raycast's origin, as seen here:

RayCast Working

The same thing happens, whenever the "box" body is moved above the ray.

RayCast working again

Notice that I'm also casting three blue rays from the bottom part of the "player" body straight down. These rays also behave strangely. In the first image above, you can see that all three rays detect the ground fixture properly. However, the moment the player's AABB is positioned as in the following image, the raycasts won't work anymore:

Blue RayCasts not working

Basically, the blue rays stop working the moment the player's AABB is moved over the left edge of any body it stands on.

Blue Rays Not Working

I've been fiddling with this for countless hours and I can't figure out what causes this strange behavior. I'm at a point where I think it is simply a bug in the world querying within the actual C# port and has nothing to do with my particular setup.

For reference, here is the code of my custom test class from the TestBed project:

public class CustomRayCastTest : Test
{
    public static CustomRayCastTest Create() => new CustomRayCastTest();

    private Body _player;

    public CustomRayCastTest()
    {
        _world.Gravity = new Box2DX.Common.Vec2(0, -6.25f);

        BodyDef islandBodyDefA = new BodyDef
        {
            AllowSleep = false,
            Angle = 0,
            FixedRotation = true,
            IsBullet = false,
            //UserData = new object(),
            IsSleeping = false,
            Position = new Box2DX.Common.Vec2(1, 15)
            //Position = new Box2DX.Common.Vec2(1, -17)
        };

        PolygonDef polygonDefIslandA = new PolygonDef
        {
            Density = 0,
            Friction = 0.4f,
            IsSensor = false,
            Restitution = 0
        };

        polygonDefIslandA.SetAsBox(6, 3);

        Body bodyIslandA = _world.CreateBody(islandBodyDefA);
        Shape shapeIslandA = bodyIslandA.CreateShape(polygonDefIslandA);

        bodyIslandA.SetMassFromShapes();

        BodyDef islandBodyDefB = new BodyDef
        {
            AllowSleep = false,
            Angle = 0,
            FixedRotation = true,
            IsBullet = false,
            //UserData = new object(),
            IsSleeping = false,
            Position = new Box2DX.Common.Vec2(12, 2)
            //Position = new Box2DX.Common.Vec2(12, -30)
        };

        PolygonDef polygonDefIslandB = new PolygonDef
        {
            Density = 0,
            Friction = 0.4f,
            IsSensor = false,
            Restitution = 0
        };

        polygonDefIslandB.SetAsBox(9, 4);

        Body bodyIslandB = _world.CreateBody(islandBodyDefB);
        Shape shapeIslandB = bodyIslandB.CreateShape(polygonDefIslandB);

        bodyIslandB.SetMassFromShapes();

        BodyDef islandBodyDefC = new BodyDef
        {
            AllowSleep = false,
            Angle = 0,
            FixedRotation = true,
            IsBullet = false,
            //UserData = new object(),
            IsSleeping = false,
            Position = new Box2DX.Common.Vec2(32, 15)
            //Position = new Box2DX.Common.Vec2(32, -17)
        };

        PolygonDef polygonDefIslandC = new PolygonDef
        {
            Density = 0,
            Friction = 0.4f,
            IsSensor = false,
            Restitution = 0
        };

        polygonDefIslandC.SetAsBox(6, 3);

        Body bodyIslandC = _world.CreateBody(islandBodyDefC);
        Shape shapeIslandC = bodyIslandC.CreateShape(polygonDefIslandC);

        bodyIslandC.SetMassFromShapes();

        // Box
        BodyDef boxBodyDef = new BodyDef
        {
            AllowSleep = false,
            Angle = 0,
            FixedRotation = true,
            IsBullet = false,
            //UserData = new object(),
            IsSleeping = false,
            Position = new Box2DX.Common.Vec2(18, 25)
            //Position = new Box2DX.Common.Vec2(18, -1)
        };

        PolygonDef boxDef = new PolygonDef
        {
            Density = 1,
            Friction = 0.2f,
            IsSensor = false,
            Restitution = 0
        };

        boxDef.SetAsBox(1, 1);

        Body boxBody = _world.CreateBody(boxBodyDef);
        Shape boxShape = boxBody.CreateShape(boxDef);

        boxBody.SetMassFromShapes();

        // Player
        BodyDef playerBodyDef = new BodyDef
        {
            AllowSleep = false,
            Angle = 0,
            FixedRotation = true,
            IsBullet = false,
            //UserData = new object(),
            IsSleeping = false,
            Position = new Box2DX.Common.Vec2(5, 21)
            //Position = new Box2DX.Common.Vec2(5, -4)
        };

        PolygonDef playerShapeDef = new PolygonDef
        {
            Density = 1,
            Friction = 0.2f,
            IsSensor = false,
            Restitution = 0
        };

        playerShapeDef.SetAsBox(1, 2);

        Body playerBody = _world.CreateBody(playerBodyDef);
        Shape playerShape = playerBody.CreateShape(playerShapeDef);

        playerBody.SetMassFromShapes();

        _player = playerBody;

    }

    public override void Keyboard(Keys key)
    {
        base.Keyboard(key);

        if(key == Keys.D)
        {
            _player.ApplyForce(new Vec2(7f * _player.GetMass(), 0), _player.GetWorldPoint(Vec2.Zero));
        }
        else if(key == Keys.A)
        {
            _player.ApplyForce(new Vec2(-7f * _player.GetMass(), 0), _player.GetWorldPoint(Vec2.Zero));
        }

        if(key == Keys.W)
        {
            _player.ApplyImpulse(new Vec2(0, _player.GetMass() * 20f), _player.GetWorldPoint(Vec2.Zero));
        }

    }

    public override void Step(Settings settings)
    {
        base.Step(settings);

        Vec2 rayStart = new Vec2(22, 40);
        Vec2 rayEnd = new Vec2(17, 1);

        Segment ray = new Segment
        {
            P1 = rayStart,
            P2 = rayEnd
        };

        var shape = _world.RaycastOne(ray, out float lambda, out Vec2 normal, false, null);

        if(shape != null)
        {
            Vec2 dir = rayEnd - rayStart;

            rayEnd = rayStart + dir * lambda;

        }

        _debugDraw.DrawSegment(rayStart, rayEnd, new Color(1, 0, 0));

        CastPlayerRays();

    }

    private void CastPlayerRays()
    {
        const int rayCount = 3;
        const float playerWidth = 2;
        const float playerHeight = 4;
        const float inset = 0.2f;
        const float rayLength = 2;

        for (int i = 0; i < rayCount; i++)
        {
            Vec2 origin = new Vec2(-(playerWidth / 2 - inset), -(playerHeight / 2 - inset));

            origin.X += i * (playerWidth / (rayCount));

            origin = _player.GetWorldPoint(origin);

            Vec2 end = origin + new Vec2(0, -1) * rayLength;

            var shape = _world.RaycastOne(new Segment { P1 = origin, P2 = end }, out float lambda, out Vec2 normal, false, null);

            if (shape != null)
            {

                Vec2 dir = end - origin;

                end = origin + dir * lambda;

            }

            _debugDraw.DrawSegment(origin, end, new Color(0, 1, 1));

        }


    }

}

Has anyone ever experienced this problem? I have no idea how to solve this. I need the raycasts to work reliably for my game project. Any help would be much appreciated.

Thank you! :)

2

2 Answers

0
votes

Just a wild guess here, since I've never worked with the C# variant of box2d. I find the function RaycastOne suspicious ... it does not exist in original box2d. If you read the documentation of Raycast here it says that the b2RayCastCallback is called several times for each fixture found in the path. RaycastOne tries maybe to simplify this to only returning the first fixture.

My idea is that by moving the boxes around, you somehow change how internally the fixtures are organized, and thus the first fixture reported by the RaycastOne just gets a different one.

I would try to replace RaycastOne and instead implement the b2RayCastCallback instead, and then you can better control fixture you want to react on. With a bit of luck, that could solve it.

0
votes

Update

I ended up migrating my project to Aether.Physics2D: https://github.com/tainicom/Aether.Physics2D

It is a much cleaner and generally more optimized port of Box2D and the bug does not occur there. So if anyone else ever finds themselves going crazy about this, it seems to really be a bug in the ported library. Switch to a different implementation if you want to save yourself some nerves.