2D物理引擎 Box2D for javascript Games 第六章 關節和馬達

2D物理引擎 Box2D for javascript Games 第六章 關節和馬達

關節和馬達

到現在你所見到的所有類型的剛體有着一些共同點:它們都是自由的並且在除碰撞的請款之外,彼此沒有依賴。

有時你可能想要剛體之間進行約束。

如果你試想一下粉碎城堡(Crush the Castle)這款遊戲,投擲器是通過某種方法將一系列的剛體連接在一起而組成的。

Box2D 允許我們通過關節(joint)來創建剛體之間的約束。

關節允許我們創建複雜的對象並使我們的遊戲更加真實。

在本章,你將學習怎樣創建比較常見類型的關節,並且你將會發現一些別的事情,

下面是本章的知識列表:

  • 通過鼠標關節拾取、拖拽以及拋擲
  • 通過距離關節保持剛體之間一個固定的距離
  • 使用旋轉關節使剛體旋轉
  • 使用發動機(motor)爲你的遊戲賦予生命

在本章最後,你將有能力通過投擲器摧毀憤怒的小鳥的關卡。

總之,關節也可以通過拾取和拖拽來實現剛體的交互。

這是你要學習的第一種類型的關節。

拾取並拖拽剛體——鼠標關節

最難的事情最先做

我們將從最難的關節之一開始

雖然蛋疼,但是必須去做,因爲這樣將使我們創建並測試別的關節變的容易(當你完成了很困難的事情後,轉而去做相對簡單些的事情時,這些事情將變得更加容易)

一個鼠標關節允許玩家通過鼠標來移動剛體,我們將創建具有以下特性的鼠標關節:

  • 通過在剛體上點擊來拾取它
  • 只要按鈕按下,剛體將隨鼠標移動
  • 一旦按鈕被釋放,剛體也將被釋放

開始的步驟與你通過本書已經掌握的腳本並無什麼區別

  1. 在 main()方法中沒有什麼新的東西,只是放置了兩個盒子形狀到舞臺上:

    大的 static 類型的地面和在其上面的很小的 dynamic 類型的盒子

    function main(){
       world = new b2World(gravity, sleep);
       debugDraw();
    
       // 地面
       var bodyDef  = new b2BodyDef();
       bodyDef.position.Set(320 / worldScale, 470 / worldScale);
       var polygonShape  = new b2PolygonShape();
       polygonShape.SetAsBox(320 / worldScale, 10 / worldScale);
       var fixtureDef  = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       var groundBody = world.CreateBody(bodyDef);
       groundBody.CreateFixture(fixtureDef);
    
       // 小盒子
       bodyDef.position.Set(320 / worldScale, 430 / worldScale);
       bodyDef.type = b2Body.b2_dynamicBody;
       polygonShape.SetAsBox(30 / worldScale, 30 / worldScale);
       fixtureDef.density = 1;
       fixtureDef.friction = 0.5;
       fixtureDef.restitution = 0.2;
       var box2 = world.CreateBody(bodyDef);
       box2.CreateFixture(fixtureDef);
    
       setInterval(updateWorld, 1000 / 60);
       
       document.querySelector('#canvas').addEventListener('mousedown', createJoint)
    }
    
  2. 不要爲在 mousedown 監聽中 createJoint() 回調方法而煩惱

    目前,我們沒有創建任何關節,所以我們只是通過當我們學習怎樣銷燬剛體時的相同的方式來查詢世界

    function createJoint(e){
       world.QueryPoint(queryCallback, mouseToWorld(e));
    }
    
  3. 現在來看看 mouseToWorld() 方法,因爲我們將要對鼠標的座標進行很多的操作,我創建一個小方法將鼠標座標轉變成b2Vec2對象的世界座標。

    function mouseToWorld(e) {
       const mouseX = e.x;
       const mouseY = e.y;
       return new b2Vec2(mouseX/worldScale,mouseY/worldScale);
    }
    
  4. queryCallback() 方法只是通過 GetType() 方法檢測剛體是否是 dynamic 類型的

    所以,很顯然你希望檢測的剛體是 dynamic 類型的,這樣你將可以拖拽這個剛體。

    在這一步,我們只是將一些文本從輸出窗口中輸出:

    function queryCallback(fixture)  {
       var touchedBody = fixture.GetBody();
       if (touchedBody.GetType() === b2Body.b2_dynamicBody) {
             console.log("will create joint here");
       }
       return false;
    }
    
  5. 其它常規的 updateWorld、debugDraw 與之前章節中無異

    測試網頁:
    image

  6. 直到你點擊地面上的 dynamic 類型的盒子時,否則將不會有任何事情發生,這時將會立即在輸出窗口出現下面的文本

    will create joint here

    事實上此處就是我們要創建鼠標關節的地方

    源碼: article/ch06/ch06-1.html

  7. 關節(Joints)類在別的包中,我們將它導入進來命名爲 :

    b2MouseJoint = Box2D.Dynamics.Joints.b2MouseJoint
    
  8. 然後我們還要聲明一個新的類變量,它將存儲我們的鼠標關節(mouse joint)

    let mouseJoint;
    

    在這裏非常直觀,b2MouseJoint 類是我們用來處理鼠標關節的,所以我聲明一個該類型的變量,叫做 mouseJoint。

  9. 最後,讓我們來創建關節(Joint)。

    一旦我們得知玩家嘗試拾取一個 dynamic 類型的剛體,我們需要在 queryCallback() 方法中來實現關節的創建。

    function queryCallback(fixture, e)  {
       var touchedBody = fixture.GetBody();
       if (touchedBody.GetType() === b2Body.b2_dynamicBody) {
             var jointDef = new b2MouseJointDef();
             jointDef.bodyA = world.GetGroundBody();
             jointDef.bodyB = touchedBody;
             jointDef.target = mouseToWorld(e);
             jointDef.maxForce = 1000 * touchedBody.GetMass();
             mouseJoint = world.CreateJoint(jointDef);
             stage.addEventListener('mousemove', moveJoint);
             stage.addEventListener('mouseup', killJoint);
       }
       return false;
    }
    

    這裏有一些新的代碼,所以讓我們來一行行的解析它:

    var jointDef = new b2MouseJointDef();
    

    我們創建一個 b2MouseJoint 對象。

    在這裏,創建關節與創建剛體沒有什麼不同。

    無論哪種情況我們都要有一個與所有調整參數以及剛體或關節自身相關的定義。

    所以我們創建了關節的定義。

    現在,我們需要一些關節參數,例如:通過關節連接的剛體。

    jointDef.bodyA = world.GetGroundBody();
    jointDef.bodyB = touchedBody;
    

    指定 bodyA 和 bodyB 屬性爲通過關節連接起來的剛體。

    因爲我們將一個剛體和鼠標連接在一起,所以 bodyA 將是地面剛體(ground body)。

    地面剛體(ground body)不是我們自己創建的用作地面的 static 類型的盒子剛體,它是不可見的,不可觸摸的剛體,它代表了 Box2D 世界。

    在現實世界中,地面剛體(ground body)就是圍繞着我們的空氣。

    它無處不在,但是我們卻看不見摸不着。

    另一面,bodyB 是我們剛要點擊的剛體,所以我們將說:“被點擊的剛體將要被定到世界中給定的點上”。

    那一個點?

    當然是鼠標指針座標,這要歸功於 target 屬性帶來的便利,它允許我們指派 b2Vec2 對象的座標

    然後就是 mouseToWorld() 方法出場的時候了:

    jointDef.target = mouseToWorld(e);
    

    現在,你可以在舞臺上拖拽你的鼠標,然後剛體將如你所期望的跟隨鼠標的指針。

    Box2D 將爲你處理這些事情,可是你需要爲關節指定作用力(force)。

    作用力越大,當你移動鼠標時剛體響應也將更加迅速。試想一下,在你的鼠標指針與剛體之間有一個橡皮帶連接着。

    彈性越大,移動越精確

    maxForce 屬性允許我們設置關節的最大作用力。

    我依據剛體的質量設置了一個很大的值:

    jointDef.maxForce=1000*touchedBody.GetMass();
    

    現在一切都決定好了,然後我們準備將在上面我們剛剛結束的定義基礎上創建關節。

    所以,讓我們通過 CreateJoint() 方法將關節添加到世界中:

    mouseJoint=world.CreateJoint(jointDef)
    
  10. 現在,我們完成了關節。

