遊戲底層邏輯,運動&&尋路(三)

上篇文章我們解釋了幾種基本的控制力,今天我們會討論幾種較爲複雜的行爲,涉及了碰撞,以及輔助圖形進行運動控制。

7、Wander徘徊(巡邏)

徘徊(四處巡邏)是一種很常見的行爲,但是要得到smoothly平滑的轉向行爲,並不是特別容易,這裏有一種借用輔助圓實現的平滑移動。

這裏寫圖片描述

如圖,我們在物體前面構造了一個輔助圓,我們控制目標點,從而用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碰撞盒這個屬性。,以後有機會再詳細介紹幾種基本的遊戲類實現。

算法之前,我們先想象一下人類避開障礙的情形(心理獨白^_^|||):

“天氣真好,晚上乾點什麼呢?”
“前面是電線杆,小心點別撞上了。”

呵呵,有點無厘頭,但是其中蘊含了一個很重要的信息,“前面是電線杆”,人類用視覺來感知障礙的存在,我們不會給遊戲實體添加視覺元素,但是我們這裏藉由輔助矩形框來實現視覺這一概念。

這裏寫圖片描述

如圖,三角形物體爲運動實體,兩條垂直線爲本地座標系的座標軸,長方形爲輔助矩形,灰色圓形爲障礙的碰撞盒,其外面的圓爲預估圓(提前檢測碰撞)。
預估圓存在的意義好比人類不會貼着障礙物移動一樣,這裏也是這樣

我們遍歷所有的obstacle,找出其中滿足如下特徵的:

  • 與輔助矩形相交(有重疊面積)
  • 圓心在物體正前方
  • 預估圓與物體x軸線相交

計算交點(斥力點)

該方法我們通過幾步來實現:
1、找出檢測以輔助矩形長度爲半徑的圓範圍內的所有障礙物

std::vector<BaseEntity*> EntityManenger::getNeighbors(BaseEntity* centreEntity, std::vector<BaseEntity*> entityList, double radius)
{
    std::vector<BaseEntity*> 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<BaseEntity*>& 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<BaseEntity*> 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(我們近似處理爲物體半徑

計算控制力

有了斥力點,我們就很容易計算控制力了,經典處理如下:

這裏寫圖片描述

分爲兩個力:平行於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(重點是機器學習在遊戲中的應用)的長篇博客,歡迎大家指正交流╰( ̄▽ ̄)╯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章