游戲中的對象按照物理規律移動,體現重力、引力、反作用力、加速度等物體特性,實現自由落體、搖擺運動、拋物線運動,以及物理碰撞現象的模擬。用于模擬物理碰撞、物理運動的引擎稱為物理引擎。?
來自瑞典斯德哥爾摩大學的Stefan Hedman基于JavaScript,開發了一款面向HTML游戲的2D物理引擎,P2物理引擎。P2和Box2D物理引擎一樣,集成了各種復雜的物理公式和算法,幫助實現碰撞、加速、自由落體等物理對象的模擬。?
P2是一個開源項目,可在GitHub下載,使用build中的p2.min.js文件,就可以開發物理應用。
1. 創建P2物理項目:
使用P2物理引擎創建物理應用的過程和Box2D類型,步驟是:創建world、創建shape、創建body剛體、實時調用step()函數更新物理模擬計算;基于形狀、剛體使用Egret或其他HTML渲染以顯示物理模擬效果。?
1)世界world:
world是P2物理引擎入口,對應World類,用于承載所有物理模擬對象。world類的構造函數為:?
function World([options]){options?:{gravity?:number[]=[0,-9.81];}}?
其中,gravity是重力加速度,這是一個Vec2類型的向量對象,默認為垂直向上的向量[0,-9.81]。將gravity設置為[0,0]可以取消重力,模擬太空失重狀態。?
2)形狀Shape:
形狀是物理模擬計算的基礎。任何物體都要有對應的形狀,才可以基于P2進行物理碰撞檢測和模擬。所有形狀對象都需要通過addShape()添加到剛體中,才可以進行碰撞模擬計算:?
var body:p2.Body=new p2.Body();?
var shape:p2.Shape=new p2.Shape();?
body.addShape(shape);?
P2中的Shape類是一個抽象的父類,要使用Box、Circle等子類。?
3)剛體Body:
剛體是P2物理引擎的核心概念和對象,擁有速度、角度、質量等物理屬性,同時包含了形狀對象,使剛體擁有具體的形狀。將剛體添加到world中,World將以剛體為單位循環遍歷,進行物理模擬計算,并將模擬的結果保存在剛體屬性中,使剛體成為碰撞對象的原型。所有的剛體都必須通過addBody()添加到P2的world中,才會進行物理模擬:?
var body:p2.Body=new p2.Body();?
var shape:p2.Shape=new p2.Shape();?
body.addShape(shape);?
world.addBody(body);?
4)貼圖Egret:
P2只是一個算法庫,以剛體為對象模型,模擬并輸出物理碰撞、運動結果。這個過程通過持續調用world中的step()方法來實現:?
function step(dt:number, timeSinceLastCalled?:number=0,maxSubSteps?:number=10)?
其中,參數dt是step方法執行的時間間隔,單位秒,通常取值為游戲幀頻的倒數; 當游戲幀頻降低時計算兩幀之間的時間差作為timeSinceLastCalled參數值,此時P2會在一次step()中進行count= timeSinceLastCalled/dt次計算,以保證物理模擬的真實性,默認值為0;參數maxSubSteps是單次step()進行物理模擬計算的最大次數,當timeSinceLastCalled不等于0時,單次step()中進行計算的次數count最大為maxSubSteps,默認值為10。?
但P2本身不具備渲染功能,無法顯示模擬結果,需要借助JavaScript渲染引擎,如Egret、Cocos2d-js、Pixi、phaser等,通過繪制或貼圖來渲染物理模擬結果。在Egret中:?
class SampleP2APP extends egret.DisplayObjectContainer{?
public constructor(){?
super();?
this.createP2App();?
}?
private world:p2.World;?
private factor:number=30;?
public createP2App():void{?
this.world=new p2.World();?
var world=this.world;?
world.gracity=[0,0];?
var shape:p2.Box=new p2.Box({width:100/this.factor, height:50/this.factor});?
var body:p2.Body=new p2.Body({mass:1});?
body.position=[275/this.factor, 100/this.factor];?
body.addShape(shape);?
worldBody(body);?
this.addEventListener(egret.Event.ENTER_FRAME, this.loop, this);?
}?
private loop(e.egret.Event):void{?
this.world.step(1/60);?
}?
}?
代碼中創建了一個重力加速度gravity=[0,0]的失重環境world;然后使用Box形狀創建尺寸100x50像素矩形;通過addShape()方法將其添加到位于(275,100)位置的剛體body中;最后通過world的addBody()方法將剛體添加到世界中,完成一個基本的P2物理應用創建。在游戲的loop更新方法中,每幀持續調用step()方法,實現P2物理模擬計算的持續更新。?
P2物理引擎中的坐標單位是米,而大部分的游戲引擎在屏幕上渲染游戲時,都是基于像素的,所以在創建剛體對象、設置形狀尺寸時,需要將像素轉換成米后再進行賦值。游戲中,1米通常看作30px,所以代碼中聲明了一個值為30的factor變量,在設置形狀尺寸或剛體時,都是使用像素坐標除以factor變量,轉換成米之后再賦值的。
2. 用p2DebugDraw實現模擬視圖:
因為P2只是進行了物理模擬計算,沒有對模擬結果進行渲染,可以基于Egret引擎編寫渲染繪圖的類,如p2DebugDraw。p2DebugDraw類的構造函數為:?
function p2DebugDraw(world:p2.World, sprite:egret.Sprite)?
其中,world是P2中創建的world對象,sprite是Stage中一個Sprite對象。p2DebugDraw將使用基本繪圖的API,在sprite對象中繪制所有剛體、關節等物理對象。修改后的代碼:?
class SampleP2APPWithDebugDraw extends egret.DisplayObjectContainer{?
public constructor(){?
super();?
this.createP2App();?
}?
private world:p2.World;?
private factor:number=30;?
private debugDraw:p2DebugDraw;?
public createP2App():void{?
this.world=new p2.World();?
var world=this.world;?
world.gracity=[0,0];?
var shape:p2.Box=new p2.Box({width:100/this.factor, height:50/this.factor});?
var body:p2.Body=new p2.Body({mass:1});?
body.position=[275/this.factor, 100/this.factor];?
body.addShape(shape);?
worldBody(body);?
var sprite:egret.Sprite=new egret.Sprite();?
this.addChild(sprite);?
this.debugDraw=new p2DebugDraw(world, sprite);?
this.addEventListener(egret.Event.ENTER_FRAME, this.loop, this);?
}?
private loop(e.egret.Event):void{?
this.world.step(1/60);?
this.debugDraw.drawDebug();?
}?
}?
p2DebugDraw仿照Box2d中的b2DebugDraw,用不同顏色和形狀表示不同類型的剛體、關節等對象,粉色為動態剛體、紫色為可動剛體、綠色為靜態剛體、黑色線段為關節、紅色線段為彈簧、綠色線段為剛體與關節點的偏移量、圓的為關節節點。?
p2DebugDraw的屬性和方法:?
p2DebugDraw提供了很多屬性和方法:?
1)isDrawAABB:boolean:是否繪制剛體的AABB最小包圍框,默認false?
2)drawDebug():繪制P2物理引擎中的所有剛體、關節等對象,需要實時調用實時更新?
3)drawShape():繪制P2世界中的任意形狀,使用時要確保形狀對應剛體已添加到world?
drawShape()方法的結構為:?
function drawShape(shape:p2.Shape, color?:number, fillColor?:boolean):void?
其中,shape為要繪制的Shape對象;color為要繪制的顏色,缺省時根據類型設置顏色;fillColor指示是否填充顏色,默認true,填充色與邊框色相同。?
4)drawConvex():將vertices數組中保存的任意多個頂點坐標,逐個使用線段連接起來,繪制出多邊形。此方法不受P2剛體約束,可在任意需要情況下使用。?
function drawConvex( vertices:number[][], color:number, alpha:number=1, fillColor:boolean = true ):void?
其中,vertices保存了需要繪制的多邊形頂點數組;color為多邊形的邊框顏色;alpha為多邊形填充顏色透明度;fillColor為是否填充顏色,默認true,填充色與邊框色相同。?
5)drawCircle():在指定位置以指定的半徑繪制圓形,該方法不受P2剛體約束,可在任意需要情況下使用。?
function drawCircle( pos:number[], radius:number, color:number, alpha:number=1, fillColor?:boolean ):void?
其中,pos為圓形繪制的目標位置,radius為圓形半徑。?
6)drawSegment():在兩點之間繪制線段,結構為:?
function drawSegment( start:number[], end:number[], color:number):void?
其中,start為線段起點,end為線段終點,color為線段顏色。?
7)drawVecAt():在指定點at處繪制向量v。用來顯示出剛體速度,結構為:?
function drawVecAt( v:number[],at:number[], color:number, markStart:boolean=false ):void?
其中,v為要繪制的向量;at為開始繪制向量的起點;color為向量顏色;markStart為是否用圓點表示出向量的起點,默認false。?
8)drawVecTo():繪制向量v,指向指定點to。?
function drawVecTo( v:number[],to:number[], color:number, markStart:boolean=false ):void?
其中,v為要繪制的向量;to為向量的終點;color為繪制的顏色;markStart為是否用圓點表示出向量的終點,默認false。
3. P2中的形狀:
形狀是物理引擎進行碰撞模擬計算的依據,是剛體最基本的屬性。P2中使用Shape類來表示形狀,通過剛體的addShape()方法,將形狀添加到剛體中之后,就可以隨著剛體的移動、旋轉不斷更新,并進行碰撞檢測了。為剛體添加形狀的示例代碼為:?
var shape:p2.Shape=new p2.Shape();?
var body:p2.Body=new p2.Body();?
body.addShape(shape);?
Shape類本身并不參與剛體的創建,而是由其幾個子類完成一些常見形狀的模擬。這些形狀包括圓形Circle、矩形Box、膠囊Capsule、粒子Particle、線段Line、平面Plane、海拔形狀HightField、多邊形Convex等。
1)形狀:
游戲中人物形狀各種各樣,但在碰撞檢測時,出于效率考慮,大都被簡化為簡單的圓形、矩形等形狀。P2預置了包括圓形、矩形在內的一些常用的、簡單的形狀。?
⑴圓形Circle:
圓形是P2中的基本形狀,構造函數為:?
function Circle({options?:{radius:number})?
其中,options是包含所有屬性的集合對象;radius是圓形的半徑,默認值為1。?
⑵矩形Box:
矩形通過width和height屬性來創建,構造函數為:?
function Box(options:{width?:number, height?:number})?
其中,width為矩形寬度,默認為1;height為矩形高度,默認值為1。?
⑶膠囊Capsule:
可以認為是一種圓角矩形,長度length,高度2*Radius,兩端是半徑為Radius的半圓形。?
function Capsule(options?:{length?:number, radius?:number})?
其中,length為膠囊長度;radius為膠囊形狀兩端半圓的半徑,同時也是膠囊的高度。?
⑷粒子Particle:
粒子就是微小的顆粒,P2物理引擎中的粒子半徑和面積為0。粒子參與碰撞檢測,效果與半徑為1px的圓形一樣。構造函數簡單,不需要任何參數:?
function Particle()?
⑸線段Line:
長度為length的線段,看上去與高度為1的Box形狀一樣,但算法上省去對高度的檢測。?
function Line(options?:{length?:number})?
⑹平面Plane:
平面沿y軸負方向無限擴展,同時在x軸方向的寬度無限,像地平面。構造函數為:?
function Plane()?
創建時沒有任何參數,初始情況下是一個倒置的穹,實際應用時一般要通過調整剛體的角度angle,使plane平面朝向不同的方向,來模擬墻體。示例代碼:?
var shape:p2.Plane=new p2.Plane();?
var body:p2.Body=new p2.Body({mass:1, position:[274/this.factor, 200/this.factor]});?
body.addShape(shape);?
body.angle=Math.PI/4;?
this.world.addBody(body);?
代碼中的平面會隨著剛體的角度angle繞坐標點[274,200]順時針旋轉45°。?
⑺海拔形狀Heightfield:
這是一個類似于Plane的形狀,但不是平的,而是一組y坐標組成的高低不平的山形,這些山丘之間的間隔都是固定的elementWidth。HeightField形狀也是朝y軸負方向無限擴展的,水平方向的寬度是elementWidth與山丘數量的乘積。構造函數為:?
function Heightfield(options?:{heights:number[], minValue?:number,maxValue?:number, elementWidth?:number})?
其中,heights是保存每個山丘高度的數組;minValue是山丘高度的最小值,設置后heights中的小于minValue的值設置為minValue;maxValue為heights中高度的最大值,設置此值后heights中大于maxValue的值設置為maxHeight;elementWidth為每個山丘之間的間隔,默認為1。?
定義Heightfield形狀就是定義heights屬性中y坐標數組,同時x坐標以elementWidth為步長逐一增加。目前,Heightfield不支持旋轉,始終朝y軸負方向擴展,碰撞檢測也不精確,邊緣位置容易出現穿透現象。?
⑻多邊形Convex:
Convex是一個多邊形形狀,可以根據一組定義好的頂點坐標創建對應的多邊形形狀。?
function Convex(options?:{vertice?:number[][], axes?:number[][[]})?
其中,vertices保存了頂點坐標的數組,這是一個二維數組,每個元素是由x和y坐標組成的一維數組,如vertics=[[-1,-1],[1,-1],[1,1],[-1,-1]];axes表示多邊形各個邊的對稱軸,同樣是一個二維數組,且長度應與vertices一致,此參數通常可以使用默認值,系統根據vertices中的頂點自動計算得出垂直于各個邊的法向量。使用時首先要將多邊形的頂點坐標保存到數組中,然后將其作為vertices參數傳遞給Convex:?
var points=[[-1,-1],[1,-1],[1,1],[-1,-1]];?
var shape:p2.Convex=new p2.Convex({vertices:points});?
var body:p2.Body=new p2.Body({mass:1});?
body.addShape(shape);?
this.world.addBody(body);?
不推薦使用頂點坐標直接創建多邊形,因為可能出現凹多邊形,而P2中的碰撞檢測是基于凸多邊形的,需要將凹多邊形分解成多個小的凸多邊形,還需要重新計算分解后的重心、形狀偏移,這些計算并不在Convex類中。建議使用Body類的fromPolygon()實現。
2)P2中形狀碰撞關系:
P2中形狀碰撞不完善,一些形狀之間無法實現碰撞,各個形狀之間的碰撞關系見表:
? | Circle | Plane | Box | Convex | Particle | Line | Capsule | Heightfield | Ray |
Circle | Yes | - | - | - | - | - | - | - | - |
Plane | Yes | - | - | - | - | - | - | - | - |
Box | Yes | Yes | Yes | - | - | - | - | - | - |
Convex | Yes | Yes | Yes | Yes | - | - | - | - | - |
Particle | Yes | Yes | Yes | Yes | - | - | - | - | - |
Line | Yes | Yes | ? | ? | - | - | - | - | - |
Capsule | Yes | Yes | Yes | Yes | Yes | ? | Yes | - | - |
Heightfield | Yes | - | Yes | Yes | ? | ? | ? | - | - |
Ray | Yes | Yes | Yes | Yes | - | Yes | Yes | Yes | - |
其中?部分是有待完善的。
3)形狀屬性:?
p2的幾種內置形狀各有自己的屬性,但都繼承自Shape類,所以有共同的屬性,包含在Shape的構造函數內:?
function Shape(options?:{?
position?:number[],?
angle?:number,?
collisionGroup?:number,?
collisionMask?:number,?
sensor?:boolean,?
collisionResponse?:boolean,?
type?:number,?
material?:Material});?
其中,position是形狀相對于本地坐標中心的偏移量,這個偏移量會影響剛體的重心;angle為形狀在剛體本地坐標系統中傾斜的角度;collisionGroup為碰撞分組,與collisionMask一起使用,限制當前形狀只與指定條件的形狀發生碰撞;collisionMask為碰撞篩選,與collisionGroup一起使用,限制當前形狀只與指定條件的形狀發生碰撞;sensor設置形狀是否為敏感區域,默認false,如果設置為true,則該形狀不參與碰撞模擬,只作為感應區域;collisionResponse設置當與其他剛體發生碰撞時,當前形狀是否進行碰撞模擬,默認true,如果設為false,碰撞發生時當前形狀會穿過碰撞剛體;type為剛體類型,取值范圍是Shape. CIRCLE和Shape.BOX等常量之一,不需要設置該參數,系統會自動設置。?
使用collisionGroup和collisionMask屬性時,有兩個形狀si和sj,對si的collisionGroup屬性與sj的collisionMask屬性按位與運算,再對sj的collisionGroup屬性與si的collisionMask屬性按位與運算,如果兩個結果均不為0,則忽略此次si與sj的碰撞檢測。因為要按位運算,所以一般設置成2的倍數。?
material為材質,是一個Material對象,用來設置形狀發生碰撞時表現出的響應特性,如摩擦力、彈性系數等?
實際上,Material類中并不包含摩擦力、彈性系數等屬性,它只是一個標志類,只是一個id,真正實現碰撞響應特性的是ContactMaterial類。ContactMaterial類用來為添加了merialA和merialB標識的兩個形狀設置獨特的碰撞響應特性,構造函數為:?
function ContectMaterial(?
materialA:Material, materialB:Material,?
options?:{?
friction:number,?
restitution:number,?
stiffness:number,?
relaxation:number,?
frictionStuffness:number,?
frictionRelaxation:number,?
sufaceVelocity:number})?
其中,materialA是形狀shapeA的材質標識,materialB是形狀shapeB的材質標識,options是兩個形狀發生碰撞時響應特性的設置;friction是兩個形狀接觸面的摩擦系數,默認0.3;restitution是兩個形狀碰撞時的彈性系數,默認0;stiffness等同于ContactEquation的stiffness屬性,只碰撞時形狀表面的硬度,默認1000000,當stiffness較小時形狀之間可以重疊并有一個排斥力促使分離,形成海綿或水平的彈性表面效果;relaxation等同于ContactEquation的relaxation,指當形狀之間重疊后,在一次碰撞模擬計算過程中實施排斥力的次數,可以想象成粘稠度,取值大時剛體速度衰減快;sufaceVelocity是兩個剛體接觸時,接觸面方向上兩個剛體的相對速度,如果其中一個剛體靜止,則sufaceVelocity表示另一個剛體的速度,常用來模擬傳送帶效果。
4)形狀貼圖:
游戲中,物理對象需要對應的游戲圖像素材來呈現,需要使用圖片對剛體形狀貼圖。貼圖的過程,實際上是根據物理模擬后剛體的信息,實時更新圖片素材的坐標和角度。步驟為:?
⑴保存剛體與素材圖片的對應關系:?
Egret游戲中的圖片素材,通常是通過RES對象加載進來,需要為剛體添加一個自定義屬性,用來保存對應素材的引用。示例:?
body.userData=image;?
⑵遍歷world中所有剛體,查找擁有自定義素材屬性的剛體:?
world中所有的剛體都保存在bodies數組中,通過數組的foreach()方法,可以遍歷其中的每一個body,如果定義的body.userData屬性不為null,則進行貼圖:?
this.world.bodies.forEach(function(body:p2.Body){?
if(body.userData!=null){?
//更新素材坐標和角度?
}?
}?
⑶根據剛體信息,實時跟新對應圖像素材的坐標和角度:?
在ENTER+FRAME事件處理函數中,將剛體坐標和角度屬性賦值給素材,實時更新》?
body.userData.x=body.positon[0]*this.factor;?
body.userData.y=body.positon[1]*this.factor;?
body.userData.rotation=body.angle*180/Math.PI;?
Egret加載的圖片,原點默認為左上角,而剛體的原點處于其重心,根據形狀不同,重心位置也不同。在更新素材圖片前,需要根據剛體重心的坐標偏移量(offsetX,offsetY)設置圖片的anchorOffsetX和anchorOffsetY屬性。示例代碼:?
private bindAsset(body:p2.Body, assetName:string):void{?
var offset:number[]=[];?
body.updateAABB();?
var bodyWidth:number=body.aabb.upperBoumd[0]-body.aabb.lowerBound[0];?
var bodyHeight:number=body.aabb.upperBoumd[1]-body.aabb.lowerBound[1];?
var asset:egret.Bitmap=new egret.Bitmap();?
asset.texture=RES.getRes(assetName);?
asset.scaleX=bodyWidth*this.factor/asset.width;?
asset.scaleY=bodyHeight*this.factor/asset.height;?
this.addChild(asset);?
p2.vec2.subtract(offset, body.position, body.aabb.lowerBound);?
asset.anchorOffsetX=offset[0]/assets.scaleX*this.factor;?
asset.anchorOffsetY=offset[1]/assets.scaleY*this.factor;?
body.userData=asset;?
}?
通過body.updateAABB()更新剛體的aabb屬性,然后才能獲取到正確的upperBoumd和lowerBound數組的值。通過向量的subtract()方法,計算剛體坐標position,是相對于剛體最小包圍盒左上角aabb.lowerBound的偏移量,保存在offset變量中。代碼中,按照縮放比例將offset偏移量賦值asset的anchorOffsetX和anchorOffsetY屬性,調整圖片控制點,實現圖片與剛體的完全貼合。
4. 剛體屬性:
P2可以創建多種形狀的剛體,剛體除了形狀外還有各種屬性,有數十種,大概分為4類,速度相關、角度相關、對象相關,還有其他屬性。
1)速度相關屬性:
⑴position:number[]=[0,0];?
表示剛體在全局坐標系的位置,是一個二維向量Vec2,默認[0,0]。任何物體都需要通過position屬性進行精確定位和移動,p2在進行物理模擬時會自動更新剛體位置坐標,也可以在需要時直接修改其屬性值:body.position=[100,100];?
⑵velocity:number[]=[0,0];?
表示剛體的線性速度,單位像素/秒,是一個二維向量。P2使用包含兩個元素的數組表示速度,第1個元素表示x分量,第2個元素表示y分量。?
⑶damping:number=0;?
表示線性速度阻尼,也稱速度衰減系數,取值在0~1之間,默認0。假設剛體當前線速度為v,經過一次模擬計算后,受阻尼影響,剛體線性速度變為v*(1-damping),也就是剛體的線速度會隨時間的以1-damping為倍數下降。damping可以用來模擬剛體與地面之間的摩擦力。?
⑷fixedX:boolean=false;?
設定是否固定剛體的x坐標,默認false。當設置為true時,剛體只能在y軸方向上下移動。?
⑸fixedY:boolean=false;?
設定是否固定剛體的y坐標,默認false。當設置為true時,剛體只能在x軸方向左右移動。?
設定了fixedX及fixedY屬性后,需要調用updateMassProperties()方法,才能使其發揮作用。?
⑹force:number[]=[0,0];?
表示剛體當前受到的作用力大小,是一個二維向量。P2通過剛體的force來模擬外力的作用,施加的外力作用于剛體的中心位置,不會引起旋轉。如果模擬施加一個外力到物體頂部而推倒物體,P2通過Body類的applyForce()方法,可以自定義外力作用的位置,模擬類似旋轉效果。?
因為world調用step()方法后,force屬性將被置零,如果需要模擬持續施加作用力,需要在step()方法前持續設置force屬性。?
⑺gravityScale:number=1;?
設置當前剛體的重力加速度縮放比例,默認為1。如果當前重力加速度為gravity=[x,y],那么設置gravityScale后,重力加速度為[x*gravityScale, y*gravityScale]。?
如果設置gravityScale為0,當前剛體將不受重力加速度影響;若gravityScale設為負值,那么剛體將沿重力加速度反方向移動。
2)角度相關屬性:
⑴angle:number=0;?
表示剛體角度,為弧度值,正值增大為順時針旋轉。?
⑵angularVelocity:number=0;?
表示剛體角速度,即旋轉速度,默認為0。如果angularVelocity>0,剛體順時針旋轉;如果angularVelocity<0,剛體逆時針旋轉。angularVelocity單位是弧度/秒,因為P2更新step()方法是每幀調用,需要轉換成弧度/幀。假設游戲幀頻為fps,角速度為a1,轉換后的每幀弧度為:a2=a1/fps。?
⑶angularDamping:number=0.1;?
表示剛體角速度阻尼,取值范圍0~1,默認0.1。假設當前角速度為r,P2進行一次模擬計算后,受阻尼影響,剛體角速度將變為r*(1-damping)。?
⑷angularForce:number=0;?
表示剛體在角速度方向上受到扭力大小,單位N,默認0。angularForce作用的結果是使剛體旋轉,取值越大,剛體角速度變化越大。如果angularForce>0,扭力會促使剛體順時針旋轉;如果angularForce<0,扭力使剛體逆時針旋轉。實際上,在剛體上施加angularForce扭力后,形成一個旋轉加速度a,單位弧度/平方秒,如果剛體質量為m,它們之間的關系為:?
a=angularForce/m?
也即剛體加速度以每秒弧度為a的變化量遞增。如果使用角度/幀為單位,公式相應轉換為:?
a=dt*(angularForce/m)*(180/Math.PI)?
⑸fixedRotation:boolean=false;?
設置是否固定剛體角度,默認false。當fixedRotation為true時,剛體角度不會因碰撞或運動而發生變化,比如保持人一直處于垂直站立狀態。設置fixedRotation屬性后,也需要調用updateMassProperties()方法才能起作用。
3)對象相關屬性:
⑴shapes;Shape[];?
表示剛體中包含的所有形狀,均保存在這個shapes數組中。通過遍歷數組,可以對剛體中的每個形狀單獨進行操作。如果要計算剛體整體形狀的面積,就需要遍歷shapes屬性中的所有形狀,通過area屬性,將這些形狀的面積累加起來,得到整體形狀的面積,代碼為:?
var totalArea=0;?
for(var i=0; i<this.shapes.length;i++){?
titalArea+=this.shapes[i].area;?
}?
這個功能實際上已經集成到了Body類的getArea()方法中,可以直接調用這個方法。?
⑵world:World;?
表示剛體當前所在的world。通常情況下,一個P2應用中只有一個world世界,但需要時也可以創建多個world。無論是一個還是多個world,都可以通過world屬性獲取剛體所在的world,然后進行addBody()、Raycast()等操作。
4)其他屬性:
⑴id:number;?
表示剛體的唯一標識。P2中的每一個剛體都是唯一的,在碰撞檢測等過程中,可以通過id屬性來對指定的剛體進行識別。?
⑵type:number;?
剛體類型。P2中可用的剛體類型有3種,靜態剛體、可動剛體、動態剛體,默認為靜態剛體。type的屬性值為3個常量:?
·靜態剛體Body.STATIC:靜態剛體在world中始終保持靜止,不受重力影響,其坐標、角度不會因為碰撞而發生變化,一般使用綠色表示靜態剛體。?
·可動剛體Body.KINEMATIC:這種剛體是可動的,但不受重力影響,其坐標、角度不會因為碰撞而發生變化。所謂可動,指可通過設置剛體的velocity或angularVelocity等屬性,使其動起來,常使用紫色表示。?
·動態剛體Body.DYNAMIC:是最常用的類型,在重力作用下可進行自由落體運動,通過velocity、angularVelocity、force等屬性可使其動起來;當碰撞發生時,速度和角度相應發生變化,進行物理碰撞模擬,常常使用粉色表示。?
⑶mass:number;?
表示剛體的質量。P2中的剛體,沒有密度density屬性,mass充當了密度角色,默認為0,此時剛體為靜態剛體。當mass>0,剛體類型自動轉換為動態剛體,數值越大,慣性越大。?
⑷ccdIterations:number=10;?
當剛體高速運動時,P2會在一次step()中進行連續多次碰撞檢測,ccdIterations即表示這個檢測次數,默認為10。ccdIterations越大,碰撞模擬越精確,但計算效率也降低。?
⑸ccdSpeedThreshold:number=-1;?
只CCD碰撞檢測的最低速度,即速度超過ccdSpeedThreshold門限時,P2將對剛體進行連續碰撞檢測,默認為-1,此時不進行連續碰撞檢測。?
⑹collisionResponse:boolean=true;?
指定當與其他剛體發生碰撞時當前剛體是否會進行碰撞模擬,默認為true。如果設為false,則碰撞檢測發生時,當前剛體不進行碰撞模擬,會穿過剛體,但此時仍會觸發碰撞事件,并產生ContactEquation。?
⑺allowSleep:boolean=true;?
指定是否允許剛體進入睡眠狀態,默認true。正常情況下,P2會遍歷world中的每個剛體,對其進行紋理模擬計算。當剛體的速度為0,處于靜止狀態時,剛體會進入睡眠狀態,P2將不再對其進行物理模擬計算,以提升效率。這個屬性起作用,world的allowSleeping= World.BODY_SLEEPING。?
⑻sleepSpeedLimit:number=0.2;?
為剛體進入睡眠狀態時的最小速度,默認0.2,即當剛體速度小于sleepSpeedLimit時,剛體進入睡眠狀態。其前提是剛體的allowSleep為true。?
⑼sleepState:number=Body.AWAKE;?
為剛體當前睡眠狀態,默認為喚醒狀態。剛體睡眠狀態有3種:?
·Body.AWAKE:剛體處于喚醒狀態,P2對剛體進行正常的物理模擬計算,更新其屬性?
·Body.SLEEPY:當剛體的速度為sleepSpeedLimit,剛體進入SLEEPY瞌睡狀態,該狀態下P2也對剛體進行物理模擬計算?
·Body.SLEEPING:當剛體進入SLEEPY狀態超時時間sleepTimeLimit,才進入SLEEPING睡眠狀態。此時,P2在遍歷所有剛體,將通過當前剛體,不對其進行物理模擬計算。?
⑽sleepimeLimit:number=1;?
為剛體從瞌睡Body.SLEEPY狀態進入睡眠狀態Body.SLEEPING狀態需要的時間,默認為1秒。?
⑾idleTime:number;?
剛體已經進入睡眠Body.SLEEPING狀態的時長。
5. 剛體操作:
1)addBody和removeBody:
World類中的addBody()和removeBody()分別用來上P2世界添加和刪除剛體。所有創建好的剛體,必須通過addBody()添加到P2世界中,才可以進行碰撞模擬:?
var body:p2.Body({mass:1, position:[1,1]});?
this.world.addBody(body);?
當物體被子彈擊中,或超出屏幕范圍時,需要刪除剛體,可以通過removeBody()將其從P2世界中刪除,同時還可以避免不必要的計算。示例代碼:?
this.world.removeBody(bodyToRemove);?
2)addShape和removeShape:
Body類中的addShape()和removeShape()分別用來向剛體中添加和刪除形狀。使用addShape():?
var shape:p2.Rectangle=new p2.Rectangle(100,50);?
var body:p2.Body=new p2.Body({mass:1, position:[1,1]);?
body.addShape(shape);?
其實,addShape()中還有其他參數,構造函數為:?
function addShape(shape:p2.Shape, offset:number, angle:number)?
其中參數除了被添加的形狀shape,還有兩個參數,offset為形狀相對于坐標原點的偏移量,angle為形狀的角度,這兩個參數都是相對于剛體本地坐標系而言,即會隨剛體的變化而變化。?
可以通過removeShape()方法將指定的形狀從剛體中刪除。?
形狀的添加或刪除,并不會改變剛體的重心坐標,因此可用來模擬不倒翁效果。?
3)adjustCenterOfMass:
調整重心位置。剛體中增加或移去形狀后,重心并不會自動改變,可以使用adjustCenterOf Mass()方法,使剛體重心重新回到中心位置。這個方法不帶任何參數,也沒有返回值。?
4)applyForce:
作用力可以讓剛體狀態改變,通過applyForce()方法,可以在指定點worldPoint對剛體施加一個作用力,形成一個加速度或扭力,改變剛體的線速度或角速度。構造函數為:?
function applyForce(force:number[], worldPoint:number[])?
其中,force是要施加的作用力,這是一個二維向量; worldPoint是一個全局坐標點,表示force在剛體上的作用點,當此點不在剛體重心位置時,剛體角度也會發生變換。?
5)applyImpulse:
如果要瞬間改變剛體的狀態,如子彈彈出膛效果,需要使用applyImpulse()對其施加沖量,使剛體的速度和角速度瞬間發生變化。?
function applyImpulse(impulse:number[], relativePoint:number[])?
其中,impulse為要施加的沖量,沖量有大小和方向,是一個二維向量; relativePoint是一個本地坐標點,表示沖量的作用點,當作用點不在剛體中心時剛體角度也會發生變化。?
6)sleep和wakeup:?
Body的sleep()方法強制使剛體進入睡眠狀態,此時除了sleepState處于Body.SLEEPING狀態外,剛體的速度、角速度、受到的作用力、扭力等都會全部清零。?
function sleep(){?
this.sleepState=Body.SLEEPING;?
this.angularVelocity=0;?
this.angularForce=0;?
vec2.set(this.velocity, 0, 0);?
vec2.set(this.force, 0, 0);?
this.emit(Body.sleepEvent);?
};?
剛體進入睡眠狀態后,通過調用wakeup()方法,可以將剛體強制喚醒,但僅僅是將sleepState設置為Body.AWAKE,睡眠狀態前的速度、角度等屬性不會恢復:?
function wakeup(){?
var s=this.sleepState;?
this.sleepState=Body.AWAKE;?
this.idleTime=0;?
if(s!==Body.AWAKE){?
this.emit(Body.wakeUpEvent);?
}?
}?
可以使用sleep()方法實現類似冰凍效果。?
7)emit、on、off、has:
P2物理引擎中,通過EventEmitter類實現事件派發機制,并通過emit()、on()、off()、has()方法分別實現派發、監聽、取消監聽,以及檢測是否包含指定事件功能。?
⑴emit():派發自定義事件,事件類型使用任意字符串表示:?
function emit(event:Object)?
其中,event為要派發的自定義事件,是一個Object類型,用于包含任何需要通過事件傳遞的信息。在自定義事件時,event對象至少要包含名為type的字符串屬性值,表示事件名稱:?
body.emit({type:"movingUp"});?
⑵on():監聽對應的自定義函數?
監聽到事件對象,將作為參數傳遞給事件監聽函數,示例代碼為:?
var onMyEvent=function(event){?
console.log("onMyEvent is fired, because I am moving up.");?
};?
body.on("movingUp", onMyEvent);?
on()方法的參數是字符串類型的事件名稱,并在事件觸發時調用一個處理函數。?
⑶off():取消監聽?
當不需要監聽某個事件時,將事件名稱和事件監聽函數作為參數傳遞給off()方法,就可以取消監聽。示例代碼:?
body.off("movingUp", onMyEvent);?
⑷has():檢查對象是否包含指定的事件監聽?
Body類因為繼承了EventEmitter類,所以也擁有上面4個方法。通過這些方法,可以方便地對剛體的某些特定狀態進行監聽,如睡眠或蘇醒。Body類內置了睡眠和蘇醒事件:?
·sleepEvent:當剛體進入睡眠狀態時派發該事件,對應事件名稱為sleep,監聽使用body. on("sleep", onMyEvent);?
·wakeupEvent:當剛體從睡眠狀態恢復至蘇醒狀態時,派發wakeupEvent事件,事件名為wake,監聽使用body.on("wake", onMyEvent);?
·sleepyEvent:當剛體進入瞌睡狀態時,派發該事件。Body有一個sleepSpeedLimit屬性,當剛體的速度小于sleepSpeedLimit值時就進入瞌睡狀態;如果該狀態持續時間超過body. sleepTimeLimit,則剛體進入睡眠狀態。sleepyEvent對應的事件名為sleepy,監聽使用body. on("sleepy", onMyEvent);?
除了上述內置事件,還可以自定義事件,比如在step()方法中持續判斷body.velocity.y是否小于0,來檢測剛體向上運動,派發自定義moveUp事件:?
public loop():void{?
this.world.step(60/1000);?
this.debugDraw.drawDebug();?
if(this.bodyRef.velocity[1]<0){?
this.bodyRef.emit({type:"myEvent"});?
}?
}?
8)fromPolygon:
fromPolygon用于將多邊形分解成一個個小的形狀,然后組合成完整的多邊形。?
function fromPolygon(path:number[][], [options]):boolean?
其中,path保存了多邊形頂點數組,options為可選屬性,定義多邊形分解的相關設置。?
options包括的選項有:?
·optimalDecomp=false:是否進行最佳分解,默認false,開啟該選項會降低計算速度?
·skipSimpleCheck=false:是否進行頂點交叉的判斷,如果確定不存在交叉點可設為true?
·removeCollinearPoints=false:是否剔除共線頂點,false表示不剔除?
如果多邊形創建成功返回true,否則返回false。導致創建失敗有幾種原因,比如創建的形狀中有空洞、頂點和邊之間有交叉。可以編程將鼠標光標移動的軌跡中的點作為多邊形的頂點。也就是隨手創建剛體。?
9)hitTest:
用于實現指定坐標點與剛體的碰撞檢測,并將檢測到的碰撞剛體保存到數組中返回。?
function hitTest(worldPoint:number[][], bodies:Body[], precision:number):Body[]?
其中,worldPoint為要檢測的坐標點,是一個全局坐標點;bodies為要檢測的剛體清單,可以將需要檢測的剛體保存到bodies數組中,實現有針對性的碰撞檢測。如果要對所有的剛體進行檢測,可以直接設置該參數為world.bodies,即:hitTest(worldPoint, world.bodies);?
參數precision為檢測精度,默認為0,取值越大檢測精度越高,計算效率會降低,一般使用默認值,對尺寸極小的物體,如particle和line需要設置。方法的返回值為保存了與worldPoint發生碰撞所有剛體的數組。?
hitTest()最常用用于檢測鼠標光標點擊、拖曳效果。P2中沒有鼠標光標事件,可以通過hitTest()來判斷鼠標點是否與剛體發生碰撞,然后調整剛體的position至鼠標位置,實現對剛體的拖曳。為了便于拖曳,在鼠標點擊時通過sleep()方法將剛體設為睡眠狀態,使其不受重力影響,可以精確地隨鼠標光標移動,當拖曳完成彈起時再使用wakeUp()方法重新喚醒剛體,恢復重力的作用。?
10)getAABB:
獲取剛體的最小包圍盒AABB(Axis Align Bounding Box),最小包圍盒AABB是指包圍形狀的最小矩形框。可以通過設置剛體的isDrawAABB=true,以顯示出剛體的AABB。AABB對象將矩形對象的左下角和右下角坐標分別保存在屬性lowerBound和upperBound中。?
11)getArea:?
獲取剛體的當前所有形狀的面積總和。?
12)setDensity:
設置剛體的密度,結構為:?
function setDensity(density:number)?
其中,density為要設置的密度,是取值大于0的數值。?
實際上,P2中的剛體并沒有密度屬性,當調用setDensity()方法后,Body會根據剛體當前的面積area計算出對應的質量并保存在mass屬性中。?
13)overlaps:
檢測當前剛體與指定剛體是否有重疊,并返回檢測結果,結構為:?
function overlaps(body:Body):boolean?
其中,body為要檢測的目標剛體。如果剛體之間有重疊則返回true,否則返回false。?
目標剛體與被檢測剛體需添加到world中,才能確保overlaps()的正常運行。通常并不需要使用overlaps()進行重疊檢測,因為P2在實施碰撞檢測和模擬時已經完成了這些內容,只有當剛體不進行碰撞模擬時才需要使用overlaps()。比如在界面中放置一個物體,當此位置已經存在物體時不能放置。?
14)toWorldFrame和toLocalFrame:
toWorldFrame()是將剛體本地坐標系統中的坐標點轉換成全局坐標點,toLocalFrame()是將全局坐標點轉換成剛體本地坐標系統中的坐標點。通過這兩個方法,可以實現本地坐標和全局坐標之間的轉換。?
function toWorldFrame(out:number[], localPoint:number[]):void?
function toLocalFrame(out:number[],worldPoint:number[]):void?
其中,out為轉換后的本地坐標或全局坐標,將保存在該屬性對應的變量中; localPoint為要轉換的本地坐標;worldPoint為要轉換的全局坐標。?
15)raycast:
射線投射技術用于實現線段與形狀的碰撞檢測,通常用來模擬人物的視野、距離探測等。實現射線投射,要先從起點from到終點to構建一條射線ray,然后檢測與該線段發生碰撞的剛體,并保存在一個RaycastResult對象中。raycast結構為:?
function raycast(result:RaycastResult, ray:Ray)?
其中,result是保存了碰撞剛體、碰撞點、碰撞距離等信息的一個RaycastResult對象;ray是用于檢測的射線,是一個Ray類,包含起點from、終點to等屬性。使用射線投射,需要用到Ray類和RaycastResult類。?
①Ray類:
Ray類的構造函數為:?
function Ray([options])?
其中,options參數是一個Object對象,初始化Ray對象時,可以保持options缺省,然后再通過Ray屬性進行設置。Options對象中包含一些參數:?
·from:number[]:線段的起點,一個二維向量?
·to:number[]:線段的終點,一個二維向量?
·mode:number:射線的碰撞檢測模式,取值為3個常量,分別為Ray.ALL、Ray.CLOSEST、Ray.ANY。Ray.ALL模式下,raycast()函數會檢測所有與射線ray發生碰撞的剛體;Ray.CLOSEST模式下,返回檢測到的碰撞剛體中距離起點from最近的剛體;Ray.ANY模式下,當ray檢測到第1個碰撞剛體時會立刻停止檢測并返回該剛體,這時的第1個剛體與創建時的順序有關。?
·callback:Function:回調函數,當raycast()檢測到碰撞剛體后會立即調用回調函數,該屬性只適用于rayCastAll()?
·collisionMask:number:與collisionGroup配合使用,對檢測剛體進行篩選?
·collisionGroup:number:與collisionMask配合使用,對檢測剛體進行篩選,只有滿足條件的剛體才參與碰撞檢測,檢測條件為:?
(this.collisionGroup&body.collisionMask)&&(body.collisionGroup&this.collisionMask)==true?
·skipBackface:是否忽略射線ray反方向與剛體的碰撞點,值為false時只檢測正方向?
·checkCollisionResponse:配合Body中的collisionResponse屬性,如果剛體的屬性collisionResponse和checkCollisionResponse同時為true,則不對該剛體進行檢測。?
·direction:射線方向,可以缺省,取決于from和to屬性?
·length:射線從起點from到終點to的間距?
②RaycastResult類:
RaycastResult類用于保存射線與剛體的碰撞信息,包括幾個屬性:?
·body:當前碰撞檢測中,與射線ray發生碰撞的剛體?
·shape:當前碰撞剛體中,與射線ray發生重疊的形狀?
·fraction:射線起點from到碰撞點之間的距離distance與射線長度length的比例?
·normal:垂直于射線碰撞邊的法向量,只讀屬性,默認-1?
RaycastResult類還有一些方法:?
·hasHit():是否檢測到與射線ray發生碰撞的剛體,當檢測到時返回true?
·getHitDistance():當檢測到與射線發生碰撞的剛體時,碰撞點與射線起點from間的距離?
function getHitDistance(ray:Ray):number?
·getHitPoint():獲取碰撞點位置,返回結果是一個全局坐標點?
此方法的結構為function getHitPoint(out:number[], ray:Ray):void?
其中,out為用于保存碰撞點坐標的二維向量,ray為當前進行碰撞檢測的射線。?
·stop():用于立即停止碰撞檢測?
因為raycast()方法中的RaycastResult會重復使用,所以回調函數中獲取到的是最后一次碰撞檢測信息。
6. 碰撞處理:
P2可以實現物體碰撞模擬,同時在碰撞過程中派發一些事件實現碰撞檢測,將碰撞信息及時反饋,以添加相應的特效。?
P2中,當兩個剛體的最小包圍盒AABB發生重疊,碰撞就開始了;然后剛體的形狀發生重疊,同時P2會對重疊進行修復,使剛體朝對方的反方向移動,來消除形狀重疊;當形狀不再有重疊時,整個碰撞過程結束。可以把碰撞過程分為4個階段:?
·postBroadphase:AABB開始發生重疊,但形狀并沒有發生接觸?
·beginContact:剛體形狀開始發生重疊,剛體繼續保持原有速度移動?
·preSolve:剛體形狀發生了重疊,但P2還未進行碰撞處理?
·endContact:P2已經完成了碰撞處理,并為碰撞剛體重新分配了速度,同時剛體形狀分離,不再有重疊?
1)碰撞事件:
碰撞過程會產生4個事件,postBroadphase、preSolve、beginContact、endContact。在碰撞事件派發后,可以通過world類的on方法來監聽事件,并在監聽處理函數中進行對應的處理。on方法的結構為:?
function on(type:String, listener:functtion)?
其中,type為監聽事件名,為一個字符串,取值為對應的事件名稱;listener為事件監聽函數,當監聽到碰撞事件后,會自動調用監聽函數,并將碰撞事件對象作為參數傳遞給監聽處理函數。碰撞監聽函數的參數event是一個Object對象,對應碰撞事件對象,其中保存碰撞信息。?
在on事件處理函數中,this指的是派發事件的對象context,也就是world對象,而不再是主類中的this。因此,需要在on函數之前,將this指針保存到一個局部變量中,然后才能在事件處理函數中通過這個局部變量訪問主類中的變量和方法。?
on()方法監聽碰撞事件時,會將對應的碰撞事件對象作為參數傳遞給監聽處理函數,這些碰撞事件對象為:postBroadphaseEvent、preSolveEvent、beginContactEvent、endContactEvent。?
①postBroadphaseEvent:
當兩個剛體的AABB發生重疊時,不斷派發postBroadphase事件,并將碰撞信息保存到對應的postBroadphaseEvent對象中,該對象包含屬性pairs,即保存碰撞剛體對的數組。因為尚未進行剛體形狀的碰撞檢測,所以此時對pairs數組中的碰撞進行刪減可以取消對應碰撞剛體之間的碰撞模擬。但刪除pairs后,將不再進行beginContact和endContact事件派發。?
②preSolveEvent:
在剛體形狀發生重疊時,P2會根據動量守恒定律,對碰撞對象的速度和角度重新計算,實現碰撞模擬,這個過程稱為solve。preSolve事件在碰撞模擬過程前派發,在preSolve事件處理函數中進行一些處理可以取消或干預碰撞的模擬。?
preSolveEvent對象的屬性有:?
·contactEquations:保存當前碰撞產生的所有contactEquation對象的數組?
·frictionEquations:保存當前碰撞產生的所有frictionEquation對象的數組?
preSolve事件會隨step()方法不斷派發,在剛體形狀之間沒有重疊前,contactEquations屬性中并不包括contactEquation對象,所以代碼中要先進行判斷:contactEquations.length>0。?
③beginContactEvent:
當剛體形狀發生重疊時會派發beginContact事件,并將碰撞信息保存在對應的beginContact Event對象中,其中屬性有:?
·shapeA:發生碰撞的形狀A?
·shapeB:發生碰撞的形狀B?
·bodyA:形狀shapeA對應的剛體?
·bodyB:形狀shapeB對應的剛體?
·contactEquations:保存當前碰撞產生的所有contactEquation對象的數組?
④endContactEvent:
當前兩個碰撞剛體的形狀分離而不再重疊時會派發endContact事件,并將碰撞信息保存在對應的endContactEvent對象中,包含的屬性有:?
·shapeA:發生碰撞的形狀A?
·shapeB:發生碰撞的形狀B?
·bodyA:形狀shapeA對應的剛體?
·bodyB:形狀shapeB對應的剛體?
2)碰撞信息Equation:
在P2引擎中,Equation類用來保存碰撞發生時的碰撞點、碰撞向量等信息。一般用其兩個子類ContactEquation和FrictionEquation來保存不同類型的信息。?
①ContactEquation:
碰撞過程中,因接觸而產生的碰撞信息,如碰撞點、碰撞向量、碰撞剛體等,都保存在ContactEquation對象中。在preSolve和beginContact階段,會產生多個ContactEquation對象,并分別保存在preSolveEvent和beginContactEvent事件對象的ContactEquation屬性中。?
·shapeA:發生碰撞的形狀A?
·shapeB:發生碰撞的形狀B?
·bodyA:形狀shapeA對應的剛體?
·bodyB:形狀shapeB對應的剛體?
·contactPointA:自bodyA的坐標起,到碰撞點的全局向量?
·contactPointB:自bodyBA的坐標起,到碰撞點的全局向量?
·enabled:是否對當前的ContactEquation對象進行碰撞模擬,默認true,如果在preSolve階段將其設為false可以取消碰撞模擬?
·firstImpact:是否為第1次碰撞,當第1次碰撞step()完成后,firstImpact立即設為false?
·normalA:垂直于剛體碰撞邊的法向量,這是一個全局的單位向量?
·restitution:碰撞剛體之間的碰撞彈性系數,取值0~1。該系數只影響當前碰撞的模擬,如果對shapeA和shapeB設置contactMaterial,其restitution屬性不受影響。?
②FrictionEquation:
碰撞過程中,因剛體相對運動而產生的摩擦等碰撞信息,保存在FrictionEquation對象中。在碰撞的preSolve階段會產生FrictionEquation對象,并保存在preSolveEvent事件對象的FrictionEquation屬性中。FrictionEquation對象包含的屬性有:?
·shapeA:發生碰撞的形狀A?
·shapeB:發生碰撞的形狀B?
·bodyA:形狀shapeA對應的剛體?
·bodyB:形狀shapeB對應的剛體?
·frictionCoefficient:碰撞時剛體之間的碰撞系數,取值越大速度衰減越快,只影響當前碰撞模擬。如果有對shapeA和shapeB設置contactMaterial,其friction屬性不受影響。?
·t:碰撞邊的切線方向向量?
需要注意,frictionCoefficient屬性需要在world.solver.frictionIterations>0情況下才起作用,frictionIterations默認是0,需要在創建world時設置其值。
7. 關節:
P2中使用Constraint及其子類表示關節,也就是將兩個剛體按照指定的規則約束在一起,形成有規律的、相互限制的運動模擬。P2關節模擬中,兩個剛體沒有通過任何剛體連接,只是通過算法模擬出關節運動軌跡。為了更加直觀,p2DebugDraw類中使用黑色的線段表示連接剛體的連桿,黑點圓的表示關節節點anchor。?
P2中關節有5種,每一種都有獨特的約束規則,包括距離關節DistanceConstraint、齒輪關節GearConstraint、鎖定關節LockConstaint、位移關節PrismaticConstraint、旋轉關節Revilute Constraint。?
1)距離關節DistanceConstraint:
按照指定的距離distance將兩個剛體約束在一起,其中任何一個剛體的位置發生變化,會牽著另一個剛體運動,以保證兩者的間距為distance。但是兩個剛體的角度不受約束,可以繞著節點旋轉。DistanceConstraint構造函數為:?
function DistanceConstraint(bodyA:Body, bodyB:Body, options:object)?
其中,bodyA和bodyB為受約束的剛體,options為關節設置選項,可以缺省,P2以默認值設置,其中選項為:?
·distance:兩個剛體受到約束時保持的間距,默認為添加關節時兩個剛體之間的間距?
·localAnchorA:關節點相對于剛體bodyA本地坐標系統的坐標系,默認[0,0]?
·localAnchorB:關節點相對于剛體bodyB本地坐標系統的坐標系,默認[0,0]?
·maxForce:剛體運動中,如果距離不等于distance,為保持距離而對剛體施加的最大作用力,默認為Number.MAX_VALUE?
除了上面的構造函數中的參數,距離關節還包含一些屬性:?
·lowerLimit:設置距離關節約束范圍下限,即bodyA到bodyB的距離最小值,默認為0,該屬性必須大于0。只有當lowerLimitEnabled為true時才起作用。?
·lowerLimitEnabled:是否設置距離關節約束范圍下限,默認false。?
·upperLimit:設置距離關節約束范圍上限,即bodyA到bodyB的距離最大值,默認為0,該屬性必須大于0。只有當upperLimitEnabled為true時才起作用。?
·upperLimitEnabled:是否設置距離關節約束范圍上限,默認false?
·position:bodyA和bodyB的當前間距?
可以通過joint.collideConnected屬性為true,避免平臺和車輪之間的碰撞。創建完成后需要使用world的addConstraint(joint)方法將關節加入世界。?
2)齒輪關節GearConstraint:
按照指定的比例ratio,將兩個剛體的角度angleA和angleB約束為angle=angleB*ratio。其中任何一個剛體的角度變換,都會牽著另一個剛體的角度變化,以確保兩個剛體角度的比例為ratio。剛體的坐標位置不受約束,可以自由向任意方向移動。構造函數為:?
function GearConstraint(bodyA:body, bodyB:Body, options:Object)?
其中,bodyA和bodyB為受關節約束的兩個剛體,options為關節設置選項,可以缺省,P2會按默認值進行設置。選項為:?
·angle:兩個剛體的相對角度差,齒輪關節會將一個剛體的角度減去該角度差后,再保證角度變化量的比例為ratio?
·ratio:兩個剛體的角度變化量的比例,當ratio=2時,bodyB旋轉180°,bodyA只轉90°?
·maxForce:當兩個剛體的角度比例不是ratio時,為將其約束為ratio而對剛體施加的最大扭力?
齒輪關節還有兩種方法:?
·setMaxForce(force):當bodyB的角度偏離angle,齒輪關節對bodyB施加的最大扭力?
·getMaxForce():number:獲取setMaxForce()中設置的最大作用力?
3)鎖定關節LockConstraint:
將兩個剛體綁定在一起,使其相對坐標位置、角度差保存不變,仿佛被釘在一起。此關節中的任何剛體坐標或角度發生變化,都會牽著另一個剛體的坐標和角度變化,以確保兩個剛體相對坐標和角度分別為localOffsetB和localAngleB。構造函數:?
function LockConstraint(bodyA:Body, bodyB:body, options:Object)?
其中,bodyA和bodyB為受關節約束的兩個剛體,options為關節設置選項,可以缺省,P2會按默認值進行設置。選項為:?
·localOffsetB:剛體bodyB在關節約束下,相對于bodyA本地坐標系的偏移量,默認為添加關節時兩個剛體的相對位置?
·localAngleB:剛體bodyB在關節約束下,相對于bodyA本地坐標系統的角度,默認時為添加關節時兩個剛體的相對角度?
·maxForce:當兩個剛體未達到關節約束的localOffsetB和localAngleB,為使其達到約束指定狀態,而可以施加的最大作用力,默認Number.MAX_VALUE?
LockConstraint還包含幾個方法:?
·setMaxForce(force):當bodyB的位置偏離localOffsetB,或角度差不等于localAngleB時,鎖定關節對bodyB施加的最大作用力。?
·getMaxForce():number:獲取setMaxForce()中設置的最大作用力?
4)位移關節PrismaticConstraint:
將剛體bodyB的運動方向,限定為在剛體bodyA本地坐標系統中的一個指定向量。構造函數為:?
function PrismaticContraint(bodyA:Body, bodyB:Body, options:Object)?
其中,bodyA和bodyB為受約束的剛體,options為關節設置選項,可以缺省,P2以默認值設置,其中選項為:?
·maxForce:當bodyB相對于bodyA的位置偏離localAxisA時,為使其恢復到約束位置,可以施加的最大作用力,默認為Number.MAX_VALUE?
·localAnchorA:控制點anchorA在bodyA本地坐標系下的坐標,默認[0,0]?
·localAnchorB:控制點anchorB在bodyB本地坐標系下的坐標,默認[1,0]?
·localAxisA:剛體受到約束時只可以在該坐標軸方向上移動,這是剛體bodyA坐標系下的一個向量,默認[1,0]?
·disableRotationalLock:是否禁止bodyB繞節點旋轉,默認false,即bodyB不能自由旋轉,此值只有在構造函數中設置才起作用。?
·upperLimitEnabled:是否開啟bodyB移動方向上限,默認false,此時可以沿localAxisA正方向無限移動?
·upperLimit:設置bodyB沿localAxisA正方向可以移動的最大距離,默認為1?
·lowerLimitEnabled:是否開啟bodyB移動方向下限,默認false,此時可以沿localAxisA負方向無限移動?
·lowerLimit:設置bodyB沿localAxisA負方向可以移動的最大距離,默認為0,該屬性值要小于upperLimit?
除了上述在構造函數中的參數,PrismaticConstraint還有其他一些屬性:?
·motorEnabled:是否開啟馬達屬性,與motorSpeed配合使用。開啟后,關節會對bodyB施加作用力,使其線速度達到motorSpeed,并在約束范圍內一直保持該速度。開啟或關閉馬達屬性,要用enableMotor()和disableMotor()方法。?
·motorSpeed:開啟馬達屬性后,bodyB的目標速度值?
·position:在localAxisA上,bodyB相對于bodyA的當前位置?
PrismaticConstraint還有一些方法,用于調整關節的相關屬性:?
·setLimits(lower, upper):設置位移關節的上下限,其中lower一定要小于upper?
·disableMotor():關閉馬達屬性?
·enableMotor():開啟馬達屬性?
可以創建一個空剛體來固定關節。所謂空剛體,就是沒有包含任何形狀對象的剛體,所以不會與任何剛體發生碰撞模擬。?
5)旋轉關節RevoluteConstraint:
限制兩個剛體只能繞指定的控制點旋轉,該控制點是剛體bodyA本地坐標系下的坐標。其中一個剛體的位置或角度發生變化時,為了確保控制點和剛體的相對位置不變,另一個剛體也會被牽制發生位置和角度的變化。構造函數為:?
function RevoluteConstraint(bodyA:Body, bodyB:Body, options:Object)?
其中,bodyA和bodyB為受關節約束的兩個剛體,options為關節設置選項,可以缺省,P2會按默認值進行設置。選項為:?
·worldPivot:全局坐標系下的關節節點,bodyA和bodyB均受約束,只能繞該節點旋轉。設置該節點后,旋轉關節會自動計算localPivotA和localPivotB本地節點。?
·localPivotA:節點worldPivot在bodyA剛體本地坐標系統下的坐標,默認[0,0]?
·localPivotB:節點worldPivot在bodyB剛體本地坐標系統下的坐標,默認[0,0]?
·maxForce:當剛體坐標偏離節點時,為使其恢復到節點位置,可以施加的最大作用力,默認為Number.MAX_VALUE?
RevoluteConstraint還包含幾個方法:?
·setLimits(lower:number, upper:number):設置bodyB繞節點旋轉角度的上下限,值為弧度?
·enableMotor():開啟馬達屬性,與setMotorSpeed()配合使用,關節會對bodyB施加扭力,使其達到setMotorSpeed()?
·disableMotor():關閉馬達屬性?
·setMotorSpeed(speed):設置bodyB的目標角速度,只有開啟馬達屬性后才其作用。?
·getMotorSpeed():number:讀取馬達的當前速度?
旋轉關節常用于模擬小車運動。
8. 彈簧Spring:
P2中用來約束剛體運動的還有彈簧Spring。彈簧除約束兩個剛體之間的運動軌跡外,通過damping阻尼和stiffness剛度系數等屬性,使得剛體在向目標移動時,出現類似彈簧的簡諧運動。Spring只是抽象的父類,參與運動模擬的是兩個子類LinearSpring和RotationalSpring。?
1)LinearSpring:
LinearSpring是線性彈簧,對剛體的約束行為和距離關節DistanceConstraint相同,按照指定的距離restLength將兩個剛體約束在一起,其中任何一個剛體的位置發生變化,會牽制著另一個剛體運動,以保證兩者的間距為distance。在運動過程中,剛體bodyB呈現簡諧運動。兩個剛體的角度不受約束,可以繞節點旋轉。構造函數:?
function LinearSpring(bodyA:Body, bodyB:Body, options:Object)?
其中,bodyA和bodyB為受彈簧約束的兩個剛體,options為關節設置選項,可以缺省,P2會按默認值進行設置。選項為:?
·stiffness:彈簧的剛度系數,默認100。?
·damping:彈簧做簡諧運動過程中的阻尼系數,默認1?
·restLength:彈簧不受力狀態下的長度,默認為worldAnchorA和worldAnchorB間的距離?
·localAnchorA:剛體bodyA本地坐標系下的節點坐標,默認[0,0]?
·localAnchorB:剛體bodyB本地坐標系下的節點坐標,默認[0,0]?
·worldAnchorA:彈簧節點在全局坐標系下的坐標,設置后將自動轉換并覆蓋localAnchorA?
·worldAnchorB:彈簧節點在全局坐標系下的坐標,設置后將自動轉換并覆蓋localAnchorB?
2)RotationalSpring:
RotationalSpring是扭力彈簧,對剛體的約束類似齒輪關節,按照指定的restAngle約束兩個剛體之間的角度差。當剛體的角度不等于restAngle時,bodyB會進行簡諧運動旋轉,直至角度差恢復至restAngle。兩個剛體的坐標位置不受約束,可以自由移動。?
function LinearSpring(bodyA:Bodt, bodyB:Body, options:Object)?
其中,bodyA和bodyB為受彈簧約束的兩個剛體,options為關節設置選項,可以缺省,P2會按默認值進行設置。選項為:?
·restAngle:彈簧不受力無簡諧運動下剛體bodyA和bodyB間的角度差,默認為創建扭力彈簧時兩個剛體之間的角度差?
·stiffness:彈簧的剛度系數,默認100。?
·damping:彈簧做簡諧運動過程中的阻尼系數,默認1