9RIA-ladeng6666 发表于 2018-2-6 15:21:08

【9RIA—ladeng6666】—【Box2D系列教程 新番】

本帖最后由 TKCB 于 2018-2-6 15:32 编辑

转载:9RIA游戏开发者社区(天地会)
作者:ladeng6666(拉登大叔)
作者博客:http://www.ladeng6666.com/blog/


【Box2D系列教程-导航帖】—拉登大叔出品(总贴)



没错,Box2D可以帮我们轻松的解决物理碰撞模拟的问题,但是人类是贪婪的,我们不满足于此,并希望能够快Box2D一步,预先知道下一步或者将来,刚体的运动轨迹,就像在《愤怒的小鸟》中,当我们拉动弹弓后,可以看到小鸟将要飞行的轨迹。

又或者,可以让刚体听我们的话,指哪打哪,就像《弹弹堂》中攻击敌人可以百发百中。

这一节,我们就来学习一下如何对刚体运动轨迹未卜先知,进而百发百中。

本节知识点
为实现以上两个游戏中的效果。本节将相关内容分解为一下3个知识点

(该知识点源自iforce2d.net,对英文该兴趣的同学,点击 这里 查看原文)

计算刚体的位置。第n个timeStep后,刚体的位置。用于描绘小鸟飞行的轨迹。
计算刚体最大高度。以初始速度v0飞出时,可以达到的最大高度。
计算刚体初始速度。为达到某个指定位置,而需要的初始速度,实现《弹弹堂》中的百发百中。

计算刚体的位置
初中的时候,我们都学过自由落体运动,假设重力加速度为a,那么经过t秒后,物体下落的距离h可以用下面的公式计算出来


但是在Box2D的世界里,使用这个公式是不准确的。因为Box2D是一个以delta为频率的数字采样世界,无法完美无缺的还原世界中的运动轨迹。
我们知道,在物体以a为加速度进行加速运动时,t秒后物体的速度v,可以用下面的公式计算出来。


v和t之间以加速度为斜率,成连续的线性关系,如下图所示。

而在Box2D的数字世界里,每个delta之间,物体是以当时的速度vt进行匀速运动的,所以随着时间t的不断增加,速度v是以delta为单位称阶梯形上升。如下图所示:

要知道,图中的速度曲线与坐标轴形成的形状面积,就是经过时间t后,刚体运动的距离。所以Box2D模拟出来的运动距离,要比实际少一些。而缺少的部分刚好是阶梯形状缺口的面积,如下图所示:

所以,接下来针对Box2D中物体运动的距离,我们要做的是,计算阶梯形状的面积。为此,可以讲锯齿分解成一个个小的矩形,每个矩形的宽为delta,高度为vn=a*delta*n,计算出每个矩形的面积dn,然后累加起来,如下图所示:

将图中每个矩形的面积累加起来,将vn替换为tna,同时t=ndelta后,所以刚体移动的距离转换成下面的公式:

如果,刚体有初始速度v0的话,那么要在以上公式d的基础上,累加初始速度移动的距离da = v0*n*delta。公式如下:

把这个公式,定义到名为getPositionWhen()的函数中,转换成代码如下所示:
    private function getPositionWhen(pos:b2Vec2, v0:b2Vec2, n:Number):b2Vec2{
      var newPos:b2Vec2 = new b2Vec2();
      var dx:Number,dy:Number;

      var delta:Number = 1/stage.frameRate;
      var a:Number = world.GetGravity().y;
      dy = v0.y*delta*n + delta*delta*a*(n+1)*n/2;

      dx = v0.x * delta*n;

      newPos.y = pos.y + dy;
      newPos.x = pos.x + dx;
      return newPos;
    }


参数说明如下:
    pos:刚体当前的坐标位置
    v0:刚体移动的初始速度
    n:经过的timestep数


计算最大高度
虽然,在移动距离上,Box2D模拟出的结果与实际有些差异。但在速度上,经过时间t = n * delta后,刚体的速度还是符合下面的公式的:


我们知道,因为受到重力的作用,刚体在上升过程中,会渐渐慢下来,最终速度vt=0,此时刚体到达最大高度。根据上面的公式,可以计算出vt=0时,经过的timestep数量n:

然后将计算出的n作为参数,传递一个getPositionWhen(),既可以返回最大高度位置。
把以上计算过程,定义到名为getHighestPosition()函数中,代码如下所示:

    private function getHighestPoint(pos:b2Vec2,v0:b2Vec2):b2Vec2{
      var delta:Number = 1/stage.frameRate;
      var a:Number = world.GetGravity().y;
      var n:Number = -v0.y/delta /a;
      return getPositionWhen(pos,v0,n);
    }


计算刚体初始速度
人类得到的越多,就越是贪婪。已经知道了如何计算刚体的位置,以及最大高度,但我们更想知道,如果已知某个坐标位置p,要以多大的速度发射炮弹,可以百步穿杨,击中目标位置p。
实现这一点并不困难,我们假定炮弹到达目标位置时,速度刚好为0,即目标位置为最大高度。那么根据getHighestPosition()函数中的公式,我们可以得知,初始速度v0和到达目标位置,所经过的timestep数n的关系为:


