2D物理引擎 Box2D for javascript Games 第四章 將力作用到剛體上

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

2D物理引擎 Box2D for javascript Games 第四章 將力作用到剛體上

將力作用到剛體上

Box2D 是一個在力作用下的世界,它可以將力作用於剛體上,從而給我們一個更加真實的模擬。

但是,如果你想要移動剛體,發射子彈,拋擲小鳥,駕駛汽車和當你在玩物理遊戲時你看到的一切令人起勁的事情,那麼你必須要手動的將力作用剛體上。

在本章,你將學習怎樣透過不同的方法將力作用於剛體上使它移動,管理很多新的Box2D特徵,包含如下:

  • 將力作用到剛體上
  • 將衝量作用到剛體上
  • 設定剛體的線性速率
  • 知道剛體的質量
  • 根據你的遊戲設定需要應用正確的力的作用方式
  • 在你的遊戲中使用物理和非物理資源

透過本章的學習,你將有能力建立憤怒的小鳥的一個關卡,包含小鳥的發射器。

蘋果掉落,修正

傳說牛頓發現重力是因為一個蘋果掉下來砸到了他。你將學習用力將一些物件從地面提升起來。

我們將從三個 dynamic 型別的球體放置在一個 static 型別的地面上的指令碼開始。

下面將是你開始的基礎程式碼

function init() {
    var   b2Vec2 = Box2D.Common.Math.b2Vec2
    ,  b2AABB = Box2D.Collision.b2AABB
    ,	b2BodyDef = Box2D.Dynamics.b2BodyDef
    ,	b2Body = Box2D.Dynamics.b2Body
    ,	b2FixtureDef = Box2D.Dynamics.b2FixtureDef
    ,	b2Fixture = Box2D.Dynamics.b2Fixture
    ,	b2World = Box2D.Dynamics.b2World
    ,	b2MassData = Box2D.Collision.Shapes.b2MassData
    ,	b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape
    ,	b2CircleShape = Box2D.Collision.Shapes.b2CircleShape
    ,	b2DebugDraw = Box2D.Dynamics.b2DebugDraw
    ,  b2MouseJointDef =  Box2D.Dynamics.Joints.b2MouseJointDef
    ;

    var worldScale = 30; // box2d中以米為單位,1米=30畫素
    var gravity = new b2Vec2(0, 10);
    var sleep = true;
    var world;
    var velIterations = 10;// 速率約束解算器
    var posIterations = 10;// 位置約束解算器
    var sphereVector;

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

    sphereVector = [];
    for(var i=0; i<3; i++){
        sphereVector.push(sphere(170+i*150, 410, 40));
    }

    setInterval(updateWorld, 1000 / 60);
    }
    main();


    function sphere(px, py, r){
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(px/worldScale, py/worldScale);
    bodyDef.type = b2Body.b2_dynamicBody
    bodyDef.userData = py;
    var circleShape = new b2CircleShape(r/worldScale);
    var fixtureDef = new b2FixtureDef();
    fixtureDef.shape = circleShape;
    fixtureDef.density = 2;
    fixtureDef.restitution = .4
    fixtureDef.friction = .5;
    var theSphere = world.CreateBody(bodyDef);
    theSphere.CreateFixture(fixtureDef);
    return theSphere;
    }

    function floor(){
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(320/worldScale, 465/worldScale);
    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);
    }


    function updateWorld() {
    world.Step(1/30, 10, 10);// 更新世界模擬
    world.DrawDebugData(); // 顯示剛體debug輪廓
    world.ClearForces(); // 清除作用力
    }

    //setup debug draw
    function debugDraw(){
    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);
    }
};

原始碼在: article/ch04/ch04-1.html

你應該很熟悉上面的程式碼了,它只是對之前章節的圖騰指令碼稍作修改而成的。

雖然向 sphere()方法傳入以畫素為單位的中心座標和半徑後,會建立一個 dynamic 型別的球體,但是 updateWorld、floor、debugDraw 和之前是一樣沒變

