游戏底层逻辑,运动&&寻路(三)

上篇文章我们解释了几种基本的控制力,今天我们会讨论几种较为复杂的行为,涉及了碰撞,以及辅助图形进行运动控制。

7、Wander徘徊(巡逻)

徘徊(四处巡逻)是一种很常见的行为,但是要得到smoothly平滑的转向行为,并不是特别容易,这里有一种借用辅助圆实现的平滑移动。

游戏底层逻辑,运动&&寻路(三)_第1张图片

如图,我们在物体前面构造了一个辅助圆,我们控制目标点,从而用seek方法控制运动行为,如果我们让目标点在圆周上运动,就可以产生一个力,他有如下属性:

  • 一定向前(分力向物体的当前运动方向)
  • 单帧转角范围为切线与x轴的夹角
  • 大小为圆与x轴交点的范围

这样我们就可以控制辅助圆的相对位置来控制wander行为,比如我们想要总的运动轨迹为一个不规则的圆,我们可以将圆放到第一象限(逆时针旋转)或者第四象限(顺时针旋转),圆的位置为max_double,则进行直线运动。

接下来我们只需要写一个关于目标点在圆周上随机移动的方法就行(这里可以随便写,比如用三角函数),这里有一种实现:

  • 目标点一定范围内随机移动
  • 将其投影到圆上(获取其单位向量)

我们首先为SteeringBehaviors类添加新的成员变量:

//wander attributes
    Vec2 _wanderTarget;//目标点

    double _wanderRadius;//辅助圆半径

    double _wanderDistance;//辅助圆离物体的距离

    double _wanderJitter;//给目标点的随机位置一个限制值,这是为了减少速度变化过快产生的抖动

接下来是wander函数的实现:

Vec2 SteeringBehaviors::wander()
{
    //random move
    _wanderTarget = Vec2(Random::Rand(-1, 1)*_wanderJitter,
        Random::Rand(-1, 1)*_wanderJitter);
    //project it onto the circle
    _wanderTarget = _wanderTarget.getNormalized()*_wanderRadius;
    //add jitter
    Vec2 targetOnLocal = _wanderTarget + Vec2(_wanderJitter, 0);

    //we use sprite instead of entity itself
    if (_ownerVehicle->getSprite()->getPosition() != _ownerVehicle->position())
    {
        _ownerVehicle->getSprite()->setPosition(_ownerVehicle->position());
    }

    Vec2 targetOnWorld = _ownerVehicle->getSprite()->convertToWorldSpaceAR(targetOnLocal);

    return targetOnWorld - _ownerVehicle->position();
}

思考

wander方法讲述了一种倒推算法的途径,我们可以想象物体运动的情况,想要得到一个平滑无抖动的运动效果,就是要限制运动方向和大小的变化,此处有几个特征:
1、一定向前
2、方向变化有一定趋势,在一段时间内总是朝着一个方向转弯
知道这两点就能处理这个问题:控制力必然由两个力合成,其中一个力必然朝向物体运动方向(条件一)
另一个力控制转弯方向,我们只需要记录上一个力的方向(此处为记录目标点的位置),在此基础之上变化即可(条件二)
还有一个控制转弯方向的方法就是变化越大让力越小,此处利用了圆形,当然我们可以是一条直线(斜率为负值)
由此可知,此处的辅助图形可以有很多,目标点的控制也可以有很多方案,当然,经过大量的数据测试,调整参数也是必不可少的


8、obstacleAvoidance避开障碍

接下来的算法可能会遇到obstacle地形实体这个概念,在我们的实现中,该类继承了BaseEntity这个类,暂时用到的只有BoudingBox碰撞盒这个属性。,以后有机会再详细介绍几种基本的游戏类实现。

算法之前,我们先想象一下人类避开障碍的情形(心理独白^_^|||):

“天气真好,晚上干点什么呢?”
“前面是电线杆,小心点别撞上了。”

呵呵,有点无厘头,但是其中蕴含了一个很重要的信息,“前面是电线杆”,人类用视觉来感知障碍的存在,我们不会给游戏实体添加视觉元素,但是我们这里借由辅助矩形框来实现视觉这一概念。

游戏底层逻辑,运动&&寻路(三)_第2张图片

如图,三角形物体为运动实体,两条垂直线为本地坐标系的坐标轴,长方形为辅助矩形,灰色圆形为障碍的碰撞盒,其外面的圆为预估圆(提前检测碰撞)。
预估圆存在的意义好比人类不会贴着障碍物移动一样,这里也是这样

我们遍历所有的obstacle,找出其中满足如下特征的:

  • 与辅助矩形相交(有重叠面积)
  • 圆心在物体正前方
  • 预估圆与物体x轴线相交

计算交点(斥力点)

