源碼:http://files.cnblogs.com/flash3d/hitTest.rar
左邊繪制靜態圖形 右邊繪制動態圖形 可由方向鍵控制移動
本人原本對碰撞測試不大熟,偶爾在API上見到過hitTestObject()函數,一直都天真的以為此函數能對圖形的區域進行准確的碰撞…直到昨天和人聊起這個函數,哪知as3為我們提供的碰撞測試函數hitTestObject()只能檢測顯示對象的矩形邊框(靠!那還檢測個毛啊!),另外還有一個函數hitTestPoint()雖然有檢測實際區域的能力,可只能進行點的檢測,功能實在有限啊~
hitTestObject()
其實as3還提供一個非矢量的碰撞檢測,BitmapData里有個hitTest函數,確實能實現兩個顯示對象之間的碰撞檢測,不過BitmapData要從draw函數得來,將矢量數據變成位圖數據才能處理,對於需要旋轉或者變形的碰撞,hitTest並不支持,只能將顯示對象變形后重新draw下來,效率可想而知…
綜合以上種種~我就想自己寫一個碰撞類
這次只是拋磚引玉做一個簡單的多邊形碰撞測試
更加復雜的碰撞還要靠大家自己去思考實踐
碰撞的數學理論基礎:
一個碰撞的充分不必要條件:顯示對象的實際邊界有相交
不必要是因為,當一個顯示對象會處在另一個顯示對象內部時,邊界沒有相交而他們確實碰撞了(顯示對象實心的話)
此條件的不必要性導致的問題就是,當物體運動的路徑不是連續的,那么一個物體就有可能不通過邊界相交,直接穿越到另外一個物體的內部,而碰撞檢測程序卻完全檢測不到,此時碰撞失效了。也就是說,只基於邊界相交的碰撞檢測依賴於過程。
另外還有個不幸的消息就是,任何的flash動畫,物體的運動路徑都是不連續的(只可意會不能言傳+_+),不過只要物體每幀改變的位置足夠小(相對於物體的尺寸來說,所以在這個示例中,只要你把多邊形畫得足夠小,還是可以穿殼的~),那么基本上不會出現“穿殼”的問題。
這種基於邊界相交檢測的碰撞檢測,實際上只要再增加一個小小的檢測之后,就能解決穿殼到物體內部的問題,處於簡單起見,這個問題之做一個小小的提示,有興趣可以去思考下。
下面先介紹檢測兩條線段相交的檢測方法
首先 應該先了解向量積的計算方法和意義:有兩個向量,向量1 <x1,x2> 向量2 <x2,y2>,那么 向量1*向量2=x1*y2-x2*y1=-(x1*y2-x2*y1)=-向量2*向量1。而且 向量1*向量2 的意義為用右手定則從向量1以不超過180的角度轉向向量2,大拇指指向就是 向量1*向量2 的方向 也就是說如果向量1轉向向量2為逆時針,那么他們的向量積就大於0,如果是順時針,那么就小於0。
這個原理的意義在於 如果兩個點在一個位於原點的向量(暫稱原向量)兩側,那么那兩個點各自和原點組成的向量必將在原向量的順時針和逆時針兩側。那么原向量和兩個向量分別的向量積必定異號
如果那兩個點連成線段(稱其新線段),那么原向量所在的直線必然和這個線段相交。
要是將新線段看做一個原向量,得出異號的結論的話,那么我們就能證明,新線段所在的直線和原向量所在的線段(稱其舊線段)相交。
既然舊線段所在直線和新線段相交,新線段所在直線和舊線段相交,那么兩線段必然相交!
(現在說還是有點抽象~估計有人已經犯困了~或者睡着了Z z z,下面注釋程序的時候會更加具體的解釋)
一下復制為hitTest.as保存在源文件目錄下即可
package {
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.display.Graphics;
public class hitTest extends Sprite {
public var color:uint;//可以在調用drawShape()之前設置的線條顏色屬性
private var lineArray:Array;//用來儲存多邊形每條邊的信息的數組
private var drawStart:Boolean;//一個判斷多邊形繪制是否正在進行中的布爾值
private var pointN:int;//在多邊形繪制中用來計數正在繪制的點的序數
public function hitTest():void {//構造函數 對必要的變量進行賦初值
lineArray=new Array();
color=0x0000FF;
this.x=0;
this.y=0;
}
public function init():void {//初始化函數 寫這個函數的目的是不必對對象進行重新構造 而對它進行歸零操作。對於像能被添加到顯示列表上的的一些對象,由於其被添加添加到顯示列表的時候,顯示列表會創建一個對象引用的副本,如果要徹底刪除這個對象,重新建立一個對象,必須先將其從顯示列表刪除,並保證任何一個應用都不指向這個對象,然后構建新對象時,這個對象才可能從內存刪除(這個是FP的事了~),所以與其花那么多力氣刪除對象,還不如直接寫個歸零函數將其歸零來的方便。
lineArray=new Array();//對數組進行歸零
drawStart=false;//對布爾值進行歸零
this.graphics.clear();//清除所有graphics的繪圖
this.x=0;
this.y=0;//對位置進行歸零
}
public function drawShape():void {//調用此函數開始繪制多變形
lineArray=new Array();
drawStart=false;
this.x=0;
this.y=0;
this.graphics.clear();//以上語句和init()中一樣,直接調用init()也可以。增加以上語句的目的是當才重復調用此函數時,保證舞台上只有一個多邊形。
this.graphics.lineStyle(1,color,1);//設置多邊形的邊界線條樣式
parent.addEventListener(MouseEvent.CLICK,drawLine);//當該對象的母容器被點擊時,調用drawLine真正開始繪制邊界了
}
private function drawLine(e:MouseEvent):void {
if (drawStart) {//當此次點擊是本次多邊形繪制的第一次點擊,那么drawStart將為false,如果非第一次點擊,那么將執行下面代碼
this.graphics.lineTo(e.stageX,e.stageY);//直接從上次繪制結束的點或者是moveTo的點繪制到當前鼠標的位置
pointN++;//繪制的點加1
lineArray[pointN]={a:e.stageX,b:e.stageY,c:0,d:0};//創建一條線的數據,並儲存線的開始點
lineArray[pointN-1].c=e.stageX;
lineArray[pointN-1].d=e.stageY;//把這個點儲存到上一條線的結束點
} else {
this.graphics.moveTo(e.stageX,e.stageY);//如果繪制的是第一個點,那么把會知道移動到鼠標位置
pointN=0;//繪制的點歸零
lineArray[pointN]={a:e.stageX,b:e.stageY,c:0,d:0};//創建一條線的數據,然后儲存開始點
drawStart=true;//標記已開始繪制
}
}
public function endDrawShape():void {//結束繪制函數
drawStart=false;//標記結束繪制
parent.removeEventListener(MouseEvent.CLICK,drawLine);
if (lineArray[0]!=null) {//存在至少一條數據
this.graphics.lineTo(lineArray[0].a,lineArray[0].b);//從最后一個繪制點畫一條線到第一個點
lineArray[pointN].c=lineArray[0].a;
lineArray[pointN].d=lineArray[0].b;//最后一條線的數據的結束點為開始點
}
}
private function simpleLineTest(l1p1x:Number,l1p1y:Number,l1p2x:Number,l1p2y:Number,l2p1x:Number,l2p1y:Number,l2p2x:Number,l2p2y:Number):Boolean {//這個函數是核心,它檢測兩條線是否相交,每條線有四項數據(開始點的兩個坐標,結束點的兩個坐標),所以兩條線總共需要8個參數
var line1p1:Number;
line1p1=(l1p2x-l1p1x)*(l2p1y-l1p1y)-(l2p1x-l1p1x)*(l1p2y-l1p1y);//第一條線段的向量和(第一條線段的開始點與第二條線段的開始點組成的向量)的向量積
var line1p2:Number;
line1p2=(l1p2x-l1p1x)*(l2p2y-l1p1y)-(l2p2x-l1p1x)*(l1p2y-l1p1y);//第一條線段的向量和(第一條線段的開始點與第二條線段的結束點組成的向量)的向量積
var line2p1:Number;
line2p1=(l2p2x-l2p1x)*(l1p1y-l2p1y)-(l1p1x-l2p1x)*(l2p2y-l2p1y);//第二條線段的向量和(第二條線段的開始點與第一條線段的開始點組成的向量)的向量積
var line2p2:Number;
line2p2=(l2p2x-l2p1x)*(l1p2y-l2p1y)-(l1p2x-l2p1x)*(l2p2y-l2p1y);//第二條線段的向量和(第二條線段的開始點與第一條線段的結束點組成的向量)的向量積
if ((line1p1*line1p2<=0)&&(line2p1*line2p2<=0)) {//判斷方法在先前講過
return true;//相交
} else {
return false;//否則不相交
}
}
public function lineTest(la:hitTest):Boolean {//判斷另外一個對象與本對象是否有任何一條線相交 這個也就是碰撞檢測了
for (var n:int=0; n<la.lineArray.length; n++) {
for (var m:int=0; m<lineArray.length; m++) {//兩個for循環嵌套,對兩個對象的所有線段進行遍歷檢測,一旦發現有相交,就返回true
if (simpleLineTest(lineArray[m].a+this.x,lineArray[m].b+this.y,lineArray[m].c+this.x,lineArray[m].d+this.y,la.lineArray[n].a+la.x,la.lineArray[n].b+la.y,la.lineArray[n].c+la.x,la.lineArray[n].d+la.y)) {
return true;
}
}
}
return false;
}
}
}
主程序的第一幀代碼:
//bg是舞台背景,還有四個按鈕已經定義好的按鈕
import hitTest;//吧剛才寫的類導入
var up:Boolean=false;
var down:Boolean=false;
var left:Boolean=false;
var right:Boolean=false;//這四個變量是控制第二個多邊形的四個方向的運動的,初始false,就是不運動
var aTest:hitTest=new hitTest();
var bTest:hitTest=new hitTest();//建立兩個空的碰撞檢測對象
bg.addChild(aTest);
bg.addChild(bTest);//把兩個對象都添加到bg上
aTest.color=0x00FF00;//以一個多邊形的線條顏色設為綠色
stage.addEventListener(KeyboardEvent.KEY_DOWN,setDown);//鍵盤按下發生事件
stage.addEventListener(KeyboardEvent.KEY_UP,setUp);//鍵盤起來 發生事件
stage.addEventListener(Event.ENTER_FRAME,mov);//讓第二個多邊形移動
drawA.addEventListener(MouseEvent.CLICK,drawANow);//開始繪制第一個多邊形
drawB.addEventListener(MouseEvent.CLICK,drawBNow);//開始繪制第二個多邊形
stopA.addEventListener(MouseEvent.CLICK,stopANow);//停止繪制第一個多邊形
stopB.addEventListener(MouseEvent.CLICK,stopBNow);//停止繪制第二個多邊形
var lsx:Number=0;
var lsy:Number=0;//這兩個變量是用來儲存第二個多邊形移動后的上一步的位置
function drawANow(e:MouseEvent):void {//此函數調用第一個多邊形的繪制圖形函數
aTest.drawShape();
}
function drawBNow(e:MouseEvent):void {//此函數調用第二個多邊形的繪制圖形函數
bTest.drawShape();
}
function stopANow(e:MouseEvent):void {//此函數調用第一個多邊形的停止繪制函數
aTest.endDrawShape();
}
function stopBNow(e:MouseEvent):void {//此函數調用第二個多邊形的停止繪制函數
bTest.endDrawShape();
}
function setDown(e:KeyboardEvent):void {//鍵盤按下
if (e.keyCode==Keyboard.DOWN) {//如果是反向鍵向下
down=true;//則允許向下運動
}
if (e.keyCode==Keyboard.UP) {//如果方向鍵向上
up=true;//允許向上運動
}
if (e.keyCode==Keyboard.LEFT) {//如果方向鍵向左
left=true;//允許向左運動
}
if (e.keyCode==Keyboard.RIGHT) {//如果方向鍵向右
right=true;//允許向右運動
}
}
function setUp(e:KeyboardEvent):void {//原理同上 就是哪個鍵放開 哪個方向的運動就不允許
if (e.keyCode==Keyboard.DOWN) {
down=false;
}
if (e.keyCode==Keyboard.UP) {
up=false;
}
if (e.keyCode==Keyboard.LEFT) {
left=false;
}
if (e.keyCode==Keyboard.RIGHT) {
right=false;
}
}
function mov(e:Event):void {//根據四個方向的布爾值和碰撞檢測的結果運動,實現碰撞的運動效果
if (left==true) {//如果能向做運動
bTest.x--;//位置向左
} else if (right==true) {//如果向左不成立,切且能向右
bTest.x++;//向右
}
if (aTest.lineTest(bTest)) {//運動完成后碰撞
bTest.x=lsx;//返回運動前的位置
} else {
lsx=bTest.x;//沒碰撞的話,保存當前位置
}
if (up==true) {//一下同理
bTest.y--;
} else if (down==true) {
bTest.y++;
}
if (aTest.lineTest(bTest)) {
bTest.y=lsy;
} else {
lsy=bTest.y;
}
}