【一統江湖的大前端(8)】matter.js 經典物理

【一統江湖的大前端(8)】matter.js 經典物理

示例代碼託管在:http://www.github.com/dashnowords/blogshtml

博客園地址:《大史住在大前端》原創博文目錄前端

華爲雲社區地址:【你要的前端打怪升級指南】java

在前端開發領域,物理引擎是一個相對小衆的話題,它一般都是做爲遊戲開發引擎的附屬工具而出現的,獨立的功能演示做品經常給人好玩可是無處可用的感受。仿真就是在計算機的虛擬世界中模擬物體在真實世界的表現(動力學仿真最爲常見)。仿真能讓畫面中物體的運動表現更符合玩家對現實世界的認知,好比在《憤怒的小鳥》遊戲中被彈弓發射出去小鳥或是由於被撞擊而坍塌的物體堆,還有在《割繩子》小遊戲中割斷繩子後物體所發生的單擺或是墜落運動,都和現實世界的表現近乎相同,遊戲體驗一般也會更好。git

用物理引擎能夠幫助開發者更快速地實現諸如碰撞反彈、摩擦力、單擺、彈簧、布料等等不一樣類型的仿真效果。物理引擎一般並不須要處理和畫面渲染相關的事務,而只須要完成計算仿真的部分就能夠了,你能夠把它理解成MVC模型中的M層,它和用於渲染畫面的V層理論上是獨立。matter.js提供了基於canvas2D API的渲染引擎,p2.js在示例代碼中提供了一個基於WebGL實現的渲染器,在開發社區也能夠找到p2.js與CreateJS或Egret聯合使用的示例。遊戲引擎和物理引擎的聯合使用並無想象中那麼複雜,實際上只須要完成不一樣引擎之間的座標系映射就能夠了,熟練地開發者可能會喜歡這種「低耦合」帶來的靈活性,但對於初級開發者而言無疑又提升了使用門檻。github

一.經典力學回顧

經典力學的基本定律就是牛頓三大運動定律或與其相關的力學原理,它能夠用來描述宏觀世界低速狀態下的物體運動規律,也爲遊戲開發中的物理仿真提供了計算依據,大多數仿真都是基於經典力學的公式或其簡化形式進行計算模擬的,使用率較高的公式定律包括:canvas

  • 牛頓第必定律數組

    牛頓第必定律又稱慣性定律,它指出任何物體都要保持勻速直線運動或靜止狀態,直到外力迫使它改變運動狀態爲止。ide

  • 牛頓第二定律函數

    牛頓第二定律是指物體的加速度與它所受的外力成正比,與物體的質量成反比,加速度的方向與物體所受合外力速度相同,它能夠模擬物體加速減速的過程,計算公式爲(F爲合外力,m爲物體質量,a爲加速度):

  • 動量守恆定理

    若是一個系統不受外力或所受外力的矢量和爲零,那麼這個系統的總動量保持不變,動量即質量m和速度v的乘積,它一般被用於模擬兩物體碰撞,動量守恆定律的計算公式能夠由牛頓第二定律推導得出(F爲合外力,t爲做用時長,m爲物體質量,v2爲末速度,v1爲初速度):

  • 動能定理
    合外力對物體所作的功,等於物體動能的變化量,公式表達以下(W爲合外力作功,m爲物體質量,v2爲末速度,v1爲初速度):

當合外力爲一個恆定的力時,它所作的功能夠經過以下公式進行計算(W爲合外力作功,F爲合外力大小,S爲物體運動的距離):

  • 胡克定律

    胡克定律指出當彈簧發生彈性形變時,彈簧的彈力F和其伸長量(或壓縮量)x成正比,它是物理仿真中進行彈性相關計算的主要依據,相關公式以下(F表示彈力,k表示彈性係數,x表示彈簧長度和無彈力時的長度差):

利用經典力學的相關原理,就能夠在計算機中模擬物體的物理特性,對於勻速圓周運動、單擺、電磁場等的模擬均可以依據相關的物理原理進行仿真,本節中再也不展開。

二. 仿真的實現原理

2.1 基本動力學模擬