測試網頁,你會發現三個球體依次擺地面上

image

每一個球體都儲存在 sphereVector 中,從左到右分別是:sphereVector[0],sphereVector[1],sphereVector[2]。

我們開始讓球體彈跳吧。在這個示例中有三個 dynamic 型別的剛體,因為它們將用來讓你學習三種不同的方式將力作用於剛體上,每一種都有優點和缺點。

力,衝量和線速率

讓我們直接來看看Main()方法中新加的這幾行程式碼:

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

    sphereVector = [];
    for(var i=0; i<3; i++){
        sphereVector.push(sphere(170+i*150, 410, 40));
    }

    var force = new b2Vec2(0,-15);
    var sphereCenter = sphereVector[0].GetWorldCenter(); 
    sphereVector[0].ApplyForce(force, sphereCenter);
    sphereCenter = sphereVector[1].GetWorldCenter();
    sphereVector[1].ApplyImpulse(force,sphereCenter);
    sphereVector[2].SetLinearVelocity(force);

    setInterval(updateWorld, 1000 / 60);
}

這是本章的核心,並且有著很多新的知識,所以讓我們,一行行的來解釋它們:

var force = new b2Vec2(0,-15);

首先,我們需要建立一個 b2Vec2 型別的變數,它將代表我們想要作用到所有球體上的力。設定為(0,-15),意味著這是一個垂直方向的力,它將是球體跳躍起來。

光有一個力是不夠的,我們還需要一個應用力的作用點。在本例中,我們想將力的作用在每個球體的質心。

var sphereCenter = sphereVector[0].GetWorldCenter();

我們可以使用 b2Body 的 GetWorldCenter() 方法,來確定剛體質心。它將返回一個 b2Vec2 物件的質心。

在這裡,shpereCenter 變數將包含左邊球體的質心。

sphereVector[0].ApplyForce(force,sphereCenter);

之前的那些程式碼展示了第一種將力作用到剛體上的方法。

ApplyForce() 方法將力作用到一點上,通常使用的牛頓(N)為單位。如果力沒有作用在質心,它將產生一個扭矩並影響角速度,這就是為什麼我們想要獲得質心的原因。

如果剛體在睡眠狀態,ApplyForce() 將可以喚醒它們。

sphereCenter=sphereVector[1].GetWorldCenter();

當我們完成來了將力作用的左邊的球體時,那麼讓我們將逐一集中到中間的球體上。

我們將像之前一樣獲取質心,然後新增另一種作用力的方法。

sphereVector[1].ApplyImpulse(force,sphereCenter);

ApplyImpulse() 方法將一個衝量作用到一個點上,通常使用牛頓每秒(n/s),將會立刻改變剛體的速率。

如果衝量的作用點不是質心的話,那麼它將會改變角速度,但是在本例中沒有出現,ApplyImpulse() 方法也將喚醒在睡眠狀態的剛體。

現在我們完成了對中間的球體作用力,讓我們來看看右邊的球體:

sphereVector[2].SetLinearVelocity(force);

SetLinearVelocity 方法設定質心的線速度。

它不像 ApplyForce() 和 ApplyImpulse() 方法那樣需要第二個引數,因為它一直是作用在質心的。

你是否對球體將怎樣反應力的作用而剛到疑惑?

那麼我們新增一些程式碼來看看球體做了些什麼吧。

首先我們在 sphere() 方法中將球體的垂直座標儲存到 userData 屬性中:

function sphere(px, py, r){
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(px/worldScale, py/worldScale);
    bodyDef.type = b2Body.b2_dynamicBody
    bodyDef.userData = py;
    var circleShape = new b2CircleShape(r/worldScale);
    var fixtureDef = new b2FixtureDef();
    fixtureDef.shape = circleShape;
    fixtureDef.density = 2;
    fixtureDef.restitution = .4
    fixtureDef.friction = .5;
    var theSphere = world.CreateBody(bodyDef);
    theSphere.CreateFixture(fixtureDef);
    return theSphere;
}

