Box2d - 用javascript构建物理世界

曾经火遍整个地球的手游《愤怒的小鸟》,其问世离不开一个物理引擎---- 那就是 Box2d

1.Box2d的由来

Box2D是一个模拟2d空间刚体的仿真库。 最早只是Erin Catto(这家伙好像现在在暴雪工作.)在GDC大会上展示的一个Demo, Box2D发展到现在已经有诸多影分身,常见的是 Flash (as)版 , 还有Python版, Java版, C++版, JS版......

就是上面这个家伙.. 逛了一下他的 twitter ,里面全是守望先锋的广告....

Box2D的版本虽然很多,但其API却几乎没有什么变化,为了快速上手,下面我使用大家比较熟悉的 JS 版来做讲解...

GitHub地址 : https://github.com/hecht-software/box2dweb

建议初学者不要用哪个 Emscripten 的box2d ,最然他的star比较多. (Emscripten是一种可以将c++ 通过 WebIDL bindings 编译成 JS 的技术 )

2.让我们从最基础的几个概念开始:

目前,小伙伴们只需要宏观感性的理解这几个概念即可,暂时不必关心代码的问题.

1、空间(world):

世间万物,所有物体,无不占据空间,无论你是一颗星球,还是一颗沙粒,都需要在这个宇宙中占据一定的空间,这便引出了Box2D的第一个概念 叫:“空间” (在 box2d 中称之为 world),这个“空间”概念是最基本的,他是所有物体的容器,所以既然我们要模拟一个真实的物理世界, 那么最先要做的事情就是开辟出一个足够大的空间,然后才能再把你想要的东西塞进去.

2、刚体(body):

有了我们自己的创造的 “空间” 这还远远不够,一个空空如也的世界 并不是我们想要的,我们要制造足够多的“物体” 这样,我们创造的“world”才会精彩, 这个物体的概念在 Box2d中 称为“刚体”顾名思义,刚体是无法发生形变的物体 ,box2d的刚体是十分多样且复杂的, 但简单来说, 你只需要把它放在你创造空间里,他就能感受到重力,具备物体最基本的特性。

3、材质(fixture):

材质的概念十分简单,因为他和我们日常生活中的“材料”的概念十分相似,材质需要和刚体 相互依存,比如你创造了一个球体,如果没有材质的概念,我们就很难模拟他的物理状态, 我们所知道的,仅仅是它是个圆的,所以我们要为他创建材质,比如是一个铁球? 那么一定很沉。 是一个乒乓球? 那就很轻, 球的表面是光滑的还是粗糙的?等等, 材质+刚体 二者共同描述了一个我们真正需要物体。

4、形状(shape):

形状是材质的一个属性,用来描述刚体的碰撞模型. 他们主要有 b2ChainShape, b2EdgeShape, b2PolygonShape, b2CircleShape. 形状之间可以自由组合 形成更复杂的形状.

总之: 刚体=形状+材质.

例如: 铅球=圆形 + 铁 ,书=方形 + 纸

3.动手创造一个简单的世界

我们来看看如何用代码创建一个box2d世界:

//创建一个带有重力的世界(box2d中所有的刚体都必须放在“世界”这个大容器中)
var gravity = new b2Vec2(0, 9.8);  //定义重力向量,x轴0 y轴向下,加速度是9.8.
var doSleep = true; //当物体停止运动以后,停止对对象的物理模拟。
world = new b2World(gravity, doSleep);//new 一个 名字叫 world 的世界变量

我们通过 world = new b2World(gravity, doSleep); 获得了一个world对象,gravity为这个世界的重力, 这个对象一般来讲只会创建一次,(平行宇宙的概念那是 爱因斯坦才能理解的, 对于我们凡人,一个世界已经足够复杂了 :)

doSleep 这个东西 是说物体一旦停止运动了,便停止对他的模拟,以提高运行效率。

至此 终于有了一个属于你自己的世界,内牛满面吧。。。。。

