计算刚体运动轨迹
没错,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的负担,不推荐。
联系作者
请问怎么控制刚体的运动轨迹,比如一个手形状的刚体,模拟手在舞台上抹除画布上的物体,运动轨迹怎么设置,直接对刚体施加一个力,还是设置刚体的线速度和角速度,或者添加关节控制手的运动,刚学box2d,想做个类似手绘视频的项目,请拉登大叔指教!
你这个需求,用不着物理引擎
boss让做出真实的效果,涉及到物体间的碰撞等等,让我用这个引擎实现,手的运动轨迹我能通过运动轨迹上的坐标直接在每帧loop的时候直接SetPosition吗?
你好,我现在在用egret和p2.js参考这篇帖子做运动轨迹效果,运行结果是球开始会沿着轨迹走,但慢慢就会偏离轨迹向下,最终就是球的落点和实际轨迹不符
代码中用到的转换p2和egret位置长度宽度等单位的factor=30, stageFrame=60, gravity=[0,10], loop(){ world.step(1/60) }, 不知道问题出在哪里
抱歉,已经不做egret和HTML5了