2013年9月30日 星期一

Box2D 物理引擎

http://book.51cto.com/art/201110/296248.htm
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类的构造函数:
  1. World(AABB world AABB,Vec2 gravity,boolean doSleep) 
World类只有这一种构造方式,它的三个参数的含义如下:
第一个参数:AABB类的实例,AABB表示一个物理模拟世界的范围;
第二个参数:Vec2实例,一个二维世界向量类,在Box2D中的最常用的一种数据类型;在这里表示物理世界的重力方向;
第三个参数:布尔值,表示在物理世界中,如果静止不动的物体是否对其进行休眠。如果设置其值为"true",则表示当物理世界开始进行模拟时,在这个物理世界中静止没有运行的物体都将进行休眠,除非物体被施加了力的作用或者与其他物体发生碰撞之后会被唤醒;如果设置其值设置为"false",那么物理世界中的所有物体不管是否静止都会一直进行物理模拟。
创建一个物理世界代码如下:
  1. AABB aabb = new AABB();// 实例化物理世界的范围对象  
  2. aabb.lowerBound.set(-100, -100);// 设置物理世界范围的左上角坐标  
  3. aabb.upperBound.set(100, 100);// 设置物理世界范围的右下角坐标  
  4. Vec2 gravity = new Vec2(0, 10);// 实例化物理世界重力向量对象   
  5. World world = new World(aabb, gravity, true);// 实例化物理世界对象  
以上代码中有两点需要注意:
(1)aabb设置物理世界范围传入的参数,不要理解成像素!在Box2d的物理世界中,被认为是现实生活中的"米(m)"单位。
(2)设置物理世界的重力向量(gravity),其两个参数在这里分别表示物理世界中的X轴与Y轴方向上的重力数值,其值的"+"" "号在这里表示X与Y轴的重力方向,X轴正值表示向右,Y轴正值表示向下;因为是模拟真实世界,所以这里的X重力向量设置为零,Y轴方向设置为现实生活中的重力值:10(可以理解为10N)。
刚才的一段代码就已经创建了一个物理世界,但只是定义了物理世界,并没有开始进行物理模拟,所以还需要world设置物理模拟:
  1. world.step(float timeStep, int iterations); 
此函数表示让物理世界开始进行物理模拟,其两个参数含义如下:
第一个参数:表示(时间步)物理世界模拟的频率 ;
第二个参数:表示(迭代值)迭代值越大模拟越精确,但性能越低。
这里要注意以下几点:
①因为物理世界模拟具有持续性,所以应该将设置放在线程中,不断的让物理世界进行模拟。
②时间步:应该与游戏的刷新率相同,否则物理世界模拟将不同步。
③迭代值:可以理解为在单次时间步中进行遍历模拟运算数据的次数。
④在Box2D中最常使用的单位是float浮点数类型,作者刚接触Box2D时,在定义物理世界模拟频率时,写成了以下错误的形式:
  1. float timeStep = 1 / 60; 
这样写导致物理世界的物体永远不运动,其原因就是"1/60"的值永远是零!所以正确书写形式应该是:
  1. float timeStep = 1f / 60f; 
到此一个物理世界真正的创建出来并且进行模拟了,但是因为物理世界中并没有放置任何的物体,所以运行项目在视觉中将看不到任何的效果,下面的章节中将开始在物理世界中创建物体。
作者推荐物理模拟的频率一般设为每秒60帧,迭代设为10,具体设置根据应用和设备性能情况而定。
在后续创建物体和关节的章节中,很多代码需要传入以"米"作为单位的数值,所以为了便于转换,可以定义一个成员变量:
  1. final float RATE = 30
在Box2D的物理世界中,为了更加贴切的模拟现实,部分函数参数不再使用"像素"而是用"米"表示。为了能将模拟的物理世界映射到手机屏幕中,定义一个屏幕与现实世界的比例变量"RATE",这个比例值作者推荐设置为30,因为一般不会修改此值,所以可以定义为final常量类型。
还要注意定义此变量不要用int类型,应该用float类型。否则会发生如同timeStep类似的状况,此值可能会比预计的小。例如:
  1. int RATE = 30  ->  45/RATE = 1 
  2. float RATE = 30 -> 43/RATE = 1.5 

