DIRECTOR 之游戏碰撞学一

来源:互联网 发布:ubuntu搜狗输入法 乱码 编辑:程序博客网 时间:2024/05/29 17:57

碰撞检测原理前言

开门见山,我们将要研究一下如何检测和处理物体与物体之间的碰撞事件。

 

先来概括了解一下,我们说,碰撞是物体运动过程中发生的事件,所以我们要从研究物体运动的原理入手,尤其是用计算机模拟的物体运动的原理,搞清楚这个,才能在此基础上进一步探讨碰撞的发生及检测处理。

不知大家都是如何另一个物体运动的,也许你会说,这个easy!改变它的位置就行了,比如,在Director中,我们想要让一个精灵水平向右移动,最简单的方法就是一句lingo:

sprite(whichsprite).loch= sprite(whichsprite).loch+1

通过改变物体的位置来形成物体的运动效果,OK!这个没有问题,但是问题出在这里,当我们向要改变物体运动的快慢怎么办呢??比如说,我们想要上面的那个精灵的运动快上5倍,该如何做呢?我以前是这样做的:

sprite(whichsprite).loch= sprite(whichsprite).loch+5

表面看来,这没有问题,这样的运动速度确实提高了5倍,对于简单的运动来说,这个没什么缺陷,但如果物体的运动比较复杂,尤其是有了碰撞事件,这就产生了很大的麻烦。来看看是什么样的麻烦。

拿两个像素点的一维运动和碰撞来举例,分别称之为Point1、Point2。假设我们赋予Point1的水平移动速度为1象素/秒,而赋予Point2的水平移动速度为-10象素/秒,假设Point1的初位置为(0,0),Point2的初位置为(30,0),如下图:


    两个点相向运动,那么,我们很容易预计,在第2秒钟时,Point1的位置为(2,0),而Point2的位置为(10,0),此时两个点相距仅8个像素,还没有相遇,让我们看看看看下一秒后会发生什么。到第三秒时,Point1的位置为(3,0),而Point2的位置为(0,0),就这样错过去了,没有发生任何碰撞。我想把这种现象称为“瞬间穿透”。

    显然,这是由于Point2的位置一下子改变了10个象素而造成的,不妨想象一下,如果想要避免这种“瞬间穿透”,似乎唯一的方法就是另两个点的运动速度必须为1象素/秒。那怎么行,那样物体的运动的快慢就永远无法改变了??不!可以改变,我们完全可以另物体每次只移动一个像素,但同时又能控制物体运动速度的快慢。这不是太矛盾了?接着往下看。

回到我们最初物体运动机制的探讨,还是那句lingo:

sprite(whichsprite).loch= sprite(whichsprite).loch+5

显然,我们是想要让精灵以每次移动5象素的速度来运动,现在,为了避免“瞬间穿透”,我们将只能使用:sprite(whichsprite).loch= sprite(whichsprite).loch+1,那么怎样改变物体运动的快慢呢?很简单,原本每秒钟移动一个像素距离,这回我让它每0.2秒移动一个像素,怎么样,这样,运动加快了吧。总而言之,就是通过控制触发物体位置改变的频率来控制物体运动的快慢,到这里,我们来对比一下两种方法的区别,本来物体运动速度都是1象素/秒,即:

sprite(whichsprite).loch= sprite(whichsprite).loch+5

现在,两种方法都以达到5象素/秒的效果为目的。

 

原始方法改变了每次位置改变的量为5,每一秒钟执行一句

sprite(whichsprite).loch= sprite(whichsprite).loch+5

 

新的方法保持每次位置改变的量为1不变,但改变的是触发位置改变事件的频率,即:

每0.2秒执行一句sprite(whichsprite).loch= sprite(whichsprite).loch+1

 

OK!基本原理就是这样,我们说讨论物体的碰撞,就是要在这两种不同的运动机制下分别讨论。对于机制一,会产生“瞬间穿透”,所以讨论的要点就是如何避免这个问题;对于机制二,“瞬间穿透”倒是没有了,所以对于简单的碰撞,直接判断两物体是否接触就可以了,但遗憾的是,我们发现了另外一个问题,那就是基于此运动机制的运动速度上限的问题。怎么会产生上限呢?接着上面的例子,我们使用第二种机制,把物体运动速度提高到

