FLASH的HITTEST好像只是HITTEST一个矩形范围,比如说一个平行四边形,它的HITTEST区域就是一个外接的最小矩形。能不能改一下,使它按照图形的外轮廓来HITTEST?
 
在 FLASH 里面有一个 hitTest() 语句,是用来检测两个电影元件在场景上是否发生重叠现象的。这个语句基本上被引伸为检测在场景上的两个移动着的电影元件是否发生碰撞。但是实际上这个 hitTest() 语句的效能是相当有限的。

先让我们看看附件里面的例1。
在例1 里面我们可以看到四个电影元件:一个小红球,一个大蓝球,一个带有边框的大蓝球,还有一个大月亮。小红球是可以被鼠标拖动的,当小红球被拖动着碰到其他电影元件的时候,被碰到的那个电影元件就表现得暗淡一些。

hitTest() 语句的一种写法是: this_MC.hitTest( that_MC ) 它的结果是一个 Boolean 值, 因此可以利用它的结果,来引发对两种不同的事件的处理。

我们知道,在FLASH 里的电影元件,不论是什么形状,这个电影元件的轮廓范围是由一个四边形限定的,这个四边形的四条边就代表了这个电影元件的顶边和底边,左边和右边。而 hitTest() 语句所检测的,正是这个范围。因此,即使小红球没有真正碰到大蓝球,只要它进入了这个范围,碰撞也就发生了。

为了使检测结果更为精确,hitTest() 语句的另一种写法是:
this_MC.hitTest( that_MC._x, that_MC._y, true )

这时候是检测一个电影元件的图形而不是它的边框范围,是否和另一个电影元件的注册点发生接触,这种检测令到小红球可以钻进月亮的包围圈里而不发生碰撞。在理论上来说, hitTest() 语句既可以归属在大物件上,也可以归属在小物件上, 但是当我们采用第二种写法时, 总是把hitTest() 语句归属在小物件上 。理由很简单,我们很容易拖着小红球的注册点去触碰大月亮的形状,但是要拖着小红球的形状去触碰大月亮的注册点则既不容易也不实际,因为即使小红球接触到大月亮本身也不算数,这就不符合本来的愿望。

一般地说,对于那些在场景上拖动一个物件,去碰撞另一个物件的工作,hitTest() 语句是可以应付的。但是 hitTest() 语句并不能解决所有的问题。让我们来看看例2。在例2里, 我们要拖动大蓝球去碰撞一个更大的圆圈, 我们要求当它们相接触时,大圆圈的红色外部就要表现得暗淡一些。我们采用 hitTest() 语句的第二种写法,结果不尽人意,因为要等到大蓝球的注册点碰到大圆圈的时候才有反应,这并不符合我们的愿望。现在看看例2-2,在例2-2里我们采用 hitTest() 语句的第一种写法,结果更坏,不管碰撞不碰撞,大圆圈的红色外部总是暗淡无光。

现在看看另一种情况,在例3里面,我们可以看到,在场景上有一个小红球,一个小绿球,和一个大蓝球。小红球和小绿球各以不同的速度撞向大蓝球。我们希望 hitTest() 语句能够检测到它们的碰撞,一旦检测到碰撞,相对的标语牌就会显示有碰撞发生。

另我们失望的是,hitTest() 语句只检测到小绿球对大蓝球的碰撞,却未能检测到小红球对大蓝球的碰撞。为什么会这样呢?因为hitTest() 语句真正做的,是要检测当 FLASH 电影播放到某一帧的时候,两个电影元件有没有发生重叠。而在 FLASH 里的所谓运动速度,实际上是指运动物件从第一帧到第二帧的位置跨度,物件本身并没有运动,只不过是它在第一帧的位置与在第二帧的位置有所不同,使我们在视觉上认为它在运动。

在例3里,小绿球以很慢的速度撞向大蓝球,它的速度是如此之慢,而大蓝球的体积是如此之大,所以小绿球是一定会在某一帧中与大蓝球发生重叠的,hitTest() 语句当然能够检测到这个碰撞。但是小红球的速度是如此之快,它完全有可能在某一帧时,恰好坐落在大蓝球的左边,但是并没有接触到大蓝球,而在下一帧时,它却坐落在大蓝球的右边,也没有接触到大蓝球,而hitTest() 语句只能检测每一帧的状况,却不能检测帧与帧之间的状况,因此hitTest() 语句当然不能对付得了这种虚幻的碰撞——根本没有重叠发生的碰撞。