该方法我们通过几步来实现:
1、找出检测以辅助矩形长度为半径的圆范围内的所有障碍物

std::vector EntityManenger::getNeighbors(BaseEntity* centreEntity, std::vector entityList, double radius)
{
    std::vector vec;
    for_each(entityList.cbegin(), entityList.cend(), [&radius,centreEntity,&vec](BaseEntity* neighborEntity)
    {
        Vec2 dis = neighborEntity->position() - centreEntity->position();

        radius += 0.5*max(neighborEntity->getSprite()->getBoundingBox().size.height,
            neighborEntity->getSprite()->getBoundingBox().size.width);

        if (centreEntity != neighborEntity&&dis.getLengthSq() < radius*radius)
        {
            vec.push_back(neighborEntity);
        }
    }
    );
    return vec;
}

2、将筛选出来的obs转到物体的本地坐标系
3、排除x为负值的obs
4、排除y值大于预估圆半径(排除与x轴不相交)

筛选出来相交obs之后,我们需要求与x轴最近的交点,他就是施加斥力最大的地方,其他交点的斥力被我们省略。
5、寻找最近交点
我们来看看前五步的代码:
新增成员变量:

double _dBoxLenth;
Vec2 SteeringBehaviors::obstacleAvoidance(const std::vector& obstacles)
{
    _dBoxLenth = minDetectionBoxLength*(1 + _ownerVehicle->speed() / _ownerVehicle->maxSpeed());

    //nearest intersection dis alone axis x
    double NIOL_x = max_double;

    //nearest intersection position on local
    Vec2 NIOL_po = Vec2::ZERO;

    //nearest obstacle
    BaseEntity* NObstacle = NULL;

    //entities within view range
    std::vector neighborObstacles = EMGR->getNeighbors(_ownerVehicle, EMGR->getVecByType(1), _dBoxLenth);

    //find the point of the force
    for_each(neighborObstacles.begin(), neighborObstacles.end(), [this,&NIOL_x,&NIOL_po,&NObstacle](BaseEntity* obstacle)
    {
        Vec2 poOnLocal = _ownerVehicle->getSprite()->convertToNodeSpaceAR(obstacle->position());

        //tips: better to use "do while(0)" struct
        if (poOnLocal.x >= 0)
        {
            double expandedRadius = obstacle->getBoundingRadius() + _ownerVehicle->getBoundingRadius();

            if (fabs(poOnLocal.y) < expandedRadius)
            {
                //find out the intersections
                double intersectionX = poOnLocal.x - (expandedRadius*expandedRadius - (poOnLocal.y)*(poOnLocal.y));

                //just determined by the positive axis X
                if (intersectionX <= 0)
                {
                    intersectionX = poOnLocal.x + (expandedRadius*expandedRadius - (poOnLocal.y)*(poOnLocal.y));
                }

                if (intersectionX < NIOL_x)
                {
                    NIOL_x = intersectionX;

                    NIOL_po = poOnLocal;

                    NObstacle = obstacle;
                }
            }
        }
    }
    );
    //to be continued

有几点忘了加以声明:
1、辅助矩形的长度与物体的当前速度正相关
2、计算时用Sprite来辅助计算本地坐标系,因为在此游戏中认为实体的坐标系属性与其Sprite完全相同(更新实体时同时更新Sprite),之所以实体不继承Sprite类是因为这样会出现大量bug,不推荐使用继承
3、预估圆的半径=obs的半径+辅助盒宽度/2(我们近似处理为物体半径

计算控制力

有了斥力点,我们就很容易计算控制力了,经典处理如下:

游戏底层逻辑,运动&&寻路(三)_第3张图片

分为两个力:平行于x轴的制动力,沿obs法线过斥力点的斥力

此处近似处理,方便计算,将此斥力处理为品行于y轴的力

这两个力的大小满反比于obs与实体的x轴距离

直接看代码:

//go on
//is obstacle exist
    if (NObstacle)
    {
        //no matter on the x or y, the closer, the stronger the for shouble be

        //the effect on x
        double multiplier = 1.0 + (_dBoxLenth - NIOL_po.x) / _dBoxLenth;
        //the effect on y
        steeringForce.y = (NObstacle->getBoundingRadius() - NIOL_po.y)*multiplier;

        //apply a braking force
        steeringForce.x = (NObstacle->getBoundingRadius() - NIOL_po.x)*brakingWeight;
    }

    //convert the force from local to world space
    return _ownerVehicle->getSprite()->convertToWorldSpaceAR(steeringForce);
}

最后别忘了转换到世界坐标系

今天先说到这里,我们下次继续


准备写一个有关游戏底层算法,物理算法,以及AI(重点是机器学习在游戏中的应用)的长篇博客,欢迎大家指正交流╰( ̄▽ ̄)╯

你可能感兴趣的:(AI,cocos2dx)