虽然P2被开发者吐槽 “刚体碰撞出错,刚体穿透时常发生,社区也不活跃,资料少,物理效果欠佳” ,但作为Egret官方推荐的物理引擎,还是有必要学习使用。
官方P2物理引擎的引入:
为了方便不同项目共用代码库,我喜欢将Egret的第三方类库集中放到EgretLibs的文件夹,并且跟其他的Egret项目并列。
将egret-game-library/physics/的libsrc文件夹下载到EgretLibs文件夹,并且重命名为:physics
修改项目的egretProperties.json文件,modules数组里增加:
{
"name": "physics",
"path": "../EgretLibs/physics"
}
编译项目,完毕之后,项目的libs/modules文件夹应该会自动创建physics文件夹和三个js/ts文件
对于egret-game-library所有的第三方类库,都可以用这个方式引入。
P2物理引擎概述:
物理引擎模拟了近似真实的物理系统,为刚性物体赋予物理效果,比如重力、旋转、弹跳、碰撞、反弹、滚动、加减速等效果,让物体的行为更加真实。一般来说物理引擎并不处理和画面渲染相关的事务,而只完成计算仿真。所以物理引擎都需要跟各种的渲染引擎配合使用。而这种配合是“低耦合”的,只需要注意各自的坐标系映射即可,开发者可以自由选择喜欢的引擎。
常用物理引擎:P2, Matter.js, Box2D, PhysicsJS, arcade...
常用渲染/游戏引擎:Egret, Layabox, Phaser, Cocos2d-x, CreateJS...
具体到Egret + P2,它们的坐标映射是:
文字描述一下:
1、P2与Egret横坐标一致,纵坐标相反
2、P2物体的原点为中心点,Egret显示对象的原点为左上角,设置display.anchorOffsetX = display.width/2;display.anchorOffsetY = display.height/2
3、P2的长度单位(米)与Egret长度单位(像素)的factor(系数)为50,计算坐标时要实时转换更新
P2引擎大致分为:世界World,形状Shape,刚体Body,然后加上Egret的绘制或者贴图,形成一个模拟的物理世界。
借用其他教程的话:
- 世界:world是P2物理引擎的入口,它类似于渲染引擎中的stage或显示对象。和addChild()函数一样,所有的物理对象都是必须通过addBody(),添加到P2的world世界中,才会进行物理模拟。
- 形状:物理游戏中进行碰撞模拟的一个前提,是有固定的形状,或者矩形,又或者圆形,或者不规则多边形,但这个形状必须是固定不变的。而类似于水这类液体的物理运动,因为没有固定的形状,无法使用P2进行模拟。
- 刚体:刚体是P2物理引擎的核心概念和对象,形状依附于刚体,将刚体添加到世界中,才能进行物理模拟,刚体是所有碰撞对象的原型。
- 渲染贴图:P2只是一个算法库,以刚体为对象模型,模拟并输出物理碰撞、运动结果。它本身不具备渲染功能,无法显示模拟的结果,而是要借助于渲染引擎,通过绘制或贴图来渲染物理模拟结果。
创建Egret + P2项目:
基于以上概述,项目中使用P2引擎大致分为以下步骤:
1,创建世界World
打开项目的main.ts文件,粘贴以下代码,在childrenCreated()方法调用this.CreateWorld();
//声明world和factor两个全局变量
private world: p2.World;
private factor: number = 50;
private CreateWorld() {
this.world = new p2.World();
//设置world为睡眠状态
this.world.sleepMode = p2.World.BODY_SLEEPING;
this.world.gravity = [0, -10];
}
2,创建形状Shape
现实中物体的形状千奇百怪,但是为了减少碰撞检测等的计算量提高运行效率,P2简化了形状:矩形Box,胶囊形状Capsule,圆形Circle,凸形状Convex,起伏平面Heightfield,线形Line,粒子Particle 和 平面Plane
//矩形
var boxShape: p2.Shape = new p2.Box({ width: 2, height: 1 });
//圆形
var circleShape:p2.Shape = new p2.Circle({radius:60});
3,创建刚体Body
刚体有各种属性,参见:Body类的属性
//Egret与P2的坐标转换
var positionX: number = Math.floor(200 / this.factor);
var positionY: number = Math.floor((this.stage.stageHeight-200) / this.factor);
var boxBody: p2.Body = new p2.Body(
{
mass: 1, //质量
position: [positionX, positionY],
angularVelocity: 1 //默认:0。表示刚体角速度,即旋转速度
});
boxBody.addShape(boxShape); //添加上一步创建的shape
this.world.addBody(boxBody);//所有刚体都需要加入到world才会参与物理计算
4,创建egret.DisplayObject显示物体
绘制的图形、Bitmap、还是Sprite,都可以显示。
//绘制Shape
//http://developer.egret.com/cn/apidoc/index/name/egret.Shape
private drawRect():egret.Shape {
var shape:egret.Shape = new egret.Shape();
shape.graphics.beginFill(0x0000ff);
shape.graphics.drawRect(200, 200, 100, 100);
shape.graphics.endFill();
return shape;
}
//Bitmap
private createBitmapByName(): egret.Bitmap {
let name = "tank_blue_png";//自定义图片资源
var result: egret.Bitmap = new egret.Bitmap();
var texture: egret.Texture = RES.getRes(name);
result.texture = texture;
return result;
}
//自定义的Sprite类
class Tank extends egret.Sprite {
private isTankStoped: boolean = true;
public constructor(isEnemy: boolean) {
super();
let tankResName;
if (isEnemy) {
let randomIndex = util.GetRandomNum(1, 4);
tankResName = "tank_red_" + randomIndex.toString() + "_png";
}
else {
tankResName = "tank_blue_" + "1" + "_png";
}
let bmp: egret.Bitmap = new egret.Bitmap();
bmp.texture = RES.getRes(tankResName);
this.addChild(bmp);
}
public StartTank() {
this.isTankStoped = false;
}
public StopTank() {
this.isTankStoped = true;
}
}
var display: egret.DisplayObject;
display = this.drawRect();//new Tank(false);// this.createBitmapByName();
display.width = (<p2.Box>boxShape).width * this.factor; //长度单位不同,注意转换
display.height = (<p2.Box>boxShape).height * this.factor;
display.anchorOffsetX = display.width / 2; //anchoroffset跟P2一样设置在中心点
display.anchorOffsetY = display.height / 2;
boxBody.displays = [display]; //修改刚刚创建的刚体的display属性
this.addChild(display);//将DisplayObject添加到舞台
绘制图形一般用于展示demo,Bitmap是静态贴图,Sprite较为灵活。实际使用看需求选择。
5,实时更新物理世界
两个步骤:
- 调用World.Step方法更新物理计算
- 将egret.DisplayObject与P2 Body的位置和角度映射
this.addEventListener(egret.Event.ENTER_FRAME, this.onEnterFrame, this);
private onEnterFrame(e: egret.Event) {
this.world.step(1 / 60);//游戏帧数的倒数
var stageHeight: number = egret.MainContext.instance.stage.stageHeight;
var l = this.world.bodies.length;
for (var i: number = 0; i < l; i++) { //遍历所有刚体
var boxBody: p2.Body = this.world.bodies[i];
var box: egret.DisplayObject = boxBody.displays[0];
if (box) {
//位置与角度的转换
box.x = boxBody.position[0] * this.factor;
box.y = stageHeight - boxBody.position[1] * this.factor;
box.rotation = 360 - (boxBody.angle + boxBody.shapes[0].angle) * 180 / Math.PI;
//此代码仅用作展示p2.Body.SLEEPING状态的物体
if (boxBody.sleepState == p2.Body.SLEEPING) {
box.alpha = 0.5;
}
else {
box.alpha = 1;
}
}
}
}
---------------------------------------------------------------------------------------------------------------
参考资料:
P2官方Github P2作者的Github,包括demo,示例,下载,等等
P2引擎中文维基 P2引擎官方Wiki的中文翻译
P2物理引擎部分文档 中文文档,介绍了World, Body, Sharp等类的属性、方法、事件,以及其他的一些开发小结
Egret Game Library Egret官方的第三方类库,包括Tiled, 键盘鼠标操作,EUI的扩展,P2引擎等等等
拉登的P2物理引擎教程 拉登的个人网站关于Egret + P2的系列文章,历史久远但介绍翔实。另有Egret + Box2D的资料。
Egret中使用P2物理引擎 比较详细的P2属性、方法等的介绍