Game Development Community

One-side collision with edge and chain shapes

by Richard Ranft · in Torque 2D Professional · 08/14/2014 (1:46 pm) · 42 replies

This follows from Meredith's description here.

Ok, here's how I did it - and it relies on the relative position of scene object to collision fixture body - in Scene::BeginContact():
// Fetch scene objects.
    SceneObject* pSceneObjectA = static_cast<SceneObject*>(pPhysicsProxyA);
    SceneObject* pSceneObjectB = static_cast<SceneObject*>(pPhysicsProxyB);
    // ---------------------------------
    // inserted this

	if (pSceneObjectA->getOneWayCol() || pSceneObjectB->getOneWayCol())
	{
		// one way collision only applies to edge or chain shapes
		if (pSceneObjectA->getOneWayCol() && (pFixtureA->GetType() == b2Shape::Type::e_chain || pFixtureA->GetType() == b2Shape::Type::e_edge))
		{
			// one or both have one way collision set, so test our directions
			b2Manifold* manifold = pContact->GetManifold();

			Vector2 nPos = Vector2(pSceneObjectA->getPosition().x, pSceneObjectA->getPosition().y);
			nPos.Normalize();
			b2Vec2 vPos = b2Vec2(nPos.x, nPos.y);

			Vector2 nLPos = Vector2(pFixtureA->GetBody()->GetLocalCenter().x, pFixtureA->GetBody()->GetLocalCenter().y);
			nLPos.Normalize();
			b2Vec2 vLPos = b2Vec2(nLPos.x, nLPos.y);

			b2Vec2 nrml = vPos - vLPos;

			Vector2 nVel = Vector2(pSceneObjectB->getLinearVelocity().x, pSceneObjectB->getLinearVelocity().y);
			nVel.Normalize();
			b2Vec2 velB = b2Vec2(nVel.x, nVel.y);

			F32 prod = atan2(nrml.x, nrml.y) - atan2(velB.x, velB.y);
			if (prod < -0.5f)
			{
				pContact->SetEnabled(false);
			}
		}
		else if (pSceneObjectB->getOneWayCol() && (pFixtureB->GetType() == b2Shape::Type::e_chain || pFixtureB->GetType() == b2Shape::Type::e_edge))
		{
			// one or both have one way collision set, so test our directions
			b2Manifold* manifold = pContact->GetManifold();

			Vector2 nPos = Vector2(pSceneObjectB->getPosition().x, pSceneObjectB->getPosition().y);
			nPos.Normalize();
			b2Vec2 vPos = b2Vec2(nPos.x, nPos.y);

			Vector2 nLPos = Vector2(pFixtureB->GetBody()->GetLocalCenter().x, pFixtureB->GetBody()->GetLocalCenter().y);
			nLPos.Normalize();
			b2Vec2 vLPos = b2Vec2(nLPos.x, nLPos.y);

			b2Vec2 nrml = vPos - vLPos;

			Vector2 nVel = Vector2(pSceneObjectA->getLinearVelocity().x, pSceneObjectA->getLinearVelocity().y);
			nVel.Normalize();
			b2Vec2 velA = b2Vec2(nVel.x, nVel.y);

			F32 prod = atan2(nrml.x, nrml.y) - atan2(velA.x, velA.y);
			if (prod > 0.0f)
			{
				pContact->SetEnabled(false);
			}
		}
	}
    // -------------------------
    // Initialize the contact.
    TickContact tickContact;
    tickContact.initialize( pContact, pSceneObjectA, pSceneObjectB, pFixtureA, pFixtureB );

Modified SceneObject with a bool and get/set() for mColOneWay, modified b2Contact with m_reEnable and use that to gate the flag Meredith mentioned above.

In b2Contact::Update() right at the top of the method:
b2Manifold oldManifold = m_manifold;

	// Re-enable this contact.
	if (m_reEnable == true)
		m_flags |= e_enabledFlag;

The one-way part is determined by the positional relation between the fixture and the SceneObject - the "solid side" is the side facing away from the SceneObject - so the local center position of the fixture has to be off-center of the SceneObject. This is sub-optimal, but there seems to be a common theme on the Box2D forums suggesting that there isn't a good way to get the actual edge normal.

To test, I threw this in TruckToy:
// -----------------------------------------------------------------------------

