AI 编程实践

自治的可移动游戏智能体

SteeringBehaviors(转向行为)

这些方法都返回执行该行为需要的力(实际是返回单位质量的物体单位时间内修正速度需要的加速度,根据F=m(v1-v0)/t, 这个返回值也是单位质量的物体具有该加速度需要的力)。

Seek (靠近某个位置)

//返回一个能让物体朝向目标移动的力
Vector2D SteeringBehavior::Seek(Vector2D TargetPos)
{
  //由目标距离和当前距离算出“期望速度”,大小是最大速度
  Vector2D DesiredVelocity = Vec2DNormalize(TargetPos - m_pVehicle->Pos())
                            * m_pVehicle->MaxSpeed();
  //期望速度 - 当前速度 = 加速度
  return (DesiredVelocity - m_pVehicle->Velocity());
}

Flee (离开某个位置)

或者叫远离某个位置。

Vector2D SteeringBehavior::Flee(Vector2D TargetPos)
{
  /* PanicDistanceSq 可以设定逃逸距离,在这个距离内,才发生逃逸。
  if (Vec2DDistanceSq(m_pVehicle->Pos(), target) > PanicDistanceSq)
  {
    return Vector2D(0,0);
  }
  */

  //Flee和和Seek一样,只是方向正好相反
  Vector2D DesiredVelocity = Vec2DNormalize(m_pVehicle->Pos() - TargetPos) 
                            * m_pVehicle->MaxSpeed();

  return (DesiredVelocity - m_pVehicle->Velocity());
}

Arrive (抵达某个位置)

Seek行为无法停在目标处,而是不断在目标位置穿来穿去。Arrive可以让物体慢慢停在目标位置

/**
*@param:deceleration  {enum Deceleration{slow = 3, normal = 2, fast = 1};}
*/
Vector2D SteeringBehavior::Arrive(Vector2D     TargetPos,
                                  Deceleration deceleration)
{
  Vector2D ToTarget = TargetPos - m_pVehicle->Pos();

  //当前位置到目标位置距离
  double dist = ToTarget.Length();

  if (dist > 0)
  {
    //根据这个Scalar和Deceleration计算到达目标位置的期望速度(不需要精确)。
    const double DecelerationTweaker = 0.3;
    //speed是希望物体到达目标时的速度大小
    double speed =  dist / ((double)deceleration * DecelerationTweaker);     

    //确保速度不超过最大速度
    speed = min(speed, m_pVehicle->MaxSpeed());
    
    // ToTarget / dist * speed更好理解, 向量/大小就是normal的过程
    Vector2D DesiredVelocity =  ToTarget * speed / dist;

    return (DesiredVelocity - m_pVehicle->Velocity());
  }

  return Vector2D(0,0);
}

Pursuit (追逐、拦截)

追逐(其实叫拦截更恰当)是预测目标的运动轨迹,然后往预测位置走。预测的难点在于预测目标走多远两者碰头。
很明显,这个“多远”正比于两者的距离,反比于两者的速度。当知道了“多远”,就能算出追逐者seek的位置。

/**
*@param: Evader {Vehicle} 逃避者
*/
Vector2D SteeringBehavior::Pursuit(const Vehicle* Evader)
{
  //当前位置的偏移向量
  Vector2D ToEvader = Evader->Pos() - m_pVehicle->Pos();
  //和目标运动方向的趋近程度(1是同方向,-1是反方向) 
  double RelativeHeading = m_pVehicle->Heading().Dot(evader->Heading());
  if(ToEvader.Dot(m_pVehicle->Heading()) > 0 && RelativeHeading < -0.95) //acos(0.95)=18°, (< -0.95)表示和目标运动反方向的夹角在18°以内,基本面对面
  {
    //直接seek目标位置
    return Seek(Evader->Pos());
  }

  //不是面对面的话,需要预测目标继续走多久,我们能追上
  //计算碰头的时间,这是个经验公式(正比于两者的距离,反比于两者的速度),为了平衡性能和准确度
  double LookAheadTime = ToEvader.Length() / (m_pVehicle->MaxSpeed() + evader->Speed());
  //为了给AI转向目标位置的时间,可以适当增加这个预计的碰头时间
  LookAheadTime += TurnAroundTime(m_pVehicle, evader->Pos());
  Vector2D FuturePos = evader->Pos() + evader->Velocity() * LookAheadTime ;
  return Seek(FuturePos);
}