Canvas動畫是一個逐幀繪製的過程,物理引擎做用的原理就是爲抽象實體增長物理屬性,在每一幀中更新它們的值並計算這些物理量形成的影響,而後再執行繪製的命令。對物體進行動力學模擬時須要使用到質量、合外力、速度、加速度等屬性,其中質量是標量值(即沒有方向的值),而合外力、速度、加速度都是矢量值(有方向的值)。不管在2D仍是3D圖形學計算中,向量計算的頻率都是極高的,若是不進行封裝,代碼中可能就會充斥着大量底層數學計算代碼,影響代碼的可讀性,爲了方便計算,咱們先將二維向量的常見操做封裝起來:

/*二維向量類定義*/
class Vector2{
    constructor(x, y){
        this.x = x;
        this.y = y;
    }
    copy() {
        return new Vector2(this.x, this.y); 
    }
    length() { 
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }
    sqrLength() { 
        return this.x * this.x + this.y * this.y; 
    }
    normalize:() { 
        var inv = 1 / this.length(); 
        return new Vector2(this.x * inv, this.y * inv);
    }
    negate() { 
        return new Vector2(-this.x, -this.y); 
    }
    add(v) { 
        return new Vector2(this.x + v.x, this.y + v.y); 
    }
    subtract(v) { 
        return new Vector2(this.x - v.x, this.y - v.y); 
    }
    multiply(f) { 
        return new Vector2(this.x * f, this.y * f); 
    }
    divide(f) { 
        var invf = 1 / f; 
        return new Vector2(this.x * invf, this.y * invf);
    }
    dot(v) { 
        return this.x * v.x + this.y * v.y; 
    }
}

爲了讓物體實例都擁有仿真必要的屬性結構,能夠定義一個抽象類,再用物體的類去繼承它就能夠了,這和你平時編寫React應用時用自定義類繼承React.Component是同樣的,僞代碼示例以下:

class AbstractSprite{
    constructor(){
            this.mass = 1; //物體質量
            this.velocity = new Vector2(0, 0);//速度
            this.force = new Vector2(0, 0);//合外力
            this.position = new Vector2(0, 0);//物體的初始位置
            this.rotate = 0; //物體相對於本身對稱中心的旋轉角度
    }
}

咱們並無在其中添加加速度屬性,使用合外力和質量就能夠計算出它,position屬性用來肯定對象繪製的位置,rotate屬性用來肯定對象的偏轉角度,上面列舉的屬性在計算常見的線性運動場景中就足夠了。事實上屬性的取捨並無統一的標準,好比要模擬天體運動,可能還須要添加自轉角速度、公轉角速度等,若是要模擬彈簧,可能就須要添加彈性係數、平衡長度等,若是要模擬檯球滾動時的表現,就須要添加摩擦力,所選取的屬性一般都是直接或間接影響物體在畫布上最終可見形態的,你能夠在子類中聲明這些特定場景中才會使用到的屬性。聲明一個新的物體類的示例代碼以下:

class AirPlane extends AbstractSprite{
    constructor(props){
        super(props);
        /* 聲明一些子屬性 */
        this.someProp = props.someProps;
    }
    /* 定義如何更新參數 */
    update(){}
    /* 定義如何繪製 */
    paint(){}
}

上面的模板代碼相信你已經很是熟悉了,狀態屬性的更新代碼編寫在update函數中便可,更新函數理論上的執行間隔大約是16.7ms,計算過程當中能夠近似認爲屬性是不變的。咱們知道加速度在時間維度的積累影響了速度,速度在時間上的積累影響位移:

仿真中過程當中的Δt是自定義的,你能夠根據指望的視覺效果去調整它,Δt越大,一樣大小的物理量在每一幀中形成的可見影響就越顯著,更新時使用向量計算來進行:

this.velocity = this.velocity.add(this.force.divide(this.mass).multiply(t));
this.position = this.position.add(this.velocity.multiply(t));

運動仿真中須要對那些體積較小但速度較快的物體多加留意,由於基於包圍盒的檢測極可能會失效,例如在粒子仿真相關的場景中,粒子是基於引力做用而運動的,初始距離較遠的粒子在相互靠近的過程當中速度是愈來愈快的,這就可能使得在連續的兩幀計算中,兩個粒子的包圍盒都沒有重疊,但實際上它們已經發生過碰撞了,而計算機仿真中就會由於逐幀動畫的離散性而錯過碰撞的畫面,這時兩個粒子又會開始作減速運動而相互遠離,總體的運動狀態就呈現爲簡諧振動的形式。因此在針對粒子系統的碰撞檢測時,除了包圍盒之外,一般還會結合速度和加速度的數值和方向變化來進行綜合斷定。

