Box2d – 用 JavaScript构建物理世界

agx300 8年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/7b9a6dbf3f4aa6f05392e31c351403d0.jpg"></p>    <p>曾经火遍整个地球的手游《愤怒的小鸟》,其问世离不开一个物理引擎—-那就是 Box2D。</p>    <h3><strong>1. Box2d 的由来</strong></h3>    <p>Box2D 是一个模拟 2d 空间刚体的仿真库。最早只是 Erin Catto (这家伙好像现在在暴雪工作)在 GDC 大会上展示的一个 Demo,</p>    <p>Box2D 发展到现在已经有诸多影分身,常见的是 Flash (as) 版 ,还有 Python 版, Java 版 C++ 版 JS 版……</p>    <p>Box2D 的版本虽然很多,但其 API 却几乎没有什么变化,为了快速上手,下面我使用大家比较熟悉的 JS 版来做讲解。</p>    <p>建议初学者不要用哪个 Emscripten 的 Box2D,最然他的star比较多。(Emscripten 是一种可以将 C++ 通过 WebIDL bindings 编译成 JS的技术)</p>    <h3><strong>2. 让我们从最基础的几个概念开始</strong></h3>    <p>目前,小伙伴们只需要宏观感性的理解这几个概念即可,暂时不必关心代码的问题。</p>    <p><strong>1、空间( world )</strong></p>    <p>世间万物,所有物体,无不占据空间,无论你是一颗星球,还是一颗沙粒,都需要在这个宇宙中占据一定的空间,这便引出了Box2D的第一个概念 叫:“空间”(在 Box2D 中称之为 world ),这个“空间”概念是最基本的,他是所有物体的容器,所以既然我们要模拟一个真实的物理世界,那么最先要做的事情就是开辟出一个足够大的空间,然后才能再把你想要的东西塞进去。</p>    <p><strong>2、刚体(body)</strong></p>    <p>有了我们自己的创造的 “空间” 这还远远不够,一个空空如也的世界 并不是我们想要的,我们要制造足够多的“物体” 这样,我们创造的“world”才会精彩,这个物体的概念在 Box2D中 称为“刚体”顾名思义,刚体是无法发生形变的物体 ,Box2D的刚体是十分多样且复杂的, 但简单来说,你只需要把它放在你创造空间里,他就能感受到重力,具备物体最基本的特性。</p>    <p><strong>3、材质(fixture)</strong></p>    <p>材质的概念十分简单,因为他和我们日常生活中的“材料”的概念十分相似,材质需要和刚体 相互依存,比如你创造了一个球体,如果没有材质的概念,我们就很难模拟他的物理状态,我们所知道的,仅仅是它是个圆的,所以我们要为他创建材质,比如是一个铁球? 那么一定很沉。 是一个乒乓球? 那就很轻, 球的表面是光滑的还是粗糙的?等等,材质+刚体 二者共同描述了一个我们真正需要物体。</p>    <p><strong>4、形状(shape)</strong></p>    <p>形状是材质的一个属性,用来描述刚体的碰撞模型。 他们主要有 b2ChainShape , b2EdgeShape , b2PolygonShape , b2CircleShape 形状之间可以自由组合形成更复杂的形状。</p>    <p>总之: 刚体=形状+材质。</p>    <p>例如: 铅球=圆形 + 铁 ,书=方形 + 纸</p>    <h3><strong>3. 动手创造一个简单的世界</strong></h3>    <p>我们来看看如何用代码创建一个 Box2D 世界:</p>    <pre>  <code class="language-javascript">//创建一个带有重力的世界(box2d中所有的刚体都必须放在“世界”这个大容器中)  var gravity = new b2Vec2(0, 9.8);  //定义重力向量,x轴0 y轴向下,加速度是9.8.  var doSleep = true; //当物体停止运动以后,停止对对象的物理模拟。  world = new b2World(gravity, doSleep);//new 一个 名字叫 world 的世界变量  </code></pre>    <p>我们通过 world = new b2World(gravity, doSleep); 获得了一个 world 对象,gravity 为这个世界的重力, 这个对象一般来讲只会创建一次,(平行宇宙的概念那是 爱因斯坦才能理解的,对于我们凡人,一个世界已经足够复杂了 :)</p>    <p>doSleep 这个东西 是说物体一旦停止运动了,便停止对他的模拟,以提高运行效率。</p>    <p>至此 终于有了一个属于你自己的世界,内牛满面吧… …</p>    <p>但是我们并不想要一个空空的世界~ ,下面让我们加点料。</p>    <h3><strong>4. 向空间中添加刚体</strong></h3>    <p>这里小伙伴们需要注意了:</p>    <ol>     <li>Box2D 中的计量单位是 <strong>米</strong> ,而不是 <strong>像素</strong> .要转换成像素,需要进行换算.(一般定义为 一米=36px)</li>     <li>Box2D 中的长度是 <strong>半长</strong> ,意思是width/2 (很奇怪的设定,不是吗?)</li>    </ol>    <p><strong>定义刚体:</strong></p>    <p>刚体的定义需要使用 b2BodyDef</p>    <pre>  <code class="language-javascript">var bodydef = new b2BodyDef(); //new 一个刚体对象  bodydef.type = b2Body.b2_staticBody; //定义刚体对象为 静态对象. 即不收外力作用的对象,如地面,墙壁.  //bodydef.type = b2Body.b2_dynamicBody; //定义刚体对象为 动态对象. 会受到外力的作用.  </code></pre>    <p><strong>定义材质:</strong></p>    <p>材质的定义需要使用 b2FixtureDef .</p>    <pre>  <code class="language-javascript">var fixDef = new b2FixtureDef();//创建一个 b2FixtureDef 对象.  fixDef.density = 1.0; // desity 密度,如果密度为0或者null,该物体则为一个静止对象  fixDef.friction =  0.9; //摩擦力(0~1)  fixDef.restitution = 1.0;// 弹性(0~1)  </code></pre>    <p><strong>定义形状:</strong></p>    <p>形状的定义需要使用一种合适的模型,可能是 b2ChainShape , b2EdgeShape , b2PolygonShape , b2CircleShape 中的任意一种。</p>    <pre>  <code class="language-javascript">//我们定义一个圆形,所以使用了b2CircleShape.  fixDef.shape = new b2CircleShape();     // 定义圆的半径是 50px  window.PTM_RATIO 是米转换像素的因子.  fixDef.shape.SetRadius(50 / window.PTM_RATIO);    </code></pre>    <p><strong>让我们把 刚体,材质,形状组合以来</strong></p>    <pre>  <code class="language-javascript">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);  </code></pre>    <p><strong>我先解释一下 bodyDef 和 body 的关系:</strong></p>    <p>简单来说 body 是刚体的实例, bodyDef 是生产 body 的工厂,有了bodyDef  就可以使用 world.CreateBody  生产出一大批body来。</p>    <p>然后再使用 body.CreateFixture(fixDef); 来为某个 body 套上材质。</p>    <p>那么 PTM_RATIO 这个是什么?</p>    <p>这货简单来说。是一个常量,一般定为 30-36 之间的一个数字。用来指示 1米 = 多少像素 , 这个有何意义呢,看图,下面慢慢来说。</p>    <p>比如同样的飞机,在地面上感觉庞然大物</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/305f7c11ae58479f324b77509bd19193.jpg"></p>    <p>但是在天空中感觉像一只小虫</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/477a22e86fe27b7d1ecdffeaf3320e4d.jpg"></p>    <p>造成这种结果,其实是 透视 规则在起作用。</p>    <p>言归正传,在我们的世界里,虽然是一个 2d 世界,但是你一样需要一个观察这个世界的“距离”,就好比你要欣赏一幅画,你不能离画太远,这样看不清了,也不能离画太近,这样看不全。 由于 Box2D 模拟物理状态时 有大量的浮点数计算,所以合理的定义一个“距离” 是十分有必要的,即可以提高运行速度,又模拟的十分真实,所以这个常量 一般定为 36。</p>    <h3><strong>5. 帧的概念.</strong></h3>    <p>简单来说,帧,其实是在每一个固定时间间隔里 周期性的把数据转换成图像的过程</p>    <p>在 JavaScript 里最简单的方法就是用 requestAnimationFrame 来实现帧的功能</p>    <pre>  <code class="language-javascript">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();  </code></pre>    <p>砖家说,人的肉眼只能分辨在1/60秒内的变化,也就是说 肉眼的“刷新率是 1/60s”,他们称之为“视觉暂留现象” 那我们也就把刷新率定在这个速度上。</p>    <p>Box2dweb 已经为我们提供了一个供开发预览的 debugview 这个 debugview 是什么东西呢? 其实就是在没有真正贴图以前,让开发人员可以看到自己定义的骨架。</p>    <pre>  <code class="language-javascript">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);  </code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/dba0b3b88df43c689ade09c9efe60f56.png"></p>    <p>其实就是使用一个 canvas 将所有的 body 以一个最简单的方式画在上面。(在前期没有贴图的时候,这些模型十分有用)</p>    <p>ok 把上面的知识串起来, 一个最简单的 Boxd2d 版本的 HelloWord 完成!</p>    <pre>  <code class="language-javascript">//创建一个带有重力的世界(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视图上渲染 请看下文介绍  </code></pre>    <h3><strong>6. 使用 userData 为模型绑定皮肤</strong></h3>    <p>现在我们已经会使用 Box2D 来创建基本刚体了,但这些颜色单一的方块是肯定无法满足产品质量要求的,下一步我们还需要为它贴上好看的皮肤。</p>    <p>由于 Box2D 最早是用 c++ 写的,在刚体对象上专门留有一个 *userData  指针来记录用户自己绑定的信息,这个指针被移植到 JS  后成为了userData属性。</p>    <p>为刚体绑定皮肤的逻辑基本是这样的:</p>    <ol>     <li>获取皮肤对象,比如在 web 中可能是一个 <img> 标签。</li>     <li>将刚体的 userData  属性指向这个皮肤对象。</li>     <li>在每一帧中,遍历所有刚体, 获取其 position angle 等信息。</li>     <li>将上述信息输出到 userData 指向的对象上。 (比如将刚体的 x , y,angle 等属性转化为 img 标签的 top left 等 css 属性)</li>    </ol>    <p>下面看一个例子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/25cbd4ff93089efbb6550b9bad94bf6d.gif"></p>    <p>在控制台中的样子</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b51e48ed9eac0709a82ea87be47aaeac.jpg"></p>    <p>原理非常简单, 就是通过不断更新 <img> 标签的 top left 与transform 值,来达到刚体与皮肤联动的效果.</p>    <p>我们来看一下核心代码实现.</p>    <pre>  <code class="language-javascript">function render() {      var bList = world.GetBodyList(); //获取刚体集合 注意他是一个链表.      while (bList) {      var b = bList;      var bList = bList.GetNext(); //获取下一个元素,如果到达最后,则返回 null      if (b.GetUserData()) {        var img = b.GetUserData();        img.style.top = b.GetPosition().y*30 - 25 + "px";        img.style.left = b.GetPosition().x * 30 - 25 + "px";        img.style.webkitTransform = "rotate(" + b.GetAngle() * 180 / Math.PI + "deg)"      }    }  }  </code></pre>    <p>只要保证在每一帧中执行 render 方法, 就可以达到上图的效果。</p>    <h3><strong>7. 总结</strong></h3>    <p>Box2D 也许是最老的 2D 引擎,  在技术日新月异的今天, 早已不再是我们的唯一选择, 比如谷歌发布的开源 2D 物理引擎 LiquidFun 专门增加了对流体的支持,还有更轻量级的   chipmunk 、  matter.js 等等,它们每一种都有自己的亮点, 但基本原理或多或少都与 Box2D 有共通之处. 尤其是像刚体、 材质、 帧这些概念。 真可谓之, 学会一种,触类旁通。</p>    <p> </p>    <p>来自:http://jdc.jd.com/archives/2110</p>    <p> </p>