实际上我们除了使用hitTest() 语句之外,仍然有其他办法检测到电影元件之间的碰撞。这就是用数学计算法来做检测。我们看看例4。在例4里面,三个大球同时静态地出现在场景里。这样安排是为了便于说明数学计算法的基本运作。在例4里完全没有采用hitTest() 语句,我们检测两个球体是否发生重叠,只需看看两个圆的中心相距有多远,检测的标准是拿两个圆的半径相加,如果两个圆的中心相距超过两圆半径之和,那么这两个圆就没有重叠,也就是没有碰撞。如果两个圆的中心相距等于或者小于两圆半径之和,那么这两个圆就发生重叠,也就是有碰撞发生。而这里所牵涉到的计算,只不过是一个直角三角形的两条直角边的平方和,等于斜边的平方。

看过例4之后,我们知道如何解决例2里的问题了,我们把答案写在例4-2里。结果令人非常满意。

现在我们来看例4-3。 例4-3的场景与例3完全一样。但是我们不再采用hitTest() 语句,而是采用数学计算法。我们利用一个 onEnterFrame 语句来迫使 FLASH 在每一帧中检测,看看两个圆的中心相距是否有机会小于两圆半径之和。不幸得很,在这里数学计算同样不能检测到那种虚幻的碰撞——根本没有发生的重叠。这是怎么一回事呢?

问题不仅仅在于采用数学计算,问题在于不要老是盯着“在某一帧中发生了什么事”,而是要检测在“帧”与“帧”之间发生了什么事。现在看看例5,例5的场景与例3也是完全一样。我们无需考虑小绿球的事了,因为连 hitTest() 语句也可以对付小绿球。在例5里可以看到,用数学计算完全能够检测到小红球对大蓝球的碰撞——尽管也是虚幻的,根本就没有发生过重叠。虽然最根本的依据仍然是直角三角形的解法,但是这里牵涉到的计算要复杂的多。

在讨论例5之前, 我们先说说以下的概念。
假设有一个点,沿着一个平面直角坐标上的一条直线作匀速运动,而我们要检测这个点在当前的位置,那么可以这样认为:

当前位置的X值 = 上次检测时的位置的X值 + 在X轴上的速度 * 自上次检测以来所经历的时间
当前位置的Y值 = 上次检测时的位置的Y值 + 在Y轴上的速度 * 自上次检测以来所经历的时间

为了方便运算,我们把小红球命名为 ball1,而把大蓝球命名为 ball2,因此我们可以这样写:

ball1._x = ball1_lasttime._x + ball1_xspeed * time;
ball1._y = ball1_lasttime._y + ball1_yspeed * time;

ball2._x = ball2_lasttime._x + ball2_xspeed * time;
ball2._y = ball2_lasttime._y + ball2_yspeed * time;

因为我们要用一个 onEnterFrame 语句来迫使 FLASH 在每一帧中作一次检测, 所以我们可以说, 上述的算式实际意义是:

当前帧ball1._x = 上一帧ball1._x + ball1_xspeed * 1;
当前帧ball1._y = 上一帧ball1._y + ball1_yspeed * 1;

当前帧ball2._x = 上一帧ball2._x + ball2_xspeed * 1;
当前帧ball2._y = 上一帧ball2._y + ball2_yspeed * 1;

其中的时间一项等于 1 , 是因为两帧之间的差就是 1 。但是如果我们规定这个时间差等于 1 , 那么我们就永远解决不了问题。因为碰撞发生在时间差还没有达到 1 的那一刻,让我们再走远点。

我们知道两圆中心在某一刻的距离是:

在X轴上的差 Xdifference = ball1._x - ball2._x;
在Y轴上的差 Ydifference = ball1._y - ball2._y;
两圆中心距离 distance = Math.sqrt( Xdifference * Xdifference + Ydifference * Ydifference );

