之前我们已经了解了如何通过Box2D创建一个物理世界,给刚体添加复杂材质,鼠标交互。在游戏开发里面我们通常要判断两个物体相互碰撞了,然后进行相应的操作。比如“愤怒的小鸟”,当小鸟碰撞到箱子的时候,我们需要知道这两个物体碰撞了,然后判断碰撞的力度(后面的教程会讲),然后对箱子进行操作。这个教程就是用来处理Box2D的碰撞检测问题。
这个教程仍然基于先前的教程,关于如何创建一个物理世界,这里就不解释了。为了要实现碰撞检测,需要使用到Box2D的B2ContactListener类。该类是一个抽象类,不能直接被实例化,它包含四个方法:BeginContact, EndContact, PreSolve, PostSolve,我们必须先继承它创建自定义的ContactListener,然后override你需要的方法。这个教程主要检测两个物体产生碰撞以及碰撞结束。因此我们override BeginContact(开始碰撞)和EndContact(碰撞结束)方法。
首先我们创建一个简单的物理世界,四个边框和三个球体,这在先前的教程有详细说明
1 package
2 {
3 import Box2D.Collision.Shapes.b2CircleShape;
4 import Box2D.Collision.Shapes.b2PolygonShape;
5 import Box2D.Collision.Shapes.b2Shape;
6 import Box2D.Collision.b2AABB;
7 import Box2D.Common.Math.b2Vec2;
8 import Box2D.Dynamics.Joints.b2MouseJoint;
9 import Box2D.Dynamics.Joints.b2MouseJointDef;
10 import Box2D.Dynamics.b2Body;
11 import Box2D.Dynamics.b2BodyDef;
12 import Box2D.Dynamics.b2DebugDraw;
13 import Box2D.Dynamics.b2Fixture;
14 import Box2D.Dynamics.b2FixtureDef;
15 import Box2D.Dynamics.b2World;
16
17 import comingx.jingle.common.Console;
18 import comingx.jingle.common.CustomContactListener;
19 import comingx.jingle.events.CollisionEvent;
20 import comingx.jingle.userdata.BallUserData;
21
22 import flash.display.GradientType;
23 import flash.display.Sprite;
24 import flash.events.Event;
25 import flash.events.MouseEvent;
26 import flash.geom.Matrix;
27 import flash.text.TextField;
28 import flash.text.TextFieldAutoSize;
29 import flash.text.TextFormat;
30
31 [SWF(width="500",height="300",frameRate="30")]
32 public class Box2DCheckCollision extends Sprite
33 {
34 //屏幕像素单位转换成物理世界的距离单位
35 private const PIXEL_TO_METER:Number = 30;
36
37 //物理世界
38 private var world:Box2D.Dynamics.b2World;
39
40 private var _mouseXWorldPhys:Number;
41 private var _mouseYWorldPhys:Number;
42 private var _mouseXWorld:Number;
43 private var _mouseYWorld:Number;
44
45 private var _mousePVec:b2Vec2 = new b2Vec2();
46 private var _groundBody:b2Body;
47 private var _mouseJoint:b2MouseJoint;
48
49 private var mouseDown:Boolean = false;
50
51 private var console:Console;
52
53 public function Box2DCheckCollision()
54 {
55 drawBackground();
56 createWorld();
57 createWall();
58 createBall();
59 createDebugDraw();
60 addEventListener(Event.ENTER_FRAME, handleEnterFrame);
61 addEventListener(MouseEvent.MOUSE_DOWN,handleMouseDown);
62 addEventListener(MouseEvent.MOUSE_UP,handleMouseUp);
63 addEventListener(MouseEvent.CLICK,handleMouseUp);
64 addEventListener(Event.MOUSE_LEAVE,handleMouseUp);
65 }
66
67 private function createWorld():void
68 {
69 //重力向量
70 var gravity:b2Vec2 = new b2Vec2(0,9.0);
71 //是否休眠
72 var doSleep:Boolean = true;
73 world = new b2World(gravity,doSleep);
74 world.SetWarmStarting(true);
75 }
76
77 private function createWall():void
78 {
79 //1.需要创建的墙刚体
80 var leftWall:b2Body;
81 //2.刚体定义
82 var leftWallBodyDef:b2BodyDef = new b2BodyDef();
83 //刚体类型和位置
84 leftWallBodyDef.type = b2Body.b2_staticBody;
85 //注意刚体的注册中心都是在物体的中心位置
86 leftWallBodyDef.position.Set(10/PIXEL_TO_METER, stage.stageHeight/2/PIXEL_TO_METER);
87 //工厂模式创建刚体
88 leftWall = world.CreateBody(leftWallBodyDef);
89
90 //3.刚体修饰物定义
91 var leftWallFixtureDef:b2FixtureDef = new b2FixtureDef();
92 //密度
93 leftWallFixtureDef.density = 1.0;
94 //摩擦粗糙程度
95 leftWallFixtureDef.friction = 0.3;
96 //力度返回程度(弹性)
97 leftWallFixtureDef.restitution = 1.0;
98
99 //4.创建墙形状
100 var leftWallShape:b2PolygonShape = new b2PolygonShape();
101 //此处参数为宽和高度的一半值
102 leftWallShape.SetAsBox(10/PIXEL_TO_METER, stage.stageHeight/2/PIXEL_TO_METER);
103
104 //将形状添加到刚体修饰物
105 leftWallFixtureDef.shape = leftWallShape;
106
107 leftWall.CreateFixture(leftWallFixtureDef);
108
109
110 //下面创建其他三面墙,共用leftwall的几个变量
111 leftWallBodyDef.position.Set((stage.stageWidth-10)/PIXEL_TO_METER, stage.stageHeight/2/PIXEL_TO_METER);
112 var rightWall:b2Body = world.CreateBody(leftWallBodyDef);
113 rightWall.CreateFixture(leftWallFixtureDef);
114
115
116 leftWallBodyDef.position.Set( stage.stageWidth/2/PIXEL_TO_METER, (stage.stageHeight-10)/PIXEL_TO_METER);
117 var bottomWall:b2Body = world.CreateBody(leftWallBodyDef);
118 leftWallShape.SetAsBox(stage.stageWidth/2/PIXEL_TO_METER, 10/PIXEL_TO_METER);
119 bottomWall.CreateFixture(leftWallFixtureDef);
120
121 leftWallBodyDef.position.Set( stage.stageWidth/2/PIXEL_TO_METER, 10/PIXEL_TO_METER);
122 var topWall:b2Body = world.CreateBody(leftWallBodyDef);
123 topWall.CreateFixture(leftWallFixtureDef);
124 }
125
126 private function createBall():void
127 {
128 var ballDef:b2BodyDef = new b2BodyDef();
129 ballDef.type = b2Body.b2_dynamicBody;
130 ballDef.position.Set(50/PIXEL_TO_METER,30/PIXEL_TO_METER);
131 var ballBig:b2Body = world.CreateBody(ballDef);
132
133 var circleShape:b2CircleShape = new b2CircleShape(30/PIXEL_TO_METER);
134
135 var ballFixtureDef:b2FixtureDef = new b2FixtureDef();
136
137 ballFixtureDef.shape = circleShape;
138 ballFixtureDef.density = 1.0;
139 ballFixtureDef.restitution = 0.5;
140
141 ballBig.CreateFixture(ballFixtureDef);
142
143 ballDef.position.Set(200/PIXEL_TO_METER, 30/PIXEL_TO_METER);
144 var ballMedium:b2Body = world.CreateBody(ballDef);
145 circleShape = new b2CircleShape(20/PIXEL_TO_METER);
146 ballFixtureDef.shape = circleShape;
147 ballMedium.CreateFixture(ballFixtureDef);
148
149 ballDef.position.Set(400/PIXEL_TO_METER, 30/PIXEL_TO_METER);
150 var ballSmall:b2Body = world.CreateBody(ballDef);
151 circleShape = new b2CircleShape(15/PIXEL_TO_METER);
152 ballFixtureDef.shape = circleShape;
153 ballSmall.CreateFixture(ballFixtureDef);
154 }
155
156 private function createDebugDraw():void
157 {
158 //创建一个sprite,可以将测试几何物体放入其中
159 var debugSprite:Sprite = new Sprite();
160 addChild(debugSprite);
161 var debugDraw:b2DebugDraw = new b2DebugDraw();
162 debugDraw.SetSprite(debugSprite);
163 //设置边框厚度
164 debugDraw.SetLineThickness(1.0);
165 //边框透明度
166 debugDraw.SetAlpha(1.0);
167 //填充透明度
168 debugDraw.SetFillAlpha(0.5);
169 //设置显示对象
170 debugDraw.SetFlags(b2DebugDraw.e_shapeBit);
171 //物理世界缩放
172 debugDraw.SetDrawScale(PIXEL_TO_METER);
173 world.SetDebugDraw(debugDraw);
174 }
175
176 private function handleEnterFrame(evt:Event):void
177 {
178 UpdateMouseWorld();
179 mouseDrag();
180
181 var timeStep:Number = 1/30;
182 var velocityInterations:int = 10;
183 var positionIterations:int = 10;
184
185 world.Step(timeStep,velocityInterations,positionIterations);
186 //在2.1版本清除力,以提高效率
187 world.ClearForces();
188 //绘制
189 world.DrawDebugData();
190 }
191
192 private function drawBackground():void
193 {
194 var bg:Sprite = new Sprite();
195 var matrix:Matrix = new Matrix();
196 matrix.translate(100,100);
197 bg.graphics.beginGradientFill(GradientType.RADIAL,[0xffffff,0xffaa00],[0.3,0.2],[0,255],matrix);
198 bg.graphics.drawRect(0,0,stage.stageWidth,stage.stage.stageHeight);
199 bg.graphics.endFill();
200 addChild(bg);
201
202 //tips
203 var tf:TextField = new TextField();
204 tf.text = "拖动球产生碰撞";
205 tf.autoSize = TextFieldAutoSize.LEFT;
206 var fomat:TextFormat = new TextFormat("Kai,华文楷体", "20", 0x555555);
207 tf.setTextFormat(fomat);
208 tf.x = tf.y = 30;
209 addChild(tf);
210 }
211
212 private function UpdateMouseWorld():void
213 {
214 _mouseXWorldPhys = this.mouseX / PIXEL_TO_METER;
215 _mouseYWorldPhys = this.mouseY / PIXEL_TO_METER;
216
217 _mouseXWorld = this.mouseX;
218 _mouseYWorld = this.mouseY;
219 }
220 private function getBodyAtMouse(includeStatic:Boolean = false):b2Body
221 {
222 _mousePVec.Set(_mouseXWorldPhys,_mouseYWorldPhys);
223 var aabb:b2AABB = new b2AABB();
224 aabb.lowerBound.Set(_mouseXWorldPhys - 0.001, _mouseYWorldPhys - 0.001);
225 aabb.upperBound.Set(_mouseXWorldPhys + 0.001, _mouseYWorldPhys + 0.001);
226 var body:b2Body = null;
227 var fixture:b2Fixture;
228
229 function getBodyCallback(fixture:b2Fixture):Boolean
230 {
231 var shape:b2Shape = fixture.GetShape();
232 if(fixture.GetBody().GetType() != b2Body.b2_staticBody || includeStatic)
233 {
234 var inside:Boolean = shape.TestPoint(fixture.GetBody().GetTransform(), _mousePVec);
235 if(inside)
236 {
237 body = fixture.GetBody();
238 return false;
239 }
240 }
241 return true;
242 }
243 world.QueryAABB(getBodyCallback, aabb);
244 return body;
245 }
246
247 private function mouseDrag():void
248 {
249 if(mouseDown && !_mouseJoint)
250 {
251 var body:b2Body = getBodyAtMouse();
252 if(body)
253 {
254 var md:b2MouseJointDef = new b2MouseJointDef();
255 md.bodyA = world.GetGroundBody();
256 md.bodyB = body;
257
258 md.target.Set(_mouseXWorldPhys,_mouseYWorldPhys);
259 md.collideConnected = true;
260 md.maxForce = 300.0 * body.GetMass();
261 _mouseJoint = world.CreateJoint(md) as b2MouseJoint;
262 body.SetAwake(true);
263 }
264 }
265
266 if(!mouseDown)
267 {
268 if(_mouseJoint)
269 {
270 world.DestroyJoint(_mouseJoint);
271 _mouseJoint = null;
272 }
273 }
274
275 if(_mouseJoint)
276 {
277 var p2:b2Vec2 = new b2Vec2(_mouseXWorldPhys,_mouseYWorldPhys);
278 _mouseJoint.SetTarget(p2);
279 }
280 }
281
282 public function handleMouseDown(e:MouseEvent):void
283 {
284 mouseDown = true;
285 }
286
287 public function handleMouseUp(e:MouseEvent):void
288 {
289 mouseDown = false;
290 }
291 }
292 }
下面为关键代码
1. 创建自定义的ContactListener
1 package comingx.jingle.common
2 {
3 import Box2D.Dynamics.Contacts.b2Contact;
4 import Box2D.Dynamics.b2ContactListener;
5
6 import comingx.jingle.events.CollisionEvent;
7 import comingx.jingle.userdata.BallUserData;
8
9 import flash.events.EventDispatcher;
10
11 public class CustomContactListener extends b2ContactListener
12 {
13 public var eventDispatcher:EventDispatcher;
14
15 public function CustomContactListener()
16 {
17 eventDispatcher = new EventDispatcher();
18 }
19
20 override public function BeginContact(contact:b2Contact):void
21 {
22 var collisionEvent:CollisionEvent = new CollisionEvent(CollisionEvent.COLLISION_START);
23 //墙的userdata为null,排除与墙的碰撞
24 if(contact.GetFixtureA().GetBody().GetUserData() != null && contact.GetFixtureB().GetBody().GetUserData() != null)
25 {
26 collisionEvent.bodyAName = (contact.GetFixtureA().GetBody().GetUserData() as BallUserData).name;
27 collisionEvent.bodyBName = (contact.GetFixtureB().GetBody().GetUserData() as BallUserData).name;
28 eventDispatcher.dispatchEvent(collisionEvent);
29 }
30 }
31
32 override public function EndContact(contact:b2Contact):void
33 {
34 var collisionEvent:CollisionEvent = new CollisionEvent(CollisionEvent.COLLISION_END);
35 if(contact.GetFixtureA().GetBody().GetUserData() != null && contact.GetFixtureB().GetBody().GetUserData() != null)
36 {
37 collisionEvent.bodyAName = (contact.GetFixtureA().GetBody().GetUserData() as BallUserData).name;
38 collisionEvent.bodyBName = (contact.GetFixtureB().GetBody().GetUserData() as BallUserData).name;
39 eventDispatcher.dispatchEvent(collisionEvent);
40 }
41 }
42 }
43 }
b2ContactListener的是个方法将会每次Box2D计算时执行,因此它会不停的运行,只要有物体产生碰撞。那么我们如何知道我们想要的物体碰撞,即哪个球和哪个球碰撞了?Box2D碰撞的两个物体通过contact.GetFixtureA().GetBody()和contact.GetFixtureB().GetBody()这两个方法获取。我们预先给这两个刚体的UserData设置一个对象,对刚体进行命名,用来识别各个刚体,这样可以根据名字来知道碰撞的双方分别是什么。因此我们创建BallUserData类,存储刚体名字(还可以有其他数据,如果你需要的话)
package comingx.jingle.userdata
{
import flash.display.Sprite;
public class BallUserData
{
public var name:String;
public var sprite:Sprite;
public function BallUserData(name:String = "unanmed", sprite:Sprite = null)
{
this.name = name;
this.sprite = sprite;
}
}
}
其中name属性标识了这个刚体的名字,sprite属性我们可以为其指定一个刚体外观。这样我们通过这个方法对刚体UserData进行赋值,从而给三个球命名,以期望在碰撞的时候知道是谁和谁碰撞了。
在入口类的createBall()方法中添加如下代码:
//对刚体进行命名
ballBig.SetUserData(new BallUserData("big"));
ballMedium.SetUserData(new BallUserData("medium"));
ballSmall.SetUserData(new BallUserData("small"));
我们看到在CustomContactListener中当两个碰撞物体的UserData不为NULL的时候(墙的UserData为NULL),即球不是与墙碰撞而是与球碰撞,此时可以抛出自定义的碰撞事件,并在事件中保存碰撞对象的名字。从而你可以在需要的地方侦听这个事件并获取到碰撞双方的名字,做相应的逻辑处理。
下面是CollisionEvent:
package comingx.jingle.events
{
import flash.events.Event;
public class CollisionEvent extends Event
{
public static const COLLISION_START:String = "collision_start";
public static const COLLISION_END:String = "collision_end";
public var bodyAName:String = "";
public var bodyBName:String = "";
public function CollisionEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
{
super(type, bubbles, cancelable);
}
}
}
2. 将ContactListener添加到物理世界
为物理世界添加碰撞侦听,并且为自定义事件创建逻辑处理。
private function initContactListener():void
{
var customContactListener:CustomContactListener = new CustomContactListener();
customContactListener.eventDispatcher.addEventListener(CollisionEvent.COLLISION_START, handleCollisionStart);
customContactListener.eventDispatcher.addEventListener(CollisionEvent.COLLISION_END, handleCollisionEnd);
world.SetContactListener(customContactListener);
}
private function handleCollisionStart(evt:CollisionEvent):void
{
console.addInfo(">>--<<" + evt.bodyAName + "和" + evt.bodyBName + "碰撞");
}
private function handleCollisionEnd(evt:CollisionEvent):void
{
console.addInfo("<<-->>" + evt.bodyAName + "和" + evt.bodyBName + "分离");
}
其中console为一个输出窗口用来显示trace信息的
package comingx.jingle.common
{
import flash.display.Sprite;
import flash.text.TextField;
import flash.text.TextFormat;
public class Console extends Sprite
{
private var _tf:TextField;
private var _Number;
private var _height:Number;
private var _info:String;
private static const PADDING:Number = 5;
public function Console(Number = 200, height:Number = 200)
{
super();
_width = width;
_height = height;
_info = "";
_tf = new TextField();
init();
}
private function init():void
{
initBG();
initTextField();
}
private function initBG():void
{
this.graphics.beginFill(0x000000);
this.graphics.drawRect(0,0,_width,_height);
this.graphics.endFill();
}
private function initTextField():void
{
_tf.width = _width - PADDING * 2;
_tf.height = _height - PADDING * 2;
_tf.x = PADDING;
_tf.y = PADDING;
_tf.wordWrap = true;
_tf.multiline = true;
_tf.text = _info;
_tf.textColor = 0xffffff;
addChild(_tf);
}
public function set Info(infoString:String):void
{
if(_info != infoString)
{
_info = infoString;
}
}
public function get Info():String
{
return _info;
}
public function addInfo(infoString:String):void
{
_info = _info + "\n" + infoString;
_tf.text = _info;
_tf.scrollV = _tf.maxScrollV;
}
}
}
最后,如果你要想愤怒的小鸟那样,碰撞之后将某个物体消失掉,那么你最好不要在handleContactStart中处理逻辑,应该在此方法中添加一个flag,然后在handleEnterFrame中,根据flag,来处理逻辑,因为handleContactStart方法执行的时候,物理世界依然进行着计算,此时对刚体操作是会出问题的。