Box2d – 用 JavaScript構建物理世界

agx300 8年前發布 | 15K 次閱讀 Box2D JavaScript開發 JavaScript

曾經火遍整個地球的手游《憤怒的小鳥》,其問世離不開一個物理引擎—-那就是 Box2D。

1. Box2d 的由來

Box2D 是一個模擬 2d 空間剛體的仿真庫。最早只是 Erin Catto (這家伙好像現在在暴雪工作)在 GDC 大會上展示的一個 Demo,

Box2D 發展到現在已經有諸多影分身,常見的是 Flash (as) 版 ,還有 Python 版, Java 版 C++ 版 JS 版……

Box2D 的版本雖然很多,但其 API 卻幾乎沒有什么變化,為了快速上手,下面我使用大家比較熟悉的 JS 版來做講解。

建議初學者不要用哪個 Emscripten 的 Box2D,最然他的star比較多。(Emscripten 是一種可以將 C++ 通過 WebIDL bindings 編譯成 JS的技術)

2. 讓我們從最基礎的幾個概念開始

目前,小伙伴們只需要宏觀感性的理解這幾個概念即可,暫時不必關心代碼的問題。

1、空間( world )

世間萬物,所有物體,無不占據空間,無論你是一顆星球,還是一顆沙粒,都需要在這個宇宙中占據一定的空間,這便引出了Box2D的第一個概念 叫:“空間”(在 Box2D 中稱之為 world ),這個“空間”概念是最基本的,他是所有物體的容器,所以既然我們要模擬一個真實的物理世界,那么最先要做的事情就是開辟出一個足夠大的空間,然后才能再把你想要的東西塞進去。

2、剛體(body)

有了我們自己的創造的 “空間” 這還遠遠不夠,一個空空如也的世界 并不是我們想要的,我們要制造足夠多的“物體” 這樣,我們創造的“world”才會精彩,這個物體的概念在 Box2D中 稱為“剛體”顧名思義,剛體是無法發生形變的物體 ,Box2D的剛體是十分多樣且復雜的, 但簡單來說,你只需要把它放在你創造空間里,他就能感受到重力,具備物體最基本的特性。

3、材質(fixture)

材質的概念十分簡單,因為他和我們日常生活中的“材料”的概念十分相似,材質需要和剛體 相互依存,比如你創造了一個球體,如果沒有材質的概念,我們就很難模擬他的物理狀態,我們所知道的,僅僅是它是個圓的,所以我們要為他創建材質,比如是一個鐵球? 那么一定很沉。 是一個乒乓球? 那就很輕, 球的表面是光滑的還是粗糙的?等等,材質+剛體 二者共同描述了一個我們真正需要物體。

4、形狀(shape)

形狀是材質的一個屬性,用來描述剛體的碰撞模型。 他們主要有 b2ChainShape , b2EdgeShape , b2PolygonShape , b2CircleShape 形狀之間可以自由組合形成更復雜的形狀。

總之: 剛體=形狀+材質。

例如: 鉛球=圓形 + 鐵 ,書=方形 + 紙

3. 動手創造一個簡單的世界

我們來看看如何用代碼創建一個 Box2D 世界:

//創建一個帶有重力的世界(box2d中所有的剛體都必須放在“世界”這個大容器中)
var gravity = new b2Vec2(0, 9.8);  //定義重力向量,x軸0 y軸向下,加速度是9.8.
var doSleep = true; //當物體停止運動以后,停止對對象的物理模擬。
world = new b2World(gravity, doSleep);//new 一個 名字叫 world 的世界變量

我們通過 world = new b2World(gravity, doSleep); 獲得了一個 world 對象,gravity 為這個世界的重力, 這個對象一般來講只會創建一次,(平行宇宙的概念那是 愛因斯坦才能理解的,對于我們凡人,一個世界已經足夠復雜了 :)

doSleep 這個東西 是說物體一旦停止運動了,便停止對他的模擬,以提高運行效率。

至此 終于有了一個屬于你自己的世界,內牛滿面吧… …

但是我們并不想要一個空空的世界~ ,下面讓我們加點料。

4. 向空間中添加剛體

這里小伙伴們需要注意了:

  1. Box2D 中的計量單位是 ,而不是 像素 .要轉換成像素,需要進行換算.(一般定義為 一米=36px)
  2. Box2D 中的長度是 半長 ,意思是width/2 (很奇怪的設定,不是嗎?)

定義剛體:

剛體的定義需要使用 b2BodyDef

var bodydef = new b2BodyDef(); //new 一個剛體對象
bodydef.type = b2Body.b2_staticBody; //定義剛體對象為 靜態對象. 即不收外力作用的對象,如地面,墻壁.
//bodydef.type = b2Body.b2_dynamicBody; //定義剛體對象為 動態對象. 會受到外力的作用.

