2D物理引擎 Box2D for javascript Games 第五章 碰撞處理
碰撞處理
考慮到 Box2D 世界和在世界中移動的剛體之間遲早會發生碰撞。
而物理遊戲的大多數功能則依賴於碰撞。在憤怒的小鳥中,小鳥摧毀小豬的城堡時,便是依賴碰撞而實現的;
在圖騰破壞者中,當神像墜落到圖騰上或摔碎在地面上,這是由於碰撞而實現的。
Box2D 已經爲我們完成了所有用來解決碰撞的任務,並在無需我們編寫代碼的前提下運行模擬。
另外,在有些情況下,出於遊戲設置的目的我們需要與碰撞進行交互
試想一下憤怒的小鳥,猛烈的撞擊一塊木質的圍牆將可以摧毀它,但是通常 Box2D 碰撞按慣例是不會處理木質圍牆的摧毀工作的。
此外,如果圖騰破壞者中的神像撞擊地面,該關卡將會失敗,但是同樣 Box2D 只是管理碰撞,而不會去在意遊戲設置的規則。
這就是爲什麼我們有時需要分析碰撞,並且很幸運,Box2D 允許我們實現這個功能,這要感謝接觸(contacts):一個通過Box2D創建的對象,用來管理兩個夾具間的碰撞。
在本章,你將學習怎樣使用接觸(contacts)來處理碰撞,以及其它的一些知識:
- 創建自定義的接觸(contacts)監聽
- 確定哪些剛體發生碰撞
- 確定碰撞的強度
- 遍歷所有碰撞涉及的剛體
通過本章的學習,你將能夠管理任何類型的碰撞,而且通過管理遊戲中的碰撞還將完成憤怒的小鳥和圖騰破壞者的關卡。
碰撞檢測
碰撞管理的第一步是:知道兩個剛體之間什麼時候發生碰撞以及什麼時候不在發生碰撞。
你是否還記的你在第二章,向世界添加剛體中創建的項目,小球在地面彈跳?
我們將再次使用這個項目來獲得儘可能多的關於小球和地面之間碰撞的信息。
-
向 main()方法中添加三行簡單的代碼,如下所示:
function main(){ var worldScale = 30; // box2d中以米爲單位,1米=30像素 var gravity = new b2Vec2(0, 9.81); var sleep = true; var world = new b2World(gravity, sleep); var velIterations = 10;// 速率約束解算器 var posIterations = 10;// 位置約束解算器 var bodyDef = new b2BodyDef(); var fixtureDef = new b2FixtureDef(); world.SetContactListener(new CustomContactListener()); bodyDef.position.Set(320/worldScale, 30/ worldScale); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.userData="Ball"; var circleShape = new b2CircleShape(25/worldScale); fixtureDef.shape = circleShape; fixtureDef.density = 1; fixtureDef.restitution = .6; fixtureDef.friction = .1; var theBall = world.CreateBody(bodyDef); theBall.CreateFixture(fixtureDef); // 定義矩形地面 bodyDef.position.Set(320/worldScale, 470/worldScale); // 複用定義剛體 bodyDef.type = b2Body.b2_staticBody; bodyDef.userData="Floor"; var polygonShape = new b2PolygonShape(); polygonShape.SetAsBox(320/worldScale, 10/worldScale); fixtureDef.shape = polygonShape; // 複用夾具 var theFloor = world.CreateBody(bodyDef); theFloor.CreateFixture(fixtureDef); function updateWorld() { world.Step(1/30, 10, 10);// 更新世界模擬 world.DrawDebugData(); // 顯示剛體debug輪廓 world.ClearForces(); // 清除作用力 } setInterval(updateWorld, 1000 / 60); //setup debug draw var debugDraw = new b2DebugDraw(); debugDraw.SetSprite(document.getElementById("canvas").getContext("2d")); debugDraw.SetDrawScale(worldScale); debugDraw.SetFillAlpha(0.5); debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit); world.SetDebugDraw(debugDraw); //update function update() { world.Step(1 / 60, 10, 10); world.DrawDebugData(); world.ClearForces(); }; }
-
你已經知道怎樣使用userData屬性了,所以方法以及整個章節的核心在於下面這行代碼:
world.SetContactListener(new CustomContactListener());
Box2D 允許我們創建一個支持我們管理碰撞所需的所有的事件的自定義接觸(contacts)監聽,例如當一個碰撞開始和結束,你可以使用與在 javascript 項目中處理鼠標或鍵盤事件相同的方式來處理它們。
源碼在: article/ch05/ch05-1.html
Box2D 內建的碰撞監聽
不需要太多的 Box2D 資源類,你應該知道它不但通過自身解決碰撞,而且還提供了四個有趣的監聽,它們允許你可以與碰撞交互,檢索信息或者甚至修改一些參數。
所有事情將在一個 CustomContactListener
類中來管理,這要歸功於 SetContactListener() 方法,它允許我們創建自定義的接觸回調。
class CustomContactListener extends b2ContactListener {
BeginContact(contact) {
console.log("a collision started");
var fixtureA = contact.GetFixtureA();
var fixtureB = contact.GetFixtureB();
var bodyA = fixtureA.GetBody();
var bodyB = fixtureB.GetBody();
console.log("first body: "+bodyA.GetUserData());
console.log("second body: "+bodyB.GetUserData());
console.log("---------------------------");
}
EndContact(contact) {
var fixtureA =contact.GetFixtureA();
var fixtureB =contact.GetFixtureB();
var bodyA = fixtureA.GetBody();
var bodyB = fixtureB.GetBody();
console.log("first body: "+bodyA.GetUserData());
console.log("second body: "+bodyB.GetUserData());
console.log("---------------------------");
}
}
CustomContactListener
類的目的是爲了覆蓋 Box2D 的 BeginContact() 和 EndContact() 方法,默認的方法什麼也做不了,我們通過他們獲得關於碰撞的信息。
將碰撞開始和結束輸出到輸出窗口
顧名思義,BeginContact 事件是當兩個夾具重疊時被調用,那麼EndContact事件是當兩個夾具不再重疊時被調用。
-
讓我們來一行行的分析BeginContact()方法的代碼:
var fixtureA:b2Fixture=contact.GetFixtureA(); var fixtureB:b2Fixture=contact.GetFixtureB();
如你所見,BeginContact()方法有一個b2Contact對象作爲參數傳入。
它包含了我們此刻需要的所有碰撞信息。GetFixtureA() 和 GetFixtureB() 方法將返回碰撞所涉及的夾具。我將它們保存在fixtureA和fixtureB變量中
-
然後,因爲我需要的是剛體,我需要從夾具來獲得剛體。你應該已經知 道關於 GetBody() 方法了:
var bodyA:b2Body=fixtureA.GetBody(); var bodyB:b2Body=fixtureB.GetBody();
-
bodyA 和bodyB 是發生碰撞的兩個剛體。我將在輸出窗口輸出一些文本:
console.log("first body: "+bodyA.GetUserData()); console.log("second body: "+bodyB.GetUserData());
我將它們的 userData 輸出到輸出窗口,它們是 Floor 和 Ball。
BeginContact 方法和 BeginContact() 方法的代碼一樣,所以不需要再解釋它,EndContact() 方法的代碼。
-
測試網頁,然後每一次小球在地面彈起,你應該在控制檯看到下面輸出的文本:
a collision started second body: Ball --------------------------- a collision ended second body: Ball ---------------------------
真棒!因爲現在只要剛體發生碰撞,你就知道了碰撞開始和結束的準確時間。
但是,Box2D 接觸(contact)監聽還給了我們另外的兩個回調方法,分別是 PreSolve()和 PostSolve()方法。
檢測當你要解決碰撞和當你解決了碰撞
PreSole 事件是在碰撞檢測之後,碰撞決算之前的期間被調用,所以你可以在碰撞決算之前與它進行交互。
Post-Solve 事件是當碰撞決算之後發生,它可以使我們知道碰撞的衝量
-
將下面的兩個方法添加到 CustomContactListener 類中
PreSolve(contact , oldManifold ) { if (contact.GetManifold().m_pointCount>0) { console.log("a collision has been pre solved"); var fixtureA = contact.GetFixtureA(); var fixtureB = contact.GetFixtureB(); var bodyA = fixtureA.GetBody(); var bodyB = fixtureB.GetBody(); console.log("first body: "+bodyA.GetUserData()); console.log("second body: "+bodyB.GetUserData()); console.log("---------------------------"); } }
-
目前,PreSolve 方法與 BeginContact() 和 EndContact() 方法有着相同的代碼,
但是這些代碼只有在下面的 if 語句爲 true 時纔會執行:
if (contact.GetManifold().m_pointCount>0) {}
Box2d 將觸點(contact point)集合到一個 manifold 結構中,PreSolve() 方法有時會在 manifold 中沒有觸點的情況下被調用,而這時我們想在至少有一個觸點的情況下執行它。
-
PostSolve
PostSolve(contact, impulse) { console.log("a collision has been post solved"); var fixtureA = contact.GetFixtureA(); var fixtureB = contact.GetFixtureB(); var bodyA = fixtureA.GetBody(); var bodyB = fixtureB.GetBody(); console.log("first body: "+bodyA.GetUserData()); console.log("second body: "+bodyB.GetUserData()); console.log("impulse: "+impulse.normalImpulses[0]); console.log("---------------------------"); }
PostSolve 方法和別的方法擁有一樣的代碼,唯一的不同之處就是下面的這行代碼
console.log("impulse: "+impulse.normalImpulses[0]);
如你所見,PostSolve() 方法有一個作爲衝量的參數。
b2ContactImpulse 對象的 normalImpulses 屬性返回一個包含所有碰撞產生的衝量的 Vector 對象。
獲取第一個也是唯一的一個衝量,它代表了碰撞的強度。
-
再次測試網頁,小球每次在地面彈起時,你應該會看到下面的輸出文本:
a collision started second body: Ball --------------------------- a collision has been pre solved second body: Ball --------------------------- a collision has been post solved second body: Ball impulse: 57.07226654021458 --------------------------- a collision ended second body: Ball ---------------------------
-
每一次彈跳,所有的四個方法都被調用,隨着衝量的遞減,小球彈跳的高度也越來越低。
這就是接觸監聽的工作過程,這個順序絕不會改變,下面是它的步驟,從第一部到最後一步:
- 當碰撞被檢測到,BeginContact 事件被調用。
- PreSolve 事件在碰撞決算之前調用。
- PostSolve 事件在碰撞決算之後被調用。
- 當不再有碰撞發生時調用 EndContact事件,如果剛體一直碰撞,它就不會觸發,除非小球在地面上,不在彈起纔會觸發
源碼在: article/ch05/ch05-2.html
學習了這些知識,讓我們在圖騰破壞者中做一些有趣的事情吧!
在圖騰破壞者中檢測神像墜落地面
我們設定當神像墜落到地面(這是一個在底部的 static 類型的剛體)時,遊戲玩家本關卡失敗。
-
拿出我們在第三章,剛體的交互中創建的圖騰破壞者項目,項目中使用文本框監視神像屬性,那麼現在我們將在floor()方法中向floor剛體指派一個自定義數據,如下所示:
function floor(){ var bodyDef = new b2BodyDef(); bodyDef.position.Set(320/worldScale, 465/worldScale); bodyDef.userData="floor"; var polygonShape = new b2PolygonShape(); polygonShape.SetAsBox(320/worldScale, 15/worldScale); var fixtureDef = new b2FixtureDef(); fixtureDef.shape = polygonShape; fixtureDef.restitution = .4; fixtureDef.friction = .5; var theFloor = world.CreateBody(bodyDef); theFloor.CreateFixture(fixtureDef); }
現在我們有一種可以識別地面的方法。
-
目前,設置的所有監聽以及一個自定義接觸類只是爲了檢測一個碰撞(神像墜落地面),這將會浪費CPU資源。
這裏將發生若干種碰撞:圖騰磚塊與圖騰磚塊,圖騰磚塊與地面以及神像與圖騰磚塊,但是我們只需要檢測神像與地面之間的碰撞。
-
當你只是想要處理少數碰撞時,我建議你不要通過自定義接觸監聽類來管理所有事情,而是像你已經學習過循環遍歷剛體的類似方式循環遍歷碰撞。
-
然後,我們使用一個布爾變量 gameOver 來儲存遊戲狀態。
當變量值爲 true 時神像接觸地面損壞然後遊戲失敗;
當變量值爲false時意味着遊戲仍在進行。
默認的值時false,因爲一開始關卡中的神像是完好的。
var gameOver:Boolean=false;
-
大多數的新代碼需要寫在
updateWorld()
方法中。目的是爲了在每一次世界步中檢測神像碰撞,而不是通過設置監聽。
很顯然,這部分代碼只會在遊戲進行中執行,所以我們以下面的方式改變 updateWorld() 方法:
function updateWorld() { world.Step(1/30, 10, 10);// 更新世界模擬 world.DrawDebugData(); // 顯示剛體debug輪廓 world.ClearForces(); // 清除作用力 if (! gameOver) { for (var b = world.GetBodyList(); b; b = b.GetNext()) { if(b.GetUserData() == 'idol'){ idolBody = b; // 輸出部分信息到 $text var position = idolBody.GetPosition(); var xPos = Math.round(position.x * worldScale); var yPos = Math.round(position.y * worldScale); var angle = idolBody.GetAngle()*radToDeg; var velocity = idolBody.GetLinearVelocity(); var xVel = Math.round(velocity.x * worldScale); var yVel = Math.round(velocity.y * worldScale); $text.innerHTML = (xPos+','+yPos+'<br/>'+angle+'<br/>'+xVel+','+yVel); // 檢測神像與地面是否碰撞 for (var c = b.GetContactList(); c; c=c.next) { var contact = c.contact; var fixtureA = contact.GetFixtureA(); var fixtureB = contact.GetFixtureB(); var bodyA = fixtureA.GetBody(); var bodyB = fixtureB.GetBody(); var userDataA = bodyA.GetUserData(); var userDataB = bodyB.GetUserData(); if (userDataA === "floor" && userDataB === "idol") { levelFailed(); } if (userDataA === "idol" && userDataB === "floor") { levelFailed(); } } } } } }
基本核心的代碼是下面的這行:
for (var c:b2ContactEdge=b.GetContactList(); c; c=c.next) {
與應用 GetBodyList() 方法遍歷 Box2D 世界中的所有剛體相同的概念
GetContactList()方法允許我們遍歷所有發生接觸的剛體
既然這樣,這個循環只有在我們處理的對象時神像時纔會被執行。
b2ContactEdge 被用來將剛體和接觸連接在一起,當我們尋找接觸時,我們必須從每一個b2ContactEdge對象檢索b2Contact對象,我們已經熟悉了這樣的操作做了。
所以我們將一下面的方式來獲得對接觸的訪問:
var contact = c.contact;
此刻,這個循環與本章開始所說明的回調方法非常的相似。
我以同樣的方式編寫了代碼,讓你看到這是相同的概念,如下所示:
if (userDataA=="floor" && userDataB=="idol") { levelFailed(); } if (userDataA=="idol" && userDataB=="floor") { levelFailed(); }
也可以像下面這樣編寫:
if (userDataA=="floor" || userDataB=="floor") { levelFailed(); }
這是因爲我已經知道了神像是碰撞中兩個剛體之中的一個。
-
levelFailed 方法只是處理遊戲結束畫面
因此玩家將不能再摧毀圖騰磚塊,設置gameOver變量爲true以及顯示一個遊戲結束的文本。
function levelFailed(){ $text.innerHTML = "Oh no, poor idol!!!"; gameOver = true; }
-
測試遊戲,使神像墜落到地面,然後遊戲將結束。
只要處理一個碰撞很容易。加入在憤怒的小鳥遊戲中,要處理小豬被消滅以及磚塊被銷燬,那將會怎樣呢?
在憤怒的小鳥中銷燬磚塊並消滅小豬
讓我們打開在第四章,將力作用到剛體上創建的項目,項目中使用了橡皮彈弓,我們就從這裏開始吧。
-
首先,我們對磚塊添加一些自定義的數據,給它們一個名字,代碼添加在 brick() 方法中:
function brick(px, py, w, h, s){ var bodyDef = new b2BodyDef(); bodyDef.position.Set(px/worldScale, py/worldScale); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.userData = "brick"; bodyDef.userData = s; var polygonShape = new b2PolygonShape(); polygonShape.SetAsBox(w/2/worldScale, h/2/worldScale); var fixtureDef = new b2FixtureDef(); fixtureDef.shape = polygonShape; fixtureDef.density = 2; fixtureDef.restitution = .4; fixtureDef.friction = .5; var theBrick = world.CreateBody(bodyDef); theBrick.CreateFixture(fixtureDef); }
-
然後,讓我們來創建小鳥要消滅的小豬,它將用一個圓形來代替。
在 pig() 方法中沒有什麼新的知識,只是創建了一個圓形並給它水平和垂直的座標以及半徑,這些單位都是像素。
function pig(pX, pY, r ) { var bodyDef = new b2BodyDef(); bodyDef.position.Set(pX/worldScale,pY/worldScale); bodyDef.type=b2Body.b2_dynamicBody; bodyDef.userData="pig"; var pigShape = new b2CircleShape(r/worldScale); var fixtureDef = new b2FixtureDef(); fixtureDef.shape=pigShape; fixtureDef.density=1; fixtureDef.restitution=0.4; fixtureDef.friction=0.5; var thePig = world.CreateBody(bodyDef); thePig.CreateFixture(fixtureDef); }
請注意一下小豬的自定義數據。
-
在 main() 方法中,我們需要創建一個自定義的接觸監聽,因爲這裏有很多的碰撞需要管理。
幸運的是,你已經知道了怎樣去實現這些。
同時,不要忘記創建小豬。
function main(){ world = new b2World(gravity, sleep); // 添加碰撞檢測 world.SetContactListener(new CustomContactListener()); debugDraw(); floor(); brick(402,431,140,36); brick(544,431,140,36); brick(342,396,16,32); brick(604,396,16,32); brick(416,347,16,130); brick(532,347,16,130); brick(474,273,132,16); brick(474,257,32,16); brick(445,199,16,130); brick(503,199,16,130); brick(474,125,58,16); brick(474,100,32,32); brick(474,67,16,32); brick(474,404,64,16); brick(450,363,16,64); brick(498,363,16,64); brick(474,322,64,16); // 畫出小豬 pig(474,232,16); // 畫出大圓 var slingCanvas = new createjs.Shape(); slingCanvas.graphics.setStrokeStyle(1, "round").beginStroke("white"); slingCanvas.graphics.drawCircle(0, 0, slingR); stage.addChild(slingCanvas); slingCanvas.x = slingX; slingCanvas.y = slingY; // 畫出小鳥 theBird.graphics.setStrokeStyle(1, "round").beginStroke("white"); theBird.graphics.beginFill('white').drawCircle(0,0,15); stage.addChild(theBird); theBird.x = slingX; theBird.y = slingY; // 拖動小鳥 theBird.on("pressmove", birdMove); theBird.on("pressup", birdRelease) createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.on("tick", function(){ stage.update();// 這是 CreateJS 舞臺更新所需要的 world.DrawDebugData(); // 爲了顯示出createjs對象,這裏不再繪製box2d對象至canvas world.Step(1/30, 10, 10);// 更新世界模擬 world.ClearForces(); // 清除作用力 }); }
-
這個項目的核心代碼在 CustomContactListener 類中,正是你要創建的:
class CustomContactListener extends b2ContactListener { KILLBRICK = 25; KILLPIG = 5; PostSolve(contact, impulse) { console.log("a collision has been post solved"); var fixtureA = contact.GetFixtureA(); var fixtureB = contact.GetFixtureB(); var dataA = fixtureA.GetBody().GetUserData(); var dataB = fixtureB.GetBody().GetUserData(); var force = impulse.normalImpulses[0]; switch (dataA) { case "pig" : if (force > this.KILLPIG) { fixtureA.GetBody().SetUserData("remove"); } break; case "brick" : if (force > this.KILLBRICK) { fixtureA.GetBody().SetUserData("remove"); } break; } switch (dataB) { case "pig" : if (force > this.KILLPIG) { fixtureB.GetBody().SetUserData("remove"); } break; case "brick" : if (force > this.KILLBRICK) { fixtureB.GetBody().SetUserData("remove"); } break; } } }
-
雖然你在本章的開始已經學習了 CustomContactListener 類是怎樣工作的。總之,還是讓我們再回顧一下吧:
KILLBRICK = 25; KILLPIG = 5;
KILLBRICK 和 KILLPIG 兩個常量分別代表了消滅磚塊和小豬所需的衝量值。
這意味着,一個磚塊要被摧毀,相關碰撞產生的衝量要大於25牛頓每秒,然而要消滅小豬相關的碰撞產生的衝量要大於5牛頓每秒。
這兩個值只是隨意設置的,我設置它們只是爲了向你展示怎樣使用它們,然後你可以將它們改成你想要的數值。
但是要記住,這會對遊戲設置的影響很大。
將數值設置的很大將會使磚塊和小豬難以被消滅,同樣 設置數值過小,可能會因爲他們的重量而使結構崩潰。
這都要取決於你。
另外,在原始的憤怒的小鳥中材料有不同的類型,例如玻璃和木頭,它們將影響磚塊被摧毀時所需要的衝量。
你應該有能力來設置材料的類型,正如你在圖騰破壞者中所做的那樣。
總之,無需爲此而擔心,因爲你將在稍後完善它。
-
獲取碰撞的夾具:
var fixtureA = contact.GetFixtureA(); var fixtureB = contact.GetFixtureB();
-
然後,獲取剛體的自定義數據:
var dataA = fixtureA.GetBody().GetUserData(); var dataB = fixtureB.GetBody().GetUserData();
-
最後,獲取碰撞的力:
var force = impulse.normalImpulses[0];
-
現在我們有了查看每一個碰撞發生期間的所有信息。是時候做一些判斷了:
switch (dataA) { case "pig" : if (force > this.KILLPIG) { fixtureA.GetBody().SetUserData("remove"); } break; case "brick" : if (force > this.KILLBRICK) { fixtureA.GetBody().SetUserData("remove"); } break; }
-
從第一個剛體開始,我們通過 switch 語句來判斷處理的是磚塊還是小豬,然後,我們將看碰撞的強度是否足夠摧毀或消滅它。
在此例中,我們沒有立刻銷燬它,爲此我們將銷燬的操作放置在了執行完 tick 方法中(原 updateWorld 方法,updateWorld 方法內的邏輯移動到了 tick 內)。
我們不想在時間步正在執行計算的時候,將剛體從世界中移除,所以我們只是將要移除的剛體的自定義數據進行標記,如下面所示:
fixtureA.GetBody().SetUserData("remove");
-
相同的方式將被應用到下一個剛體上,直到最後在 tick 回調方法中將所有標記爲 remove 的剛體全部銷燬。
createjs.Ticker.on("tick", function(){ stage.update();// 這是 CreateJS 舞臺更新所需要的 world.DrawDebugData(); // 爲了顯示出createjs對象,這裏不再繪製box2d對象至canvas world.Step(1/30, 10, 10);// 更新世界模擬 world.ClearForces(); // 清除作用力 // 銷燬標記爲 remove 的剛體 for (var b = world.GetBodyList(); b; b=b.GetNext()) { if (b.GetUserData()=="remove") { world.DestroyBody(b); } } });
-
測試遊戲,瞄準後射擊,你應該可以看到磚塊和小豬從世界中移除:
就是這樣,憤怒的小鳥關卡開發越來越多的使用了你所學的Box2D特性。
小結
在本章,你學習了怎樣使用自定義監聽和循環遍歷所有的碰撞中的剛體來碰撞進行交互。
你也很好的完善了圖騰破壞者和憤怒的小鳥的雛形。
你是否有能力創建不同類型的磚塊,然後當玩家消滅小豬時顯示一條信息呢?
你行的,試試吧!
本文相關代碼請在
https://github.com/willian12345/Box2D-for-Javascript-Games
注:轉載請註明出處博客園:王二狗Sheldon池中物 ([email protected])