9.2 hitTestObject与 hitTestPoint
到目前为止,我们已经学习了物体在其空间的内交互运动。接下来研究一下物体之间的交互运动。这就需要确定物体间何时发生了碰撞,这就是我们所讲的碰撞检测(Collision detection 或 Hit testing)。
本章我会尽量将所有需要掌握的相关知识告诉大家。其中包括两个影片的碰撞,影片与点之间的碰撞,用距离检测碰撞以及多物体碰撞检测方法。首先,来看一下有什么现成的碰撞检测方法。
9.1 碰撞检测方法
碰撞检测的思想非常非常简单。 我们只要知道两个物体是否有在同一时间内某个部分处在了同一位置上。当然,也许物体不只两个,这就需要知道其中的一个是否和其它的物体发是了碰撞。检测碰撞的方法有很多:
- 可以是对物体实际像素的检测(Sprite 或 MovieClip),也就是判断两个影片中的图形是否重叠?对于这种检测方法,要以影片内图形的实际可见像素判断,还是以影片的矩形边界来判断呢?这就涉及到 hitTest 方法中内置的两个可选项,用来满足不同方面的需求。
- 碰撞也可以根据距离来判断。获得两个物体间的距离,然后问“物体间是否近得足够发生碰撞了?”,应用这种方法时需要计算并判断距离。
每种方法都有它们各自的用途。具体实现会在本章进行讲解。至于发生碰撞后该做些什么,本章并不涉及。因为,这些内容我们会在第十一章介绍动量守恒时详细地为大家讲解。
早先 Flash 中的影片剪辑都有内置 hitTest 方法,这个方法有很多种用途。而现在已经将它划分为两种方法,这样做更加合理。hitTestObject 方法用于判断两个显示对象间是否发生了碰撞,而 hitTestPoint 方法用于判断某个点与显示对象间是否发生了碰撞。
9.2.1 碰撞检测两个Sprite
使用hitTestObject判断两个Sprite是否碰撞也许是最简单的碰撞检测方法。调用这个函数作为Sprite的方法,将另一个Sprite的引用作为参数传入。注意,虽然我说的是Sprite,但这两种方法都是 DisplayObject 类的成员, 对于所有继承自显示对象类的子类, 如 MovieClip, Bitmap, Video, TextField 等都可以使用。格式如下:
sprite1.hitTestObject(sprite2)
通常于在 if 语句中使用:
if(sprite1.hitTestObject(sprite2)) { // 碰撞后的动作 }
如果两个Sprite发生了碰撞则返回 true,并执行 if 语句块的内容。一切事物都是把双刃剑。碰撞检测方法越简单,其精确度就越低;相反,检测的精确度越高,则实现起来就越复杂。因此,这个最简单的方法,精确度也是最差的。
那么在碰撞检测中精确度意味着什么呢?不就是判断两个物体有没有冲突吗?我也希望问题只有这么简单。
回过头来看问题:知道两个Sprite的位置,如何判断它们是否碰撞?最简单的判断方法是这样执行的:拿来一个物体,绕着它画一个矩形。再复制出一个相同的影片,两个之间进行碰撞检测。最后判断两个矩形之间是否有相交的地方。如果有,则发生碰撞。用矩形包围物体就是我们所熟知的矩形边界(bounding box)。当我们在 Flash IDE 中点击一个舞台元件时,就会看到一圈蓝色的轮廓线,如图 9-1 所示。
图 9-1 矩形边界
当然,在 Flash 播放器中,并非先画上一个矩形再进行判断。一切都是根据Sprite的位置和大小计算出来的。
为什么这种方法不精确?因为,一旦两个矩形边界相交,则必然会产生碰撞。下面请见图 9-2,这几对图形中,哪两个相交了?
图 9-2 哪一对相交了?
很明显, 只有那两个正方形碰到了,对吧?好的,下面为它们画上矩形边界, 再从 Flash的视角观察一下。结果如图 9-3 所示。
图 9-3 并非我们希望的结果
对于 Flash 来说,每对图形是相交的。大家不相信的话,请看下面这个文档类ObjectHitTest.as,用到了我们前面创建的 Ball 类,请确认这个类存在于类路径中:
package { import flash.display.Sprite; import flash.events.Event; public class ObjectHitTest extends Sprite { private var ball1:Ball; private var ball2:Ball; public function ObjectHitTest() { init(); } private function init():void { ball1 = new Ball(); addChild(ball1); ball1.x = stage.stageWidth / 2; ball1.y = stage.stageHeight / 2; ball2 = new Ball(); addChild(ball2); ball2.startDrag(true); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { if(ball1.hitTestObject(ball2)) { trace("hit"); } } } }
本例创建了两个 Ball 类的实例,并将其中一个设置为可拖拽的。在每帧中使用hitTestObject 方法判断两个影片是否发生了碰撞。注意,当我们从上,下,左,右靠近目标时,结果都是正确的。但是如果倾斜着靠近物体,就有问题了。当然,如果物体都是矩形的话,不会有什么问题。但物体要是越不规则,那么结果就越不准确。所以,当发生碰撞的物体不是矩形时,大家就要格外小心。
下面举一个使用 hitTestObject 判断矩形物体的例子。这里要用到一个崭新的 Box类,与 Ball 类的非常相似,相信大家理解起来一定没有问题,代码如下:
package { import flash.display.Sprite; public class Box extends Sprite { private var w:Number; private var h:Number; private var color:uint; public var vx:Number = 0; public var vy:Number = 0; public function Box(width:Number=50, height:Number=50, color:uint=0xff0000) { w = width; h = height; this.color = color; init(); } public function init():void { graphics.beginFill(color); graphics.drawRect(-w / 2, -h / 2, w, h); graphics.endFill(); } } }
设计思想是, box 从屏幕上方落到下。box 落到舞台底部或其它 box 上时,就算放置好了。下面是代码(文档类 Boxes.as):
package { import flash.display.Sprite; import flash.events.Event; public class Boxes extends Sprite { private var box:Box; private var boxes:Array; private var gravity:Number = 0.2; public function Boxes() { init(); } private function init():void { boxes = new Array(); createBox(); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { box.vy += gravity; box.y += box.vy if(box.y + box.height / 2 > stage.stageHeight) { box.y = stage.stageHeight - box.height / 2; createBox(); } for(var i:uint = 0; i < boxes.length; i++) { if(box != boxes[i] && box.hitTestObject(boxes[i])) { box.y = boxes[i].y - boxes[i].height / 2 - box.height / 2; createBox(); } } } private function createBox():void { box = new Box(Math.random() * 40 + 10, Math.random() * 40 + 10); box.x = Math.random() * stage.stageWidth; addChild(box); boxes.push(box); } } }
在onEnterFrame 方法中,首先判断 box 位置是否低于舞台底部。如果是则停止,然后创建下一个 box。在 for 循环中,判断当前 box 是否碰撞到了其它 box。首先要判断,它不是和自己碰撞的,然后是整个程序的重点 hitTestObject,用来判断当前 box 是否与其它 box 发生碰撞。如果是则将当前 box 放在那个 box 的上面,随后创建一个新的 box。如果我们将本例中,Box 类换成 Ball 类,就会出现物体悬浮在空中的情景。
9.2.2 Sprite与点的碰撞检测
hitTestPoint 的工作方法有些不同,还带有一个可选参数。这个方法在判断两个Sprite的碰撞时并不常用。该方法中有两个 Number 类型的参数,用于定义点。根据Sprite是否与某点相交,返回 true 或 false。最基本的形式如下(100,100 代表点的 x,y 坐标):
sprite.hitTestPoint(100, 100);
同样,可以在 if 语句中使用它来判断碰撞为 true 时要执行的代码。
回到问题上:怎样才算碰撞?这时又看到我们的老朋友矩形边界了。Flash 只检查我们给出的点是否在Sprite的矩形边界内。用文档类 PointHitTest.as 来看测试一下:
package { import flash.display.Sprite; import flash.events.Event; public class PointHitTest extends Sprite { private var ball:Ball; public function PointHitTest() { init(); } private function init():void { ball = new Ball(); addChild(ball); ball.x = stage.stageWidth / 2; ball.y = stage.stageHeight / 2; addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { if(ball.hitTestPoint(mouseX, mouseY)) { trace("hit"); } } } }
这次使用鼠标的 x,y坐标判断与小球碰撞的点。当鼠标接近小球,可能还没有真正碰触到目标时就产生了碰撞。同样情况下,如果使用矩形方块,就不会有问题了。因此,这个方法看起来也只对矩形物体有用。但是这里还有一个叫shapeFlag 的选项。
9.2.3 使用shapeFlag 执行碰撞检测
shapeFlag 是 hitTestPoint 方法的第三个参数是可选的。其值是 Boolean 类型的,因此只有 true 和 false。将 shapeFlag 设置为 true 意味着碰撞检测时判断Sprite中可见的图形,而不是矩形边界。注意,shapeFlag 只在能用在检测点与Sprite的碰撞中。如果是两个Sprite的碰撞就不能用这个参数了。
来实验一下,只需要在原有基础上增加一个值,将 hitTestPoint 变为:
if(ball.hitTestPoint(mouseX, mouseY, true))
(如果不想使用 shapeFlag,可将参数设为 false,与不写参数是完全等价的)用小球测试一下这个版本的碰撞。我们发现这回鼠标只有在真正碰撞到Sprite的图形时才会检测到碰撞。
这样我们就实现了非常精确的碰撞检测,但是它并不能应对所有的情况。问题在于使用的点只有一个,很难看出到底Sprite的哪些部分碰触到了其它Sprite。也许大家的第一反应是用:
sprite1.hitTestPoint(sprite2.x, sprite2.y, true)
但是,如果这样的话,我们只能判断出 sprite2 的注册是否在sprite1 上。这种用途实在有限。因为 sprite2 的任何部分都有可能碰到 sprite1。实际应用中,最好选择鼠标或很小的影片作为碰撞点对象,因为它们两个之间也许只有一两个像素的偏差。
人们曾试图用这种方法检测物体四周上的点。例如,有一个星形的Sprite,我们可以计算出星形五个顶点的位置,然后判断每个顶点与另一个Sprite是否发生碰撞。但是如果有两个星形Sprite,我们就需要用这个星形的五个顶点与另一个星形的五个顶点进行碰撞判断。如果只是星形还好说,但要是其它不规则图形,还需要更多的顶点。可以想象这将占用大量的 CPU 资源。使用简单的碰撞检测方法,光是两个星形就要判断多达十次。这就是我们要追求准确度所付出的代价。
9.2.4 hitTest 总结
那么在两个不规则的物体间如何检测碰撞?很遗憾,用 hitTest 方法无法实现。
下面总结一下,hitTest 的基本设置:
- 对于矩形Sprite,使用hitTestObject(displayObject)。
- 对于非常小的Sprite, 使用 hitTestPoint(x, y, true) (注意将shapeFlag 设置为 true) 。
- 对于非常不规则的Sprite片图形,如果不要求非常精确或自定义一些解决方法的话,那么也可以使用hitTestPoint(x, y, true)。
本章内容远没有结束,下面我们还有超越内置方法的碰撞检测法。如果对象是圆形的物体,那么使用距离碰撞检测方法将是最好的选择。我们会发现原来有很多种图形都属于圆形。
文件下载(已下载 3263 次)发布时间:2011/6/23 上午10:26:45 阅读次数:11250