上篇文章我們解釋了幾種基本的控制力,今天我們會討論幾種較爲複雜的行爲,涉及了碰撞,以及輔助圖形進行運動控制。
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(重點是機器學習在遊戲中的應用)的長篇博客,歡迎大家指正交流╰( ̄▽ ̄)╯