AS3实现RPG游戏地图引擎

来源:互联网 发布:传奇霸业至尊宝石数据 编辑:程序博客网 时间:2024/05/01 07:21

 

最近在学习rpg碰撞检测,找到一遍好文,现在贴出来和大家一起分享
原文来自于http://bbs.mhhf.net/showtopic-361197.html,作者是SKDD


目前已经完成精确到像素的任意不规则形状的碰撞检测算法。同时也考虑将地图信息转为二维数组以便A*自动寻径。这样在操作上用碰撞,AI上用A*,一定能给玩家带来完美的游戏体验。

这里的碰撞检测主要依靠封装好的类PPCD(perfectPixelCollisionDetection),它的接口如下:

类PPCD:

1. 返回发生碰撞的矩形边界:
  1. getCollisionRect (        target1:DisplayObject,        
  2.         target2:DisplayObject,        
  3.         commonParent:DisplayObjectContainer,
  4.         
  5.         pixelPrecise:Boolean = true,
  6.         
  7.         tolerance:Number = 0        ):Rectangle
复制代码
2. 返回发生碰撞的矩形边界的中心点:
  1. getCollisionPoint (        target1:DisplayObject,        
  2.         target2:DisplayObject,        
  3.         commonParent:DisplayObjectContainer,
  4.         
  5.         pixelPrecise:Boolean = true,
  6.         
  7.         tolerance:Number = 0        ):Point
复制代码
3. 验证两个对象是否碰撞:
  1. isColliding (        target1:DisplayObject,        
  2.         target2:DisplayObject,        
  3.         commonParent:DisplayObjectContainer,
  4.         
  5.         pixelPrecise:Boolean = true,
  6.         
  7.         tolerance:Number = 0        ):Boolean
复制代码
参数说明:

target1,target2:需要做碰撞检测的两个对象;
commonParent:两个对象所在的共同的容器;
pixelPreciese:是否精确到像素。若设置为false,则和DisplayObject.hitTestObject()工作原理相同。
tolerance:容许量,默认为0。这个数值设置开始进行碰撞检测的DisplayObject的最小alpha值。比如有个对象有阴影效果,其阴影的alpha值大概在20以下。那么如果在进行碰撞检测的时候不希望把阴影也包含进去,则需要将tolerance设置为20。否则另外一个物体碰到阴影也算有碰撞。


工作原理说明:

1. 找出两个对象边界矩形的交叉区域,若没有则没有发生碰撞
2. 创建一个与交叉区域同样大小的bitmap
3. 将第一个对象相交的部分以红色绘制在bitmap中
4. 将第二个对象的相交部分以白色绘制在bitmap中,BlendMode(混合模式)为DIFFERENCE。
5. 找出颜色为cyan的区域并返回其边界矩形。若没有则说明没有发生碰撞

此算法快速高效,一次检测只需要花2-3ms的时间。详细的算法参见源文件PPCD.fla,另有2个SWF演示:

1. 拖动小球,当小球碰到不规则物体时碰撞区域的边界矩形会以绿色标记出来,同时bitmap会在左上角显示。注意物体的阴影部分没有参与碰撞检测(合理设置tolerance参数)。
http://www.namipan.com/d/5e62326 ... 44e3767b05207070000
http://www.fs2you.com/files/98e07085-49dc-11dd-b7b8-0014221f4662/

2. 纯演示动画,看就好。^-^
http://www.namipan.com/d/6456a50 ... cbda4e56ea196070000
http://www.fs2you.com/files/98f32f1c-49dc-11dd-991f-0014221f4662/

有了这样一个强大的类,像素级的碰撞检测就不成问题了。不过因为PPCD类返回的是边界矩形,以及人物可以在不同的方向以不同的方式碰撞,再加上人物碰到障碍物之后需要有一定的自动“滑动”的功能,碰撞检测的逻辑还是挺复杂的,下面简要介绍一下。


碰撞检测逻辑

比较长和复杂,因为碰撞情况很多,准备好@_@哦? ^^b
详细算法在5楼和6楼,帖子超长了,呵呵。


SWF效果预览:

曲线为主:
http://www.namipan.com/d/9862425 ... d6a5c4fd2276e1b0000
http://www.fs2you.com/files/98fb1375-49dc-11dd-998c-0014221f4662/

直线为主:
http://www.namipan.com/d/4205132 ... b458a61e8386d1b0000
http://www.fs2you.com/files/98fda719-49dc-11dd-9109-0014221f4662/

