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; } } } }
原文地址:http://blog.allanbishop.com/box2d-2-1a-tutorial-%E2%80%93-part-4-collision-detection/
Collision Point
In my last Box2D tutorial, Collision detection , the goal was to be able to detect when a ball made contact with the ground.
In the comments section John asked a good question and that was how could we figure out the specific point of the collision on the ball/ground? While I did give a brief answer I thought I would make this information a little more accessible by creating small demo/tutorial covering it.
If you remember in our last tutorial we created our ballContactListener’s BeginFunction function. This function had a b2Contact listener from which we could determine the two b2Bodies involved in the collision. It is in this function that we can also get list of points describing the point of contact that we are after.
This information is stored in the manifold of the b2Contact. We have to convert the manifold into a world manifold and only then we can retrieve the points as a Vector of b2Vec2.
var manifold:b2WorldManifold = new b2WorldManifold(); contact.GetWorldManifold(manifold); var points:Vector.<b2Vec2> = manifold.m_points;
From my experiments it seems that only the first item in the vector is of use so I just grab it rather than looping over the list. The other thing I noticed was the contact point was not per pixel accurate, but not far off, so bare that in mind. I have included a zip of the demo project if you want to have a look at all the code.
CollisionPointDemo.zip
原文地址:http://blog.allanbishop.com/box-2d-2-1a-tutorial-part-4-extended-contact-point/