2.2 碰撞模擬

碰撞,是指兩個或兩個物體在運動中相互靠近或發生接觸時,在較短的時間內發生強相互做用的過程,它一般都會形成物體運動狀態的變化。碰撞模擬通常使用徹底彈性碰撞來進行計算,它是一種假定碰撞過程當中不發生能量損失的理想情況,這樣的碰撞過程就能夠利用動量守恆定律和動能守恆定律進行計算:

公式中只有V1’和V2’是未知量,聯立方程就能夠求得碰撞後速度的計算公式:

在引擎檢測到碰撞發生時只須要根據公式來計算碰撞後的速度就能夠了,能夠看到公式中使用到的屬性都已經在抽象物體類中進行了聲明,須要注意的是速度合成須要進行矢量運算。徹底彈性碰撞只是爲了方便計算的假設狀況,大多數狀況下咱們並不須要知道碰撞形成的能量損失的確切數值,因此若是想要模擬碰撞形成的能量損失,能夠在每次碰撞後將系統的總動能乘以0~1之間的係數來達到目的。

另外一種典型的場景是物體之間發生非對心碰撞,也就是物體運動方向的延長線並不通過另外一個物體的質心,運動模擬時爲了簡化計算一般會忽略物體因碰撞形成的旋轉,將物體的速度先分解爲指向另外一物體質心方向的份量和垂直於該連線的份量,接着使用彈性對心碰撞的公式來求解對心碰撞的部分,最後再將碰撞後的速度與以前的垂直份量進行合成獲得碰撞後的速度。

你沒必要擔憂物理仿真中繁瑣的計算細節,大多數經常使用的場景均可以使用物理引擎快速實現,學習原理並非爲了重複去製造一些簡陋的「輪子」,而是讓你在面對引擎不適用的場景時能夠本身去實現相應的開發。

三. 物理引擎matter.js

3.1 《憤怒的小鳥》的物理特性分析

《憤怒的小鳥》是一款物理元素很是豐富的遊戲,本節中以此爲例進行一個簡易的練習。遊戲中首先須要實現一個模擬的地面,不然全部物體就會直接墜落到畫布之外,接着須要製做一個彈弓,當玩家在彈弓上按下鼠標並向左拖動時,彈弓皮筋就會被拉長,且中間部位就會出現一隻即將被彈射出去的小鳥。當玩家鬆開鼠標時,彈弓皮筋因爲拉長而積蓄的彈性勢能會逐漸轉變成小鳥的動能,從而將小鳥發射出去,這時小鳥的初速度是向斜上方的,在後續的運動過程當中會由於受到重力和空氣阻力的影響而逐漸改變,重力垂直向下且大小不變,而空氣阻力與合速度方向相反,整個飛行過程當中就須要在每一幀中更新小鳥的速度。畫面的右側一般是一個由各類各樣不一樣材質的物體佈景和綠色的豬頭組成的靜態物體堆,當小鳥撞擊到物體堆後,物體堆會發生坍塌,物體堆的各個組成部分都會遵循物理定律的約束而改版狀態,從而呈現出仿真的效果,坍塌的物體堆壓到綠色豬頭後會將其消滅,當全部的豬頭都被消滅後,就能夠進入下一關了。

咱們先使用matter.js爲整個場景創建物理模型,而後再使用CreateJS創建渲染模型,經過座標和角度同步來爲各個物理模型添加靜態或動態的貼圖。爲了下降建模的難度,本節的示例中將彈弓皮筋的模型簡化爲一個彈簧,只要能夠將小鳥彈射出去便可。

3.2 使用matter.js 構建物理模型

matter.js的官方網站提供的示例代碼以下,它能夠幫助開發者熟悉基本概念和開發流程,你能夠在【官方代碼倉】中找到更多示例代碼:

var Engine = Matter.Engine,
     Render = Matter.Render,
   World = Matter.World,
     Bodies = Matter.Bodies;

// create an engine
var engine = Engine.create();

// create a renderer
var render = Render.create({
    element: document.body,
    engine: engine
});