總之,大家將期望在保持按下鼠標的左鍵按鈕後,只要移動鼠標,剛體也將移動,或者一旦釋放鼠標左鍵,剛體也將被釋放。

所以,首先我們需要添加兩個監聽,來檢測當鼠標移動和鼠標左鍵被釋放時所發生的事件:

stage.addEventListener('mousemove', moveJoint);
stage.addEventListener('mouseup', killJoint);

注: stage 爲 const stage = document.querySelector('#canvas'); 網頁上的 canvas

  1. 現在,回調方法 moveJoint() 將在鼠標使用時被調用,然後它只要更新關節的目標即可。

在之前的部分通過 target 屬性設置的 b2MouseJointDef 對象的目標,你是否還記得呢?

現在你可以直接通過 b2MouseJoint 自身的 SetTarget() 方法來更新目標。

哪兒是新的目標的位置呢?新的鼠標位置:

function moveJoint(e){
   mouseJoint.SetTarget(mouseToWorld(e));
}
  1. 當鼠標的左鍵被釋放,killJoint 方法將被調用,所以我們將通過 Destroy() 方法移除關節,這和我們銷燬剛體時使用的 DestroyBody() 一樣。

此外,將關節的變量設置爲 null,然後顯示的移除監聽:

function killJoint(e){
   world.DestroyJoint(mouseJoint);
   mouseJoint = null;
   stage.removeEventListener('mousemove', moveJoint);
   stage.removeEventListener('mouseup', killJoint);
}

現在你可以去拾取另一個剛體了。

但是糟糕的是,世界中只有一個 dynamic 類型的剛體,但是我們將在幾秒鐘內添加很多東西。

  1. 測試網頁,拾取並拖拽 dynamic 類型的盒子。

在調試繪圖中將以一條藍綠色的線來代表關節,並且箭頭代表了鼠標移動的方向