源文件:http://www.namipan.com/d/f960d7b ... 799d0efcb500c930100
http://www.fs2you.com/files/99003323-49dc-11dd-9d8e-0014221f4662/


目前没有发现BUG,不过还没有经过仔细调试,不敢保证完美。如果有朋友不幸遇见BUG了M我一下呀,谢谢咯~~ ^-^


PPCD.zip (100.76 KB)

 

 

这个是源文件

 

 

首先来回忆一下人物移动在程序中的控制方法。舞台上的人物man具有成员dirx和diry,它们的值可以等于-step,0或step。step是每次移动的步长。man在发生ENTER_FRAME事件的时候将自己的x和y分别以dirx和diry递增,以达到移动的目的。当玩家按下键盘或鼠标后,人物需要运动的方向由onDirEvent或onMouseEvent函数解析出来并存放到dirArr数组,然后调用函数setDir来适当更改man.dirx和man.diry以改变人物的运动状态。

人物的8方向运动包含4个轴向和4个斜向。斜向分解开来其实就是人物同时在x向和y向运动,所以在写代码的时候是不需要单独处理的。那么只需要考虑人物在一个轴向运动时可能发生的各种碰撞情况,另一个轴向如法炮制就可以了。在具体讨论之前,先有必要介绍一下判断中需要用到的一些MC工具和一些变量的定义。

直接用在地图上跑动的人物图像来判断碰撞并不是一个好办法。因为如果直接让人物去碰障碍物,画面上的显示要么是人会挡住障碍物被碰到的部分,要么是障碍物挡住人被碰到的部分,而无论哪一种都是不理想的画面。我们要做的是:在发生遮挡之前就判断好人物下一步运动是否会碰到障碍物而发生重叠遮挡,如果是的话就根本不让人物再继续前进了。

要做到这个判断其实很简单。假设所有人物跑动的动画都封装在一个限定大小的MC里,人物跑动的步长为step。如下图所示:
PPCDvars.jpg PPCDsitu1.JPG 
我的人物跑动动画每一帧的大小始终都不会超出图中蓝色矩形部分。而红色矩形是一个在各个方向都比蓝色矩形宽一个step长度的矩形(4个顶点被另外的MC挡住了,以后说明)。现在用红色矩形(名为blk)代替实际人物动画来判断与场景中障碍物的碰撞情况就没有问题了。若障碍物碰到了blk,如情况B,人物停止向障碍物的方向运动。如果没有碰到,最极端的情况是A,障碍物刚好与blk擦边。但blk比man宽一个step,下一步人物也只会运动到C位置,也就是障碍物刚好与实际人物动画擦边。这样就避免了碰撞检测中的遮挡问题。实际应用时,将blk放到人物运动动画的一个图层里,并在人物初始化的时候设置blk的visible属性为false就好了。

如上两幅图所示,这里引进了几组新变量,它们的意思解释如下:
假设人物与障碍物碰撞了,并且使用PPCD.getCollisionPoint得到了碰撞边界矩形的中心点(如图B、C中的点和方框所示):
  1. var pt:Point=PPCD.getCollisionPoint(this.blk, map, mapCon);
复制代码
要知道man在哪个方向、多远距离碰到了障碍,只需要比较man的坐标(x, y)和pt的坐标(pt.x, pt.y)就行了。这里对这两个点的坐标作差:
var dx:Number=pt.x-this.x;
var dy:Number=pt.y-this.y;

使用dx和dy的方便之处在于它们的符号就代表着碰撞发生的方向。为了以后算法的方便,还有两个变量分别保存上一次碰撞时的dx和dy。当没有碰撞时,这两个值将被清零:
var dxl:Number=0;
var dyl:Number=0;

注意,若仅在x轴向做运动时,pt.x最多只会在C图的位置。换句话说,dx的绝对值应该大于等于w,其中w的定义为:
var w:Number=(width-step)/2;
同理也有var h:Number=(height-step)/2;
这在后面判断“擦边”运动时会用到。另外,因为blk放进了man里,所以这里man的width和height属性其实就别是blk的宽和高。

好了,现在开始正题,碰撞检测。

原理很简单:碰撞肯定是由人物向某个方向(这里假定为x负方向,如情况A所示)运动产生的。为了检测方便,先引入两个变量来存放当前人物的运动方向:
var dirxl:int=sign(dirx);
var diryl:int=sign(diry) ;
其中sign(n)是符号函数,n为正返回1,为负返回-1,等于0则返回0。这两个变量也将在没有碰撞时被清零。