定義材質:

材質的定義需要使用 b2FixtureDef .

var fixDef = new b2FixtureDef();//創建一個 b2FixtureDef 對象.
fixDef.density = 1.0; // desity 密度,如果密度為0或者null,該物體則為一個靜止對象
fixDef.friction =  0.9; //摩擦力(0~1)
fixDef.restitution = 1.0;// 彈性(0~1)

定義形狀:

形狀的定義需要使用一種合適的模型,可能是 b2ChainShape , b2EdgeShape , b2PolygonShape , b2CircleShape 中的任意一種。

//我們定義一個圓形,所以使用了b2CircleShape.
fixDef.shape = new b2CircleShape();
 
// 定義圓的半徑是 50px  window.PTM_RATIO 是米轉換像素的因子.
fixDef.shape.SetRadius(50 / window.PTM_RATIO);  

讓我們把 剛體,材質,形狀組合以來

var bodydef = new b2BodyDef();
bodydef.type = b2Body.b2_dynamicBody;
 
var fixDef = new b2FixtureDef();
fixDef.density = 1.0; // desity 密度,如果密度為0或者null,該物體則為一個靜止對象
fixDef.friction =  0.9; //摩擦力(0~1)
fixDef.restitution = 1.0;// 彈性(0~1)
 
fixDef.shape = new b2CircleShape();
fixDef.shape.SetRadius(50 / window.PTM_RATIO);
bodydef.position.Set(250 / window.PTM_RATIO, 0 / window.PTM_RATIO);
 
var body = world.CreateBody(bodydef); //注意 bodydef 并不是 new 出來的.
body.CreateFixture(fixDef);

我先解釋一下 bodyDef 和 body 的關系:

簡單來說 body 是剛體的實例, bodyDef 是生產 body 的工廠,有了bodyDef  就可以使用 world.CreateBody  生產出一大批body來。

然后再使用 body.CreateFixture(fixDef); 來為某個 body 套上材質。

那么 PTM_RATIO 這個是什么?

這貨簡單來說。是一個常量,一般定為 30-36 之間的一個數字。用來指示 1米 = 多少像素 , 這個有何意義呢,看圖,下面慢慢來說。

比如同樣的飛機,在地面上感覺龐然大物

但是在天空中感覺像一只小蟲

造成這種結果,其實是 透視 規則在起作用。

言歸正傳,在我們的世界里,雖然是一個 2d 世界,但是你一樣需要一個觀察這個世界的“距離”,就好比你要欣賞一幅畫,你不能離畫太遠,這樣看不清了,也不能離畫太近,這樣看不全。 由于 Box2D 模擬物理狀態時 有大量的浮點數計算,所以合理的定義一個“距離” 是十分有必要的,即可以提高運行速度,又模擬的十分真實,所以這個常量 一般定為 36。

5. 幀的概念.

簡單來說,幀,其實是在每一個固定時間間隔里 周期性的把數據轉換成圖像的過程

在 JavaScript 里最簡單的方法就是用 requestAnimationFrame 來實現幀的功能

var last = 0;
function animate(time){
    var timeStep = time-last;
    last = time;
    //velocityInterations 是對速率的糾正程度, 越高計算量越大.
    var velocityInterations = 10;
 
    //velocityInterations 是對位置的糾正程度, 越高計算量越大.
    var positionIterations = 10;
 
    world.Step(timeStep, velocityInterations, positionIterations);
    world.ClearForces();
    world.DrawDebugData(); //繪制綁定到debug視圖上渲染
    requestAnimationFrame(animate);
}
animate();

磚家說,人的肉眼只能分辨在1/60秒內的變化,也就是說 肉眼的“刷新率是 1/60s”,他們稱之為“視覺暫留現象” 那我們也就把刷新率定在這個速度上。

Box2dweb 已經為我們提供了一個供開發預覽的 debugview 這個 debugview 是什么東西呢? 其實就是在沒有真正貼圖以前,讓開發人員可以看到自己定義的骨架。

var debug = new b2DebugDraw();
debug.SetSprite(document.getElementById("canvas").getContext("2d"));
debug.SetLineThickness(1);
debug.SetFillAlpha(0.9);
debug.SetAlpha(1);
debug.SetDrawScale(PTM_RATIO);
debug.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debug);

其實就是使用一個 canvas 將所有的 body 以一個最簡單的方式畫在上面。(在前期沒有貼圖的時候,這些模型十分有用)

ok 把上面的知識串起來, 一個最簡單的 Boxd2d 版本的 HelloWord 完成!

