http://book.51cto.com/art/201110/296249.htm
http://book.51cto.com/art/201110/296250.htm
http://book.51cto.com/art/201110/296251.htm
http://book.51cto.com/art/201110/296252.htm
http://book.51cto.com/art/201110/296253.htm
第7章 Box2D 物理引擎
7.1 Box2D概述
Box2D是一款用于2D游戏的物理引擎。在Box2D物理世界里,创建出的每个物体都更接近于真实世界中的物体,让游戏中的物体运动起来更加真实可信,让游戏世界看起来更具交互性。
Android平台常见的十几款游戏引擎中,例如:Rokon、AndEngine、libgdx等物理引擎都封装了Box2D物理引擎,可见Box2D在物理引擎中占据了多重要的位置。
Box2D在很多平台都有对应的版本:Flash版本、Iphone版本、Java版本(JBox2D)等等,在本书中开发Android语言采用的是Java,所以这里介绍的对应Box2D平台也是Java平台,称为JBox2D,对应的版本为JBox2d 2.0。
7.2 将Box2D类库导入Android项目中
添加Box2D类库到Android项目详细步骤如下:
新建项目"HelloBox2d"(创建Android项目与之前创建项目步骤无差异)。
然后在项目里添加一个"lib"目录用于存放Box2D类库(".jar")。
右键项目"HelloBox2d",选中"New"→"Folder"选项,然后出现"New Folder"窗口,如图7-1所示。
图7-1 New Folder |
图7-1所示的窗口中,在"Folder name"文本框中填写目录名,这个目录名可以自定义,一般导入外部类库目录都使用lib与libs来命名,然后单击"Finish"按钮完成目录的创建。
此时只是将Box2D提供的类库放入Android项目中,但是并没有添加到项目的类库中,所以还需要将Box2D包添加到项目类库中!
右击项目"HelloBox2d"选中"Properties "→"Java Build Path"→"Libraries"→"Add JARs...",选中"HelloBox2d"项目下lib目录下的Jbox2d类库的jar包,最后一直单击"OK"按钮返回即可,如图7-2、图7-3所示。
图7-2 项目配置 |
图7-3 项目中添加Box2d类库 |
到此,在Android项目中添加JBox2D物理引擎类库的步骤就结束了,现在就开始进入Box2D引擎的物理世界吧!
7.3 物理世界与手机屏幕坐标系之间的关系
本节来讲解一下物理世界与手机屏幕坐标系之间的关系。假设创建一个200米的物理世界,然后观察其物理世界与手机屏幕之间的坐标系关系,如图7-4所示。
图7-4 物理世界与手机屏幕坐标系关系图 |
从图7-4中可以很清晰的看出,手机屏幕的左上角(0,0)坐标,正是物理世界的中心点坐标;手机屏幕绘制图形时,一般默认以左上角作为锚点!而在Box2d的物理世界中,一个新的Body(物体)等被创建出来之后,默认以其质心(可以近似为中心点)作为锚点;如图7-5所示,是"在屏幕上绘制一张图片,并且在物理世界中添加一个物体"的位置关系图。
图7-5 默认放置的位置对比图 |
除此之外,Box2D为了使物体与关节等更加贴切的模拟现实,在Box2D引擎中使用的长度单位是"米(m)",所以Box2D引擎中的一些方法的长度参数不再是以像素为单位,而是需要转换成"米";反之,从Box2D引擎函数返回值中得到的长度值也是以"米"做单位的,使用其值前需要将其转换为像素,然后再使用。
关于米与像素之间的换算关系,其实是通过自定义一个现实与屏幕的比例关系进行换算的,后续章节会有讲解。
7.4 创建Box2D物理世界
World(jbox2d.dynamics.World)类就是Box2D引擎中的物理世界。不管是Body(物体)还是Joint(关节)都必须放在Box2D这个物理世界中,因为物理世界也是有范围的,一旦物体和关节不在世界范围内,它们将不会进行物理模拟。
下面就来创建一个物理世界,范例项目对应的源代码为"7-4(Box2d物理世界)"。
首先看一下World类的构造函数:
- World(AABB world AABB,Vec2 gravity,boolean doSleep)
World类只有这一种构造方式,它的三个参数的含义如下:
第一个参数:AABB类的实例,AABB表示一个物理模拟世界的范围;
第二个参数:Vec2实例,一个二维世界向量类,在Box2D中的最常用的一种数据类型;在这里表示物理世界的重力方向;
第三个参数:布尔值,表示在物理世界中,如果静止不动的物体是否对其进行休眠。如果设置其值为"true",则表示当物理世界开始进行模拟时,在这个物理世界中静止没有运行的物体都将进行休眠,除非物体被施加了力的作用或者与其他物体发生碰撞之后会被唤醒;如果设置其值设置为"false",那么物理世界中的所有物体不管是否静止都会一直进行物理模拟。
创建一个物理世界代码如下:
- AABB aabb = new AABB();// 实例化物理世界的范围对象
- aabb.lowerBound.set(-100, -100);// 设置物理世界范围的左上角坐标
- aabb.upperBound.set(100, 100);// 设置物理世界范围的右下角坐标
- Vec2 gravity = new Vec2(0, 10);// 实例化物理世界重力向量对象
- World world = new World(aabb, gravity, true);// 实例化物理世界对象
以上代码中有两点需要注意:
(1)aabb设置物理世界范围传入的参数,不要理解成像素!在Box2d的物理世界中,被认为是现实生活中的"米(m)"单位。
(2)设置物理世界的重力向量(gravity),其两个参数在这里分别表示物理世界中的X轴与Y轴方向上的重力数值,其值的"+"" "号在这里表示X与Y轴的重力方向,X轴正值表示向右,Y轴正值表示向下;因为是模拟真实世界,所以这里的X重力向量设置为零,Y轴方向设置为现实生活中的重力值:10(可以理解为10N)。
刚才的一段代码就已经创建了一个物理世界,但只是定义了物理世界,并没有开始进行物理模拟,所以还需要world设置物理模拟:
- world.step(float timeStep, int iterations);
此函数表示让物理世界开始进行物理模拟,其两个参数含义如下:
第一个参数:表示(时间步)物理世界模拟的频率 ;
第二个参数:表示(迭代值)迭代值越大模拟越精确,但性能越低。
这里要注意以下几点:
①因为物理世界模拟具有持续性,所以应该将设置放在线程中,不断的让物理世界进行模拟。
②时间步:应该与游戏的刷新率相同,否则物理世界模拟将不同步。
③迭代值:可以理解为在单次时间步中进行遍历模拟运算数据的次数。
④在Box2D中最常使用的单位是float浮点数类型,作者刚接触Box2D时,在定义物理世界模拟频率时,写成了以下错误的形式:
- float timeStep = 1 / 60;
这样写导致物理世界的物体永远不运动,其原因就是"1/60"的值永远是零!所以正确书写形式应该是:
- float timeStep = 1f / 60f;
到此一个物理世界真正的创建出来并且进行模拟了,但是因为物理世界中并没有放置任何的物体,所以运行项目在视觉中将看不到任何的效果,下面的章节中将开始在物理世界中创建物体。
作者推荐物理模拟的频率一般设为每秒60帧,迭代设为10,具体设置根据应用和设备性能情况而定。
在后续创建物体和关节的章节中,很多代码需要传入以"米"作为单位的数值,所以为了便于转换,可以定义一个成员变量:
- final float RATE = 30;
在Box2D的物理世界中,为了更加贴切的模拟现实,部分函数参数不再使用"像素"而是用"米"表示。为了能将模拟的物理世界映射到手机屏幕中,定义一个屏幕与现实世界的比例变量"RATE",这个比例值作者推荐设置为30,因为一般不会修改此值,所以可以定义为final常量类型。
还要注意定义此变量不要用int类型,应该用float类型。否则会发生如同timeStep类似的状况,此值可能会比预计的小。例如:
- int RATE = 30 -> 45/RATE = 1
- float RATE = 30 -> 43/RATE = 1.5
7.5 创建矩形物体
在学习创建矩形物体之前,首先要理解几个基本概念:
大家可以想象一下现实生活中的物体基本上都是由圆形与多边形组成,所以在Box2d物理世界中存在两种2D图形,一种是圆形,一种是多边形。
在Box2D中物体的创建都应该设置质量、摩擦力与恢复力这三个基本属性。
Box2D属于工厂模式,也就是说在Box2D的物理世界中创建物体,都是由工厂(World)生成的,而不是new出来的。
World创建一个物体的步骤则分为以下三步:
首先创建物体皮肤。
然后创建物体刚体。
最后通过皮肤与刚体信息去创建一个物体。
简单熟悉了Box2D创建一个物体的步骤后,下面来创建一个多边形,添加在物理世界中,项目对应的源代码为"7-5(在物理世界中添加多边形)"。
在项目中添加一个createPolygon函数,代码如下:
- public Body createPolygon(float x, float y, float width, float height,
- boolean isStatic) {
- // ---创建多边形皮肤
- PolygonDef pd = new PolygonDef(); // 实例一个多边形的皮肤
- if (isStatic) {
- pd.density = 0; // 设置多边形为静态
- } else {
- pd.density = 1; // 设置多边形的质量
- }
- pd.friction = 0.8f; // 设置多边形的摩擦力
- pd.restitution = 0.3f; // 设置多边形的恢复力
- // 设置多边形快捷成盒子(矩形)
- // 两个参数为多边形宽高的一半
- pd.setAsBox(width / 2 / RATE, height / 2 / RATE);
- // ---创建刚体
- BodyDef bd = new BodyDef(); // 实例一个刚体对象
- // 设置刚体的坐标
- bd.position.set((x + width / 2) / RATE, (y + height / 2) / RATE); // ---创建Body(物体)
- Body body = world.createBody(bd); // 物理世界创建物体
- body.createShape(pd); // 为Body添加皮肤
- body.setMassFromShapes(); // 将整个物体计算打包
- return body;
- }
以上代码中,各个属性的含义说明如下:
质量(density):当物体质量设置为0时,此物体视为"静态物体";所谓"静态物体"表示不需要运动的物体;比如现实生活中的山、房门等这些没有外力不会发生运动的物体则认为是静态不运动的。
摩擦力(friction):取值通常设置在0~1之间,0意味着没有摩擦,1会产生最强摩擦。
恢复力(restitution):取值也通常设置在 0 ~ 1 之间,0 表示物体没有恢复力,1表示物体拥有最大恢复力。
刚体设置坐标的时候,需要传入现实生活中的"米"做为参数单位,所以这里除以比例"RATE",将像素单位转换为"米"。
BodyDef.position.set(float x, float y)方法,设置Body相对于物理世界的坐标。
在此之前已经介绍过,物理世界中创建出的物体默认放置的位置是以物理中心点为锚点,那么为了让其与手机屏幕绘制图形位置重合,需要将其物理的X位置加上其宽的一半,其物体的Y位置加上其高的一半,这样就相当于将其Body的锚点设置成了左上角,如图7-6所示。
(点击查看大图)图7-6 物体与图形坐标重合 |
项目运行效果如图7-7所示。
(点击查看大图)图7-7 矩形Body项目运行效果图 |
7.6 让物体在屏幕中展现
从7.4节"创建Box2D物理世界"到7.5节"创建矩形物体"这两小节中可以看出,貌似这一切都与屏幕绘制没有任何的关系。
是的,屏幕绘制的图形与Box2D无关!Box2D引擎只负责提供物理世界的模拟数据。换言之,如果想让屏幕中显示一个附有重力的图形,那么则需要一个重力物体运动轨迹的数据,而Box2D提供的Body正是一个拥有重力的物体。通过将Body在模拟的物理世界中的运动数据传给绘制的图形,绘制的图形就会沿着提供的运动轨迹来运行,也就相当于图形拥有了重力。
- Vec2 position = body.getPosition();
- polygonX = position.x * RATE-polygonWidth/2;
- polygonY = position.y * RATE-polygonHeight/2;
通过Body 的getPosition函数得到Body的中心点的位置,然后通过比例转换成像素,再分别赋值给屏幕绘制的图形X,Y坐标。
还要注意一点:此方法获取的是物体的中心点坐标,所以还需要将其X坐标减去物体的宽的一半,Y坐标减去物体的高的一半,得到其左上角坐标。当然如果图形是以中心点进行绘制的话,就可以获取中心点直接将坐标传递给绘制的图形即可。
因为物理世界是在不断的模拟,所以也要不断去获取物体在物理世界的最新坐标,然后传递给绘制的图形,图形就会按照物体在物理世界中的运动轨迹去"运动"。