既然在这个运动方向碰撞了,则要禁止人物再向该方向运动。对于这里假定的x向就是:
if(dx!=0 && Math.abs(dy)<h){
  停止再向这边运动!
}
之所以要加一句Math.abs(dy)<h是由于下列这样的情况:
PPCDsitu7.jpg 
如上图,虽然有碰撞点但这时x向并不是真正的有障碍。这种障碍碰到了人物边缘但人物仍可以“擦边”运动的情况只需要做:
if(dx!=0 && Math.abs(dy)>=h){
  if(dirx){
    dirxl=sign(dirx);
  }
}
为什么要把运动方向存进dirxl稍后就清楚了。

要让人物在x负向停下来,这里不可以直接做操作man.dirx=0。因为dirx和diry是函数setDir运作的关键,在setDir外随意它们更改会导致setDir不能正常工作。那么这里再引进两个新的局部变量:
var ctrx:uint=1;
var ctry:uint=1;

并将man的位置递增语句改为:
this.x+=dirx*ctrx;
this.y+=diry*ctry;
这样若要停止人物运动,只需要将相应的ctr变量赋值为0就好了。

禁止人物再向该方向运动的办法如下:
dirxl=sign(dirx);
if(dirx*dirxl>0){
  ctrx=0;
}

先将碰撞时人物的运动方向存进dirxl。当人物继续向该方向运动时会使ctrx=0,人物被停止向该方向递增。而当人物改变了运动方向后,ctrx依然为1,人物被允许以dirx递增x位置。这就是为什么特意引进dirxl、diryl的原因:使程序保存造成碰撞的运动方向,并拿它与当前人物的运动方向来对比,进而通过ctrx、ctry决定是否允许人物继续运动。

碰到障碍后仅仅让人物在舞台上停止运动并不是个好主意,这只会让这个RPG游戏的主人公看起来很呆。更好的做法是:让程序能够自动使人物滑离障碍物从而继续前进。有了之前定义的众多变量的帮助,其实这一点太容易办到了:
if(dirx!=0 && diry==0){
  this.y+=-sign(dy)*step;
  if(dy){
    diryl=-sign(dy);
  }
}

别忘了之前说过,使用dx和dy的方便之处在于它们的符号就代表着碰撞发生的方向。要滑离障碍物,只需要向dy的反方向走就行了。如果玩家没有手动控制y向移动,则让代码来帮他吧,只是别忘了把y向的运动方向保存进diryl,因为在向y向的运动途中也可能再碰到障碍,这也就带出了逻辑最复杂的地方。



如下图所示,情况A、B、C都是代表人物向x负方向移动碰到障碍的情况,上面已经讲述过了。而D、E、F则是在x负向被禁止后,玩家手动(擦边运动)或代码滑动人物在y向的位置时可能产生的碰撞情况。
PPCDsitu2.jpg 
D其实已经解决了解决:若是从手动控制人物在y向“擦边”走得来,y向运动则会在y轴对应的if(dy!=0 && Math.abs(dx)<w)判断里被停止;若是代码自动滑动得来,由于碰撞后dy反号了,代码会再把人物重新向上滑动。而E则是新情况了:手动的话虽然y向有if(dy!=0 && Math.abs(dx)<w)阻止,但此时dx=0!这样会造成禁止x负向运动的判断if(dx!=0 && Math.abs(dy)<h)被跳过。代码的话dx=0,滑动也会被跳过。而F更糟糕:dx和dy都等于0!所以现在得为这两种情况分别添写新处理方案,否则这些碰撞将不被处理,人物将可以穿墙而过!

好在E的dy还不为0,至少y向运动还有if(dy!=0 && Math.abs(dx)<w)阻止,这个判断也正是处理y向上对应的A、B、C情况的代码。由于禁止x向运动的代码被跳过了,这里需要再在这个if里面嵌套个if(dx==0)来补上,这样情况E就解决了:
if(dx==0){
  if(dirxl && Math.abs(dy)<h){
    if(dirx*dirxl>0){
      ctrx=0;
    } else if(dirx*dirxl<0){
      dyl==100;
      dirxl=0;
      unHit=true;
    }
  }
}
其中else语句中的清零是为了防止bug,马上在下一种碰撞情况G里有解释,它对应的情况如下图所示:
PPCDsitu5.jpg 
要导致dx=0还有一种情况,那就是大图中最后一种情况G:最初向上走然后碰到一个刚好在人物y中轴线上的障碍,或者一个大面积的Y向障碍。这种情况也一并在这个if(dy!=0 && Math.abs(dx)<w)里的if(dx==0)里解决。

