使用新的物理模拟引擎加速强化学习
Posted 雨夜的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用新的物理模拟引擎加速强化学习相关的知识,希望对你有一定的参考价值。
强化学习(RL) 是一种流行的教学机器人导航和操纵物理世界的方法,其本身可以简化并表示为刚体之间的交互1(即,当对它们施加力时不会变形的固体物理对象)。为了便于在实际时间内收集训练数据,RL 通常利用模拟,其中任意数量的复杂对象的近似值由许多由关节连接并由执行器提供动力的刚体组成。但这带来了一个挑战:RL 代理通常需要数百万到数十亿的模拟帧才能精通简单的任务,例如步行、使用工具或组装玩具积木。
虽然通过回收模拟帧在提高训练效率方面取得了进展,但一些 RL 工具通过将模拟帧的生成分布到许多模拟器来回避这个问题。这些分布式模拟平台产生了令人印象深刻的结果,训练速度非常快,但它们必须在具有数千个 CPU 或 GPU 的计算集群上运行,而大多数研究人员无法访问这些集群。
在“ Brax - 用于大规模刚体仿真的可微分物理引擎”中,我们展示了一种新的物理仿真引擎,它与仅具有单个 TPU 或 GPU 的大型计算集群的性能相匹配。该引擎旨在在单个加速器上高效运行数千个并行物理模拟以及机器学习 (ML) 算法,并在互连加速器的 pod 中无缝扩展数百万个模拟。我们已经开源了引擎以及参考 RL 算法和模拟环境,这些都可以通过Colab访问。使用这个新平台,我们展示了比传统工作站设置快 100-1000 倍的培训。
物理仿真引擎设计机会刚体物理用于视频游戏、机器人、分子动力学、生物力学、图形和动画以及其他领域。为了对此类系统进行准确建模,模拟器集成了来自重力、电机驱动、关节约束、物体碰撞等的力,以模拟物理系统随时间的运动。
仔细研究当今大多数物理模拟引擎的设计方式,有一些提高效率的巨大机会。正如我们上面提到的,典型的机器人学习管道将单个学习器置于紧密反馈中,同时进行许多模拟,但在分析此架构后,人们发现:
这种布局带来了巨大的延迟瓶颈。由于数据必须通过数据中心内的网络传输,因此学习者必须等待 10,000 多纳秒才能从模拟器中获取经验。如果这种体验已经与学习者的神经网络在同一设备上,延迟将降至 <1 纳秒。
训练代理所需的计算(一个模拟步骤,然后更新代理的神经网络)被打包数据(即,在引擎内编组数据,然后转换为诸如protobuf 之类的有线格式,然后进入TCP缓冲区,然后在学习者端撤消所有这些步骤)。
每个模拟器中发生的计算非常相似,但并不完全相同。
针对这些观察结果,Brax 的设计使其物理计算在其数千个并行环境中的每一个环境中完全相同,方法是确保模拟没有分支(即,模拟“ if”逻辑因此而发散环境状态)。物理引擎中分支的一个示例是在球和墙壁之间施加接触力:将根据球是否接触墙壁执行不同的代码路径。也就是说,如果球接触墙壁,则将执行用于模拟球从墙壁反弹的单独代码。Brax 混合使用以下三种策略来避免分支:
用连续函数替换离散分支逻辑,例如使用带符号距离函数近似球壁接触力。这种方法会带来最大的效率收益。
在 JAX 的实时编译期间评估分支。许多基于环境静态属性的分支,例如两个对象是否可能发生碰撞,可以在模拟时间之前进行评估。
在模拟过程中运行分支的两侧,然后只选择所需的结果。因为这样会执行一些最终没有用到的代码,所以与上面相比,浪费了一些操作。
一旦保证计算完全一致,就可以降低整个训练架构的复杂性,以便在单个 TPU 或 GPU 上执行。这样做可以消除跨机器通信的计算开销和延迟。在实践中,这些变化将可比工作负载的训练成本降低了 100 到 1000 倍。
Brax 环境环境是微小的封装世界,定义了 RL 代理要学习的任务。环境不仅包含模拟世界的手段,还包含功能,例如如何观察世界以及在该世界中定义目标。
近年来出现了一些标准基准环境,用于测试新的 RL 算法并使用研究科学家普遍理解的指标评估这些算法的影响。Brax 包括来自流行的OpenAI 健身房的 四个此类即用型环境:Ant、HalfCheetah、Humanoid和Reacher。
Brax 还包括三种新颖的环境:物体的灵巧操纵(机器人技术中的一个流行挑战)、广义运动(一种代理前往放置在其周围任何位置的目标)以及工业机器人手臂的模拟。
性能基准分析 Brax 性能的第一步是测量它模拟大批量环境的速度,因为这是要克服的关键瓶颈,以便学习者消耗足够的经验来快速学习。
下面的这两个图显示了 Brax 可以产生多少物理步骤(环境状态的更新),因为它的任务是并行模拟越来越多的环境。左图显示,Brax 将每秒步数与并行环境的数量成线性比例,仅在 10,000 个环境时遇到内存带宽瓶颈,这不仅足以训练单个智能体,还适用于训练整个群体代理商。右图显示了两件事:第一,Brax 不仅在 TPU 上表现良好,而且在高端 GPU 上也表现良好(参见V100和P100曲线),第二,通过利用 JAX 的设备并行原语, Brax 在多个设备之间无缝扩展,每秒达到数亿个物理步骤(参见TPUv3 8x8曲线,即 64 个TPUv3芯片通过高速互连直接相互连接)。
分析 Brax 性能的另一种方法是衡量其对在单个工作站上运行强化学习实验所需时间的影响。在这里,我们将 Brax 训练流行的Ant基准环境与其OpenAI 对应物进行比较,后者由MuJoCo 物理引擎提供支持。
在下图中,蓝线代表标准工作站设置,其中学习器在 GPU 上运行,模拟器在 CPU 上运行。我们看到训练蚂蚁以合理的熟练度(y 轴上的 4000 分)运行所需的时间从蓝线的大约 3 小时下降到在加速器硬件上使用 Brax 的大约 10 秒。有趣的是,即使仅在 CPU 上(灰线),Brax 的执行速度也快了一个数量级,这得益于处于同一进程中的学习器和模拟器。
Physics Fidelity设计一个与现实世界的行为相匹配的模拟器是一个已知的难题,这项工作没有解决。尽管如此,将 Brax 与参考模拟器进行比较以确保它产生的输出至少同样有效是有用的。在这种情况下,我们再次将 Brax 与MuJoCo进行比较,后者因其模拟质量而广受好评。我们希望看到,在其他条件相同的情况下,无论是在 MuJoCo 还是 Brax 中训练,策略都具有相似的奖励轨迹。
这些曲线表明,随着两个模拟器的奖励以大致相同的速度增长,两个引擎计算物理的复杂性或解决难度相当。由于两条曲线都以大致相同的奖励达到顶峰,因此我们相信相同的一般物理限制适用于在任一模拟中尽其所能操作的代理。
我们还可以测量 Brax 保持线性动量、角动量和能量守恒的能力。
MuJoCo 的作者 首先提出了这种物理模拟质量的衡量标准,以了解模拟如何在计算越来越大的时间步长时偏离轨道。在这里,Brax 的表现与其邻居相似。
结论我们邀请研究人员通过在Brax Training Colab 中训练他们自己的策略来对 Brax 的物理保真度进行更定性的测量。学习到的轨迹与 OpenAI Gym 中看到的轨迹非常相似。
Egret中使用P2物理引擎
游戏中的对象按照物理规律移动,体现重力、引力、反作用力、加速度等物体特性,实现自由落体、摇摆运动、抛物线运动,以及物理碰撞现象的模拟。用于模拟物理碰撞、物理运动的引擎称为物理引擎。
来自瑞典斯德哥尔摩大学的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
以上是关于使用新的物理模拟引擎加速强化学习的主要内容,如果未能解决你的问题,请参考以下文章