//創建一個帶有重力的世界(box2d中所有的物體都必須被包括在一種叫“世界”的大容器中)
var gravity = new b2Vec2(0, 9.8); 
var doSleep = true; //當物體停止運動以后,停止對對象的物理模擬。
world = new b2World(gravity, doSleep);
//b2_dynamicBody說明他是一個 動態物體.
//就是說他不是一個像地面一樣一動不動的東西,當你給他一個力,它就會動。
bodyDef.type = b2Body.b2_dynamicBody; 
//定義了他在世界中的位置,PTM_RATIO 這個個東西是縮放比,后面會解釋。
bodyDef.position.Set(canvaswidth/PTM_RATIO/2,canvasheight/PTM_RATIO/2); 
var body = world.CreateBody(bodyDef) //world.CreateBody 物體有這個方法來創建
 
var fixDef = new b2FixtureDef; //這貨其實和 bodyDef一樣 是材質的 “制造機”
fixDef.density = .5; //density 定義質量
fixDef.friction = 0.4; //friction 定義表面的摩擦力
fixDef.restitution = 0.8; //定義彈性
fixDef.shape = new b2CircleShape(10/drawScale); // 定義了 形狀 : b2CircleShape 圓形
body.CreateFixture(fixDef) //把我們創操出來的 材質 綁定到一個 物體上。
 
var fixDef = new b2FixtureDef;
fixDef.density = .5;
fixDef.friction = 0.4;
fixDef.restitution = 0.8;
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_staticBody;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox(canvaswidth/PTM_RATIO/ 2, 5/PTM_RATIO);
bodyDef.position.Set(canvaswidth/PTM_RATIO/ 2,(canvasheight-5)/PTM_RATIO);
world.CreateBody(bodyDef).CreateFixture(fixDef);
 
 
var debug = new b2DebugDraw();
debug.SetSprite(document.getElementById("canvas").getContext("2d"));
debug.SetLineThickness(1);
debug.SetFillAlpha(0.9);
debug.SetAlpha(1);
debug.SetDrawScale(PTM_RATIO);
debug.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debug);
 
setInterval(update, (1000 / 60));
function update() {
var timeStep = 1 / 30;
var velocityInterations = 10;
var positionIterations = 10;
 
world.Step(timeStep, velocityInterations, positionIterations);
world.ClearForces();
world.DrawDebugData(); //繪制綁定到debug視圖上渲染 請看下文介紹

6. 使用 userData 為模型綁定皮膚

現在我們已經會使用 Box2D 來創建基本剛體了,但這些顏色單一的方塊是肯定無法滿足產品質量要求的,下一步我們還需要為它貼上好看的皮膚。

由于 Box2D 最早是用 c++ 寫的,在剛體對象上專門留有一個 *userData  指針來記錄用戶自己綁定的信息,這個指針被移植到 JS  后成為了userData屬性。

為剛體綁定皮膚的邏輯基本是這樣的:

  1. 獲取皮膚對象,比如在 web 中可能是一個 <img> 標簽。
  2. 將剛體的 userData  屬性指向這個皮膚對象。
  3. 在每一幀中,遍歷所有剛體, 獲取其 position angle 等信息。
  4. 將上述信息輸出到 userData 指向的對象上。 (比如將剛體的 x , y,angle 等屬性轉化為 img 標簽的 top left 等 css 屬性)

下面看一個例子:

在控制臺中的樣子

原理非常簡單, 就是通過不斷更新 <img> 標簽的 top left 與transform 值,來達到剛體與皮膚聯動的效果.

我們來看一下核心代碼實現.

function render() {
    var bList = world.GetBodyList(); //獲取剛體集合 注意他是一個鏈表.
    while (bList) {
    var b = bList;
    var bList = bList.GetNext(); //獲取下一個元素,如果到達最后,則返回 null
    if (b.GetUserData()) {
      var img = b.GetUserData();
      img.style.top = b.GetPosition().y*30 - 25 + "px";
      img.style.left = b.GetPosition().x * 30 - 25 + "px";
      img.style.webkitTransform = "rotate(" + b.GetAngle() * 180 / Math.PI + "deg)"
    }
  }
}

只要保證在每一幀中執行 render 方法, 就可以達到上圖的效果。

7. 總結

Box2D 也許是最老的 2D 引擎,  在技術日新月異的今天, 早已不再是我們的唯一選擇, 比如谷歌發布的開源 2D 物理引擎 LiquidFun 專門增加了對流體的支持,還有更輕量級的   chipmunk 、  matter.js 等等,它們每一種都有自己的亮點, 但基本原理或多或少都與 Box2D 有共通之處. 尤其是像剛體、 材質、 幀這些概念。 真可謂之, 學會一種,觸類旁通。

 

來自:http://jdc.jd.com/archives/2110

 

 本文由用戶 agx300 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!