這裡沒有什麼新的知識,因此我們將不再贅述 userData 屬性的作用。

現在,我們將在輸出視窗輸出一些文字,向下面這樣改變 updateWorld() 方法:

function updateWorld() {
    var maxHeight;
    var currHeight;
    var outHeight;
    world.Step(1/30,10,10);
    for (var i = 0; i<3; i++) {
        maxHeight=sphereVector[i].GetUserData();
        currHeight=sphereVector[i].GetPosition().y*worldScale;    
        maxHeight=Math.min(maxHeight,currHeight);
        sphereVector[i].SetUserData(maxHeight);
        outHeight=sphereVector[i].GetUserData();
        console.log("Sphere "+i+":"+Math.round(outHeight));
    }
    world.DrawDebugData(); // 顯示剛體debug輪廓
    world.ClearForces(); // 清除作用力
}

我想保持跟蹤每個球體達到的最大高度,所以我們將看看每一個球體的座標最小值(y軸是從上向下遞增),以畫素為單位。

我們看到只是將 userData 屬性中儲存 userData 和當前跟蹤座標的最小值,它代表了球體達到的最大高度,然後將結果輸出到控制檯。

測試影片,你將看到最右邊的球體彈跳的最高,然而剩下的球體看起來幾乎靜止在地面上

image

原始碼在: article/ch04/ch04-2.html

同時,我們來看看輸出視窗中證實我們在影片中看到的球體的彈跳高度的文字。

最右邊的文字達到了80畫素,上升高度為330畫素,中間的球體只彈跳了2畫素,最左邊的球體沒有彈跳。

  • Sphere 0:410
  • Sphere 1:408
  • Sphere 2:80

我們可能會認為響應力作用的球體太重(但是最右邊的球體彈跳的卻很高),所以我們在 sphere()方 法中將它們的密度從 2 減低到 1:

function sphere(px, py, r){
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(px/worldScale, py/worldScale);
    bodyDef.type = b2Body.b2_dynamicBody
    bodyDef.userData = py;
    var circleShape = new b2CircleShape(r/worldScale);
    var fixtureDef = new b2FixtureDef();
    fixtureDef.shape = circleShape;
    fixtureDef.density = 1;
    fixtureDef.restitution = .4
    fixtureDef.friction = .5;
    var theSphere = world.CreateBody(bodyDef);
    theSphere.CreateFixture(fixtureDef);
    return theSphere;
}

讓我們再次測試網頁並看看發生了什麼:

  • Sphere 0:410
  • Sphere 1:401
  • Sphere 2:80

第一件事你應該注意到了,最右邊的球體達到的高度和之前一樣,和密度的改變沒有關係。

這是因為 SetLinearVelocity() 方法只設定剛體的線速度,與剛體的質量和之前的速度無關。它只設定速度,就是這樣!

另一方面,中間的球體比之前彈跳的高度多了一點,所以這意味著ApplyImpulse()方法依賴於剛體的質量。

原始碼在: article/ch04/ch04-3.html

應用衝量來得到線速度

現在,給兩個睡眠的球體相同的質量和作用力

SetLinearVelocity() 方法設定速度和質量無關,

而 ApplyImpulse()方法則受質量的影響,

如果我們將 ApplyImpulse() 方法中的力設定為 SetLinearVelocity() 方法中力的質量倍數(將SetLinearVelocity()方法設定的力乘以剛體質量),

向下面這樣改動 main() 方法:

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

    sphereVector = [];
    for(var i=0; i<3; i++){
        sphereVector.push(sphere(170+i*150, 410, 40));
    }

    var force = new b2Vec2(0 ,-15);
    var forceByMass = force.Copy();
    forceByMass.Multiply(sphereVector[1].GetMass());
    var sphereCenter = sphereVector[0].GetWorldCenter();
    sphereVector[0].ApplyForce(force,sphereCenter);
    sphereCenter = sphereVector[1].GetWorldCenter();
    sphereVector[1].ApplyImpulse(forceByMass, sphereCenter);
    sphereVector[2].SetLinearVelocity(force);

    setInterval(updateWorld, 1000 / 60);
    }
    main();

