Box2D 2.1a Tutorial – Part 4 (Collision Detection)

Up until this point we have learnt how to create a Box2D world, add bodies, add joints to those bodies, and to texture them. While Box2D handles all the collision detection and resolution of the physics, it would also be useful for us to be able to determine when and what objects collide. So this will be what we will learn today.

In this tutorial we will create a simple Box2D world and populate it with a red and blue ball that drops down from the sky. When each ball collides with the ground we will reposition them at their original location from which they fell.

Once again we will extend MouseJointTutorial to save us from coding from scratch.

To be able to detect when collisions happen requires use of a b2ContactListener. The b2ContactListener can be considered an abstract class (even though AS3 doesn’t actually support them) as we do not instantiate it directly but have to extend it first. It would probably be a good idea to have a look at the b2ContactListener class yourself. You will observe the following functions that we can override.

public virtual function BeginContact(contact:b2Contact):void { }
public virtual function EndContact(contact:b2Contact):void { }
public virtual function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void {}
public virtual function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void { }

We will only be overriding BeginContact today since that is all we need for determining when the balls hit the ground. So lets get started.
First, we will create the b2Body balls and textures. You should know this process pretty well by now so I won’t go into lengths to explain it.

package
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import General.Input;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Collision.Shapes.b2CircleShape;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2BodyDef;

	public class CollisionDetectionTutorial extends MouseJointTutorial
	{
		public const RADIANS_TO_DEGREES:Number = 57.2957795;
		public const DEGREES_TO_RADIANS:Number = 0.0174532925;
		private var _blueBall:b2Body;
		private var _redBall:b2Body;
		private var _blueBallTexture:MovieClip;
		private var _redBallTexture:MovieClip;

		override protected function setup():void
		{
			_blueBall = createBall(300, 200, 50);
			_redBall = createBall(500, 300, 50);

			_redBallTexture = new RedBallTexture();
			addChild(_redBallTexture);
			_blueBallTexture = new BlueBallTexture();
			addChild(_blueBallTexture);
		}

		protected function createBall(x:Number, y:Number, radius:Number):b2Body
		{
			var robotBody:b2BodyDef = new b2BodyDef();
			robotBody.type = b2Body.b2_dynamicBody;
			robotBody.fixedRotation = true;
			robotBody.position.Set(x / PIXELS_TO_METRE, y / PIXELS_TO_METRE);
			var body:b2Body = _world.CreateBody(robotBody);

			var robotBodyDef:b2CircleShape = new b2CircleShape();
			robotBodyDef.SetRadius(radius / PIXELS_TO_METRE);

			var robotBodyFixtureDef:b2FixtureDef = new b2FixtureDef();
			robotBodyFixtureDef.shape = robotBodyDef;
			robotBodyFixtureDef.restitution = 0.7;
			robotBodyFixtureDef.friction = 0.5;

			body.CreateFixture(robotBodyFixtureDef);
			return body;
		}

		override protected function update(e:Event):void
		{
			var timeStep:Number = 1 / 60;
			var velocityIterations:int = 6;
			var positionIterations:int = 2;

			UpdateMouseWorld();
			MouseDestroy();
			MouseDrag();

			_world.Step(timeStep, velocityIterations, positionIterations);
			_world.ClearForces();
			updateTextures();
			General.Input.update();
		}

		private function updateTextures():void
		{
			_redBallTexture.x = _redBall.GetPosition().x * PIXELS_TO_METRE;
			_redBallTexture.y = _redBall.GetPosition().y * PIXELS_TO_METRE;
			_redBallTexture.rotation = _redBall.GetAngle() * RADIANS_TO_DEGREES;

			_blueBallTexture.x = _blueBall.GetPosition().x * PIXELS_TO_METRE;
			_blueBallTexture.y = _blueBall.GetPosition().y * PIXELS_TO_METRE;
			_blueBallTexture.rotation = _blueBall.GetAngle() * RADIANS_TO_DEGREES;
		}
	}
}

Now we can get to the actual collision detection code. Create a class and call it BallContactListener, have it extend b2ContactListener and override the BeginContact function .

package
{
	import Box2D.Dynamics.Contacts.b2Contact;
	import Box2D.Dynamics.b2ContactListener;

	public class BallContactListener extends b2ContactListener
	{
		public function BallContactListener()
		{
		}

		override public function BeginContact(contact:b2Contact):void
		{
		}
	}
}

In our setup function we will create an instance of this BallContactListener and pass it to our world via the SetContactListener function.

		override protected function setup():void
		{
			var ballContactListener = new BallContactListener();
			_world.SetContactListener(ballContactListener);

			_blueBall = createBall(300, 200, 50);
			_redBall = createBall(500, 300, 50);

			_redBallTexture = new RedBallTexture();
			addChild(_redBallTexture);
			_blueBallTexture = new BlueBallTexture();
			addChild(_blueBallTexture);
		}