10象素/秒,OK,只要每0.l秒移动一象素就行,那么1000象素/秒呢?每一毫秒移动一象素!那么,10000象素/秒呢,0.1毫秒??请问,lingo中哪来的0.1毫秒,即使是更低级的编程语言,恐怕也有个最小时间单位难以逾越吧。这样,对于lingo, 1000象素/秒就成为了速度上限。更何况,如果需要处理的运动物体多了,恐怕1000象素/秒也只是个理论值而已。我们只能想方设法在某种程度上解决这个问题,这个咱们以后再详述。所以,到这里,总结一下第二种机制下需要解决和研究的问题,那就是解决速度上限所带来的种种弊端,更为本质地说,就是要解决我们计算机中无法避免的最小时间单位的问题。好!下一讲我们继续。

 

碰撞检测原理之绝对运动篇

在前言中,我们知道要研究碰撞,就要分两种不同的运动机制来研究,第一种是基于改变触发位置改变事件的频率的机制,我们不妨称之为绝对运动机制,第二种基于改变每次位置的改变量的机制,说起来很饶口,不清楚的话,一定要再去回顾一下前言哦!

这次我们就绝对运动机制来研究一下。主要研究目的有两个:

1. 如何实现绝对运动,包括绝对运动的速度控制。

2. 绝对运动下的物体碰撞处理。

 

实现绝对运动的关键是控制物体运动速度的处理,由绝对运动原理,我们知道,物体每次改变的位置的偏移量是不变的,永远为1,所以我们要很方便的控制触发位置改变事件的频率,因此,要用到计时器的概念。所谓计时器,就是每隔一定的时间间隔,就触发某一指定事件的机制,下面是一个简单的计时器的例子:

Time=Time+1--时钟

LastTime=Time—上次触发超时事件的时间

Timer=5--触发超时事件时间间隔

if Time-LastTime> Timer then

  TimeUp()

  LastTime=Time

end if

不难看出,如果我们的TimeUp()句柄如果是这样:

on TimeUp

  sprite(1).loch= sprite(1).loch+1

end

那么改变计时器中Timer的值就可以改变触发位置改变事件的频率,也就达到了改变物体运动快慢的要求。

以上只说明一下原理,那还远远不够,因为我们并不习惯于改变触发位置改变事件的频率而改变物体运动的速度,而是希望能够直接设置物体的运动速度。这就需要我们自定义一个函数来分析我们传递的希望达到的速度值从而再由程序内部处理改变相应的触发位置改变事件的频率。形象点说,我们希望物体运动速度为1象素/秒,那么好,程序就将自动分析,哦,我应该每秒钟让物体的位置改变1象素,如果我们希望物体运动速度为5象素/秒,程序就会每0.2秒让物体的位置改变1象素。

另外,一维的运动显然不够,如果要产生更为复杂的二维运动效果,就需要为物体的另一维上的运动也增加一个计时器。

还有一个机制也很重要,那就是我们运动物体的运动必然要体现在图像的移动上,物体的位置改变了,就需要在舞台的新位置上重新描绘改物体的图像,不妨称之为图像刷新,那么,为了尽可能的减少系统的负担,我们不能每时每刻都进行图像刷新,只要在需要刷新时再刷新就好。于是我们整个物体运动的运算都先是仅对对象的位置属性进行处理,图像刷新的频率也由我们定,图像刷新越高,运动越平滑,但系统的负担越大,图像刷新越低,物体运动效果越跳跃,但系统的负担会轻些。

原理叙述完毕,下面是一个比较完整的实例(源文件ABSMoveMent.dir),采用面向对象的机制。

首先在通道1、2上添加两个精灵,精灵的位置随大家好啦,然后建立一个父脚本,脚本内容如下:

 

 

Property MySprite--本对象所使用的精灵

Property speed_x,speed_y--运动物体的水平上和垂直方向上的绝对运动速度,其值只能为-1,0,1

Property MyTime--本对象的时钟系统

Property MoveXTimer,MoveXRate--本对象的水平运动计时器和触发水平位置改变的频率

Property MoveYTimer,MoveYRate--本对象的垂直运动计时器和触发水平位置改变的频率