![image](https://img2023.cnblogs.com/blog/405426/202310/405426-20231027210409932-908314168.png)

源碼: article/ch06/ch06-2.html

現在,你能夠拖拽並拋擲剛體了,讓我們來創建一個新的盒子吧,然後學習另一種類型的關節。

讓剛體之間保持給定的距離——距離關節

距離關節可能是最容易理解和處理的關節。

它只需要設置兩個對象的點之間的距離,然後,無論發生什麼,它都將保持對象的點之間的距離爲你所設置的數值。

所以,我創建一個新的盒子並將它與之前已經存在於這個世界的盒子通過距離關節連接在一起。

讓我們來對 main() 方法做一些小的改動吧:

function main(){   
   world = new b2World(gravity, sleep);
   debugDraw();

   // 地面
   var bodyDef  = new b2BodyDef();
   bodyDef.position.Set(320 / worldScale, 470 / worldScale);
   var polygonShape  = new b2PolygonShape();
   polygonShape.SetAsBox(320 / worldScale, 10 / worldScale);
   var fixtureDef  = new b2FixtureDef();
   fixtureDef.shape = polygonShape;
   var groundBody = world.CreateBody(bodyDef);
   groundBody.CreateFixture(fixtureDef);

   // 小盒子
   bodyDef.position.Set(320 / worldScale, 430 / worldScale);
   bodyDef.type = b2Body.b2_dynamicBody;
   polygonShape.SetAsBox(30 / worldScale, 30 / worldScale);
   fixtureDef.density = 1;
   fixtureDef.friction = 0.5;
   fixtureDef.restitution = 0.2;
   var box2 = world.CreateBody(bodyDef);
   box2.CreateFixture(fixtureDef);

   bodyDef.position.Set(420/worldScale,430/worldScale);
   var box3 = world.CreateBody(bodyDef);
   box3.CreateFixture(fixtureDef);
   var dJoint = new b2DistanceJointDef();
   dJoint.bodyA = box2;
   dJoint.bodyB = box3;
   dJoint.localAnchorA = new b2Vec2(0,0);
   dJoint.localAnchorB = new b2Vec2(0,0);
   dJoint.length = 100 / worldScale;
   var distanceJoint = world.CreateJoint(dJoint);
   
   setInterval(updateWorld, 1000 / 60);
   
   stage.addEventListener('mousedown', createJoint)
}

對於剛剛創建的 box3 剛體我們沒有什麼需要註釋的,它只是我們創建的又一個盒子而已,但是我將要一行行的來說明一下創建的距離關節

var dJoint = new b2DistanceJointDef();

首先,你應該使用 Box2D 的方式來創建關節的定義,所以在這裏使用 b2DistanceJointDef 來創建距離關節的定義。

注意,請自己在程序開始處引入 b2DistanceJointDef = Box2D.Dynamics.Joints.b2DistanceJointDef

和鼠標關節一樣,距離關節也有屬性需要定義,所以我們將再次看到 bodyA和 bodyB 屬性,這時爲它們分配兩個 dynamic 類型的盒子

dJoint.bodyA = box2;
dJoint.bodyB = box3;

然後,我們需要定義關節綁定到兩個剛體上的點。

localAnchorA 和 localAnchorB 屬性定義你要應用的距離關節綁定到剛體上的本地點。

注意這些本地點,它們採用的座標爲剛體自身內的座標(在本地點中,剛體中心位置座標爲(0,0)),與剛體相對世界的座標位置無關

dJoint.localAnchorA = new b2Vec2(0,0);
dJoint.localAnchorB = new b2Vec2(0,0);

最後,來定義關節的長度,這是一個在 localAnchorA 和 localAnchorB 所定義的點之間不變的距離。

這些盒子分別創建在(320,430)和(420,430)的位置,所以,這裏的距離是 100 像素。

我們不打算修改這個數值,所以 length 屬性將是

dJoint.length=100/worldScale;

現在,關節定義準備在世界中來創建關節,這些都要歸功於 b2DistanceJoint 對象——像往常一樣創建並添加關節到世界中:

var distanceJoint = world.CreateJoint(dJoint);

現在,你可以測試網頁,拾取並拖拽任何一個dynamic類型的剛體,但是,在剛體上起源點(0,0)之間距離不會改變,這要感謝距離關節。

image

源碼: article/ch06/ch06-3.html

雖然你可以通過鼠標和距離關節做很多事情,但是這裏還有另一種類型的關節,它是遊戲設計中的萬金油(原文翻譯爲:它是遊戲設計中的救生圈):旋轉關節

使剛體繞一個點旋轉——旋轉關節

旋轉關節將兩個剛體綁定到彼此的共有的錨點上,這樣剛體就只剩下一個自由度:繞着錨點旋轉。

一個最常見使用旋轉關節的地方是創建輪子和齒輪。

我們將在稍後搭建攻城機(一個投擲石塊的拋擲器)時,創建輪子。

目前,我只想添加另一個盒子到我們的腳本中然後將它綁定到地面剛體(在鼠標關節的介紹中曾提到過,它就像真實世界的空氣)上,讓你看到怎樣與旋轉關節交互。

我們只需要對 main() 方法做些許的改變, 在 var distanceJoint... 代碼後添加上:

bodyDef.position.Set(320/worldScale,240/worldScale);
var box4 = world.CreateBody(bodyDef);
box4.CreateFixture(fixtureDef);
var rJoint = new b2RevoluteJointDef();
rJoint.bodyA = box4;
rJoint.bodyB = world.GetGroundBody();
rJoint.localAnchorA = new b2Vec2(0,0);
rJoint.localAnchorB = box4.GetWorldCenter();
var revoluteJoint = world.CreateJoint(rJoint);

像往常一樣,對於創建的 box4 沒有什麼好說的,不過我將一行行的來解釋關節的創建,它是從定義 b2RevoluteJointDef 開始的

注: 在程序頂部記得引入 var b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef

var rJoint = new b2RevoluteJointDef();

處理過程和之前的方式一樣,將我們最新創建的盒子和地面剛體分配給 bodyA 和 bodyB 屬性:

rJoint.bodyA = box4;
rJoint.bodyB = world.GetGroundBody();

現在,將本地錨點關聯到剛體上,它們分別是盒子的起源點(0,0)和該點在世界中的座標。

rJoint.localAnchorA = new b2Vec2(0,0);
rJoint.localAnchorB = box4.GetWorldCenter();

那麼現在,我們來創建旋轉關節自身:

var revoluteJoint = world.CreateJoint(rJoint);

測試網頁,然後與最新創建的盒子進行交互:

嘗試拖拽它、將別的盒子放置到它的上面,做任何你想要做的事情。

可是你沒有辦法移動它,因此它將只是繞着它的錨點旋轉。

image

源碼: article/ch06/ch06-4.html

這裏有很多 Box2d 支持的關節類型,但是列舉出它們並解釋所有的關節已超出本書的範圍。

我想要你學習的是怎樣使 Box2D 來增強遊戲,而鼠標、距離以及旋轉關節已經可以讓你實現幾乎所有的事情。

你可以通過參考官方文檔來獲取完整的關節列表信息:http://box2d.org/manual.pdf

所以,我沒有將那些沒有意義的 Box2D 關節一一列舉,我將向你展示一些確實關聯到遊戲開發的東西:一個攻城機(粉碎城堡一款android的遊戲中的攻城器械)。

當憤怒的小鳥遇見粉碎城堡

如果小鳥有一個攻城機會怎樣呢?讓我們來探索這個遊戲的設置吧!

但是,首先讓我來說明一下我想要只使用距離和旋轉關節來搭建的攻城機類型。

攻城機是由兩個安裝了輪子的掛車組成。

第一個手推車可以通過玩家控制並且它是作爲卡車的車頭。

第二個掛車只是一個掛車,但是在它的上面有一個投石器。

是不是有點困惑?讓我想你展示一下它的原型吧:

image

這裏有很多事情要做,所以我們立即開始吧。

  1. 開始的一步在本書中已經無數次寫過了,main 函數改造成:

    function main(){
       world = new b2World(gravity, sleep);
       debugDraw();
    
       ground();
       var frontCart = addCart(200,430);
       var rearCart = addCart(100,430);
       setInterval(updateWorld, 1000 / 60);
    }
    
  2. 像往常我們創建一個新世界一樣,一個調試繪圖慣例,一個地面以及一個監聽添加(流程中的公有方法調用)

    這樣做是爲了保證我們在這些流程中沒有遺漏,ground() 方法如往常一樣創建一個大的 static 類型的剛體作爲地面,如下所示:

    function ground()  {
       var bodyDef = new b2BodyDef();
       bodyDef.position.Set(320/worldScale,470/worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(320/worldScale,10/worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       var groundBody = world.CreateBody(bodyDef);
       groundBody.CreateFixture(fixtureDef);
    }
    
  3. 在 main() 方法中唯一不同的是 addCart() 方法,它只是用來添加一個盒子形狀並給與座標,總的來說,這裏也沒有什麼新的東西:

    function addCart(pX, pY) {
       var bodyDef = new b2BodyDef();
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(40 / worldScale, 20 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
       var frontWheel = addWheel(pX + 20, pY + 15);
       var rearWheel = addWheel(pX - 20, pY + 15);
       return body;
    }
    
  4. 方法的最後,返回剛體之前,這裏調用了兩次addWheel()方法,它將在給定的座標創建一個球型。

    說到這裏也就沒有什麼新的東西可說了,但是你將看到我們的攻城機是個什麼形狀了。

    function addWheel(pX, pY) {
       var bodyDef = new b2BodyDef();
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var circleShape = new b2CircleShape(0.5);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = circleShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
       return body;
    }
    
  5. 在 debugDraw() 方法內確保有下面這一句,它將向我們呈現出關節:

    debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
    
  6. 此刻,兩個static類型的掛車將被創建

    測試網頁然後檢查一切是否看起來正常:

    image

    源碼: article/ch06/ch06-5.html

    記住,當你在Box2D世界中創建新的東西時,請使用static類型的剛體來查看,在沒有重力、作用力以及碰撞的情況下,你的模型的樣子。

    現在,一切看起來都不錯,讓我們將剛體變爲 dynamic 類型然後添加需要的關節。

  7. 通過在 addWheel() 方法和 addCart() 方法中添加 type 屬性將輪子和掛車設置爲 dynamic 類型的剛體。

    addWheel() 方法內添加:

    bodyDef.type = b2Body.b2_dynamicBody;
    
  8. 然後,addCart() 方法也要做同樣的修改:

    function addCart(pX, pY) {
       var bodyDef = new b2BodyDef();
       bodyDef.type = b2Body.b2_dynamicBody;
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(40 / worldScale, 20 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
       var frontWheel = addWheel(pX + 20, pY + 15);
       var rearWheel = addWheel(pX - 20, pY + 15);
    
       var rJoint = new b2RevoluteJointDef();
       rJoint.bodyA = body;
       rJoint.bodyB = frontWheel;
       rJoint.localAnchorA.Set(20 / worldScale, 15 / worldScale);
       rJoint.localAnchorB.Set(0, 0);
       var revoluteJoint = world.CreateJoint(rJoint);
       rJoint.bodyB = rearWheel;
       rJoint.localAnchorA.Set(-20 / worldScale, 15 / worldScale);
       revoluteJoint = world.CreateJoint(rJoint);
       return body;
    }
    

    但是,在 addCart() 方法中添加的代碼並不止這些。

    如你所見,我添加了兩個旋轉關節分別將每個輪子的起源點(0,0)與掛車綁定在一起。

    不要擔心通過旋轉關節綁定在一起的剛體之間的碰撞。

  9. 最後,我們需要一個距離關節來管理兩個掛車之間的距離,將它們添加到 main() 方法中:

     function main() {
       world = new b2World(gravity, sleep);
       debugDraw();
    
       ground();
       var frontCart = addCart(200, 430);
       var rearCart = addCart(100, 430);
    
       var dJoint = new b2DistanceJointDef();
       dJoint.bodyA = frontCart;
       dJoint.bodyB = rearCart;
       dJoint.localAnchorA = new b2Vec2(0, 0);
       dJoint.localAnchorB = new b2Vec2(0, 0);
       dJoint.length = 100 / worldScale;
       var distanceJoint = world.CreateJoint(dJoint);
    
       setInterval(updateWorld, 1000 / 60);
    }
    
  10. 目前,在這裏沒有什麼新的東西,但是你可以去開始構建你的攻城機。

測試網頁:

![image](https://img2023.cnblogs.com/blog/405426/202310/405426-20231027210527856-406965285.png)

源碼: article/ch06/ch06-6.html

你的掛車現在有輪子並且他們通過一個距離關節連接在了一起。

現在我們必須介紹一些新的東西,來讓玩家移動這個掛車

通過馬達控制關節

有些關節,例如旋轉關節有一個馬達的特性,在這種情況下,除非給定的最大扭矩超出範圍,不然在給定的速度下,可以使用它來旋轉關節。

學習馬達將讓你可以去開發在網頁上看到的任何汽車/卡車遊戲

  1. 創建卡車,我們需要在最右邊的掛車上應用一個馬達,所以在 addCart() 方法中,我們添加一個參數來告知我們創建的掛車是否需要一個馬達。

    改變 main() 方法,指定 frontCart 將要一個馬達,而 rearCart 不需要:

    var frontCart = addCart(200, 430, true);
    var rearCart = addCart(100, 430, false);
    
  2. 因此,addCart() 方法的聲明也將要發生改變,但是這並不是什麼新鮮事了。

    我希望你注意添加的判斷 motor爲 true 的代碼:

    function addCart(pX, pY, motor) {
       var bodyDef = new b2BodyDef();
       bodyDef.type = b2Body.b2_dynamicBody;
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(40 / worldScale, 20 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
       var frontWheel = addWheel(pX + 20, pY + 15);
       var rearWheel = addWheel(pX - 20, pY + 15);
    
       var rJoint = new b2RevoluteJointDef();
       rJoint.bodyA = body;
       rJoint.bodyB = frontWheel;
       rJoint.localAnchorA.Set(20 / worldScale, 15 / worldScale);
       rJoint.localAnchorB.Set(0, 0);
    
       if (motor) {
          rJoint.enableMotor = true;
          rJoint.maxMotorTorque = 1000;
          rJoint.motorSpeed = 5;
       }
    
       var revoluteJoint = world.CreateJoint(rJoint);
       rJoint.bodyB = rearWheel;
       rJoint.localAnchorA.Set(-20 / worldScale, 15 / worldScale);
       revoluteJoint = world.CreateJoint(rJoint);
       return body;
    }
    

    讓我們來看看新的代碼:

    rJoint.enableMotor = true;
    

    b2RevoluteJointDef 的 enableMotor 是一個布爾值屬性。

    它的默認值是 false,但是設置它爲 true 將允許我們添加爲旋轉關節添加一個馬達。

    rJoint.maxMotorTorque = 1000;
    

    maxMotorTorque 屬性是馬達可以應用的最大扭力的定義。它的值越大,馬達越加的強勁。

    請注意該屬性不控制馬達的速度,但是最大扭力可以用來達到理想的轉速。它的計量單位是牛頓米或Nm。

    最後,motorSpeed 屬性設置期望的馬達速度,計量單位是弧度/秒:

    rJoint.motorSpeed = 5;

  3. 結尾,三行與馬達相關的代碼意味着:可以使用馬達和設置它的速度爲5度/秒,使用最大扭矩爲 1000 Nm

  4. 測試網頁,然後看着你的掛車向右運動:

    image

    源碼: article/ch06/ch06-7.html

現在你可以使掛車移動了。但是怎樣通過一個鍵盤輸入來移動掛車呢?

通過鍵盤控制馬達

我們希望玩家通過方向鍵來控制掛車。

左方向鍵將掛車向左移動而右方向鍵將掛車向右移動。

  1. 讓玩家通過鍵盤控制馬達,你需要一些新的類變量

    let left = false;
    let right = false;
    let frj;
    let rrj;
    let motorSpeed = 0;
    

    left 和 right是布爾值變量,它將讓我們知道左或右方向鍵是否被按下。

    frj 和 rrj 分別是前後的旋轉關節。

    你可能會對變量名產生困惑,但是我這樣做是爲了便於代碼的佈局而儘量使用較少的字母。

    (變量名分別是front/rear revolute joint的首字母縮寫)

    motorSpeed 是當前馬達的速度,初始值爲 0

  2. 在 main()方法中,我們添加了當玩家按下或釋放按鍵時觸發的事件監聽:

    document.addEventListener("keydown", keyPressed)
    document.addEventListener("keyup", keyReleased)
    
  3. 同時在下面的回調方法中。當玩家按下左或右方向鍵時,left 或 right將變成:

    function keyPressed(e) {
       switch (e.keyCode) {
             case 37:
                left = true;
                break;
             case 39:
                right = true;
                break;
       }
    }
    

    通過相同的方式,當玩家按下左或右方向鍵時,left 或 right將變成 false:

    function keyReleased(e) {
       switch (e.keyCode) {
             case 37:
                left = false;
                break;
             case 39:
                right = false;
                break;
       }
    }
    
  4. 在 addCart() 方法中有一些改變,但是這些大多是爲了區別旋轉關節是否帶有馬達,它是通過鍵盤控制還是被動的旋轉關節。

    function addCart(pX, pY, motor) {
       var bodyDef = new b2BodyDef();
       bodyDef.type = b2Body.b2_dynamicBody;
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(40 / worldScale, 20 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
    
    
       var frontWheel = addWheel(pX + 20, pY + 15);
       var rearWheel = addWheel(pX - 20, pY + 15);
       var rJoint = new b2RevoluteJointDef();
       rJoint.bodyA = body;
       rJoint.bodyB = frontWheel;
       rJoint.localAnchorA.Set(20 / worldScale, 15 / worldScale);
       rJoint.localAnchorB.Set(0, 0);
    
       if (motor) {
             rJoint.enableMotor = true;
             rJoint.maxMotorTorque = 1000;
             rJoint.motorSpeed = 0;
             frj = world.CreateJoint(rJoint);
       } else {
             var rj = world.CreateJoint(rJoint);
       }
    
       rJoint.bodyB = rearWheel;
       rJoint.localAnchorA.Set(-20 / worldScale, 15 / worldScale);
    
       if (motor) {
             rrj = world.CreateJoint(rJoint);
       } else {
             rj = world.CreateJoint(rJoint);
       }
       return body;
    }
    

    主要的不同是用來創建旋轉關節的變量。

    前掛車需要馬達將使用 frj 和 rrj 類變量 來代表旋轉關節,而後掛車不需要馬達只要使用本地變量即可。

  5. 核心的腳本編寫在 updateWorld() 方法中

    它依據被按下的按鍵來調節 motorSpeed 變量(我通過每次乘以0.99來模擬慣性和摩擦力),

    極限速度是 5 或 -5,然後更新旋轉關節的馬達速度。

    function updateWorld() {
       if (left) {
             motorSpeed -= 0.1;
       }
       if (right) {
             motorSpeed += 0.1;
       }
       motorSpeed * 0.99;
       if (motorSpeed > 5) {
             motorSpeed = 5;
       }
       if (motorSpeed < -5) {
             motorSpeed = -5;
       }
       frj.SetMotorSpeed(motorSpeed);
       rrj.SetMotorSpeed(motorSpeed);
       world.Step(1 / 30, 10, 10);// 更新世界模擬
       world.DrawDebugData(); // 顯示剛體debug輪廓
       world.ClearForces(); // 清除作用力
    }
    
  6. SetMotorSpeed 方法將直接作用於旋轉關節(而不是它的定義)從而允許 我們及時更新馬達的速度。

  7. 測試網頁,你將可以通過左右方向鍵來控制掛車

    image

    源碼: article/ch06/ch06-8.html

現在,我們有一個可以運行的掛車,但是不要忘記在這兒我們還沒有搭建支架,使用攻城機來摧毀小豬的藏匿處。

讓一些剛體不要發生碰撞——碰撞過濾

不要被標題唬住了:我們將搭建攻城機,但是這將沒有什麼新的東西,而我希望你在每一步的學習中都能學習到新的知識,所以本節的主要目的是學習碰撞過濾。

  1. 首先要做,是讓我們來搭建攻城機。

    投擲器將通過距離關節被綁定到掛車上,這使我們能夠發射摧毀性的石塊,所以我們需要把它作爲類變量:

    let sling; 
    
  2. 在掛車上的攻城機的構造並不複雜,當 motor 爲 false 時,這裏的代碼量增加了不少,這是爲了在不使用馬達的掛車上搭建攻城機。

    在上一節源碼 addCard 方法內增加:

    function addCart(pX, pY, motor) {
       var bodyDef = new b2BodyDef();
       bodyDef.type = b2Body.b2_dynamicBody;
       bodyDef.position.Set(pX / worldScale, pY / worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(40 / worldScale, 20 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       fixtureDef.density = 1;
       fixtureDef.restitution = 0.5;
       fixtureDef.friction = 0.5;
       var body = world.CreateBody(bodyDef);
       body.CreateFixture(fixtureDef);
    
       if (!motor) {
          // 垂直長臂
          var armOrigin = new b2Vec2(0, -60 / worldScale);
          var armW = 5 / worldScale
          var armH = 60 / worldScale
          polygonShape.SetAsOrientedBox(armW, armH, armOrigin);
          body.CreateFixture(fixtureDef);
          // 旋轉臂
          bodyDef.position.Set(pX / worldScale, (pY - 115) / worldScale);
          polygonShape.SetAsBox(40 / worldScale, 5 / worldScale);
          fixtureDef.shape = polygonShape;
          fixtureDef.filter.categoryBits = 0x0002;
          fixtureDef.filter.maskBits = 0x0002;
          var arm = world.CreateBody(bodyDef);
          arm.CreateFixture(fixtureDef);
          //旋轉關節
          var armJoint = new b2RevoluteJointDef();
          armJoint.bodyA = body;
          armJoint.bodyB = arm;
          armJoint.localAnchorA.Set(0, -115 / worldScale);
          armJoint.localAnchorB.Set(0, 0);
          armJoint.enableMotor = true;
          armJoint.maxMotorTorque = 1000;
          armJoint.motorSpeed = 6;
          var siege = world.CreateJoint(armJoint);
          // 拋擲物
          var projectileX = (pX - 80) / worldScale;
          var projectileY = (pY - 115) / worldScale;
          bodyDef.position.Set(projectileX, projectileY);
          polygonShape.SetAsBox(5 / worldScale, 5 / worldScale);
          fixtureDef.shape = polygonShape;
          fixtureDef.filter.categoryBits = 0x0004;
          fixtureDef.filter.maskBits = 0x0004;
          var projectile = world.CreateBody(bodyDef);
          projectile.CreateFixture(fixtureDef);
          // 距離關節
          var slingJoint = new b2DistanceJointDef();
          slingJoint.bodyA = arm;
          slingJoint.bodyB = projectile;
          slingJoint.localAnchorA.Set(-40 / worldScale, 0);
          slingJoint.localAnchorB.Set(0, 0);
          slingJoint.length = 40 / worldScale;
          sling = world.CreateJoint(slingJoint);
       }
    
       var frontWheel = addWheel(pX + 20, pY + 15);
       var rearWheel = addWheel(pX - 20, pY + 15);
       var rJoint = new b2RevoluteJointDef();
       rJoint.bodyA = body;
       rJoint.bodyB = frontWheel;
       rJoint.localAnchorA.Set(20 / worldScale, 15 / worldScale);
       rJoint.localAnchorB.Set(0, 0);
    
       if (motor) {
             rJoint.enableMotor = true;
             rJoint.maxMotorTorque = 1000;
             rJoint.motorSpeed = 0;
             frj = world.CreateJoint(rJoint);
       } else {
             var rj = world.CreateJoint(rJoint);
       }
    
       rJoint.bodyB = rearWheel;
       rJoint.localAnchorA.Set(-20 / worldScale, 15 / worldScale);
    
       if (motor) {
             rrj = world.CreateJoint(rJoint);
       } else {
             rj = world.CreateJoint(rJoint);
       }
       return body;
    }
    

    讓我們一段一段的來分析這些代碼

    var armOrigin = new b2Vec2(0, -60 / worldScale);
    var armW = 5 / worldScale
    var armH = 60 / worldScale
    polygonShape.SetAsOrientedBox(armW, armH, armOrigin);
    body.CreateFixture(fixtureDef);
    

    上面的 5 行代碼在拋擲器和掛車之間創建了一個垂直的“長臂”。

    垂直支撐條是掛車的一部分,正如它的夾具被添加到相同的剛上。

    它是個複合對象。

    bodyDef.position.Set(pX / worldScale, (pY - 115) / worldScale);
    polygonShape.SetAsBox(40 / worldScale, 5 / worldScale);
    fixtureDef.shape = polygonShape;
    fixtureDef.filter.categoryBits = 0x0002;
    fixtureDef.filter.maskBits = 0x0002;
    var arm = world.CreateBody(bodyDef);
    arm.CreateFixture(fixtureDef);
    

    這是一個旋轉臂,投擲器的一部分。

    它是作爲一個單獨剛體被創建的,因此它將通過旋轉關節綁定到掛車的垂直支撐條上。

    var armJoint = new b2RevoluteJointDef();
    armJoint.bodyA = body;
    armJoint.bodyB = arm;
    armJoint.localAnchorA.Set(0, -115 / worldScale);
    armJoint.localAnchorB.Set(0, 0);
    armJoint.enableMotor = true;
    armJoint.maxMotorTorque = 1000;
    armJoint.motorSpeed = 6;
    var siege = world.CreateJoint(armJoint);
    

    上面是旋轉關節。它有一個馬達來旋轉投擲器。

    var projectileX = (pX - 80) / worldScale;
    var projectileY = (pY - 115) / worldScale;
    bodyDef.position.Set(projectileX, projectileY);
    polygonShape.SetAsBox(5 / worldScale, 5 / worldScale);
    fixtureDef.shape = polygonShape;
    fixtureDef.filter.categoryBits = 0x0004;
    fixtureDef.filter.maskBits = 0x0004;
    var projectile = world.CreateBody(bodyDef);
    projectile.CreateFixture(fixtureDef);
    

    拋擲物——這個剛體通過攻城機發射,它在投擲器的頭部——將通過一個距離關節綁定到旋轉臂上。

    var slingJoint = new b2DistanceJointDef();
    slingJoint.bodyA = arm;
    slingJoint.bodyB = projectile;
    slingJoint.localAnchorA.Set(-40 / worldScale, 0);
    slingJoint.localAnchorB.Set(0, 0);
    slingJoint.length = 40 / worldScale;
    sling = world.CreateJoint(slingJoint);
    

    然後,我們通過距離關節將完成投擲器。

    一切看起來很容易,但是你還遺漏了下面的兩行在創建垂直支撐條時的代碼:

    fixtureDef.filter.categoryBits = 0x0002;
    fixtureDef.filter.maskBits = 0x0002;
    

    下面的兩行代碼同樣也被遺漏在創建拋擲物時:

    fixtureDef.filter.categoryBits = 0x0004;
    fixtureDef.filter.maskBits = 0x0004;
    

    你已經知道通過旋轉關節綁定在一起的剛體之間是不會發生碰撞。

    不幸的是,拋擲物不是旋轉關節的一部分,所以將會和垂直臂發生碰撞,所以拋擲器將無法工作,除非我們發現一種可以避免垂直臂和拋擲物之間發生碰撞的方法。

    Box2d 有一個特性是碰撞過濾,它允許你阻止夾具間的碰撞。

    碰撞過濾允許我們對夾具的 categoryBits 進行設置。

    通過這個方法,更多的夾具可以被放置在一個組中。

    然後,你需要爲每個組指定它們將於那個組發生碰撞,通過設置 maskBits 屬性

    在我們剛剛看過的四行代碼中,掛車和拋擲物被放置在了不同的類別中

    它們被允許只能與同一類別的夾具發生碰撞,所以拋擲物和垂直臂之間將不會發生碰撞,這樣拋擲器將可以自由的旋轉了。

    這樣做,拋擲物也將不會和地面發生碰撞,但是我們將在稍後修復這個問題。

  3. 最後一件事,玩家將能夠通過釋放向上的方向鍵來銷燬距離關節從而發射拋擲物,所以我們在 keyPressed() 方法中的 switch 語句中添加另一個條件

    function keyReleased(e) {
       switch (e.keyCode) {
             case 37:
                left = false;
                break;
             case 39:
                right = false;
                break;
             case 38 :
                world.DestroyJoint(sling);
                break;
       }
    }
    

    DestroyJoint 方法將從世界中移除一個關節

  4. 測試網頁,左右移動攻城機,然後通過上方向鍵發射拋擲物

    image

    源碼: article/ch06/ch06-9.html

這是一個很大的成就。

在 Box2D 中搭建一個攻城機並不容易,但是你做到了。那麼我們開始消滅小豬吧!

將它們放在一起

是時候將我們知道的所有Box2D知識整合到一起了,來創建憤怒的小鳥和摧毀城堡的最終混合版吧!

  1. 首先,我們對 ground() 方法做一點修改 bodyDef.position 來讓它將地面放置在我們最近一次創建憤怒的小鳥模型時的地面位置相同的地方:

    function ground() {
       var bodyDef = new b2BodyDef();
       bodyDef.position.Set(320/worldScale, 465/worldScale);
       var polygonShape = new b2PolygonShape();
       polygonShape.SetAsBox(320 / worldScale, 10 / worldScale);
       var fixtureDef = new b2FixtureDef();
       fixtureDef.shape = polygonShape;
       var groundBody = world.CreateBody(bodyDef);
       groundBody.CreateFixture(fixtureDef);
    }
    
  2. 然後,我們需要更新 main() 方法來加入自定義事件監聽、磚塊以及小豬

    function main() {
       world = new b2World(gravity, sleep);
       world.SetContactListener(new CustomContactListener());
       debugDraw();
    
       ground();
    
       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 frontCart = addCart(200, 430, true);
       var rearCart = addCart(100, 430, false);
       var dJoint = new b2DistanceJointDef();
       dJoint.bodyA = frontCart;
       dJoint.bodyB = rearCart;
       dJoint.localAnchorA = new b2Vec2(0, 0);
       dJoint.localAnchorB = new b2Vec2(0, 0);
       dJoint.length = 100 / worldScale;
       var distanceJoint = world.CreateJoint(dJoint)
    
    
       document.addEventListener("keydown", keyPressed)
       document.addEventListener("keyup", keyReleased)
    
       setInterval(updateWorld, 1000 / 60);
    }
    
  3. 很顯然,你需要像最近一次創建憤怒的小鳥模型時一樣,添加 brick() 和 pig() 方法,並且讓 CustomContactListener 類來處理碰撞。

    CustomContactListener 還記得吧,在第 5 章 Box2D 內建的碰撞監聽 一節我們自己創建的類

  4. 然後,我們對 addCart() 方法做一點修改

    通過使用自定義數據(userData屬性)爲我們的掛車部分起一個名字,增加它們的重量——直到增加的重量通過拋擲物足夠摧毀小豬的城堡,然後我們也將移除過濾。

    function addCart(pX, pY, motor) {
       ... 代碼省略
       bodyDef.userData="cart";
       ... 代碼省略
       ... 代碼省略
       fixtureDef.density = 15;
       ... 代碼省略
       bodyDef.userData="projectile";
    
    }
    

    爲小車,和 拋擲物 添加 userData,將

  5. 因此,我們使用一個自定義接觸監聽,我們將使用 CustomContactListener 類來禁止攻城機和拋擲物之間的碰撞。

  6. 最後,updateWorld()方法必須也要改變,添加用來包含移除剛體的代碼。

    function updateWorld() {
       if (left) {
             motorSpeed -= 0.1;
       }
       if (right) {
             motorSpeed += 0.1;
       }
       motorSpeed * 0.99;
       if (motorSpeed > 5) {
             motorSpeed = 5;
       }
       if (motorSpeed < -5) {
             motorSpeed = -5;
       }
       frj.SetMotorSpeed(motorSpeed);
       rrj.SetMotorSpeed(motorSpeed);
       world.Step(1 / 30, 10, 10);// 更新世界模擬
       world.DrawDebugData(); // 顯示剛體debug輪廓
       world.ClearForces(); // 清除作用力
       // 移除剛體
       for (var b = world.GetBodyList(); b; b = b.GetNext()) {
             if (b.GetUserData() == "remove") {
                world.DestroyBody(b);
             }
       }
    }
    
  7. 最後但並不意味着最少,有些關於碰撞的新知識需要學習。

    下面是我怎樣使用 PreSolve() 回調方法來決定如果掛車與拋擲物發生碰撞,將在接觸決算之前禁止碰撞發生,將這個方法添加到 CustomContactListener 類中

    PreSolve(contact, oldManifold)  {
          var fixtureA =contact.GetFixtureA();
          var fixtureB =contact.GetFixtureB();
          var dataA =fixtureA.GetBody().GetUserData();
          var dataB =fixtureB.GetBody().GetUserData();
          if (dataA=="cart" && dataB=="projectile") {
             contact.SetEnabled(false);
          }
          if (dataB=="cart" && dataA=="projectile") {
             contact.SetEnabled(false);
          }
    }
    

    這裏都是你已經學習過的處理碰撞的知識。

    我只是檢查發生碰撞的掛車和拋擲物,如果是,則通過 setEnabled() 方法禁止接觸發生。

  8. 測試網頁,然後如預期的,你將有一個攻城機摧毀小豬的城堡

    image

    源碼: article/ch06/ch06-10.html

一切運行順利,我們對它完全滿意,不是嗎?

告訴你個祕密,在某些情況下,你將會看到拋擲物似乎穿過了磚塊而沒有發生接觸。

這個是 Box2D 的 bug 嗎?或者在接觸回調中有什麼錯誤嗎?

全都不是,它只是一個你還沒有發現的 Box2D 的特徵,不過你將會很快接觸到它了。

小結

在本書最長也最難的一章中,你學習了怎樣使用鼠標、距離以及旋轉關節來使遊戲設置更加的高級。

爲什麼不去嘗試搭建一個投石機呢?


本文相關代碼請在

https://github.com/willian12345/Box2D-for-Javascript-Games

注:轉載請註明出處博客園:王二狗Sheldon池中物 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章