At the moment our BeginContact function in the BallContactListener is empty. We know that it takes in a b2Contact as a parameter and we can take at a guess that it will supply us with the information we need to determine who collided with who.

So lets look what is available. We can see that we can call GetFixtureA and GetFixtureB to return a b2Fixture. From those fixtures we can retrieve a b2Body. The problem is, while we have the b2Bodies that have collided, how do we determine which bodies they correspond with? The answer is at the moment we can’t. Fortunately b2Bodies have two functions that will solve this problem for us, those being GetUserData and SetUserData which allow us to set any data we want as a property. We can leverage this to pass in a string to the body. So in our setup function lets assign the names to the relevant bodies. For best practise I am going to make a class called BodyType and have it contain public static const strings of the names.

		override protected function setup():void
		{
			_world.SetContactListener(ballContactListener);
			_groundBody.SetUserData(BodyType.GROUND);

			_blueBall = createBall(300, 200, 50);
			_blueBall.SetUserData(BodyType.BLUE_BALL);
			_redBall = createBall(500, 300, 50);
			_redBall.SetUserData(BodyType.RED_BALL);

			_redBallTexture = new RedBallTexture();
			addChild(_redBallTexture);
			_blueBallTexture = new BlueBallTexture();
			addChild(_blueBallTexture);
		}

We can now go back into your BallContactListener since we have all the data we need. The one remaining question unanswered is which body will be in which fixture, fixture A or B? We can’t really be sure, so we have to test for both cases.

		override public function BeginContact(contact:b2Contact):void
		{
			if((contact.GetFixtureA().GetBody().GetUserData() == BodyType.BLUE_BALL && contact.GetFixtureB().GetBody().GetUserData() == BodyType.GROUND)
			||(contact.GetFixtureA().GetBody().GetUserData() == BodyType.GROUND && contact.GetFixtureB().GetBody().GetUserData() == BodyType.BLUE_BALL))
			{

			}

			if((contact.GetFixtureA().GetBody().GetUserData() == BodyType.RED_BALL && contact.GetFixtureB().GetBody().GetUserData() == BodyType.GROUND)
			|| (contact.GetFixtureA().GetBody().GetUserData() == BodyType.GROUND && contact.GetFixtureB().GetBody().GetUserData() == BodyType.RED_BALL))
			{

			}
		}

Now that we have the logic in our BeginContact function for determining if a ball collides with the ground we need a way of handling this in our main class. For now we will just use a event dispatcher.

package
{
	import flash.events.Event;
	import Box2D.Dynamics.Contacts.b2Contact;
	import flash.events.EventDispatcher;
	import Box2D.Dynamics.b2ContactListener;

	public class BallContactListener extends b2ContactListener
	{
		public static const BLUE_BALL_START_CONTACT:String = "blueBallStartContact";
		public static const RED_BALL_START_CONTACT:String = "redBallStartContact";
		public var eventDispatcher:EventDispatcher;

		public function BallContactListener()
		{
			eventDispatcher = new EventDispatcher();
		}

		override public function BeginContact(contact:b2Contact):void
		{
			if((contact.GetFixtureA().GetBody().GetUserData() == BodyType.BLUE_BALL && contact.GetFixtureB().GetBody().GetUserData() == BodyType.GROUND)
			||(contact.GetFixtureA().GetBody().GetUserData() == BodyType.GROUND && contact.GetFixtureB().GetBody().GetUserData() == BodyType.BLUE_BALL))
			{
				eventDispatcher.dispatchEvent(new Event(BLUE_BALL_START_CONTACT));
			}

			if((contact.GetFixtureA().GetBody().GetUserData() == BodyType.RED_BALL && contact.GetFixtureB().GetBody().GetUserData() == BodyType.GROUND)
			|| (contact.GetFixtureA().GetBody().GetUserData() == BodyType.GROUND && contact.GetFixtureB().GetBody().GetUserData() == BodyType.RED_BALL))
			{
				eventDispatcher.dispatchEvent(new Event(RED_BALL_START_CONTACT));
			}
		}
	}
}

After adding the listeners in the CollisionDetectionTutorial class it would be fair to assume that we could add in the code for repositioning the bodies in them. However, if you try to do that you will see that this does not work the way intended. The problem is when the bContactListener is called, it is happening at some stage during which all the physics calculations are happening and so modifying the Box2D world causes problems. We need to wait until it is safe to update items in the world. A good place that is safe is in the update function. So our listeners instead will set a Boolean flag so that in our update function can check those values and then know what to do.