function TruckToy::createOneWayWall( %this, %posX, %posY, %startX, %startY, %endX, %endY )
{
    %obj = new SceneObject();   
    %obj.setBodyType( "static" );
    %obj.setPosition( %posX, %posY );
    %obj.setSize( 0.25, 0.25 );
    %obj.setSceneLayer( TruckToy.ObstacleDomain );
    %obj.setSceneGroup( TruckToy.ObstacleDomain );
    %obj.setCollisionGroups( TruckToy.GroundDomain, TruckToy.ObstacleDomain );
    %obj.setDefaultFriction( TruckToy.ObstacleFriction );
    %obj.createEdgeCollisionShape( %startX, %startY, %endX, %endY );
    %obj.OneWayCollision = true;
    %obj.setAwake( false );
    SandboxScene.add( %obj );

    return %obj;
}
Then called it from ::reset():
%this.createOneWayWall(-92, TruckToy.FloorLevel + 0.75, 1, -3, 1, 3);

I should just make another branch on GitHub....

About the author

I was a soldier, then a computer technician, an electrician, a technical writer, game programmer, and now software test/tools developer. I've been a hobbyist programmer since the age of 13.

#21
09/10/2014 (4:57 pm)
I think that's a great idea, actually. Or perhaps I can plead my case to the Box2D guys. I really think it would be better placed in Box2D itself so that you can more easily use the actual normal of the collision shape itself. But your idea is fantastic considering it does not require fiddling with Box2D.

So, reject that and I'll fix it up, test, and re-submit.
#22
09/10/2014 (9:02 pm)
It just occurred to me that changing the platform shape to a sensor would drop everything on the platform. Iterating through all collision shapes on the "colliding" object would run the risk of either missing other collisions or upsetting the presence of an already existing sensor on the "colliding" object.
#23
09/10/2014 (10:11 pm)
Good point, I hadn't thought about other objects on the one-way platform.

Another idea: do we have access to the (potential) collision normals when the ShouldCollide override is called in the contact filter? Could we filter the one-way collision out there?

Or what about using SceneObject's CollisionSuppress property? Could we have the code setup more or less like it is now, disable the contact on the first tick (if needed) and then flag the moving object to suppress the collision for the rest of the contact time, and disabling CollisionSuppress after that?
#24
09/10/2014 (11:45 pm)
On second thought, CollisionSuppress would filter out all collisions with the moving object, so you could have a case where a projectile or something else should collide with the moving object during its transition through the platform but wouldn't.

Hmm , tough nut to crack here.
#25
09/11/2014 (8:25 am)
I am now convinced that it would be better to plead our case to the Box2D guys. I mean, this seems like it opens up some interesting possibilities that might extend beyond this use case. Actually, I'm still surprised that this is not how edge and chain shapes work by default. In the 3D world collision only happens against the "front" of a polygon by default - normally there is no reason to check for collision if the object we're colliding with is already inside the shape....

And it is a tiny change....
#26
09/11/2014 (8:25 am)
Grr... slow forums are slow....
#27
09/17/2014 (9:39 pm)
Finally found some time to play with this. Putting a slightly modified version of Richard's code in the ContactFilter did the trick - working one way platforms without having to touch the Box2D library. SourceTree is complaining about me needing to commit my working changes with FMOD before I can switch branches and create a clean branch for this change, so for now here's what my ContactFilter.cc file looks like.

#28
09/17/2014 (9:39 pm)
bool ContactFilter::ShouldCollide(b2Fixture* pFixtureA, b2Fixture* pFixtureB)
{
    // Debug Profiling.
    PROFILE_SCOPE(ContactFilter_ShouldCollide);

    PhysicsProxy* pPhysicsProxyA = static_cast<PhysicsProxy*>(pFixtureA->GetBody()->GetUserData());
    PhysicsProxy* pPhysicsProxyB = static_cast<PhysicsProxy*>(pFixtureB->GetBody()->GetUserData());

    // If not scene objects then cannot collide.
    if ( pPhysicsProxyA->getPhysicsProxyType() != PhysicsProxy::PHYSIC_PROXY_SCENEOBJECT ||
         pPhysicsProxyB->getPhysicsProxyType() != PhysicsProxy::PHYSIC_PROXY_SCENEOBJECT )
         return false;

    SceneObject* pSceneObjectA = static_cast<SceneObject*>(pPhysicsProxyA);
    SceneObject* pSceneObjectB = static_cast<SceneObject*>(pPhysicsProxyB);

    // No contact if either objects are suppressing collision.
    if ( pSceneObjectA->mCollisionSuppress || pSceneObjectB->mCollisionSuppress )
        return false;

    // Check collision rules for one way shapes.
    if ( pSceneObjectA->mCollisionOneWay || pSceneObjectB->mCollisionOneWay )
    {
        // Filter out one way collisions.
        bool result = FilterOneWay(pSceneObjectA, pSceneObjectB, pFixtureA, pFixtureB);

        if (result)
            return false;
    }

    // Check collision rule A -> B.
    if ( (pSceneObjectA->mCollisionGroupMask & pSceneObjectB->mSceneGroupMask) != 0 &&
         (pSceneObjectA->mCollisionLayerMask & pSceneObjectB->mSceneLayerMask) != 0 )
         return true;

    // Check collision rule B -> A.
    if ( (pSceneObjectB->mCollisionGroupMask & pSceneObjectA->mSceneGroupMask) != 0 &&
         (pSceneObjectB->mCollisionLayerMask & pSceneObjectA->mSceneLayerMask) != 0 )
         return true;

    return false;
}