如果发生情况G了,虽然dx=0,但人物仍可以在x轴向上左右擦边运动,并且可能碰到障碍,如下图所示:
PPCDsitu3.jpg 
碰到障碍后的明显变化是dy由原来的>h变为<h,所以这里要处理由G擦边运动而产生的碰撞思路很简单:
1. 若在擦边运动,存运动方向dirxl=sign(dirx);(前文第一次提到擦边时已经这样处理了);
2. 存当前擦边时的dy值:dyl=dy;
3. 若dy没变,则没有碰撞;若dy变了,撞了,则停止该x轴向运动;
4. 撞了若向相反的x向运动,则认为离开x向障碍,开始了新的擦边运动,dirxl清零;

这里要注意的是,如果反向x运动的话需要清零dirxl,这是因为只要反向走一步就一定没有再碰到了x向障碍了,而继续反向行走则可能遇到新的x向障碍,若不清零dirxl将导致反向x障碍穿墙bug。其中还有的dxl和unHit清零在下面的算法代码里解释。

上面的思路翻译为代码则是下面第二个else中的语句:
if(dx==0){
  if(dirxl && Math.abs(dy)<h){
    if(dirx*dirxl>0){
      ctrx=0;
    } else if(dirx*dirxl<0){
      dyl==100;
      dirxl=0;
      unHit=true;
    }
  } else {

    if(dyl==100){// 记录这时的dy
      dyl=dy;
    }


    if(dy!=dyl){// dy变了,说明x方向也遇到障碍
      if(unHit){// 记录此时x运动方向
        dirxl=sign(dirx);
        unHit=false;
      }

      if(dirx*dirxl>0){// 不可再向该x方向运动
        ctrx=0;
      } else if(dirx*dirxl<0){ // 反向x运动,认为x向没有碰撞了
        dyl=100;
        dirxl=0;
        unHit=true;
      }
    }

  }
}

引入unHit是因为碰到x向障碍后dy会变。但是若此时要反x向走离开x向障碍,dy也会变。而只有第一中情况的dy变化需要阻止当前x向运动,所以添加一个unHit来区分这两种情况。否则人物在擦边运动时碰到新障碍就被“黏住”走不掉了。同样要注意的是反x向行走后的清零防止bug。

好了,到这里就只有最后一种情况F没有处理了。其实很简单,因为导致情况F碰撞的x、y运动方向之前一定都已经存在dirxl、diryl里了,这里要做的只是简单的屏蔽,这表现在最后两个if判断:
if(dx==0 && dy==0){
  var ptArr:Array=hitPoints();
  if(ptArr.length==1){
    dirxl=-ptArr[0].x;
    diryl=-ptArr[0].y;
  }


  if(dirxl==0 && dirx!=0){
    dirxl=sign(dirx);
  }
  if(diryl==0 && diry!=0){
    diryl=sign(diry);
  }

  if(dirx*dirxl>0){
    ctrx=0;
  }
  if(diry*diryl>0){
    ctry=0;
  }
}

唯一的例外是下面的情况:
PPCDsitu6.jpg 
解决这个问题的办法也很简单:倒数第三、第四个if:记录当前运动方向并阻止。

最后需要特别注意的情况是两边都全部被碰到,如下图:
PPCDsitu4.jpg 
虽然这种情况的得来和判断其实是和情况F一样的,但考虑到这种情况和其它碰撞情况之间的转换以及各种变量清零的麻烦和可能导致的bug,就特意写了一小段新算法来处理。

这种情况的处理思路上也很简单:有3个顶点都被撞到了,那找出剩下一个顶点的方向并只允许朝它的方向运动就好了。这其实就是最开始的代码段
var ptArr:Array=hitPoints();
if(ptArr.length==1){
  dirxl=-ptArr[0].x;
  diryl=-ptArr[0].y;
}
做的事情。而判断各个顶点是否被碰撞借用了挡住红色blk顶点的那4个绿色顶角正方形。hitPoints函数首先找出没有被碰撞的顶点,然后把这个顶点的方向push到一个数组并返回。如上图例子,ptArr就是一个含有一个Point的数组:prArr[0]=(1, -1)。

至此所有的碰撞检测情况就都分析完了。虽然情况繁多但逻辑并不晦涩,循序渐进仔细理清调理就好了。实际的检测代码也并不长。完整的检测代码参见源文件。