package
{
	import flash.display.MovieClip;
	import flash.events.Event;
	import General.Input;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Collision.Shapes.b2CircleShape;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2BodyDef;

	public class CollisionDetectionTutorial extends MouseJointTutorial
	{
		public const RADIANS_TO_DEGREES:Number = 57.2957795;
		public const DEGREES_TO_RADIANS:Number = 0.0174532925;
		private var _blueBallContact:Boolean = false;
		private var _redBallContact:Boolean = false;
		private var _blueBall:b2Body;
		private var _redBall:b2Body;
		private var _blueBallTexture:MovieClip;
		private var _redBallTexture:MovieClip;

		private function onRedBallStartContact(e:Event):void
		{
			_redBallContact = true;
		}

		private function onBlueBallStartContact(e:Event):void
		{
			_blueBallContact = true;
		}

		override protected function setup():void
		{
			var ballContactListener = new BallContactListener();
			ballContactListener.eventDispatcher.addEventListener(BallContactListener.BLUE_BALL_START_CONTACT, onBlueBallStartContact);
			ballContactListener.eventDispatcher.addEventListener(BallContactListener.RED_BALL_START_CONTACT, onRedBallStartContact);
			_world.SetContactListener(ballContactListener);
			_groundBody.SetUserData(BodyType.GROUND);

			_blueBall = createBall(300, 200, 50);
			_blueBall.SetUserData(BodyType.BLUE_BALL);
			_redBall = createBall(500, 300, 50);
			_redBall.SetUserData(BodyType.RED_BALL);

			_redBallTexture = new RedBallTexture();
			addChild(_redBallTexture);
			_blueBallTexture = new BlueBallTexture();
			addChild(_blueBallTexture);
		}

		protected function createBall(x:Number, y:Number, radius:Number):b2Body
		{
			var robotBody:b2BodyDef = new b2BodyDef();
			robotBody.type = b2Body.b2_dynamicBody;
			robotBody.fixedRotation = true;
			robotBody.position.Set(x / PIXELS_TO_METRE, y / PIXELS_TO_METRE);
			var body:b2Body = _world.CreateBody(robotBody);

			var robotBodyDef:b2CircleShape = new b2CircleShape();
			robotBodyDef.SetRadius(radius / PIXELS_TO_METRE);

			var robotBodyFixtureDef:b2FixtureDef = new b2FixtureDef();
			robotBodyFixtureDef.shape = robotBodyDef;
			robotBodyFixtureDef.restitution = 0.7;
			robotBodyFixtureDef.friction = 0.5;

			body.CreateFixture(robotBodyFixtureDef);
			return body;
		}

		override protected function update(e:Event):void
		{
			var timeStep:Number = 1 / 60;
			var velocityIterations:int = 6;
			var positionIterations:int = 2;

			UpdateMouseWorld();
			MouseDestroy();
			MouseDrag();

			_world.Step(timeStep, velocityIterations, positionIterations);
			_world.ClearForces();
			updateTextures();
			General.Input.update();
			checkCollisions();
		}

		private function updateTextures():void
		{
			_redBallTexture.x = _redBall.GetPosition().x * PIXELS_TO_METRE;
			_redBallTexture.y = _redBall.GetPosition().y * PIXELS_TO_METRE;
			_redBallTexture.rotation = _redBall.GetAngle() * RADIANS_TO_DEGREES;

			_blueBallTexture.x = _blueBall.GetPosition().x * PIXELS_TO_METRE;
			_blueBallTexture.y = _blueBall.GetPosition().y * PIXELS_TO_METRE;
			_blueBallTexture.rotation = _blueBall.GetAngle() * RADIANS_TO_DEGREES;
		}

		private function checkCollisions():void
		{
			if(_blueBallContact)
			{
				_blueBall.SetPosition(new b2Vec2(300 / PIXELS_TO_METRE, 200 / PIXELS_TO_METRE));
				_blueBall.SetLinearVelocity(new b2Vec2(0, 0));
				_blueBallContact = false;
			}

			if(_redBallContact)
			{
				_redBall.SetPosition(new b2Vec2(500 / PIXELS_TO_METRE, 300 / PIXELS_TO_METRE));
				_redBall.SetLinearVelocity(new b2Vec2(0, 0));
				_redBallContact = false;
			}
		}
	}
}

EDIT: I would like to thank SticksStones for going to the effort to improve this code. You can find his version here. Please note that these Box2D tutorials are aimed at teaching the Box2D concepts and are in no way meant to be an indication of how you should structure your code unless otherwise stated. Quite often I will write a tutorial to minimize the amount of new/foreign code compared to the previous one. Of course this can come at the expense of code quality so please do not directly implement the code without thinking about how you can design it better! :)

That is it. This wraps up the basics of Box2D.

  • allanbishop

    Great! The persistence paid off! The monster animations are cool.