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

池中物王二狗發表於2023-10-27

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池中物 (willian12345@126.com)

相關文章