//-----------------------------------------------------------------------------

bool ContactFilter::FilterOneWay(SceneObject* pSceneObjectA, SceneObject* pSceneObjectB, b2Fixture* pFixtureA, b2Fixture* pFixtureB)
{
    // One way collisions only apply to edge or chain shapes.
    if ((pFixtureA->GetType() == b2Shape::Type::e_chain || pFixtureA->GetType() == b2Shape::Type::e_edge) ||
        (pFixtureB->GetType() == b2Shape::Type::e_chain || pFixtureB->GetType() == b2Shape::Type::e_edge))
    {
        // Convenience renaming.
        SceneObject* pPlatformObject = NULL;
        SceneObject* pMovingObject = NULL;
        b2Fixture* pFixturePlatform = NULL;
        b2Fixture* pFixtureObject = NULL;

        if (pSceneObjectA->mCollisionOneWay)
        {
            pPlatformObject = pSceneObjectA;
            pMovingObject = pSceneObjectB;
            pFixturePlatform = pFixtureA;
            pFixtureObject = pFixtureB;
        }
        else if (pSceneObjectB->mCollisionOneWay)
        {
            pPlatformObject = pSceneObjectB;
            pMovingObject = pSceneObjectA;
            pFixturePlatform = pFixtureB;
            pFixtureObject = pFixtureA;
        }

        // Attempting to "cheat" by just getting a bounding box for the shape and using that center.
        b2Vec2 collisionCentroid;
        b2AABB* box = new b2AABB();

        if (pFixturePlatform->GetType() == b2Shape::Type::e_chain)
        {
            const b2ChainShape* shape = pPlatformObject->getCollisionChainShape(0);
            shape->ComputeAABB(box, pPlatformObject->getTransform(), 0);
            collisionCentroid = box->GetCenter();
        }
        else
        {
            const b2EdgeShape* shape = pPlatformObject->getCollisionEdgeShape(0);
            shape->ComputeAABB(box, pPlatformObject->getTransform(), 0);
            collisionCentroid = box->GetCenter();
        }

        // We no longer need the bounding box, so delete it.
        delete(box);

        // Get normalized vector from platform shape to platform object.
        b2Vec2 nLPos = pPlatformObject->getPosition();
        nLPos = nLPos - collisionCentroid;
        nLPos.Normalize();

        // Get normalized velocity vector of the moving object.
        b2Vec2 nVel = pMovingObject->getLinearVelocity();
        nVel.Normalize();

        // Calculate the dot product.
        F32 product = b2Dot(nLPos, nVel);

        // If the result is less than zero, we have a pass through condition so flag as true.
        if (product < 0.0f)
            return true;
    }

    // The moving object should collide with platform, so flag as false.
    return false;
}
#29
09/18/2014 (6:31 am)
Sweet - I can add this to mine and make a PR pretty easily.
#30
09/18/2014 (12:00 pm)
Ah, already pushed this to Github. github.com/GarageGames/Torque2D/pull/238

Thanks for the offer though Richard. I had added a few things to SceneObject as well - clone support for the one way property and TorqueScript accessors.
#31
09/18/2014 (12:21 pm)
If anyone wants a quick and dirty toy I whipped up to test the basic one way functionality - here is the code of it: gist.github.com/lilligreen/dd34d35c5a9496e824d2. Just click around the screen or drag the circle.

Gist is a pretty nice way to share code without the hassle of going through the normal Git route. Highly recommend it.
#32
09/18/2014 (12:21 pm)
Nice catch - awesome work team!

Now to come up with a better way to define the front vector....

[EDIT]
One another note....

If you have exposed properties to script do we really need get/set methods?
// if this works
%obj.oneSideCol = true;
%oneSide = %obj.oneSideCol;
// then do we really need
%obj.setOneSideCol(true);
%oneSide = %obj.getOneSideCol();
To me it just seems like more code to maintain, but that's just my personal feelings on it.
#33
09/26/2014 (6:46 am)
I've fiddled with this over the weekend and came up with a solution that works pretty well for my needs! It also works regardless of the angle of the platform.