但是我们并不想要一个空空的世界~ ,下面让我们加点料。

4.向空间中添加刚体.

这里小伙伴们需要注意了.

  1. Box2D中的计量单位是,而不是像素.要转换成像素,需要进行换算.(一般定义为 一米=36px)
  2. Box2D中的长度是半长,意思是width/2 (很奇怪的设定,不是吗?)


定义刚体:

刚体的定义需要使用 b2BodyDef

var bodydef = new b2BodyDef(); //new 一个刚体对象
bodydef.type = b2Body.b2_staticBody; //定义刚体对象为 静态对象. 即不收外力作用的对象,如地面,墙壁.
//bodydef.type = b2Body.b2_dynamicBody; //定义刚体对象为 动态对象. 会受到外力的作用.

定义材质:

材质的定义需要使用 b2FixtureDef .

var fixDef = new b2FixtureDef();//创建一个 b2FixtureDef 对象.
fixDef.density = 1.0; // desity 密度,如果密度为0或者null,该物体则为一个静止对象
fixDef.friction =  0.9; //摩擦力(0~1)
fixDef.restitution = 1.0;// 弹性(0~1)

定义形状:

形状的定义需要使用一种合适的模型,可能是 b2ChainShape, b2EdgeShape, b2PolygonShape, b2CircleShape 中的任意一种.

//我们定义一个圆形,所以使用了b2CircleShape.
fixDef.shape = new b2CircleShape();

// 定义圆的半径是 50px  window.PTM_RATIO 是米转换像素的因子.
fixDef.shape.SetRadius(50 / window.PTM_RATIO); 

让我们把 刚体,材质,形状组合以来..

var bodydef = new b2BodyDef();
bodydef.type = b2Body.b2_dynamicBody;

var fixDef = new b2FixtureDef();
fixDef.density = 1.0; // desity 密度,如果密度为0或者null,该物体则为一个静止对象
fixDef.friction =  0.9; //摩擦力(0~1)
fixDef.restitution = 1.0;// 弹性(0~1)

fixDef.shape = new b2CircleShape();
fixDef.shape.SetRadius(50 / window.PTM_RATIO);
bodydef.position.Set(250 / window.PTM_RATIO, 0 / window.PTM_RATIO);

var body = world.CreateBody(bodydef); //注意 bodydef 并不是 new 出来的.
body.CreateFixture(fixDef);

我先解释一下 bodyDef 和 body 的关系:

简单来说 body是刚体的实例, bodyDef是生产body的工厂,有了bodyDef 就可以使用world.CreateBody 生产出一大批body来。 然后再使用 body.CreateFixture(fixDef); 来为某个body套上材质.

那么 PTM_RATIO 这个是什么?

这货简单来说。是一个常量,一般定为 30-36 之间的一个数字。用来指示 1米 = 多少像素 , 这个有何意义呢,看图,下面慢慢来说。

比如同样的飞机,在地面上感觉庞然大物

但是在天空中感觉像一只小虫

造成这种结果,其实是透视规则在起作用.

言归正传,在我们的世界里,虽然是一个2d世界,但是你一样需要一个观察这个世界的“距离”,就好比你要欣赏一幅画,你不能离画太远,这样看不清了, 也不能离画太近,这样看不全. 由于 box2d 模拟物理状态时 有大量的浮点数计算, 所以合理的定义一个“距离” 是十分有必要的,即可以提高运行速度,又模拟的十分真实,所以这个常量 一般定为 36。

5.帧的概念.

简单来说,帧,其实是在每一个固定时间间隔里 周期性的把数据转换成图像的过程

在javascript里最简单的方法就是用requestAnimationFrame来实现帧的功能