這裡有幾個新概念。

首先,看看 Copy() 方法,它是用來複製一份 b2Vec2 物件,為了避免原來的向量透過引用被修改。

然後,Mutiply() 方法將 b2Vec2 值乘以一個給定的數值,在本例中,這個數值是球體的質量。

b2Body 的 GetMass() 方法將返回剛體的質量單位為千克。

最後,我們定義一個新的力,變數名為 forceByMass,我們將它作用到中間的球體上。

測試網頁,你將看到中間和右邊的球體彈跳到相同的高度:

image

原始碼在: article/ch04/ch04-4.html

控制檯輸出的文字如下:

  • Sphere 0:410
  • Sphere 1:80
  • Sphere 2:80

中間和右邊的球體都以相同的方式響應力的作用了,然而 左邊的球體仍然停留在原先的位置。

到底出了什麼問題呢?

這與時間相關,一個衝量作用的時間是一瞬間,一個力作用時間是持續的,然而通常處理物理問題的計量單位是秒。

這意味著我們想要左邊的球體像其它兩個球一樣做出反應,我們應該將中間球體的力乘以模擬一秒所需要的時間步。

應用力來獲得線速度

因為 Step() 方法的工作週期是 1/30 秒,我們需要將力乘以 30,所以我們向下面這樣改變 main() 方法:

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

    sphereVector = [];
    for(var i=0; i<3; i++){
        sphereVector.push(sphere(170+i*150, 410, 40));
    }

    var force = new b2Vec2(0, -15);
    var forceByMass = force.Copy();
    forceByMass.Multiply(sphereVector[1].GetMass());
    var forceByMassByTime = forceByMass.Copy();
    forceByMassByTime.Multiply(30);
    var sphereCenter = sphereVector[0].GetWorldCenter();
    sphereVector[0].ApplyForce(forceByMassByTime, sphereCenter);
    sphereCenter = sphereVector[1].GetWorldCenter();
    sphereVector[1].ApplyImpulse(forceByMass, sphereCenter);
    sphereVector[2].SetLinearVelocity(force);

    setInterval(updateWorld, 1000 / 60);
}
main();

這裡的概念和之前我們建立新的力和應用力到最左邊的球體時一樣。我將中間球體的力乘以 30。

測試網頁,現在所有的球體將會有著相同的反應:

image

原始碼在: article/ch04/ch04-5.html

同時,輸出視窗中輸出的文字證實了我們透過三種不同的方式控制三個相同的球體獲得了相同的效果:

  • Sphere 0:80
  • Sphere 1:80
  • Sphere 2:80

到目前為止,你學習了將力應用到剛體上的基礎知識。

總之,這個過程很簡單,因為你只是將力作用的睡眠狀態的剛體上。

為了在移動的或與其它剛體發生碰撞的剛體上實現相同效果將會困難的多。

雖然你可以選擇你喜歡的方式將力作用到剛體上,但是,下面關於根據遊戲開發的需要選擇使用力的方式的建議是很有用的:

  • 當你想要停止一個剛體(無論它處於什麼狀態,碰撞或移動)並讓它開始一個新的狀態時,我們使用線速度(LinearVelocity)來設定。

    在遊戲設計中,試想一下一個移動的平臺或一個敵人的巡邏區域:在每個時間步中,你可以設定它的線速度然後它將直接向另一個方向移動。

  • 當你想在單個時間步中應用所有力到已有力作用的剛體上時,使用衝量(Impulse)來設定。在遊戲設計中,將使用衝量使角色跳動。

  • 當你想要給你的角色在一定時間內作用一個推力時,使用力(Force)來設定。例如:一個噴氣火箭揹包。

但是,最重要的是嘗試!相同的事情可以使用不同的方法,這取決你找到一種適合你的作用力的方式。

將力應用到真實的遊戲中

