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.

Page «Previous 1 2 3 Last »
#1
08/14/2014 (1:59 pm)
Oops - it's broken - Have to remember to test more thoroughly.....

I figured I could just get a vector from the fixture local center to the SceneObject position and use the reverse of that as the "face normal". I think my math is off though - still hammering at it....
#2
08/14/2014 (6:38 pm)
I went ahead and pulled my junky code out (honestly, still don't really know what I'm doing here, either...)

// Fetch scene objects.
    SceneObject* pSceneObjectA = static_cast<SceneObject*>(pPhysicsProxyA);
    SceneObject* pSceneObjectB = static_cast<SceneObject*>(pPhysicsProxyB);

	b2Fixture* pFixturePlatform = NULL;
	b2Fixture* pFixtureObject = NULL;

	if (pSceneObjectA->getOneWayCollision())
	{
		pFixturePlatform = pFixtureA;
		pFixtureObject = pFixtureB;
	}
	else if (pSceneObjectB->getOneWayCollision())
	{
		pFixturePlatform = pFixtureB;
		pFixtureObject = pFixtureA;
	}
	
	/// Check if we need to handle some oneway collision
	if (pFixturePlatform)
	{ // This code handles one-way collisions

		PhysicsProxy* pPhysicsProxyPlatform = static_cast<PhysicsProxy*>(pFixturePlatform->GetBody()->GetUserData());
		PhysicsProxy* pPhysicsProxyObject = static_cast<PhysicsProxy*>(pFixtureObject->GetBody()->GetUserData());

		SceneObject* pSceneObjectPlatform = static_cast<SceneObject*>(pPhysicsProxyPlatform);
		SceneObject* pSceneObjectObject = static_cast<SceneObject*>(pPhysicsProxyObject);
		
		int numPoints = pContact->GetManifold()->pointCount;
		b2WorldManifold worldManifold;
		pContact->GetWorldManifold(&worldManifold);
		int numMovingPoints = 0;
		//check if contact points are moving into platform
		for (int i = 0; i < numPoints; i++)
		{
			b2Vec2 pointVelPlatform = pSceneObjectPlatform->getLinearVelocityFromWorldPoint(worldManifold.points[i]);
			b2Vec2 pointVelOther = pSceneObjectObject->getLinearVelocityFromWorldPoint(worldManifold.points[i]);
			b2Vec2 relativeVel = pSceneObjectPlatform->getLocalVector(pointVelOther - pointVelPlatform);

			if (relativeVel.y > 1) //if moving down faster than 1 m/s, handle as before
				numMovingPoints++;//point is moving into platform, leave contact solid and exit
			else if (relativeVel.y > -1)
				{ //if moving slower than 1 m/s
				//borderline case, moving only slightly out of platform
				b2Vec2 relativePoint = pSceneObjectPlatform->getLocalPoint(worldManifold.points[i]);
				float platformFaceY = 0.5f;//front of platform, from fixture definition :(
				if (relativePoint.y > platformFaceY - 0.05)
					numMovingPoints++;//contact point is less than 5cm inside front face of platfrom
				}
		}
		if (numMovingPoints > 0)
			pContact->SetEnabled(false);
	}

    // Initialize the contact.
    TickContact tickContact;
    tickContact.initialize( pContact, pSceneObjectA, pSceneObjectB, pFixtureA, pFixtureB );
#3
08/14/2014 (10:28 pm)
I had reached the conclusion that I would have to get the positions of all of the vertices in the collision shape so I could calculate the centroid for the shape - that way you could define "away from the body center" as the direction you'd be allowed to pass through the shape. Thanks for sharing - It'll be useful!
#4
08/15/2014 (10:59 am)
Ok, this seems to work:
// one way collision only applies to edge or chain shapes
	if ((pSceneObjectA->getOneWayCol() || pSceneObjectB->getOneWayCol()) && (pFixtureA->GetType() == b2Shape::Type::e_chain || pFixtureA->GetType() == b2Shape::Type::e_edge))
	{
		// convenience renames....
		SceneObject* pPlatformObject = NULL;
		SceneObject* pMovingObject = NULL;
		b2Fixture* pFixturePlatform = NULL;
		b2Fixture* pFixtureObject = NULL;

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

		b2Manifold* manifold = pContact->GetManifold();

		// attempting to "cheat" by just getting a bounding box for the shape and using that center
		b2Vec2 collisionCentroid;
		b2AABB* bbox = new b2AABB();
		if (pFixturePlatform->GetType() == b2Shape::Type::e_chain)
		{
			const b2ChainShape* shape = pPlatformObject->getCollisionChainShape(0);
			shape->ComputeAABB(bbox, pPlatformObject->getTransform(), 0);
			collisionCentroid = bbox->GetCenter();
		}
		else
		{
			const b2EdgeShape* shape = pPlatformObject->getCollisionEdgeShape(0);
			shape->ComputeAABB(bbox, pPlatformObject->getTransform(), 0);
			collisionCentroid = bbox->GetCenter();
		}
		delete(bbox);

		Vector2 nLPos = Vector2(collisionCentroid.x, collisionCentroid.y);
		nLPos = pPlatformObject->getPosition() - nLPos;
		nLPos.Normalize();
		b2Vec2 vLPos = b2Vec2(nLPos.x, nLPos.y);

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

		F32 prod = b2Dot(vLPos, velB);
		if (prod < 0.0f)
		{
			pContact->SetEnabled(false);
		}
	}