var last = 0;
function animate(time){
    var timeStep = time-last;
    last = time;
    //velocityInterations 是对速率的纠正程度, 越高计算量越大.
    var velocityInterations = 10;

    //velocityInterations 是对位置的纠正程度, 越高计算量越大.
    var positionIterations = 10;

    world.Step(timeStep, velocityInterations, positionIterations);
    world.ClearForces();
    world.DrawDebugData(); //绘制绑定到debug视图上渲染
    requestAnimationFrame(animate);
}
animate();

砖家说,人的肉眼只能分辨在1/60秒内的变化,也就是说 肉眼的“刷新率是 1/60s”,他们称之为“视觉暂留现象” 那我们也就把刷新率定在这个速度上

Box2dweb 已经为我们提供了一个供开发预览的 debugview 这个 debugview是什么东西呢? 其实就是在没有真正贴图以前,让开发人员可以看到自己定义的骨架。

var debug = new b2DebugDraw();
debug.SetSprite(document.getElementById("canvas").getContext("2d"));
debug.SetLineThickness(1);
debug.SetFillAlpha(0.9);
debug.SetAlpha(1);
debug.SetDrawScale(PTM_RATIO);
debug.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debug);

其实就是使用一个canvas将所有的body以一个最简单的方式画在上面。(在前期没有贴图的时候,这些模型十分有用)

ok 把上面的知识串起来, 一个最简单的 Boxd2d 版本的 HelloWord 完成!

    //创建一个带有重力的世界(box2d中所有的物体都必须被包括在一种叫“世界”的大容器中)
    var gravity = new b2Vec2(0, 9.8); 
    var doSleep = true; //当物体停止运动以后,停止对对象的物理模拟。
    world = new b2World(gravity, doSleep);
    //b2_dynamicBody说明他是一个 动态物体,就是说他不是一个像地面一样一动不动的东西,当你给他一个力,它就会动。
    bodyDef.type = b2Body.b2_dynamicBody;  
    //定义了他在世界中的位置,PTM_RATIO 这个个东西是缩放比,后面会解释。
    bodyDef.position.Set(canvaswidth/PTM_RATIO/2,canvasheight/PTM_RATIO/2);  
    var body = world.CreateBody(bodyDef)  //world.CreateBody 物体有这个方法来创建

    var fixDef = new b2FixtureDef; //这货其实和 bodyDef一样 是材质的 “制造机”
    fixDef.density = .5;  //density 定义质量
    fixDef.friction = 0.4; //friction 定义表面的摩擦力
    fixDef.restitution = 0.8; //定义弹性
    fixDef.shape = new b2CircleShape(10/drawScale); // 定义了 形状 : b2CircleShape 圆形
    body.CreateFixture(fixDef) //把我们创操出来的 材质 绑定到一个 物体上。

    var fixDef = new b2FixtureDef;
    fixDef.density = .5;
    fixDef.friction = 0.4;
    fixDef.restitution = 0.8;
    var bodyDef = new b2BodyDef;
    bodyDef.type = b2Body.b2_staticBody;
    fixDef.shape = new b2PolygonShape;
    fixDef.shape.SetAsBox(canvaswidth/PTM_RATIO/ 2, 5/PTM_RATIO);
    bodyDef.position.Set(canvaswidth/PTM_RATIO/ 2,(canvasheight-5)/PTM_RATIO);
    world.CreateBody(bodyDef).CreateFixture(fixDef);

    var debug = new b2DebugDraw();
    debug.SetSprite(document.getElementById("canvas").getContext("2d"));
    debug.SetLineThickness(1);
    debug.SetFillAlpha(0.9);
    debug.SetAlpha(1);
    debug.SetDrawScale(PTM_RATIO);
    debug.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
    world.SetDebugDraw(debug);

    setInterval(update, (1000 / 60));
    function update() {
    var timeStep = 1 / 30;
    var velocityInterations = 10;
     var positionIterations = 10;

    world.Step(timeStep, velocityInterations, positionIterations);
    world.ClearForces();
     world.DrawDebugData(); //绘制绑定到debug视图上渲染 请看下文介绍

};
留言:

称呼:*

邮件:

网站:

内容: