[★★★][AS語法][AS技巧]關於flash的HITTEST

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 發生, 那全憑我們的愛好而定。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章