之前我們已經了解了如何通過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 _width:Number;
private var _height:Number;
private var _info:String;
private static const PADDING:Number = 5;
public function Console(width: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方法執行的時候,物理世界依然進行着計算,此時對剛體操作是會出問題的。