SyntaxHighlighter

Saturday 5 March 2011

Promised update!

Two posts today. First of all, a Sphero update. I originally intended to spend about a day implementing wall-crawling. In reality its taken me over a week, mostly due to me not understanding how Farseer works.

Needless to say I got there in the end, and I have a better understanding of Farseer as a result, which I'll go into in a second. First though, a video of wall-crawling in action:


Not much to look at, I know, but a lot of blood sweat and tears (ok, coffee, pondering, and swearing at my monitor) went into getting it working, so I feel I should share some of the lessons I've learned.

My 2d terrain is made up of EdgeShapes, which are polygons with 2 vertices and a single edge. In other words, lines!
In order to stick to the various surfaces, I decided to look at all of the Fixtures that the character is touching each frame, check to see if they're terrain, and if they are, add the collision normals between the character and each fixture together, and apply a force to the character in the opposite direction.
That probably wasn't very clear, so here's a handy diagram to demonstrate the idea:

The first problem I hit was, well, it didn't work. My character, instead of sticking to surfaces, was instead floating above them.
Debugging showed that although most of the normals were reporting correctly, some were coming out as straight up.
Let me back track a bit, and show you how I implemented this:

{
    {
        ContactEdge tempContactEdge = circle.Body.ContactList;
        Contact tempContact;
        Vector2 normalSum = Vector2.Zero;
        int count = 0;
        if (tempContactEdge != null)
        {
            while (tempContactEdge != null)
            {
            if (tempContactEdge.Contact.FixtureA.CollisionFilter.IsInCollisionCategory(Category.Cat11) || tempContactEdge.Contact.FixtureA.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                tempContact = tempContactEdge.Contact;
                count++;
            }
            else if (tempContactEdge.Contact.FixtureB.CollisionFilter.IsInCollisionCategory(Category.Cat11) || tempContactEdge.Contact.FixtureB.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                tempContact = tempContactEdge.Contact;
                count++;
            }
            else
            {
                tempContactEdge = tempContactEdge.Next;
                continue;
            }
            if (tempContact.Manifold.PointCount != 0)
            {
                Vector2 tempNormal;
                FixedArray2<Vector2> tempPoints;
                tempContact.GetWorldManifold(out tempNormal, out tempPoints);
                normalSum += tempNormal;
            }
            else
            {
                count--;
            }
            tempContactEdge = tempContactEdge.Next;
        }
        if (count > 0)
        {
            circle.Body.IgnoreGravity = true;
            normalSum /= count;
            normalSum.Normalize();
            normalSum *= (-gravForce);
            circle.Body.ApplyForce(normalSum);
        }
    }
}

I grab a reference to the characters contact list, which is essentially a doubly linked list of Contacts. I iterate through it, looking for Contacts that contain fixtures that are in my terrain collision category, and then for those that are, I call the contact's GetWorldManifold() method, which gives me back a collision normal and the manifoldPoints of the collision in world coordinates. I've included a quick diagram to illustrate what a manifold is below, just in case you're not familiar with it:




I keep a cumulative sum of the normals as well as a count of how many there are, and then take an average at the end. Then all that remains is to normalize the average (just in case), and apply a force on at the centre of our character in the opposite direction.


So you can see that random additions of normals pointing in the wrong direction could cause problems!


After some digging inside Farseer I discovered that GetWorldManifold() returns Vector2.UnitY when called on a contact which has manifold.pointCount = 0 (the points it is referring to are those in the diagram above).


This confused me no end. My understanding of the engine was that a contact being included in an object's contact list meant that the two objects had collided. Turns out that this isn't quite the case.


After posting on the Farseer forums (a great bunch, just make sure you search before asking!) to see if this was a bug, I was put straight. A contact is formed when two AABBs (think of them as large bounding boxes) intersect. Each shape or fixture has a AABB to quickly check for possible collisions. The engine can then check whether the two Fixtures associated with the AABBs have collided and make them react appropriately. If you'd like an illustration of AABBs and what they do, download the Testbed solution from the Farseer codeplex page and hit F4 while its running.

So whenever my character got to the end of an EdgeShape, it would hit the AABB of the next EdgeShape, but wouldn't yet be in contact with the shape itself, causing manifold.pointCount to be zero, and giving me my floaty behaviour.

This was easy to fix, I just had to make sure manifold.pointCount > 0 and it was fixed. Or so I thought.

It worked just fine for concave edges and shapes, I.e. the inside of a room, which was exactly what my test map happened to be.

However, when I then built a map with an island in the middle, the character would get to a convex (or 'pointy') join between edges, and then fall off.

If we look back at the algorithm I described, it should be fairly obvious what was happening: the character would lose contact with all fixtures, and my code then reapplied gravity until it touched another fixture, I.e the floor.

So my solution was to attach a second, larger circle fixture to the character body with zero density, and set isSensor to true.

The idea was to use this as a backup, so that when the main character fixture loses contact with all fixtures, we can use the sensor to check if there are any other fixtures near by, and we can use that to give us the direction of our wall crawling force.

The final issue I came across was that sensors don't generate manifolds, so I had to manually calculate the normals on the EdgeShapes.

After all that, I ended up with the video above. *Phew*

That was a bit long winded, so for those of you that prefer code, I've added the finished code snippet below.



{
    {
        ContactEdge tempContactEdge = circle.Body.ContactList;
        Contact tempContact;
        Vector2 normalSum = Vector2.Zero;
        int count = 0;
        if (tempContactEdge != null)
        {
            while (tempContactEdge != null)
            {
            if (tempContactEdge.Contact.FixtureA.CollisionFilter.IsInCollisionCategory(Category.Cat11) || tempContactEdge.Contact.FixtureA.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                tempContact = tempContactEdge.Contact;
                count++;
            }
            else if (tempContactEdge.Contact.FixtureB.CollisionFilter.IsInCollisionCategory(Category.Cat11) || tempContactEdge.Contact.FixtureB.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                tempContact = tempContactEdge.Contact;
                count++;
            }
            else
            {
                tempContactEdge = tempContactEdge.Next;
                continue;
            }
            if (tempContact.Manifold.PointCount != 0)
            {
                Vector2 tempNormal;
                FixedArray2<Vector2> tempPoints;
                tempContact.GetWorldManifold(out tempNormal, out tempPoints);
                normalSum += tempNormal;
            }
            else if (tempContactEdge.Contact.FixtureA.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                EdgeShape eShape = (EdgeShape)tempContactEdge.Contact.FixtureA.Shape;
                Vector2 v = eShape.Vertex2 - eShape.Vertex1;
                Vector2 w = sensorCircle.Body.Position - eShape.Vertex1;
                float t = Vector2.Dot(w, v) / Vector2.Dot(v, v);
                v = eShape.Vertex1 + (t * v) - sensorCircle.Body.Position;
                v.Normalize();
                v *= -1;
                normalSum += v;
            }
            else if (tempContactEdge.Contact.FixtureB.CollisionFilter.IsInCollisionCategory(Category.Cat12))
            {
                EdgeShape eShape = (EdgeShape)tempContactEdge.Contact.FixtureB.Shape;
                Vector2 v = eShape.Vertex2 - eShape.Vertex1;
                Vector2 w = sensorCircle.Body.Position - eShape.Vertex1;
                float t = Vector2.Dot(w, v) / Vector2.Dot(v, v);
                v = eShape.Vertex1 + (t * v) - sensorCircle.Body.Position;
                v.Normalize();
                v *= -1;
                normalSum += v;
            }
            else
            {
                count--;
            }
            tempContactEdge = tempContactEdge.Next;
        }
        if (count > 0)
        {
            circle.Body.IgnoreGravity = true;
            normalSum /= count;
            normalSum.Normalize();
            normalSum *= (-gravForce);
            circle.Body.ApplyForce(normalSum);
        }
    }
}

That's it for this post, but I'll be putting another post up in a bit with a short tutorial on 2d terrain in Farseer.
See you in a bit.

No comments:

Post a Comment