使用p2制作跳跃游戏

基本用法

目前 Egret 已经支持p2物理系统,p2是一套使用 JavaScript 写的2D刚体物理引擎。其中包括碰撞检测,接触,摩擦等等。下面我们通过一个简单的示例来学习该物理引擎的基本用法。

在物理世界中加入两个常见形状的物理实体并运转。

创建刚体

所谓刚体,就是在外力作用下,物体的形状和大小(尺寸)保持不变,而且内部各部分相对位置保持恒定(没有形变)的理想物理模型。 在物理引擎中简言之,就是一个独立的物体,可以有相对于其他物体的位移、旋转,并且可以跟它们产生碰撞。 在egret中创建刚体很简单:

//创建刚体
const body: p2.Body = new p2.Body();

创建形状

实际显示可能有多种不同的形状,p2引擎已经准备了丰富的类型,以适应各种不同的需要。 我们举两个简单的例子,一个是矩形,一个是平面:

//创建宽4单位、高2单位的矩形形状
const shpRect: p2.Shape = new p2.Rectangle(4, 2);
//创建平面形状
const shpPlane: p2.Plane = new p2.Plane();

其中的单位是在物理引擎中设置的单位,跟实际的像素不是一个概念。具体的用法demo有提及,如果希望深入理解请学习底部的进阶详细教程。

为形状加入物理特性

每一个形状具有显示特性,要在物理引擎中计算其物理特性,那必须要将每一个独立形状绑定到刚体。 接下来分别为刚才创建的形状绑定:

//绑定矩形到刚体
const bodyRect: p2.Body = new p2.Body();
bodyRect.addShape(shpRect);
//绑定平面到刚体
const bodyPlane: p2.Body = new p2.Body();
bodyPlane.addShape(shpPlane);

将刚体加入物理世界

所有需要物理引擎计算的显示对象,我们先绑定到刚体,然后需要添加到一个物理世界或物理空间,即一个World实例中。World是以刚体作为单位来进行各种物理模拟及计算的,如下所示:

//创建world对象
const world: p2.World = new p2.World();
//将之前创建的刚体加入world
world.addBody(bodyRect);
world.addBody(bodyPlane);

使world动起来

都准备好了,然后就按特定的频率运行物理世界,用world.step即可:

//添加帧事件侦听
egret.Ticker.getInstance().register(function (dt) {
//使世界时间向后运动
world.step(dt / 1000);
}, this);

优化性能的小技巧

我们可以设置刚体一定时间后自动进入睡眠状态以提高性能,一行搞定:

world.sleepMode = p2.World.BODY_SLEEPING;

与egret中显示对象的结合

本教程主要说明如何创建特定形状,赋予物理特性,并模拟在一个物理世界中。之后我们需要创建对应的显示对象,然后在 world.step 执行之后,取出相应的刚体,将显示对象的坐标属性设置为刚体的位置信息。具体实现方式可以访问下面的 egret 演示示例源码查看。

Egret与p2物理引擎使用相关传送门

Egret使用p2物理引擎示例源码

p2物理引擎github首页

p2物理引擎API文档

前文只讲述了基本用法,但是实际使用远远不止这么简单!物理引擎推出后,不少开发者都很关注,但使用物理引擎涉及很多概念。新手很难从demo学习物理引擎的具体用法,本文将详细介绍如何做一个简单的跳跃游戏。

物理对象的创建和使用

首先创建一个物理世界:

const world: p2.World;
//创建Body,并加入world中:
const 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
const boxBody: p2.Body = world.bodies[i];
//从p2.Body中取出显示对象
const 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过程,直接传入参数才有效。

const rectShape: p2.Rectangle = new p2.Rectangle(4, 2);

如果换成:

const 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 function createGround(world: p2.World, container: egret.DisplayObjectContainer, id: number, vx: number, w: number, h: number, resid: string, x0: number, y0: number): p2.Body {
const 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);
const p2rect: p2.Rectangle = new p2.Rectangle(city.phys.P2Space.extentP2(w), city.phys.P2Space.extentP2(h));
p2body.addShape(p2rect);
const 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, 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 function createPlayer(world: p2.World, container: egret.DisplayObjectContainer, id: number, resid: string, xLanding: number, yLanding: number): p2.Body {
const p2body: p2.Body = new p2.Body({ mass: 1,
fixedRotation: true,
type: p2.Body.DYNAMIC
});
p2body.id = id;
world.addBody(p2body);
/// 依照图元尺寸
const display: egret.DisplayObject = city.utils.DispUtil.createBitmapByName(resid);
display.anchorX = display.anchorY = .5;
/// 对应p2形状的宽高要根据玩家计算
const 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 function 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 function 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,这是根据玩家当前状态来判断是否可以起跳,因为我们不能允许玩家在不接触跳板或地面的情况下再次起跳!其中的判断涉及的物理引擎原理较为复杂,本篇教程就先不细讲了。

用本示例所涉及的内容,已经可以做一些简单的物理游戏了。然而物理引擎的威力,我们只发掘了一小部分。很有更强大的功能等待我们去探索!