現在,讓我將力應用到憤怒的小鳥的世界中吧!

image

上圖是憤怒的小鳥瀏覽器版的第一個關卡,這也是我們將要模仿建立的關卡。

目前,搭建憤怒的小鳥的關卡與搭建圖騰破壞者關卡並沒有什麼不同,正如你所看到的,我重複使用了在之前章節中已解釋過的大部分方法。

所以,main 函式將如下所示:

<script> 
function init() {
    var   b2Vec2 = Box2D.Common.Math.b2Vec2
        ,  b2AABB = Box2D.Collision.b2AABB
        ,	b2BodyDef = Box2D.Dynamics.b2BodyDef
        ,	b2Body = Box2D.Dynamics.b2Body
        ,	b2FixtureDef = Box2D.Dynamics.b2FixtureDef
        ,	b2Fixture = Box2D.Dynamics.b2Fixture
        ,	b2World = Box2D.Dynamics.b2World
        ,	b2MassData = Box2D.Collision.Shapes.b2MassData
        ,	b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape
        ,	b2CircleShape = Box2D.Collision.Shapes.b2CircleShape
        ,	b2DebugDraw = Box2D.Dynamics.b2DebugDraw
        ,  b2MouseJointDef =  Box2D.Dynamics.Joints.b2MouseJointDef
        ;

        var worldScale = 30; // box2d中以米為單位,1米=30畫素
        var gravity = new b2Vec2(0, 5);
        var sleep = true;
        var world;
        function main(){
        world = new b2World(gravity, sleep);
        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);

        setInterval(updateWorld, 1000 / 60);
    }
    main();
    

    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 = 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);
    }

    function sphere(px, py, r){
        var bodyDef = new b2BodyDef();
        bodyDef.position.Set(px/worldScale, py/worldScale);
        bodyDef.type = b2Body.b2_dynamicBody
        bodyDef.userData = py;
        var circleShape = new b2CircleShape(r/worldScale);
        var fixtureDef = new b2FixtureDef();
        fixtureDef.shape = circleShape;
        fixtureDef.density = 1;
        fixtureDef.restitution = .4
        fixtureDef.friction = .5;
        var theSphere = world.CreateBody(bodyDef);
        theSphere.CreateFixture(fixtureDef);
        return theSphere;
    }

    function floor(){
        var bodyDef = new b2BodyDef();
        bodyDef.position.Set(320/worldScale, 465/worldScale);
        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);
    }

    
    function updateWorld() {
        world.Step(1/30, 10, 10);// 更新世界模擬
        world.DrawDebugData(); // 顯示剛體debug輪廓
        world.ClearForces(); // 清除作用力
    }

    //setup debug draw
    function debugDraw(){
        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);
    }
};
</script>

上面的程式碼只是單純的複製/貼上之前的指令碼,所以在這裡沒有什麼

要解釋。測試網頁,你應該看到你所搭建的關卡:

image

原始碼在: article/ch04/ch04-6.html

我們搭建的關卡中並沒有建立小豬,因為現在你還沒有能力去消滅它,所以它與目前的學習沒有關係,我們將在下一個章節再提到它。

此刻,我們只要摧毀它的房子有就可以了。

在我們粉碎小豬們的藏身之處之前,我們需要繪製出一些代表橡皮彈弓和小鳥的圖形。

橡皮彈弓主要是一個讓小鳥可以在其內部移動的圓圈。

此刻,我們將使用另一個圓來代表小鳥。

物理遊戲不只是關於物理

因為這部分內容是為了實現讓玩家發射小鳥的功能,與 Box2D 無關。

所以,我會很快的講解這些知識,那麼我們開始吧!

在開始繪圖之前,我想讓你知道,你可以在你的遊戲中混合使用物理和非物理的指令碼,就像我現在要做的一樣。

在本例中,玩家的互動不是由 Box2D 管理,它只有在小鳥被釋放的時候才會被使用。

我在這裡引入了 createjs 用於繪製圖形與互動

我在 番外篇(https://www.cnblogs.com/willian/p/10391319.html) 裡介紹過了,如果你還不瞭解,那麼需要回顧一下 原始碼在 extra.html

首先,我們需要一些變數:

var theBird = new createjs.Shape();
var slingX = 100;
var slingY = 250;
var slingR = 75;

theBird 代表了可拖拽的小鳥,slingX 和 slingY 是橡皮彈弓的中心,以畫素為單位,然後

slingR 是橡皮彈弓可拖拽區域的半徑,以畫素為單位。

現在,我們需要在舞臺上繪製一些東西。我們將繪製一個大圓圈代表橡皮彈弓的可拖拽區域,然後小的圓圈代表小鳥。

同時,我們將新增一些監聽,去選擇,拖拽以及釋放小鳥。

將下面的程式碼新增到 main 方法中:

function main(){
    world = new b2World(gravity, sleep);
    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);

    // 畫出大圓
    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(); // 清除作用力
    });
    }
    main();

注意,由於要結合 2d 圖形庫,原 updateWorld 方法遷到了 createjs.Ticker.on("tick", ()=>{...}) 內

我們只是繪製了一些東西,所以沒有什麼需要說明的。現在,當玩家在小鳥上按下

滑鼠時,我們需要一個方法來執行它 birdMove

function  birdMove(e){
    const mouseX = e.stageX;
    const mouseY = e.stageY;
    theBird.x = mouseX;
    theBird.y = mouseY;
    var distanceX = theBird.x-slingX;
    var distanceY = theBird.y-slingY;
    if (distanceX*distanceX+distanceY*distanceY>slingR*slingR) {
        var birdAngle = Math.atan2(distanceY,distanceX);
        theBird.x=slingX+slingR*Math.cos(birdAngle);
        theBird.y=slingY+slingR*Math.sin(birdAngle);
    }
}

只要玩家按壓滑鼠按鈕並移動,birdMove() 方法將被呼叫,然後將在橡皮彈弓的區域內移動小鳥

注意,我們沒有在舞臺上使用 Box2D。因為用小鳥來瞄準不涉及物理,所以無需使用它,並且有一個黃金原則:在需要時,才使用物理引擎。

只有一件事情與我們的物理世界有關,那就是玩家釋放小鳥時的位置。

根據小鳥的位置和橡皮彈弓的中心,我們可以決定怎樣發射小鳥。

因此,birdRelease() 方法在此刻將只是在輸出視窗輸出釋放小鳥的座標:

function birdRelease(e){
    console.log(`bird released at ${e.target.x}, ${e.target.y}`)
}

測試網頁,你將能夠在橡皮彈弓的區域內移動你的小鳥。

image

原始碼在: article/ch04/ch04-7.html

一旦你釋放小鳥,你將看到在輸出視窗有一些釋放座標的文字。

在前面的截圖中,一個小鳥被釋放,它的位置將在控制檯被輸出:

bird released at 44,274

這是我們發射物理小鳥所需要的一切。

放置小鳥

在這裡,Box2D 將要起到它的作用。

正如我們近似的將小鳥看作圓形,那麼我們需要在玩家釋放滑鼠的相同位置建立一個 Box2D 圓形。

向下面這樣改變 birdRelease() 方法:

function birdRelease(e){
    var sphereX = theBird.x/worldScale;
    var sphereY = theBird.y/worldScale;
    var r = 15 / worldScale;
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(sphereX,sphereY);
    var circleShape  = new b2CircleShape(r);  
    var fixtureDef = new b2FixtureDef();
    fixtureDef.shape=circleShape;
    fixtureDef.density = 4;
    fixtureDef.restitution = 0.4;
    fixtureDef.friction = 0.5;
    var physicsBird = world.CreateBody(bodyDef);
    physicsBird.CreateFixture(fixtureDef);
    stage.removeChild(theBird);
}

這裡沒有什麼新的知識,我們只要建立一個和小鳥座標和尺寸相同的 static 型別的圓形即可。