因为我们知道, 两圆碰撞的条件是, 两圆中心的距离小于或者等于两圆半径之和, 现在我们硬性规定这一条件成立, 这就是说,我们硬性规定两圆碰撞,但是是在什么时间发生的呢?

我们写下这些算式:

小红球的半径 ball1_radius = ball1._width / 2;
大蓝球的半径 ball2_radius = ball2._width / 2;
碰撞时的距离 distance = ball1_radius + ball2_radius;

现在我们把两个 distance 凑到一起,

ball1_radius + ball2_radius = Math.sqrt( Xdifference * Xdifference + Ydifference * Ydifference )

这就是说, 此时此刻, 两圆中心的距离恰恰等于两圆的半径之和。我们现在是强迫两圆相碰,却掉过头来求相碰的时间。

这个方程好像很难解,因为我们必须把 ball1._x ,ball1._y ,ball2._x ,ball2._y 这些变量代进去,而这些变量本身又是一个一个的算式,然后还要把等式两边平方,去掉平方根号,剩下的就是一个二次方程。二次方程有两个解,就是说我们会得到两个关于时间的解。它的物理意义是:两圆相向移动,在某一时间它们接触对方的边缘,然后它们继续移动,当它们即将分离之际,它们的边缘会再次接触,实际上应该叫“分离”,这就是方程的两个解。

为了使算式简单一点,例5中临时增加了几个中间变量:

b1.xpos = ball1._x; ---- 其初始值为用户自定义
b1.ypos = ball1._y; ---- 其初始值为用户自定义
b2.xpos = ball2._x; ---- 其初始值为用户自定义
b2.ypos = ball2._y; ---- 其初始值为用户自定义

b1.xspeed = 小红球在 X 轴上的速度 ---- 用户自定义
b1.yspeed = 小红球在 Y 轴上的速度 ---- 用户自定义
b2.xspeed = 大蓝球在 X 轴上的速度 ---- 用户自定义
b2.yspeed = 大蓝球在 Y 轴上的速度 ---- 用户自定义

var temp1:Number = -2 * b1.xspeed * b2.xspeed + b1.xspeed * b1.xspeed + b2.xspeed * b2.xspeed;
var temp2:Number = -2 * b1.yspeed * b2.yspeed + b1.yspeed * b1.yspeed + b2.yspeed * b2.yspeed;
var temp3:Number = -2 * b1.xpos * b2.xpos + b1.xpos * b1.xpos +b2.xpos * b2.xpos;
var temp4:Number = -2 * b1.ypos * b2.ypos + b1.ypos * b1.ypos + b2.ypos * b2.ypos;
var temp5:Number = -2 * b1.xpos * b2.xspeed - 2 * b2.xpos * b1.xspeed + 2 * b1.xpos * b1.xspeed + 2 * b2.xpos * b2.xspeed;
var temp6:Number = -2 * b1.ypos * b2.yspeed - 2 * b2.ypos * b1.yspeed + 2 * b1.ypos * b1.yspeed + 2 * b2.ypos * b2.yspeed;
var temp7:Number = b1.radius + b2.radius;

var a:Number = temp1 + temp2;
var b:Number = temp5 + temp6;
var c:Number = temp3 + temp4 - temp7 * temp7;

最后得到方程的两个关于时间的解,他们都必须小于等于 1 而且大于 0 ,这是因为既然上一次检测已经过去,而下一次检测还未到来,而要等到下一次检测到来的时候,这个时间差的值才会等于 1 ,所以我们只有得到一个小数解才可能是正确的。


根据二次方程式

十 /——————————
—b — / b * b — 4 * a * c
\/
X1,X2 = ——————————————————

2 * a


t1 = ( -b + Math.sqrt( b * b - 4 * a * c ) ) / ( 2 * a );
t1 = ( -b - Math.sqrt( b * b - 4 * a * c ) ) / ( 2 * a );

当 1 >= t1 > 0 或者 1 >= t2 > 0 , 我们就知道碰撞发生,尽管其实并没有这回事。


为了看清楚 t1 和 t2, 我们在例5-2里改用小绿球来碰撞大蓝球, 由于小绿球的速度非常慢, 所以我们可以看得很清楚, 至于程序里的后续事件应该在 t1 发生还是在 t2 发生, 那全凭我们的爱好而定。