7.5 创建矩形物体
在学习创建矩形物体之前,首先要理解几个基本概念:
大家可以想象一下现实生活中的物体基本上都是由圆形与多边形组成,所以在Box2d物理世界中存在两种2D图形,一种是圆形,一种是多边形。
在Box2D中物体的创建都应该设置质量、摩擦力与恢复力这三个基本属性。
Box2D属于工厂模式,也就是说在Box2D的物理世界中创建物体,都是由工厂(World)生成的,而不是new出来的。
World创建一个物体的步骤则分为以下三步:
首先创建物体皮肤。
然后创建物体刚体。
最后通过皮肤与刚体信息去创建一个物体。
简单熟悉了Box2D创建一个物体的步骤后,下面来创建一个多边形,添加在物理世界中,项目对应的源代码为"7-5(在物理世界中添加多边形)"。
在项目中添加一个createPolygon函数,代码如下:
  1. public Body createPolygon(float x, float y,  float width, float height,  
  2.     boolean isStatic) {  
  3.     // ---创建多边形皮肤  
  4.     PolygonDef pd = new PolygonDef(); // 实例一个多边形的皮肤  
  5.     if (isStatic) {  
  6.         pd.density = 0; // 设置多边形为静态  
  7.     } else {  
  8.         pd.density = 1; // 设置多边形的质量  
  9.     }  
  10.     pd.friction = 0.8f; // 设置多边形的摩擦力  
  11.     pd.restitution = 0.3f; // 设置多边形的恢复力  
  12.     // 设置多边形快捷成盒子(矩形)  
  13.     // 两个参数为多边形宽高的一半  
  14.     pd.setAsBox(width / 2 / RATE, height / 2 / RATE);  
  15.     // ---创建刚体  
  16.     BodyDef bd = new BodyDef(); // 实例一个刚体对象  
  17.     // 设置刚体的坐标  
  18.     bd.position.set((x + width / 2) / RATE, (y +  height / 2) / RATE);   // ---创建Body(物体)  
  19.     Body body = world.createBody(bd); // 物理世界创建物体  
  20.     body.createShape(pd); // 为Body添加皮肤  
  21.     body.setMassFromShapes(); // 将整个物体计算打包  
  22.     return body;  
  23. }   