Property RefreshTimer,RefreshRate--本对象物体图像刷新计时器和刷新频率

Property MyLocation--本对象用来控制物体位置的变量

 

on new me,fSpriteNum

  MySprite=sprite(fSpriteNum)

  speed_x=0

  speed_y=0

  MoveYRate=1000

  MoveYRate=1000

  RefreshRate=100

  MyTime=0

  MyLocation=MySprite.loc

  return me

end

 

--总计时器触发事件

on TimeUp me

  MyTime=MyTime+1

  tloc=MyLocation

  --水平运动计时器

  if MyTime-MoveXTimer>MoveXRatethen

    tloc=tloc+Point(speed_x,0)

    MoveXTimer=MyTime

  end if

 

  --竖直运动计时器

  if MyTime-MoveYTimer>MoveYRatethen

    tloc=tloc+Point(0,speed_y)

    MoveYTimer=MyTime

  end if

  MyLocation=tloc

 

  --图像刷新计时器

  if MyTime-RefreshTimer>RefreshRatethen

    MySprite.loc=MyLocation

    updatestage

    RefreshTimer=MyTime

  end if

end

 

--根据希望达到的水平速度数值改变水平位置改变事件的触发频率

on setspeedxme,fspeedvalue

  if fspeedvalue>0 then

    speed_x=1

  else if fspeedvalue<0 then

    speed_x=-1

  else

    speed_x=0

    MoveXRate=1000

    exit

  end if

  fspeedvalue=abs(fspeedvalue)

  if fspeedvalue>1000 then

    fspeedvalue=1000

  end if

  MoveXRate=1000/fspeedvalue

end

 

--根据希望达到的竖直速度数值改变竖直位置改变事件的触发频率

on setspeedyme,fspeedvalue

  if fspeedvalue>0 then

    speed_y=1

  else if fspeedvalue<0 then

    speed_y=-1

  else

    speed_y=0

    MoveYRate=1000

    exit

  end if

  fspeedvalue=abs(fspeedvalue)

  if fspeedvalue>1000 then

    fspeedvalue=1000

  end if

  MoveYRate=1000/fspeedvalue

end

 

建立一个电影脚本,在电影开始时建立两个对象,分别使用前面的两个精灵,并将两个对象的水平速度分别初始化。

on startmovie

  --建立第一个物体

  object1=script("object").new(1)

  object1.setspeedx(5)--将物体水平运动速度设为5

  --建立第二个物体

  object2=script("object").new(2)

  object2.setspeedx(1)--将物体水平运动速度设为1

end

 

然后在电影脚本中用一个死循环来触发两个一建立对象的超时事件,这是为了获得最高的事件触发频率。我们可以用一个个更合理的总计时器来更好地控制各个对象的超时事件的触发。另外,我们将时时判断,如果鼠标按下,则结束程序。

on idle

  repeat while true

    Object1.TimeUp()

    Object2.TimeUp()

    if the mousedown then halt

  end repeat

end

 

好,运行电影,看看物体如愿以偿的运动起来了,改变两个物体的初始速度,看看灵不灵?表面看上去,这与我们以往的效果差不多,但如果我们现在来进行一下碰撞检测,会发现,这个过程变得很简单直接。我们只需时时判断两个物体是否重合相遇就行了,即使运动速度再大,也不用担心会发生“瞬间穿透”现象。

但是,这个运动机制还有很多的问题需要解决,而且,要想应用这种运动机制,需要完善的地方还很多,我们不妨暂时了解这个机制到这种程度,等待未来水到渠成的那一天。

此次研究的目的还有一个,那就是绝对运动下的物体碰撞处理。也许大家会问,怎么,这种机制下的碰撞不是直接判断就可以了吗?是的,这种机制带来的指时最底层碰撞判断的方便,但是想想一下,如果有多个运动物体,且每个物体的运动都很复杂,那么同时检测所有物体之间的相互碰撞就变成了一个庞大复杂的过程,怎样最高效合理地实现这个过程就是我们要解决的一个难题。这个,到现在我也没有一个满意的结果,就交给大家去探究了,所以,此次研究就到这里,不了了之?^_^

碰撞检测原理之模拟运动篇