/**
* 计算原地转向的时间
*/
double TurnAroundTime(const Vehicle* pAgent, Vector2D TargePos)
{
  //确定到目标的标准向量
  Vector2D ToTarget = Vec2DNormalize(TargePos - pAgent->Pos());
  double dot = pAgent->Heading().Dot(ToTarget);
  
  //设定一个系数,pAgent的最大转弯率越高,这个值越小。(原文“这个值越大”有误,m_dMaxTurnRate是每秒最大旋转弧度,显然应该成反比)
  //这个系数也正比于[转动角位移]
  //如果pAgent正朝向到目标位置的反方向,那么0.5意味着这个函数返回1秒的时间让pAgent转弯
  const double coefficient = 0.5;

  //如果目标在正前方,那么dot=1,返回0秒
  //如果目标在正后方,那么dot=-1,返回1秒
  return (dot - 1.0) * -coefficient; //依旧是个经验公式
}

Evade (逃避)

Evade是Pursuit的相反行为。

/**
* 和pursuit一致,除了AI体远离追赶者预计的位置
*/
Vector2D SteeringBehavior::Evade(const Vehicle* pursuer)
{
  //这次没有必要检查是否面对面了
  Vector2D ToPursuer = pursuer->Pos() - m_pVehicle->Pos();
  
  //在追赶者的威胁范围内,才逃避
  const double ThreatRange = 100.0f;
  if(ToPursuer.LengthSq() > ThreatRange * ThreatRange)
      return Vecter2D(/**0*/); 

  //计算碰头的时间,这个和pursuit一致
  double LookAheadTime = ToEvader.Length() / (m_pVehicle->MaxSpeed() + evader->Speed());
  //这里使用Flee远离这个位置
  Vector2D FuturePos = pursuer->Pos() + pursuer->Velocity() * LookAheadTime ;
  return Flee(FuturePos);
}

Wander (徘徊)

Wander的做法是在Agent前方设置一个圆,圆上面找一个点,这个点每一帧随机偏移一个距离,然后新位置投射回圆上作为点的下一个位置。

Vector2D SteeringBehavior::Wander()
{ 
  //Wander行为依赖于帧率
  //m_dWanderJitter 每秒加到目标点的抖动位移
  double JitterThisTimeSlice = m_dWanderJitter * m_pVehicle->TimeElapsed();

  //随机偏移  RandomClamped() => RandFloat() - RandFloat(),返回(-1, 1) 
  m_vWanderTarget += Vector2D(RandomClamped() * JitterThisTimeSlice,
                              RandomClamped() * JitterThisTimeSlice);

  //把目标点投射回圆上面
  m_vWanderTarget.Normalize();
  m_vWanderTarget *= m_dWanderRadius;

  //m_dWanderDistance wander圈突出在Agent前面的距离
  //这里agent本地坐标系的前边是X轴的方向
  Vector2D target = m_vWanderTarget + Vector2D(m_dWanderDistance, 0);
  //把target转换到世界空间
  Vector2D TargetWorld = PointToWorldSpace(target,
                                       m_pVehicle->Heading(),
                                       m_pVehicle->Side(),
                                       m_pVehicle->Pos());

  //这里返回的是转向力,是的这里不再是DisiredVolocity - Volocity,在Craig Reynolds的论文里,TargetWorld - m_pVehicle->Pos()就是转向力
  return TargetWorld - m_pVehicle->Pos();
}

Obstacle Avoidance (障碍躲避)

使用圆(3D用球)来描述障碍物,使用长方形盒(3D用圆柱)来描述操控的物体,盒子的宽等于物体的包围半径,长正比于速度
算法总体分2部分:首先寻找要躲避的障碍物,然后计算躲避的转向力。

寻找要躲避的障碍物