以上代码中,各个属性的含义说明如下:
质量(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在模拟的物理世界中的运动数据传给绘制的图形,绘制的图形就会沿着提供的运动轨迹来运行,也就相当于图形拥有了重力。
  1. Vec2 position = body.getPosition();  
  2. polygonX = position.x * RATE-polygonWidth/2;  
  3. polygonY = position.y * RATE-polygonHeight/2
通过Body 的getPosition函数得到Body的中心点的位置,然后通过比例转换成像素,再分别赋值给屏幕绘制的图形X,Y坐标。
还要注意一点:此方法获取的是物体的中心点坐标,所以还需要将其X坐标减去物体的宽的一半,Y坐标减去物体的高的一半,得到其左上角坐标。当然如果图形是以中心点进行绘制的话,就可以获取中心点直接将坐标传递给绘制的图形即可。
因为物理世界是在不断的模拟,所以也要不断去获取物体在物理世界的最新坐标,然后传递给绘制的图形,图形就会按照物体在物理世界中的运动轨迹去"运动"。


Android 游戏引擎libgdx之Box2D 案例实践——弹球

http://www.cnblogs.com/dwinter/archive/2013/01/14/libgdx-box2d-02.html

此作者都是基于IOS的cocos2D开发,本人参考并且转化为libgdx的cocos2d使用。
libgdx目前还主要用于Android开发,想学IOS的亲,请看上述作者的博客。
欢迎转载,但请加上来自www.cnblogs.com/dwinter或者dwintergame.com

一、直接切入主题,经过上篇的HelloBox2D,大家应该有点概念了。在Box2D的世界里,无非要干以下几件事情:
  • 首先创建一个body定义结构体,用以指定body的初始属性,比如位置或者速度。
  • 创建好body结构体后,你就可以调用world对象来创建一个body对象了。
  • 然后,你为body对象定义一个shape,用以指定你想要仿真的物体的几何形状
  • 接着创建一个fixture定义,同时设置之前创建好的shape为fixture的一个属性,并且设置其它的属性,比如质量或者摩擦力。本篇重点
  • 最后,你可以使用body对象来创建fixture对象,通过传入一个fixture的定义结构就可以了。
  • 你可以往单个body对象里面添加很多个fixture对象。这个功能在你创建特别复杂的对象的时候非常有用。比如自行车,你可能要创建2个轮子,车身等等,这些fixture可以用关节连接起来。

二、开始写代码吧,和上次框架一样,这里只说明一下主要的舞台类
world = new World(new Vector2(0, -30f), true);
首先创建一个物理世界,这里将加速度设置为30,使得下落效果明显点,你也可以自己调整看效果。
复制代码
        BodyDef bd = new BodyDef();
        bd.position.set(2, 2);
        Body groundBody = world.createBody(bd);        
        EdgeShape edge = new EdgeShape();
        edge.set(new Vector2(0f, 0f), new Vector2(48f, 0f));
        FixtureDef boxShapeDef = new FixtureDef();
        boxShapeDef.shape = edge;
        groundBody.createFixture(boxShapeDef);
复制代码
再来定义和生成一个图形——边,大家可以参照上述的步骤。
复制代码
        BodyDef ballBodyDef = new BodyDef();
        ballBodyDef.type = BodyType.DynamicBody;
        ballBodyDef.position.set(24f, 60f);
        body = world.createBody(ballBodyDef);
        body.setUserData("ball");
        CircleShape circle = new CircleShape();
        circle.setRadius(2f);
        FixtureDef ballShapeDef = new FixtureDef();
        ballShapeDef.shape = circle;
        ballShapeDef.density = 1.0f;
        ballShapeDef.friction = 0.2f;
        ballShapeDef.restitution = 0.8f;
        body.createFixture(ballShapeDef);
复制代码
再来一个图形——圆,大家可以看到红色部分为关键,球弹不弹就这里了。
  • Density 就是单位体积的质量(密度)。因此,一个对象的密度越大,那么它就有更多的质量,当然就会越难以移动。
  • Friction 就是摩擦力。它的范围是0~1.0, 0意味着没有摩擦,1代表最大摩擦,几乎移不动的摩擦。
  • Restitution 回复力。它的范围也是0到1.0. 0意味着对象碰撞之后不会反弹,1意味着是完全弹性碰撞,会以同样的速度反弹。

三、libgdx与Box2D的结合,定义一个纹理
private Texture ball;
绑定一张图片
ball = new Texture(Gdx.files.internal("Ball.jpg"));
我们不再使用测试绘制器,而是使用常用的SpriteBatch,将纹理与物体同步刷新绘制
复制代码
    @Override
    public void draw() {
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        world.step(Gdx.app.getGraphics().getDeltaTime(), 6, 2);
        batch.begin();
        batch.draw(ball, body.getPosition().x, body.getPosition().y, 5f, 5f);
        batch.end();
        super.draw();
    }
复制代码

四、程序效果
作品发布:dwintergame.com 
个人开发盈利方案: 传送门 
本文版权归作者D.Winter和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 

Android 2d物理引擎Box2d的使用示例

http://www.iteye.com/topic/439056

Box2d是一个很出名的2d物理引擎,大家可以google之。Box2d有C++,flash和Java等版本。android可以直接使用java版本的Jbox2d,但因为Jbox2d的图形渲染是使用processing库来实现的,所以,在android中使用Jbox2d的时候,图形渲染的工作就只能自己来写了。因为网上关于box2d的资料真的非常的少,特别是关于图形绘制方面,所以,虽然程序写得不是很好,还是贴上来了,先看截图:




程序很简单:蓝色的是地面,从上面掉下两个绿球和一个红色的方块,它们之间的碰撞都由box2d引擎自己来完成。
(这程序运行起来有点卡,我 也正在解决这个问题,有朋友知道答案的也请分享一下啦!)
代码如下:

package com.ray.test;

import org.jbox2d.collision.AABB;
import org.jbox2d.collision.CircleDef;
import org.jbox2d.collision.PolygonDef;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.World;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

public class MyBox2d extends Activity {
private final static int RATE = 10;//屏幕到现实世界的比例 10px:1m;
private AABB worldAABB;//创建 一个管理碰撞的世界
private World world;
private float timeStep = 1/60;//模拟的的频率
private int iterations = 10;//迭代越大,模拟约精确,但性能越低
private Body body,body2,body3;
private MyView myView;
private Handler mHandler;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams. FLAG_FULLSCREEN ,
        WindowManager.LayoutParams. FLAG_FULLSCREEN);
        
        worldAABB = new AABB();
         
       //上下界,以屏幕的左上方为 原点,如果创建的刚体到达屏幕的边缘的话,会停止模拟
        worldAABB.lowerBound.set(-100.0f,-100.0f);
        worldAABB.upperBound.set(100.0f, 100.0f);//注意这里使用的是现实世界的单位
        
        Vec2 gravity = new Vec2(0.0f,10.0f);
        boolean doSleep = true;
        

        world = new World(worldAABB, gravity, doSleep);//创建世界
        
        createBox(160, 470, 160, 10, true);     
        createBox1(160, 150, 160, 10, false);
        
        createCircle(160, 100, 10);
        createCircle1(150, 60, 10);
        timeStep = 1.0f/60.0f;
        iterations = 10;
        
        myView = new MyView(this);
        setContentView(myView);
        mHandler = new Handler();
        mHandler.post(update);
    }
    
    private Runnable update = new Runnable() {
        public void run() {
            world.step(timeStep, iterations);//开始模拟
            Vec2 position = body.getPosition();
            Vec2 position1 = body2.getPosition();
            Vec2 position2 = body3.getPosition();
            myView.x=position.x*RATE;
            myView.y=position.y*RATE;
           
            myView.x1=position1.x*RATE; 
            myView.y1=position1.y*RATE;
           
            myView.x2=position2.x*RATE;
            myView.y2=position2.y*RATE;
                myView.update();
                mHandler.postDelayed(update, (long)timeStep*1000);
        }
    };
    
    public void createBox(float x,float y,float half_width,float half_height,
                     boolean isStatic){ 
    PolygonDef shape = new PolygonDef();
    if(isStatic){shape.density = 0;}
    else{shape.density = 2.0f;}
    shape.friction = 0.8f;
    shape.restitution = 0.3f;
    shape.setAsBox(half_width/RATE, half_height/RATE);
   
    BodyDef bodyDef = new BodyDef();
    bodyDef.position.set(x/RATE, y/RATE);
    Body body1= world.createBody(bodyDef);
    body1.createShape(shape);
    body1.setMassFromShapes();
    }
    
    public void createCircle(float x,float y,float radius){
    CircleDef shape = new CircleDef();
    shape.density = 7;
    shape.friction = 0.2f;
    shape.radius = radius/RATE;
   
    BodyDef bodyDef = new BodyDef();
    bodyDef.position.set(x/RATE, y/RATE);
    body2 = world.createBody(bodyDef);
    body2.createShape(shape);
    body2.setMassFromShapes();
    }
    
    public void createCircle1(float x,float y,float radius){
    CircleDef shape = new CircleDef();
    shape.density = 7;
    shape.friction = 0.2f;
    shape.radius = radius/RATE;
   
    BodyDef bodyDef = new BodyDef();
    bodyDef.position.set(x/RATE, y/RATE);
    body3 = world.createBody(bodyDef);
    body3.createShape(shape);
    body3.setMassFromShapes();
    }
    
    public void createBox1(float x,float y,float half_width,float half_height,
                   boolean isStatic){ 
    PolygonDef shape = new PolygonDef();
    if(isStatic){shape.density = 0;}
    else{shape.density = 2.0f;}
    shape.friction = 0.3f;
    shape.setAsBox(half_width/RATE, half_height/RATE);
   
    BodyDef bodyDef = new BodyDef();
    bodyDef.position.set(x/RATE, y/RATE);
    body= world.createBody(bodyDef);
    body.createShape(shape);
    body.setMassFromShapes();
    }
    
    class MyView extends View{
    Canvas canvas;
    public float x=160,y=150;
    public float x1=160,y1=100;
    public float x2=150,y2=60;
public MyView(Context context) {
super(context);
}

public void drawBox(float x,float y){
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
canvas.drawRect(x-160, y-10, x+160, y+10, mPaint);
}
public void drawGround(){
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 460, 320, 480, mPaint);
}
public void drawCircle(float x1,float y1){
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.GREEN);
canvas.drawCircle(x1, y1, 10, mPaint);
}
public void update(){
postInvalidate();
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
drawGround();
drawBox(x, y);
drawCircle(x1, y1);
drawCircle(x2, y2);
}
   
    }
}