// create two boxes and a ground
var boxA = Bodies.rectangle(400, 200, 80, 80);
var boxB = Bodies.rectangle(450, 50, 80, 80);
var ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });

// add all of the bodies to the world
World.add(engine.world, [boxA, boxB, ground]);

// run the engine
Engine.run(engine);

// run the renderer
Render.run(render);

示例代碼中使用到的主要概念包括負責物理計算的Engine(引擎)、負責渲染畫面的Render(渲染器)、負責管理對象的World(世界)以及用於剛體繪製的Bodies(物體),固然這只是matter.js的基本功能。Matter.Render經過改變傳入的參數,就能夠在畫面中標記處物體的速度、加速度、方向及其餘調試信息,也能夠直接將物體渲染爲線框模型,它在調試環境或一些簡單場景中很是易用,但面對諸如精靈動畫管理等更爲複雜的需求時,就須要對其進行手動擴展或是直接替換渲染器。

在《憤怒的小鳥》物理建模過程當中,static屬性設置爲true的剛體都默認擁有無限大的質量,這類剛體不參與碰撞計算,只會將碰到它們的物體反彈回去,若是你不想讓世界中的物體飛出畫布的邊界,只須要在畫布的4個邊分別添加靜態剛體就能夠了。物體堆的創建也很是容易,經常使用的矩形、圓、多邊形等輪廓均可以使用Bodies對象直接建立,位置座標默認的參考點是物體的中心。當世界中的物體初始位置已經發生區域重疊時,引擎就會在工做時直接依據碰撞來處理,這可能就會致使一些物體擁有意料以外的初速度,在調試過程當中,能夠經過激活剛體模型的isStatic屬性來將其聲明爲靜態剛體,靜態剛體就會停留在本身的位置上而不會由於碰撞檢測的關係發生運動,這樣就能夠對模型的初始狀態進行檢測了,以下圖所示:

構建彈簧模型的技術被稱爲「約束」,相關的方法保存在約束模塊Matter.Constraint上。單獨存在的約束並無什麼實際意義,它須要關聯兩個物體,用來表示被關聯物體之間的約束關係,若是隻關聯了一個物體,則表示這個物體和固定錨點座標之間的約束關係,固定座標默認爲(0,0),能夠經過pointA或pointB屬性調整固定錨點的位置,《憤怒的小鳥》中使用的彈簧模型就是後一種單端固定的形式。咱們只須要找到小鳥被彈射出去時通過彈弓橫切面的位置,創建一個包含座標值的對象做爲錨點,而後再創建一個動態剛體B做爲鼠標拉動彈簧時小鳥圖案的附着點,最後在這兩個對象之間建立約束就能夠了,建立約束時須要聲明彈性係數stiffness,它代表了約束髮生形變的難易程度。這個示例中約束兩端的平衡位置是重合在一塊兒的,當玩家使用鼠標拖動小鳥圖案附着點離開平衡位置後,就能夠看到畫面上渲染出的兩點之間的彈簧約束,當用戶鬆開鼠標後,彈簧就收縮,附着點就會回到初始位置,回彈的過程是一個相似於阻尼振動的過程,約束的彈性係數越大,端點回彈時在平衡位置波動就越小。當須要模擬彈簧被壓縮時,就須要經過length屬性來定義約束的平衡距離,約束復原時就會恢復到這個平衡距離。示例代碼以下:

birdOptions = { mass: 10 },
bird = Matter.Bodies.circle(200, 340, 16, birdOptions),
anchor = { x: 200, y: 340 },
elastic = Matter.Constraint.create({
            pointA: anchor,
            bodyB: bird,
            length: 0.01, 
            stiffness: 0.25
        });

鼠標模塊Matter.Mouse和鼠標約束模塊Matter.MouseConstraint提供了鼠標事件跟蹤與用戶交互相關的能力,配合Matter.Events模塊就能夠對鼠標的移動、點擊、物體拖拽等典型事件進行監聽,使用方式相對固定,你只須要瀏覽一下官方文檔,熟悉一下引擎支持的事件就能夠了,相關示例代碼以下:

//建立鼠標對象
var mouse = Mouse.create(render.canvas);

//建立鼠標約束
Var mouseConstraint = MouseConstraint.create(engine, {
            mouse: mouse,
            constraint: {
                stiffness: 0.2,
                render: {
                    visible: false
                }
            }
        });

 //監聽全局鼠標拖拽事件