详细步骤如下:

  1. 寻找并标记在物体检测范围内的障碍物。(检测范围:2者的距离小于长方形盒的长加障碍物半径的和)
  2. 将标记的障碍物换算到目标的本地空间,这一步将去掉目标后边的物体(换算后X坐标小于0)。
  3. 扩大障碍物半径,找出和物体重叠的障碍物。(障碍物换算后的Y坐标绝对值,是否小于它的半径+操控物体AABB宽的一半)
  4. 这里只剩下和物体相交的障碍物了。接下来我们寻找离物体最近的相交点。通过简单的线-圆交点算法求出相交的点(交点的Y=0,直接通过圆方程求X),然后过滤出有最小正交点的障碍物。
Vector2D SteeringBehavior::ObstacleAvoidance(const std::vector<BaseGameEntity*>& obstacles)
{
     /**查找要躲避的障碍物*/
    
  //检测用的AABB,长度与速度正相关
  m_dDBoxLength = Prm.MinDetectionBoxLength * (1 + m_pVehicle->Speed() / m_pVehicle->MaxSpeed());

  //如果障碍物的圆在以物体AABB的长度为半径的圆里,标记这个障碍物
  m_pVehicle->World()->TagObstaclesWithinViewRange(m_pVehicle, m_dDBoxLength);

  //保存最近的相交的障碍物
  BaseGameEntity* ClosestIntersectionObstacle = NULL;
  //保存到最近的相交的障碍物的距离
  double DistToClosestIP = MaxDouble;
  //保存物体本地坐标下最近的相交的的障碍物坐标
  Vector2D LocalPosOfClosestObstacle;

  std::vector<BaseGameEntity*>::const_iterator curOb = obstacles.cbegin();

  while(curOb != obstacles.cend())
  {
    if((*curOb)->IsTagged())
    {
      //将障碍物的坐标转到物体的本地坐标系
      Vector2D LocalPos = PointToLocalSpace((*curOb)->Pos(),
                                            m_pVehicle->Heading(),
                                            m_pVehicle->Side(),
                                            m_pVehicle->Pos());
      //过滤掉X为负值的障碍物,因为它在物体后边
      if(LocalPos.x >= 0)
      {
        //如果障碍物到X轴的距离(Y的绝对值)小于它的半径加物体AABB宽度一半(物体半径),那么它就有可能是我们要查找的障碍物
        double ExpandedRadius = (*curOb)->BRadius() + m_pVehicle->BRadius();
        if(fabs(LocalPos.y) < ExpandedRadius)
        {
          //现在做线(物体的X轴)圆(障碍物,半径为ExpandedRadius)的相交测试,我们需要障碍物与X轴的交点全部在正半轴。
          //cX,cY为圆心,根据圆的方程,与X轴的交点坐标为: x = cX +/- Sqrt(r^2 - (y-cY)^2 ), y = 0
          double cX = LocalPos.x, cY = LocalPos.y;
          
          double SqrtPart = sqrt(ExpandedRadius * ExpandedRadius - cY * cY);
          
          //只需要计算x的最小正值
          double ip = cX - SqrtPart;
          if(ip <= 0.0)
          {
            ip = cX + SqrtPart;
          }

          //记录最近的障碍物数据
          if(ip < DistToClosestIP)
          {
            DistToClosestIP = ip;
            ClosestIntersectionObstacle = *curOb;
            LocalPosOfClosestObstacle = LocalPos;
          }
        }
      }
    }

    curOb ++;
  }
}

计算躲避的转向力

操控力通常分为2部分:侧向操控力和制动操控力。

{
  ...
  /**第二步计算转向力*/
  Vector2D SteeringForce;

  if(ClosestIntersectionObstacle)
  {
    //越接近障碍物,转向力应该越大
    double multiplier = 1.0 + (m_dDBoxLength - LocalPosOfClosestObstacle.x) / m_dDBoxLength;

    //计算侧向力
    SteeringForce.y = (ClosestIntersectionObstacle->BRadius() - LocalPosOfClosestObstacle.y) * multiplier;

    //施加一个制动力(运动反方向),越接近障碍物,力越大
    const double BrakingWeight = 0.2;
    SteeringForce.x = (ClosestIntersectionObstacle->BRadius() - LocalPosOfClosestObstacle.x) * BrakingWeight;
  }

  //最后把转向力转换到世界空间
  return VectorToWorldSpace(SteeringForce,
                            m_pVehicle->Heading(),
                            m_pVehicle->Side());
}
posted @ 2022-11-15 17:34  GameSprite  阅读(166)  评论(0编辑  收藏  举报