測試網頁,透過拖拽和釋放圓圈。它將會轉變為一個 static 型別的 Box2D 圓形剛體。

image

原始碼在: article/ch04/ch04-8.html

現在,我們終於可以應用力並執行模擬。

發射物理小鳥

我們需要在 birdRelease() 方法中新增一些程式碼:

function birdRelease(e){
    var distanceX = theBird.x-slingX;
    var distanceY = theBird.y-slingY;
    var velocityX = distanceX*-1/5;
    var velocityY = distanceY*-1/5;
    var birdVelocity = new b2Vec2(velocityX,velocityY);

    var sphereX = theBird.x/worldScale;
    var sphereY = theBird.y/worldScale;
    var r = 15 / worldScale;
    var bodyDef = new b2BodyDef();
    bodyDef.position.Set(sphereX,sphereY);

    bodyDef.type = b2Body.b2_dynamicBody;

    var circleShape  = new b2CircleShape(r);  
    var fixtureDef = new b2FixtureDef();
    fixtureDef.shape=circleShape;
    fixtureDef.density = 4;
    fixtureDef.restitution = 0.4;
    fixtureDef.friction = 0.5;
    var physicsBird = world.CreateBody(bodyDef);

    physicsBird.SetLinearVelocity(birdVelocity);

    physicsBird.CreateFixture(fixtureDef);
    stage.removeChild(theBird);
}

最後,我們新增了一些力,所以是時候來解釋一下這些程式碼:

var distanceX = theBird.x-slingX;
var distanceY = theBird.y-slingY;

distanceX 和 distanceY 這兩個數值代表了小鳥和橡皮彈弓中心之間的距離。

距離越大,則作用的力將越大。

var velocityX = distanceX*-1/5;
var velocityY = distanceY*-1/5;

我們之前說過力與距離有關,所以上面的程式碼將計算水平和垂直的速度。

首先,我們將水平或垂直距離乘以 -1,因為力與距離的方向相反,否則小鳥將被髮射到錯誤的方向。

然後,我們再將之前的乘積除以 5,只是為了讓小鳥發射的速度減弱一些,否則我們將發射一隻超音速小鳥。

改變這個值將影響遊戲的設定,所以需要看效果來一點點調整:

var birdVelocity = new b2Vec2(velocityX,velocityY);

最後,velocity(X/Y)變數被用來建立 b2Vec2 物件,將分配力道小鳥上。

bodyDef.type=b2Body.b2_dynamicBody;

很顯然,在我們將力作用到剛體上之前,我們必須將剛體設定為 dynamic 型別。

physicsBird.SetLinearVelocity(birdVelocity);

現在,小鳥可以起飛然後摧毀小豬的老巢了。

我使用 SetLinearVelocity() 方法將力作用到小鳥上

是因為我想讓在同一點發射的所有型別的小鳥獲得相同的速度,而與它們的尺寸和質量無關。

顯然,我可以透過像本章節開始時所展示的那樣,對 birdVelocity 進行乘法運算來使用 ApplyForce() 和 ApplyImpulse() 方法

但是,何必要自尋煩惱呢?我們的 SetLinearVelocity() 方法可以很好的勝任這個任務了,不是嗎?

測試網頁,透過拖拽然後釋放小鳥,你將能夠摧毀下面的場景。

image

原始碼在: article/ch04/ch04-9.html

如前所說,這便是你的憤怒的小鳥的關卡。雖然沒有小豬和跟蹤小鳥的攝像機,但是你將在下面的章節學習怎樣去實現它們。

小結

在本章,你學習了怎樣在你的 Box2D世界中使用力移動剛體。

透過應用力,可以實現很多事情。

從一個模型到一個撞擊遊戲,每一個物理遊戲中玩家移動剛體都是透過一些方式來使用力實現的

那麼現在,你知道了怎樣應用它們。


本文相關程式碼請在

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

注:轉載請註明出處部落格園:王二狗Sheldon池中物 (willian12345@126.com)

相關文章