目前 Egret 已经支持p2物理系统,p2是一套使用 JavaScript 写的2D刚体物理引擎。其中包括碰撞检测,接触,摩擦等等。下面我们通过一个简单的示例来学习该物理引擎的基本用法。
在物理世界中加入两个常见形状的物理实体并运转。
所谓刚体,就是在外力作用下,物体的形状和大小(尺寸)保持不变,而且内部各部分相对位置保持恒定(没有形变)的理想物理模型。 在物理引擎中简言之,就是一个独立的物体,可以有相对于其他物体的位移、旋转,并且可以跟它们产生碰撞。 在egret中创建刚体很简单:
//创建刚体
var body:p2.Body = new p2.Body();
实际显示可能有多种不同的形状,p2引擎已经准备了丰富的类型,以适应各种不同的需要。 我们举两个简单的例子,一个是矩形,一个是平面:
//创建宽4单位、高2单位的矩形形状
var shpRect:p2.Shape = new p2.Rectangle( 4, 2 );
//创建平面形状
var shpPlane:p2.Plane = new p2.Plane();
其中的单位是在物理引擎中设置的单位,跟实际的像素不是一个概念。具体的用法demo有提及,如果希望深入理解请学习底部的进阶详细教程。
每一个形状具有显示特性,要在物理引擎中计算其物理特性,那必须要将每一个独立形状绑定到刚体。 接下来分别为刚才创建的形状绑定:
//绑定矩形到刚体
var bodyRect:p2.Body = new p2.Body();
bodyRect.addShape( shpRect );
//绑定平面到刚体
var bodyPlane:p2.Body = new p2.Body();
bodyPlane.addShape( shpPlane );
所有需要物理引擎计算的显示对象,我们先绑定到刚体,然后需要添加到一个物理世界或物理空间,即一个World实例中。World是以刚体作为单位来进行各种物理模拟及计算的,如下所示:
//创建world对象
var world:p2.World = new p2.World();
//将之前创建的刚体加入world
world.addBody( bodyRect );
world.addBody( bodyPlane );
都准备好了,然后就按特定的频率运行物理世界,用world.step即可:
//添加帧事件侦听
egret.Ticker.getInstance().register(function (dt) {
//使世界时间向后运动
world.step(dt / 1000);
}, this);
我们可以设置刚体一定时间后自动进入睡眠状态以提高性能,一行搞定:
world.sleepMode = p2.World.BODY_SLEEPING;
本教程主要说明如何创建特定形状,赋予物理特性,并模拟在一个物理世界中。之后我们需要创建对应的显示对象,然后在 world.step 执行之后,取出相应的刚体,将显示对象的坐标属性设置为刚体的位置信息。具体实现方式可以访问下面的 egret 演示示例源码查看。
前文只讲述了基本用法,但是实际使用远远不止这么简单!物理引擎推出后,不少开发者都很关注,但使用物理引擎涉及很多概念。新手很难从demo学习物理引擎的具体用法,本文将详细介绍如何做一个简单的跳跃游戏。
首先创建一个物理世界:
var world:p2.World;
创建Body,并加入world中:
var p2body:p2.Body = new p2.Body(
{ mass: 1
, fixedRotation: true
, type:p2.Body.DYNAMIC
}
);
world.addBody(body);
创建Body中的参数含义,我们将在后文中结合游戏示例代码说明。
为p2.Body设置显示内容:
boxBody.displays = [display];
display是一个egret.DisplayObject实例,该语句就是用来创建p2物理世界对象和实际显示对象的关联的。物理世界发生的所有的变化,都需要设置其关联的显示对象以同步其状态。
从world中取出某个p2.Body:
var boxBody:p2.Body = world.bodies[i];
从p2.Body中取出显示对象:
var box:egret.DisplayObject = boxBody.displays[0];
首先要明了坐标系,p2的坐标系不单是原点和方向跟传统的Egret坐标系不一样,连单位也是有差别的,长度单位有一个比例,每一个涉及位置的计算,都需要按该比例进行换算得出。设该比例因数为factor = 50;
Egret显示对象egret.DisplayObject的宽度需要进行换算,即乘以该因数。 每个运行推进函数中同步显示对象与刚体的位置关系:
disp.x = body.position[0] * factor;
disp.y = stageHeight - body.position[1] * factor;
注意:高度因为坐标系不一致需要修正! 角度需要同步:
disp.rotation = 360 - body.angle * 180 / Math.PI;
对于某一个物体对象,在p2中,宽度高度是在Shape中设定的;位置和旋转却是由绑定该形状的Body设定。
这样的转换,在游戏实现过程无疑会增加开发复杂度,为此我专门为Egret开发p2物理引擎创建了一个管理类city.phys.P2Space。其中有不少服务方法是用于快速的转换p2和显示空间的尺寸以及坐标的。
注意:设置Shape尺寸的时机
创建Shape过程,直接传入参数才有效。
var rectShape:p2.Rectangle = new p2.Rectangle( 4, 2 );
如果换成:
var rectShape:p2.Rectangle = new p2.Rectangle;
rectShape.width = 4;
rectShape.height = 2;
rectShape.updateArea();
也是无法生效的!
在p2物理世界运转时,所有的涉及位置或角度的运算,我们都通过设置p2的Body来实现。 至于显示,每个Body都有一个显示列表。 在物理系统每次推进时,会遍历p2物理世界所有的Body,对每个Body所绑定的显示对象进行同步。 p2所有的坐标都是以中心为准的。因此,为了减少坐标转换计算量,应当设置显示映射的注册点为中心:
dispRect.anchorX = dispRect.anchorY = .5;
city.phys.P2Space中的syncDisplay就是专门用来同步p2物理世界所对应显示的。
屏幕 480*800。
最下边有一个地面。然后两边有墙面。
分多层的浮动跳板。
每层跳板的速度一样,颜色一样。
不同层的速度不一样,颜色也不一样。
从下到上速度逐渐加快,增加难度,即增加速率。
跳板的高度均为20。 宽度根据层数具体定。
每层跳板的速度方向与下一层相反。
操作,只需要侦听TOUCH_BEGIN,条件允许则跳起。
为了简化操作,我们不增加UI元素,设计为触摸地面左侧玩家会向左移动,地面右侧玩家会向右移动。 触摸地面以上的部分,分为左中右三部分,触摸左侧会以一个向左的角度斜跳,触摸右侧会以一个向右的角度斜跳。触摸中间部分,则会向正上方跳起。
这几种形式都比较相近,我们都用一个函数创建:
private createGround( world:p2.World, container:egret.DisplayObjectContainer
, id:number, vx:number, w:number, h:number, resid:string, x0:number, y0:number ):p2.Body{
var p2body:p2.Body = new p2.Body(
{ mass:1
, fixedRotation: true
, position: city.phys.P2Space.getP2Pos( x0 + w / 2, y0 + h / 2 )
, type: vx == 0 ? p2.Body.STATIC : p2.Body.KINEMATIC
, velocity:[ vx, 0 ]
}
);
p2body.id = id;
console.log( "位置:", p2body.position );
world.addBody( p2body );
var p2rect:p2.Rectangle = new p2.Rectangle(city.phys.P2Space.extentP2( w ),city.phys.P2Space.extentP2( h ) );
p2body.addShape( p2rect );
var bitmap:egret.Bitmap = city.utils.DispUtil.createBitmapByName( resid );
bitmap.width = w;
bitmap.height = h;
bitmap.anchorX = bitmap.anchorY = .5;
p2body.displays = [ bitmap ];
container.addChild( bitmap );
return p2body;
}
我们游戏的设计,所有物体均不需要转动,因此创建Body时,设置fixedRotation为true。 然后position设置我们用了P2Space的坐标转换服务方法getP2Pos,为了方便设置坐标,我们都使用左上角标准,因此,传入显示空间坐标时,用宽度和高度进行修正,使其在物理空间对应中心点坐标。
接下来是type,我们约定,传入的vx为0,表示静止不动,地面和墙面均应传入vx为0。 p2中Body的类型分为三种,这里我们用到两种。地面和墙面不需要移动,并且不会对力和碰撞做出反应,这正是p2.Body.STATIC的特征;浮动跳板则均为p2.Body.KINEMATIC,这种类型会根据velocity属性进行运动,也不会对力和碰撞做出反应。
接下来时velocity,为简化本游戏中的跳板运动,均设计为仅在x方向运动。
然后创建p2中的Shape,传入参数时使用了P2Space的服务函数extentP2,将显示空间尺寸,转换为p2空间尺寸。
使用这个函数,我们很轻松的可以创建3个跳板和地面及墙面:
/// 创建浮动跳板
this._vcGroundsFloating = [
this.createGround( this._pw, this, 4, 0.6, 120, 20, "rects.rect-" + "0", this._p2FloatingLimitLeft, 600 ) /// -->,this.createGround( this._pw, this, 5, -0.8, 90, 20, "rects.rect-" + "8", this._p2FloatingLimitRight, 450 ) /// <--
,this.createGround( this._pw, this, 6, 1.2, 80, 20, "rects.rect-" + "10", this._p2FloatingLimitLeft, 300 ) /// -->];
/// 创建 墙面 底部高50, 两边墙面间距50
this._vcGroundsFixed = [
this.createGround( this._pw, this, 1, 0, 640, 50, "rects.rect-" + "9", 0, 750 ) /// 地面
,this.createGround( this._pw, this, 2, 0, 50, 750, "rects.rect-" + "1", 0, 0 ) /// 左墙面
,this.createGround( this._pw, this, 3, 0, 50, 750, "rects.rect-" + "1", 430, 0 ) /// 右墙面
];
三个浮动跳板的方向相邻相反,并且宽度越往上越小。
city.phys.P2Space.syncDisplay( this._vcGroundsFixed[0] );
city.phys.P2Space.syncDisplay( this._vcGroundsFixed[1] );
city.phys.P2Space.syncDisplay( this._vcGroundsFixed[2] );
创建完毕之后,我们用P2Space.syncDisplay立即对地面和墙面进行显示同步:
这是因为,在游戏运行过程中,这3个块不需要任何运动。
玩家的形状,也是一个p2.Rectangle,创建玩家的过程跟上述诸面基本类似:
private createPlayer( world:p2.World, container:egret.DisplayObjectContainer, id:number, resid:string, xLanding:number, yLanding:number ):p2.Body{
var p2body:p2.Body = new p2.Body(
{ mass: 1
, fixedRotation: true
, type:p2.Body.DYNAMIC
}
);
p2body.id = id;
world.addBody(p2body);
/// 依照图元尺寸
var display:egret.DisplayObject =
city.utils.DispUtil.createBitmapByName( resid );
display.anchorX = display.anchorY = .5;
/// 对应p2形状的宽高要根据玩家计算
var p2rect:p2.Rectangle = new p2.Rectangle(
city.phys.P2Space.extentP2((<egret.Bitmap>display).texture.textureWidth),
city.phys.P2Space.extentP2((<egret.Bitmap>display).texture.textureHeight)
);
p2body.addShape( p2rect );
p2body.position =city.phys.P2Space.getP2Pos( xLanding, yLanding - (<egret.Bitmap>display).texture.textureHeight / 2 );
this._p2posYPlayerLanding = p2body.position[1];
p2body.displays = [display];
container.addChild(display);
return p2body;
}
玩家创建时,使用了p2中Body的第三种类型:p2.Body.DYNAMIC。
我们事先准备好了玩家的图元素材,为了保持其原始大小显示,我们根据图元纹理的宽高来设置对应p2形状的宽高。 设置玩家初始坐标时,我们参数传入的是水平中心,垂直底部的值,而传入的值需要在中心位置,因此y坐标要减去图元纹理高度的一半。我们传入的y是地面的坐标,这样初始呈现时,玩家正好站在地面上。
由于我们事先进行了充分的准备工作(特别是用了city.phys.P2Space.syncDisplay),运行物理世界的代码相当的简练:
private run( dt ):void{
this._pw.step( this.WORLD_STEP_DT );
/// 玩家
city.phys.P2Space.syncDisplay( this._pbPlayer );
/// 浮动板
if( this._vcGroundsFloating[0].position[0] > this._p2FloatingLimitRight ){
this._vcGroundsFloating[0].position[0] = this._p2FloatingLimitLeft;
}
city.phys.P2Space.syncDisplay( this._vcGroundsFloating[0] );
if( this._vcGroundsFloating[1].position[0] < this._p2FloatingLimitLeft ){
this._vcGroundsFloating[1].position[0] = this._p2FloatingLimitRight;
}
city.phys.P2Space.syncDisplay( this._vcGroundsFloating[1] );
if( this._vcGroundsFloating[2].position[0] > this._p2FloatingLimitRight ){
this._vcGroundsFloating[2].position[0] = this._p2FloatingLimitLeft;
}
city.phys.P2Space.syncDisplay( this._vcGroundsFloating[2] );
}
玩家只需要同步显示!
剩下的就是对浮动跳板的循环控制,都是在其超越边界后,重置到出发边界位置。
为了简化,我们只使用触摸来控制,在不同的区域来进行不同的控制,具体控制方法已经在设计游戏一节说明了。 代码也没有任何累赘:
private touchProcess( e:egret.TouchEvent ):void{
if( e.stageY > 750 ) { /// 地面重置
if( e.stageX < 240 ){
this._pbPlayer.velocity[0] = - this.PLAYER_VX;
}else{
this._pbPlayer.velocity[0] = this.PLAYER_VX;
}
}else{
if( city.phys.P2Space.checkIfCanJump( this._pw, this._pbPlayer ) ){
this._pbPlayer.velocity[1] = this.PLAYER_VY;
this._pbPlayer.velocity[0] = this.PLAYER_VX * ( e.stageX - 240 )/ 200 ;
}else{
city.utils.DevUtil.trace( "player no jump:", this._pbPlayer.velocity[1] );
}
}
}
需要说明的就是city.phys.P2Space.checkIfCanJump,这是根据玩家当前状态来判断是否可以起跳,因为我们不能允许玩家在不接触跳板或地面的情况下再次起跳!其中的判断涉及的物理引擎原理较为复杂,本篇教程就先不细讲了。
用本示例所涉及的内容,已经可以做一些简单的物理游戏了。然而物理引擎的威力,我们只发掘了一小部分。很有更强大的功能等待我们去探索!