So, the collision shape needs to be off center from the scene object - this sets up the "pass-through" vector. Anything moving in the direction established by this vector will pass through the shape, otherwise it is blocked. For instance, this (assuming we're still using the modified TruckToy) -
%this.createOneWayWall(-13, TruckToy.FloorLevel + 1.5, -2, 1, 2, 1);
should create a platform that one could jump up through and stand on because the pass-through vector would be from bottom center up through the shape.
#5
08/15/2014 (11:33 am)
I suppose another way to do this would be to just specify the pass-through direction as a normalized local vector....
#6
08/16/2014 (2:45 pm)
I'm going to have to try this out soon, because it would cleaner to toss in platforms as edge shapes than polygons.
#7
08/17/2014 (9:56 am)
The trick will be to ensure that the SceneObject itself is large enough to be in camera while also ensuring that the collision shape is positioned correctly relative to the SceneObject's center. And in my book, tricky isn't good. I need to think about this for a minute....
#8
08/27/2014 (2:08 pm)
Anyone had a chance to verify this - or improve on it?
#9
08/28/2014 (2:32 pm)
Sadly, I haven't. I'm changing jobs right now, so during the two-week transition I've been too busy to actually switch to this code and see how it runs. Though it's still on my todo list.
#10
08/28/2014 (4:00 pm)
Congratulations on the new job!
#11
08/30/2014 (7:01 am)
Thanks!

Well, I implemented your code in favor of mine... and changed my platform collision shapes from polygon to edge, and so far it seems to work well and seems pretty stable. At least, in my implementation. I played with the camera some, and even if my character is off camera they seem to be enjoying the one-way collision just fine.

I'll have to toss together another video eventually, but right now I'm re-writing my movement code to be cleaner and play nicer with moving platforms, so I've currently lost that 'floaty' platformer feel for the moment.
#12
08/30/2014 (8:16 am)
Okay - if it works when they're off-camera then I suppose the debug rendering is all that is affected. Cool!

I'm thinking of adding a way to just tell the object which way that vector actually points that makes sense. I just think that it would be easier than constructing your collision shape so that it was off-center in the correct direction, you know?
#13
08/30/2014 (11:31 am)
I agree. There are other things than platformers that can use this, and I had thought about that (as I originally mentioned, but wasn't using the direction variable yet - hadn't got far enough to figure out how to implement it).

For a platformer, most of my current usage scenarios will have the collision shape on top of a platform or surface. Instances where I want to let a player slip through something but not come back up would probably work fine since, again, the collision shape would be acting more like a ceiling and would probably be along the bottom of a surface.

I'm sure I can think of many ways to break this if I tried, though.
#14
08/30/2014 (2:58 pm)
Second round... the edge collision shapes seem to be holding up, even when implemented as moving platforms. Again, so far this only tackles the straight-forward case of passing up through the platform and landing on top.

#15
08/31/2014 (10:06 am)
Looking good - thanks for sharing!
#16
08/31/2014 (4:59 pm)
Very nice. I'm loving this thread and the results.
#17
09/07/2014 (10:50 am)
Aw man - decided to put this in a PR, so I diffed my code against 3.1 just to be sure....

I'll be damned if I didn't make those changes in 3.0. So, now I have to make them again in 3.1 before I can push a branch and submit a PR....

Some days.
#18
09/08/2014 (11:00 am)
Ok, pulled, pushed, tugged, yanked, prodded, shoved, dragged, and cajoled. Recompiled and retested. Then submitted a pull request.

It works in 3.0 or 3.1 - since no other changes were made in these files you could just copy them in on top of the existing ones if you wish.

If anyone wants it direct, it's here.
#19
09/10/2014 (1:59 pm)
Very cool, Richard!
#20
09/10/2014 (2:53 pm)
Thanks for the pull request Richard!

I hadn't noticed this before, but I see you altered some of the Box2D source to get this to work. I'm thinking there might be a T2D only solution though - instead of the disabling/enabling the contact, couldn't you turn the collision shape into a sensor for the pass through vector portion and then turn off the sensor for when a collision should happen?
Page «Previous 1 2 3 Last »