任意多边形的碰撞检测——向量积判断方法

源码:https://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;
}
}

posted on 2012-01-30 15:17  Clifford  阅读(10154)  评论(3编辑  收藏  举报

导航