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

目錄示例代碼托管在:http://www.github.com/dashnowords/blogs
博客園地址:《大史住在大前端》原創博文目錄
華為云社區地址:【你要的前端打怪升級指南】
- 【一統江湖的大前端(8)】matter.js 經典物理
- 一.經典力學回顧
- 二. 仿真的實作原理
- 2.1 基本動力學模擬
- 2.2 碰撞模擬
- 三. 物理引擎matter.js
- 3.1 《憤怒的小鳥》的物理特性分析
- 3.2 使用matter.js 構建物理模型
- 3.3 物理引擎牽手游戲引擎


在前端開發領域,物理引擎是一個相對小眾的話題,它通常都是作為游戲開發引擎的附屬工具而出現的,獨立的功能演示作品常常給人好玩但是無處可用的感覺,仿真就是在計算機的虛擬世界中模擬物體在真實世界的表現(動力學仿真最為常見),仿真能讓畫面中物體的運動表現更符合玩家對現實世界的認知,比如在《憤怒的小鳥》游戲中被彈弓發射出去小鳥或是因為被撞擊而坍塌的物體堆,還有在《割繩子》小游戲中割斷繩子后物體所發生的單擺或是墜落運動,都和現實世界的表現近乎相同,游戲體驗通常也會更好,
用物理引擎可以幫助開發者更快速地實作諸如碰撞反彈、摩擦力、單擺、彈簧、布料等等不同型別的仿真效果,物理引擎通常并不需要處理和畫面渲染相關的事務,而只需要完成計算仿真的部分就可以了,你可以把它理解成MVC模型中的M層,它和用于渲染畫面的V層理論上是獨立,matter.js提供了基于canvas2D API的渲染引擎,p2.js在示例代碼中提供了一個基于WebGL實作的渲染器,在開發社區也可以找到p2.js與CreateJS或Egret聯合使用的示例,游戲引擎和物理引擎的聯合使用并沒有想象中那么復雜,實際上只需要完成不同引擎之間的坐標系映射就可以了,熟練地開發者可能會喜歡這種“低耦合”帶來的靈活性,但對于初級開發者而言無疑又提高了使用門檻,
一.經典力學回顧
經典力學的基本定律就是牛頓三大運動定律或與其相關的力學原理,它可以用來描述宏觀世界低速狀態下的物體運動規律,也為游戲開發中的物理仿真提供了計算依據,大多數仿真都是基于經典力學的公式或其簡化形式進行計算模擬的,使用率較高的公式定律包括:
-
牛頓第一定律
牛頓第一定律又稱慣性定律,它指出任何物體都要保持勻速直線運動或靜止狀態,直到外力迫使它改變運動狀態為止,
-
牛頓第二定律
牛頓第二定律是指物體的加速度與它所受的外力成正比,與物體的質量成反比,加速度的方向與物體所受合外力速度相同,它可以模擬物體加速減速的程序,計算公式為(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中,這樣每個物體實際上有兩個模型與之對應,物理空間中的模型依靠物理引擎更新,負責在每一幀中為對應物體提供位置坐標和旋轉角度,并確保變化趨勢符合物理定律;渲染舞臺中的模型保存著物體的外觀樣式,依靠渲染引擎來更新和繪制,你只需要在每一幀更新物體屬性時將物理模型的關鍵資訊(通常是位置坐標和旋轉角度)同步給渲染模型就可以了,基本的邏輯流程如下所示:

按照上面的流程擴展之前的代碼并不困難,完成后的游戲畫面看起來有趣多了:

完整的代碼已上傳至代碼倉庫,相信你已經發現,最侄訓面里的物體布局和物理引擎中的布局是一樣的,物理引擎的本質,就是為每個渲染模型提供正確的坐標和角度,并保證這些資料在逐幀更新程序中的變化和相互影響符合物理定律,如果第三方物理引擎無法滿足你的需求,那么動手去實作自己的引擎吧,相信你已經知道該如何開始了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/143539.html
標籤:JavaScript
上一篇:為什么學習javascript
下一篇:js陣列去重(最優方法)筆記
