原文链接:http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-collision-avoidance--gamedev-7777
介绍
进行碰撞躲避的方法实际上非常的简单,就是在我们检测到“将要”和一个碰撞物相撞的时候,我们产生一个躲避的力来避免进行碰撞。记住,我们仅仅使用一个最近的障碍物来计算躲避的力,就算有很多的障碍物,这样的算法同样能够很好的运行。
所以可以看出,这个算法就需要解决两个问题:如何确定最近的物体,以及如何进行躲避力的产生。
读者需要注意,我们这里阐述的碰撞躲避算法并不是使用一种寻路算法来避免碰撞到障碍物。这种算法对于比较复杂的环境可能不是很适用,但是对于相对比较简单的环境来说,这种算法效果更加的出色。
朝向向量
进行碰撞躲避的首要问题就是预先处理碰撞。我们唯一需要躲避的障碍物就是距离我们的人物最近的障碍物。我们首先产生一个称为ahead的向量,这个向量实际上和我们的速度向量方向一致,所以我称之为朝向向量,但是他们的长度并不是相同的:
我们使用如下的方法来计算这个ahead向量的位置:
ahead = position + normlize(velocity) * MAX_SEE_AHEAD ;
这个ahead向量的长度定义了它在距离障碍物多远的时候就能够进行碰撞躲避。MAX_SEE_HEAD越大,那么角色反应的时间就越早,就越有可能躲避成功。但是同样的,如果太大了,那么障碍物在距离角色很远的时候就进行躲避了,所以我们需要定义合适的值:
碰撞检测
为了能够进行碰撞检测,我们这里将所有的障碍物都假设为圆形的。使用圆形的障碍物,效果非常的好,也容易实现。
为了进行碰撞检测,我们需要对使用直线-圆相交的碰撞检测,但是在这里我会使用一个更加简单有效的方法。
我们使用ahead向量再次的创建一个向量ahead2,这个向量和ahead向量方向一致,只是长度只有它的一半:
我们使用下面的方法来计算这个ahead2向量:
ahead = position + normlaize(velocity) * MAX_SEE_AHEAD ;
ahead2 = position + normlaize(velocity) * MAX_SEE_AHEAD * 0.5 ;
进行碰撞检测,我们只需要判断这两个点是否在障碍物所构成的圆形中就可以。
如果他们之间的距离小于或者等于圆形的半径,那么我们就认为这个碰撞发生了:
当d < r的时候,我们就认为预先碰撞发生了。
如果这两个点都不在原型中,那么就认为没有发生碰撞,下面是进行碰撞的代码:
private function distance(a :Object, b :Object) :Number { return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); } private function lineIntersectsCircle(ahead :Vector3D, ahead2 :Vector3D, obstacle :Circle) :Boolean { // the property "center" of the obstacle is a Vector3D. return distance(obstacle.center, ahead) <= obstacle.radius || distance(obstacle.center, ahead2) <= obstacle.radius; }
如果有多个物体挡住了路,那么我们只对最近的一个障碍物进行处理:
计算躲避力
躲避力必须要将角色推开,这样才能够使角色躲避掉障碍物。我们可以用如下的代码来计算躲避力:
avoidance_force = ahead - obstacle_center
avoidance_force = normalize(avoidance_force) * MAX_AVOID_FORCE
当我们有了躲避力之后,我们就用MAX_AVOID_FORCE来对躲避力进行缩放,这样就能够得到某个方向上特定大小的躲避力了。
躲避障碍物
下面是整个collisionAvoidance的实现,这个函数用来产生一个躲避力:
private function collisionAvoidance() :Vector3D { ahead = ...; // calculate the ahead vector ahead2 = ...; // calculate the ahead2 vector var mostThreatening :Obstacle = findMostThreateningObstacle(); var avoidance :Vector3D = new Vector3D(0, 0, 0); if (mostThreatening != null) { avoidance.x = ahead.x - mostThreatening.center.x; avoidance.y = ahead.y - mostThreatening.center.y; avoidance.normalize(); avoidance.scaleBy(MAX_AVOID_FORCE); } else { avoidance.scaleBy(0); // nullify the avoidance force } return avoidance; } private function findMostThreateningObstacle() :Obstacle { var mostThreatening :Obstacle = null; for (var i:int = 0; i < Game.instance.obstacles.length; i++) { var obstacle :Obstacle = Game.instance.obstacles[i]; var collision :Boolean = lineIntersecsCircle(ahead, ahead2, obstacle); // "position" is the character's current position if (collision && (mostThreatening == null || distance(position, obstacle) < distance(position, mostThreatening))) { mostThreatening = obstacle; } } return mostThreatening; }
躲避力必须对物体的速度产生影响,如果是基于力的系统,我们就简单的将力积蓄就可以。
steering = nothing(); // the null vector, meaning "zero force magnitude" steering = steering + seek(); // assuming the character is seeking something steering = steering + collisionAvoidance(); steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering, max_speed) position = position + velocity
由于每一次更新,我们都重新的计算躲避力,所以只要还在躲避的范围,就会一直受到这个躲避力的作用,从而躲避开来。
如果ahead向量没有与障碍物发生碰撞,那么就不需要进行躲避了。
Oops,博主自己的话语
上面的东西,只是简单对原文进行了翻译。为了很好的理解这个概念,我自己做了一个Demo。读者需要注意,上面的方法是基于力的,如果你的项目并不是基于力(和博主自己的Demo一样),而只是简单的使用速度的话,同样也可以实现。下面我用cocos2d-x来实现一个Demo。
首先,我创建四个障碍物,如下所示:
//Create 4 obstacles for(int i = 0 ; i < 4 ; i ++) { float x = 100 ; float y = 100 ; if(i >= 2) x = 380 ; if(i == 1 || i == 3) y = 220 ; m_Obstacals[i] = CCSprite::create("Obstacle.png"); m_Obstacals[i]->retain(); m_Obstacals[i]->setPosition(ccp( x, y )); this->addChild(m_Obstacals[i]); }
然后创建一个角色:
//Create the character m_Character = CCSprite::create("Character.png"); m_Character->retain(); m_Character->setPosition(ccp(0,0)); this->addChild(m_Character); m_velocity = ccp(2,4);
这个很简单,只是在窗口中贴上几张图而已。
下面是场景的Update方法,主要的任务都是在这里进行的:
void HelloWorld::update(float dt) { CCPoint pos = m_Character->getPosition(); pos = ccpAdd(pos, m_velocity); m_Character->setPosition(pos); if(pos.x < 0 || pos.x > 480) m_velocity.x =- m_velocity.x ; if(pos.y < 0 || pos.y > 320) m_velocity.y = - m_velocity.y ; //Collision avoidance //------------------------------------------------ //Step 1: Find the most closest obstacal CCPoint c_pos = m_Character->getPosition(); float length = FLT_MAX ; int index = -1 ; for(int i = 0 ; i < 4 ; i ++) { CCPoint temp_pos = m_Obstacals[i]->getPosition(); temp_pos = ccpSub(temp_pos, c_pos); float temp_length = ccpLength(temp_pos); if(temp_length < length) { length = temp_length ; index = i ; } }// end for //Step2: Check for collision CCPoint ob_pos = m_Obstacals[index]->getPosition(); float ob_radious = m_Obstacals[index]->getContentSize().width/2 ; float dynamic_velocity = MAX_SEE_VALUE ; CCPoint ahead = ccpAdd(c_pos ,cocos2d::ccpMult(ccpNormalize(m_velocity), dynamic_velocity)); CCPoint ahead2 = ccpAdd(c_pos ,cocos2d::ccpMult(ccpNormalize(m_velocity), dynamic_velocity * 0.5)); if(ccpDistance(ahead, ob_pos) > ob_radious && ccpDistance(ahead2, ob_pos) > ob_radious && ccpDistance(c_pos, ob_pos) > ob_radious) return ; //Step3 : Do avoidance CCPoint nor_velocity = ccpNormalize(m_velocity); CCPoint toCenter = ccpSub(ob_pos, c_pos); toCenter = ccpNormalize(toCenter); CCPoint avoidance_velocity = ccp(nor_velocity.y, -nor_velocity.x); if(ccpDot(toCenter, avoidance_velocity) > 0) { avoidance_velocity = ccp(-nor_velocity.y, nor_velocity.x); }// end if m_velocity = ccpAdd(m_velocity, ccpMult(avoidance_velocity, MAX_AVOIDANCE_SPEED)); if(ccpLength(m_velocity) > MAX_SPEED) { m_velocity = ccpMult(ccpNormalize(m_velocity), MAX_SPEED); } }
这个函数很简单,首先我们使用速度来对角色的位置进行更新。
然后进行边界检查,判断是否出了屏幕区域,如果是,就将速度取反。
下面的就是进行碰撞躲避的算法了。
第一步,找到最近的障碍物,这个很简单,只要遍历所有的障碍物列表,然后计算最近的障碍物即可。
第二步,判断是否进行了碰撞。这个就和原文中使用的算法一致,加上判断角色是否已经在障碍物里面的条件。
第三部,进行躲避,由于不是基于力的系统,所以我就简单的计算一个躲避速度,然后把这个速度加到物体原有的速度上即可。但是注意,我这里计算躲避速度的方法与原文不一样。原文是使用ahead - center的方式来计算一个躲避力。这种方法,在物体不是直接朝着障碍物移动的时候,有效。但是,当角色直接的相障碍物移动的时候,你会发现这样计算出来的力的方向与物体移动的方向刚好相反,也就是说,会将物体向后推走,这样的方法,在屏幕显示上感觉会很怪,不是很平滑。所以我使用垂直于角色速度方向的方向作为躲避力的方向,这样不管是不是直接朝着障碍物中心移动,我们都能够平滑的躲避。
这里需要注意,垂直于速度方向上的向量有两个,他们互相相反,如何选取哪一个向量了?我们来看下面的图:
我们从上图中可以看出,有normal1和normal2两个向量,很明显,我们要旋转normal1,但是为什么了?因为它更容易使物体进行躲避。也就是说,我们只要能够判断“更容易是物体进行躲避”的条件就可以了。
这个条件就是将normal向量与toCenter向量进行点积操作,如果值小于0那么就是我们想要的那个向量,如果大于0就不是我们想要的。如果是0的话,那么随便哪一个都可以,因为这时物体是直接向障碍物中心移动的。
好了,就到这里了。下面是这个Demo的截图:
完整源代码请到这里下载:
Understanding Steering Behavior: Collision Avoidace(行为控制之:碰撞躲避)