Events.on(mouseConstraint, 'startdrag', function(event){
    console.log(event);
})

物理引擎的更新也是逐幀進行的,能夠利用Matter.Events模塊來監聽引擎發出的事件,以每次更新計算後發出的afterUpdate事件爲例,在回調函數中判斷是否須要將小鳥彈射出去。彈射是在玩家使用鼠標向畫面左下方拖動並鬆開鼠標後發生的,咱們能夠依據小鳥附着點的位置進行彈射斷定,當小鳥處於錨點右上側並超過必定距離時,將其斷定爲可發射,發射的邏輯是生成一個新的小鳥附着點,將原約束中的bodyB進行替換,本來的附着點在約束解除後就表現爲具備必定初速度的拋物運動,飛向物體堆。示例代碼以下:

const ejectDistance = 4; //定義彈射判斷的位移閾值
const anchorX = 200; //定義彈簧錨點的x座標
const anchorY = 350; //定義彈簧錨點的y座標

//每輪更新後判斷是否須要更新約束
Events.on(engine, 'afterUpdate', function () {
     if (mouseConstraint.mouse.button === -1 
&& bird.position.x > (anchorX + ejectDistance) 
&& bird.position.y < (anchorY - ejectDistance)) {
              bird = Bodies.circle(anchorX, anchorY, 16, birdOptions);
              World.add(engine.world, bird);
              elastic.bodyB = bird;
        }
    });

須要注意的是matter.js構建的剛體模型會以物體幾何中心做爲定位參考點的。至此,簡易的物理模型就構建好了,線框圖效果以下所示:

儘管看起來有些簡陋,但它已經能夠模擬不少物理特性了,下一小節咱們爲模型進行貼圖後,它就會看起來就比較像遊戲了,物理模型的完整代碼能夠在個人代碼倉庫中獲取到。

3.3 物理引擎牽手遊戲引擎

matter.js提供的渲染器模塊Matter.Render很是適合物理模型的調試,但在面對遊戲製做時還不夠強大,好比原生Render模塊爲模型貼圖時僅支持靜態圖片,而遊戲中則每每會大量使用精靈動畫來增長趣味性,這時將物理引擎和遊戲引擎聯合起來使用就是很是好的選擇。

當你將Matter.Render相關的代碼都刪除後,頁面上就再也不繪製圖案了,可是若是你在控制檯輸出一些信息的話,就會發現示例中監聽afterUpdate事件的監聽器函數仍然在不斷執行,這就意味着物理引擎仍然在持續工做,不斷刷新着模型的物理屬性數值,只是沒有將畫面渲染到畫布上而已。渲染的工做,天然就要交給渲染引擎來處理,當使用CreateJS來開發遊戲時,渲染引擎使用的就是Easel.js。首先,使用Easel.js對全部保存在物理空間engine.world.bodies數組中的模型創建對應的視圖模型,所謂視圖模型,就是指物體的可見外觀,好比一個長方形,可能表明木頭,也可能表明石塊,這取決於你使用什麼樣的貼圖來表示它,視圖模型能夠是精靈表、位圖或是自定義圖形等任何Easel.js支持的圖形,創建後將它們依次添加到舞臺實例stage中。這樣每一個物體實際上有兩個模型與之對應,物理空間中的模型依靠物理引擎更新,負責在每一幀中爲對應物體提供位置座標和旋轉角度,並確保變化趨勢符合物理定律;渲染舞臺中的模型保存着物體的外觀樣式,依靠渲染引擎來更新和繪製,你只須要在每一幀更新物體屬性時將物理模型的關鍵信息(一般是位置座標和旋轉角度)同步給渲染模型就能夠了。基本的邏輯流程以下所示:

按照上面的流程擴展以前的代碼並不困難,完成後的遊戲畫面看起來有趣多了:

完整的代碼已上傳至代碼倉庫。相信你已經發現,最終畫面裏的物體佈局和物理引擎中的佈局是同樣的,物理引擎的本質,就是爲每一個渲染模型提供正確的座標和角度,並保證這些數據在逐幀更新過程當中的變化和相互影響符合物理定律。若是第三方物理引擎沒法知足你的需求,那麼動手去實現本身的引擎吧,相信你已經知道該如何開始了。

相關文章
相關標籤/搜索