Disclaimer---------------

I did modify my Box2D Fixtures so that I can specify One-way collision per collision shapes. With a huge compositesprite/tile map, I have several edge collision shapes; setting the entire object to be one-way would not work for my needs.

End of Disclaimer--------------

Here's my recipe :

I convert the desired edge to a vector and Normalize it

b2Vec2 EdgeVec;
EdgeVec.Set(shape->m_vertex2.x - shape->m_vertex1.x, shape->m_vertex2.y - shape->m_vertex1.y);
EdgeVec.Normalize();

I get the Normal from this Edge and Normalize it as well

b2Vec2 EdgeNormal;
EdgeNormal.Set(-EdgeVec.y, EdgeVec.x);
EdgeNormal.Normalize();

I then create a 4-sided polygon (not AABB) from the following points

1 - The first point of the edge
2 - A point extruded from point 1 along the normal.
3 - A point extruded from point 4 along the normal.
4 - The last/second point of the edge.

b2PolygonShape poly;

b2Vec2 polypoints[4];
polypoints[0].Set(-EdgeVec.x, -EdgeVec.y);
polypoints[1].Set(-EdgeVec.x + EdgeNormal.x, -EdgeVec.y + EdgeNormal.y);
polypoints[2].Set(EdgeVec.x + EdgeNormal.x, EdgeVec.y + EdgeNormal.y);
polypoints[3].Set(EdgeVec.x, EdgeVec.y);
poly.Set(polypoints, 4);

I then get the PlayerVelocity...Normalize
b2Vec2 MoverLin = mover->getLinearVelocity();
MoverLin.Normalize();

I define a simple 0,0-centered transform and test whether the Normalized Player Velocity falls within the box we've drawn.

b2Transform Zero;
Zero.SetIdentity();

bool result = poly.TestPoint(Zero, MoverLin);
return !result;

Note that result is TRUE if we are inside the box, which means that we should pass through the platform. That's why we return the opposite of result.

Hope it inspires rather than confuses :)
#34
09/26/2014 (10:15 am)
Oh - see, I was relying on Box2D to give me information about its shapes. This is very good, and should make everyone happy! And you no longer have to worry about the shape's position relative to the object.

I still think Box2D could provide this information cheaper if they were so inclined, but this is very good.
#35
09/26/2014 (1:49 pm)
Very inspiring Simon, I must bow to your awesomeness. This whole thread is pretty great, a really nice example of how open source should work - multiple people sharing code and iterating on it for one or more nice solutions. Hopefully we see more of this in the future.

As for Box2D, it's not a project that is hosted on Github - so we can't just submit a pull request. If you look through the commit history of the library on Google Code, it isn't something that sees regular updates. So if we really want one way collisions to be part of stock T2D, we might just have to go down the route of using a modified version of Box2D.
#36
09/26/2014 (3:33 pm)
@Richard - missed the question on get/set methods earlier. I suppose they are a bit redundant, maybe someone like Mich knows why they are kept around?
#37
09/26/2014 (4:49 pm)
I think for any case where the attribute can be accessed as a property the get/set is redundant. But that's just my opinion - I personally would not want to maintain the extra code if it is actually superfluous.
#38
09/26/2014 (8:20 pm)
@Richard : Yeah, the getter setters get ultra annoying in T2D when you work at the C++ level. Once you get used to it, it's fine but it does feel superfluous.

@Mike : Happy to help!
#39
12/05/2015 (6:54 am)
@Richard: We're still considering if we should merge the pull request that spawned from this thread. The thread and pull request don't really end conclusively. Would you say the pull request is still the best way to handle the situation without modifying box2D? If so, I'll merge the pull request, but only if you're able to provide me with some documentation on how the new functions should be used. It doesn't have to be wiki ready - just something I can use as a basis to write documentation. Thanks!

@Simon and Mike: Your two cents are welcome too :)
#40
12/05/2015 (11:54 am)
As far as I can tell getting Simon to clarify on his final solution might be better, but Mike's pull request should do the trick and is better than not having this in stock. It's very handy for platformers, but also just in general as there are probably enough cases for one-way walls across genres to make this a useful general engine tool.

By the way, someone should start following Andrew Mac's T6 stuff. I believe he has done the majority of the work necessary to get binary extensions to the engine working. This means that features that aren't in the engine can be loaded at runtime via .dll files. It was something that I experimented with a long while back - I got frustrated with all of the export definitions and dropped it.