之所以我们会考虑绝对运动的机制,是因为这种运动机制能够避免“瞬间穿透现象”。但不可否认,目前,想要马上应用这种运动机制是不现实的。那么,如果我们保留原有的运动机制,能否通过一些手段来避免或者说纠正“瞬间穿透现象”呢?答案是肯定的。这里,我们感谢网友Truka的技术支持,正是他的研究成果,使我们的碰撞得到了最现实的解决方案。

以下一段话摘自Truka的技术文章:

 

任何游戏都是基于帧的形式来运作的,就是说游戏中的事物是跳动式进行的(无论它多么接近于连续进行,但用远不可能达到),而自然界的事物则是无限连续的。在检测碰撞这个问题上确实可以用提高检测频度来获得更精确的碰撞点位置,但是我觉得这样做的代价太大,一个游戏必须有稳定的帧速率,提高检测频度无疑会使游戏速度直线下降。所以我在制作桌球的时候放弃了这种思路,取而代之是另一种思路,我称它为穿透纠正。
如果要检测两个物体之间有没有发生碰撞,必须实时监测两者之间的距离(这个代价是很小的)。在两物体发生碰撞之前,它们的边缘距离(对于两个球来说就是它们的圆心距离减去它们的半径之和)必然是在逐渐缩小(否则就不会发生碰撞),在它们发生碰撞之后边缘距离必然是逐渐扩大。如果我们实时监测着这个边缘距离增量值,当检测到增量值由负变为正的一刻,程序就转入穿透纠正模块。
    此时可以确定碰撞发生点肯定在物体此刻的位置和它的上一个位置(上一帧的位置)之间。如果要计算出足够精确的碰撞发生点,我们需要几个值:物体此刻的位置和速度,物体在前面一帧时的位置和速度。这4个值很容易得到。
    接下来是计算足够精确的碰撞发生点。思路是这样的,让物体从上一帧的位置放慢速度慢慢前进到此刻的位置。当然这个过程需要放大,才能得到足够的精度,也就是说水平方向和垂直方向的速度必须分别取上一帧时的值后再按比例缩小。在这个过程中需要检测两物体之间的距离,当它们边缘距离的绝对值在一个允许的范围之内时便停止这个过程并认为此时物体的位置就是碰撞发生位置(对于不规则外形的物体可以用检测重叠函数代替检测边缘距离的过程)。当然在这个过程中如果两物体都在运动的话,两物体的穿透纠正需要同时进行。穿透纠正这个过程是快速的数值推算,是不可见的。另外说明一下,非直线非匀速运动同样适用。

许多时候会发生一些不必要的穿透纠正,也就是说穿透纠正的过程中得不到碰撞点的情况,如果不采取任何措施,程序一样可以正常运行,只是会增加一些不必要的运算量消耗资源。
    如果把满足穿透纠正的条件改一下,增加一项检测就可以在很大程度上减少不必要的穿透纠正发生。这项检测就是两物体间边缘距离是否在满足发生碰撞的最大范围之内(对于两个圆形物体来说,这个范围略大于它们的半径之和,对于不规则物体还需要再扩大一点范围),如果不是则认为是一次不必要的穿透纠正。

这个范围与物体大小有关。对于不规则物体,这个范围不太好确定,此时采用重合函数检测比较可取,因为此时物体的移动可以认为是连续的(推算时的"移动速度"不大于1像素/帧)。

以上就是原理说明,并且感谢Truka提供的源文件范例(TowBalls.dir),在这个实例中除了讨论过的穿透纠正以外,还有嵌入纠正的处理。
    操作方法:按下鼠标左键吸引红球,右键吸引白球。<shift>和<ctrl>可以辅助操作,建议操作时同时按下<shift>和<ctrl>。

不难看出,基于改变每次位置的改变量的运动机制所面临的问题仅仅是在物体碰撞一刹那可能会发生“瞬间穿透”现象,因此我们就要跟踪物体的运动情况和物体之间的距离变化情况以判断物体之间是否发生过碰撞现象,然后再经过计算得出碰撞的精确位置,根据碰撞后的计算结果调整物体的运动状态,这种碰撞解决方案是在Director常用的运动机制的基础上进行的纠正计算,因此,不仅能够保留Director动画机制的优势,由有效地将其劣势减到最低,确实是一种合理的方案。