假设炮弹发射位置与目标位置p的垂直距离为d,根据getPositionWhen()中的公式,可以得到n,与距离d的关系为:


将n替换为 –v0/a/delta后,可以到只包含未知数v0的一个一元二次方程,具体如下:

根据一元二次方程的求解公式:

将速度与距离公式对应整理成一元二次方程求解公式形式,可以轻松的得到v0的求解结果。

解方程后,我们可以得到两个结果,分别表示向上和向下的速度。因为AS3的坐标系统中,y>0是向下的,所以这里v0.y<0的结果。
将以上计算过程,集成到名为getVelocityToPosition()函数中,代码如下:

    private function getVelocityForPosition(from:b2Vec2,to:b2Vec2):b2Vec2{
      var dy:Number = to.y-from.y;
      var dx:Number = to.x -from.x;

      if ( dy >= 0 )
            return new b2Vec2();

      var delta:Number = 1 / stage.frameRate;
      var aGravity:Number = delta * delta * world.GetGravity().y; // m/s/s

      var a:Number = 0.5 / aGravity;
      var b:Number = 0.5;
      var c:Number = dy;

      var quadraticSolution1:Number = ( -b - Math.sqrt( b*b - 4*a*c ) ) / (2*a);
      var quadraticSolution2:Number = ( -b + Math.sqrt( b*b - 4*a*c ) ) / (2*a);

      var vy:Number = quadraticSolution1;
      if ( vy > 0){
            vy = quadraticSolution2;
      }

      var vx:Number = dx/(-vy/aGravity*delta);

      return new b2Vec2(vx,vy*stage.frameRate);
    }
}


举个栗子
好了,明白了计算刚体运动估计的算法和公式,下面该上示例了。



下载本节源文件,运行CalcForceFromPosition.swf文件,效果如下图所示,点击舞台任意位置,左下角的小气会向鼠标位置抛出,并准确的击中鼠标位置。
与此同时,在小球抛出之前,它的运动轨迹已经被计算好,并用黑色的圆圈绘制出来,小球会不偏不倚的从绘制好的路径上飘过。
另外,红色点是小球运动的最高点



大痔过程
关于初始速度、和最高点,使用前面介绍的getVelocityForPosition()和getHighestPoint()都可以直接获取,这里重点说明一下轨迹的绘制。
我们知道,通过前面介绍的getPositionWhen()函数,可以获取任意时刻刚体的位置。那么,如果使用for循环,从1到50(或者更多)多次调用getPositionWhen()计算敢提位置,就会得到连续的刚体移动轨迹,然后再在update()函数中,在这些位置上绘制圆圈,就可以看到连续的轨迹了。代码如下所示:

      override protected function update(e:Event):void{
            super.update(e);

            var v:b2Vec2 =velocity;
            var p:b2Vec2 = new b2Vec2(100/30,300/30);
            for (var i:int = 0; i < 50; i++)
            {
                var np:b2Vec2 = getPositionWhen(p,v,i);
                LDEasyDebug.debug.DrawCircle(np,3/30,new b2Color(0,0,0));
            }
            var highestPoint:b2Vec2 = getHighestPoint(p,v);
            LDEasyDebug.debug.DrawSolidCircle(highestPoint,5/30,new b2Vec2(),new b2Color(1,0,0));
      }


后记
另外还有一个问题,文章中没有提及。这里我们介绍的是刚体没有发生碰撞时的移动轨迹计算,如果在移动过程,刚体与其他对象发生了碰撞,就无法使用教程中的方法,实现轨迹计算了。
这种情况下,会牵扯到碰撞计算的模拟,这是一个复杂的过程,此时可以创建另外一个与当前Box2D世界完全相同的world(但不对其进行渲染),然后在这个world中预先调用step(),并将运行的结果和数据,在当前的world中渲染出来。
不过这是一个非常消耗性能的做法,因为程序会进行两次(当前world,不渲染的world)渲染,当世界中刚体较多时,会加大CPU的负担,不推荐。

页: [1]
查看完整版本: 【9RIA—ladeng6666】—【Box2D系列教程 新番】

感谢所有支持论坛的朋友:下面展示最新的5位赞助和充值的朋友……更多赞助和充值朋友的信息,请查看:永远的感谢名单

SGlW(66139)、 anghuo(841)、 whdsyes(255)、 longxia(60904)、 囫囵吞澡(58054)

下面展示总排行榜的前3名(T1-T3)和今年排行榜的前3名的朋友(C1-C3)……更多信息,请查看:总排行榜今年排行榜

T1. fhqu1462(969)、 T2. lwlpluto(14232)、 T3. 1367926921(962)  |  C1. anghuo(147)、 C2. fdisker(27945)、 C3. 囫囵吞澡(58054)