視覺感知
視覺感知是一種常見的感知。
在許多即時戰略游戲或者類DOTA游戲里,一個單位的視覺感知往往是圓形范圍的。

扇形視野
當然在其他大部分俯視角游戲里,一個智能體的視覺感知應該是類似現實人眼觀看的扇形范圍。

對於橫板游戲,可以把視野“豎”起來,檢測方式無多少差別。
對於空間更加復雜的3D游戲,可能需要視錐體(立體)檢測。
一個更快的技巧是照樣做成扇形檢測,只是再額外增加高度差檢測(即看作2.5D處理)。
但是視野實際還需考慮阻擋問題,這里提供1種主流解決視野遮擋的思路:
在所在區域的所有潛在目標進行遍歷,每次遍歷 先判斷是否在扇形范圍內,
再做一條智能體到目標的射線,若射線碰到的第一個物體是該目標,則感知到該目標。

進一步的優化則可以預先“規划”好區域,構建潛在可視集(PVS),盡可能過濾不必要的目標,縮小所在區域的潛在目標數量
(例如屋外看不到房內的人,也就可以過濾掉房內的人),那么檢測速度就非常快。
示例(C++):
//視野感知
class ViewPerception {
public:
//進行一次視野感知探測
void check(Vector2 position) {
//先清理上次的結果
perceptionResult.clear();
//逐個潛在目標檢測
for (Object* target : potentialTargets) {
//運用簡單的數學運算判斷點是否在扇形范圍:
//先進行距離判斷是否在半徑內。
Vector2 offset = target.getPosition() - position;
float distanceSq = offset.lengthSquare();
if (distanceSq > radiusSq)continue;
//look向量和射線單位向量的數量積絕對值 若大於 數量積限制,
//則證明該射線離look向量的角度 超出數量積限制的對應角度。
float dotproduct = fabs(offset.normalize().dot(look));
if (dotproduct > dotproductlimit)continue;
//最后使用射線檢測第一個碰到的物體是不是目標物體
//若射線第一個碰到的物體是目標物體,則可視為 看見了該物體
if (raycast(position, target->position).result.object == target) {
perceptionResult.emplace_back(target);
}
}
}
private:
Vector2 look; //朝前的單位向量
float radiusSq; //扇形半徑的平方
float dotproductlimit; //數量積限制
std::vector<Target*> perceptionResult; //感知到的目標(結果)
//....
};
TIP:判斷點在圓形范圍應比較距離的平方和半徑的平方,每次判斷就可以減少一次開方的運算。
橢圓視野
上述扇形視野有幾個缺陷:
- 智能體應該能看到貼近側方的物體(甚至能感知到貼近背后的位置)
- 智能體對於正前方向應該能看的更遠
基於這些缺陷,我們加了一個圓形和狹長的扇形視野范圍(紫色點為智能體):

但是這樣計算量就提升了不少,一個代替方法是使用橢圓型視野(紫色點為智能體):

橢圓(任意轉向)的長軸長為2a,短軸長為2b,兩個焦點離圓心的距離是c和-c(而且\(c^2=a^2-b^2\))。
橢圓上任意一點到兩個焦點的距離之和必等於2a,利用這個性質可推理出:
若某個點與橢圓上兩個焦點距離之和小於2a,則必在橢圓內。
因此我們只要預設好常量值:
- a:取決於視野的長度
- b:取決於視野的寬度
- c:由\(c^2=a^2-b^2\)計算出
- d1,d2:在智能體位置正前方d1距離的點為焦點c1,正前方d2距離的點為焦點c2。
bool ViewPerception::checkPointInEllipse(Vector2 targetPosition){
Vector2 c1 = this->position + this->look * d1;
Vector2 c2 = this->position + this->look * d2;
if(distance(c1,targetPosition)+distance(c2,targetPosition) <= 2*a)return true;
return false;
}
橢圓型的視野不僅能解決上述缺陷,在現實中也更貼近人類視覺的模型,計算量也只略高於一個扇形視野計算。
基於分片的高性能視域搜索系統
在很多策略游戲里,視域(Line-of-Sight,簡稱LOS)是很重要的概念。
在典型RTS游戲里,視域分為可見區域,不可見區域,已探索(但不可見)區域,說白了就是戰爭迷霧機制。
為了實現視域系統,我們先把游戲世界分為一個個整齊的分片(可以是正方形網格,六邊形...)。
當我們檢測某個分片是否可見時,直觀的做法是直接判斷該分片位置是否位於玩家視野幾何形狀。潛在的問題是,分片越多需要檢測的次數呈幾何級數增長。
而更高性能的做法是:
1. 首先每個分片記錄一個數值(一般是用二維數組記錄),用於記錄該分片是否可視。
在實現時為滿足更復雜的需求可以記錄額外的數據:
- 多單位視野共享,應該用一個計數,當其中一個單位不再看見該分片時,可以減少計數,而不是直接修改為不可視。
- 多方視野,應該用一個(可能多個)Byte值,其中每個位表示某方視野是否可見。這可以用在觀戰系統,隨時屏蔽某一方或者只關注某一個玩家的視野。
- 多種視域類型,例如可見區域,不可見區域,已探索區域...則得記錄枚舉值。
2. 每幀將玩家的舊視野(上一幀的視野)對應的所有分片數值修改為不可視,然后根據新視野(當前幀的視野)對應的所有分片數值修改為可視。
在修改的時候,我們可以用一個LOS模板來幫助我們快速找到視野分片,並修改之。
這個LOS模板實際上就是列表數組,每一行記錄該行所有視野分片的位置:
for(int i = 0 ; i < LOStemplate.size() ; ++i){
for(int j = 0 ; j < LOStemplate[i].size(); ++j){
tiles[positionX + i - offset][positionY + LOStemplate[i][j]] = true;
}
}
得益於LOS模板,我們不僅可以引入圓形LOS,還可以引入類似手電筒視野的LOS:

由於游戲里玩家可能轉向,對於一些非圓形LOS,我們可以准備多個LOS模板(例如對應90°,60°,30°..方向的LOS模板):

因為LOS模板完全可以通過預計算先算出來,所以使用它的CPU開銷只與它的視野分片數相關而不是與地圖分片數相關,這個性能開銷已經很不錯了。
3. 當需要檢測分片是否可見時,直接訪問記錄來獲取。
一個技巧是,不要主動搜索,而是利用分片記錄來主動通知:
例如當一個單位需要搜索視野內的一個敵人時,不是在該單位的LOS模板范圍內主動遍歷搜索敵人所在的分片,
而是敵人自己根據當前位置的分片數據(多方視野記錄),主動通知可看到該分片的單位。
簡單來說,思想是基於事件驅動而非輪詢,效率也提升的相當不錯。
聽覺感知
聽覺感知一般比較簡單粗暴:一個圓形/球形范圍檢測,
而且一般還無需考慮阻擋問題(現實中的聲音傳播可近似看作無阻擋)。

另外的,聽覺感知一般需要得到的信息:
- 聲音來源(例如發出聲音的生物)
- 聲音大小和距離
通過簡單的線性計算,由聲音大小和距離可以計算出實際接受聲音的大小。
將這個信息作為額外數據交由決策使用。
(例如一個警衛,聽到太大的聲音就進入敵對狀態,小的聲音則進入警戒狀態)
示例(C++):
//聽覺感知
class ListenPerception {
public:
//進行一次聽力感知探測
void check(Vector2 position) {
perceptionResult.clear();
//逐個潛在聲源檢測
for (Voice& voice : potentialVoices) {
//判斷目標點是否在圓形范圍,即距離是否在半徑內。
Vector3 offset = voice.getPosition() - position;
float distanceSq = offset.lengthSquare();
if (distanceSq > radiusSq)continue;
//實際聲音大小會隨着距離增大而衰減
float volume = voice.getVolume() / distanceSq;
perceptionResult.emplace_back(voice.getTarget(),volume);
}
}
private:
float radiusSq; //范圍半徑
std::vector<std::pair<Target*, float>> perceptionResult; //感知到的目標+實際聲音大小(結果)
};
其它感知
這個其實應該叫雜項感知,因為一般來說,視覺感知和聽力感知已經足夠一個基本的智能體所需感知了。
但極少情況還可能一些智能體需要知道各種雜項信息(例如隊長給警衛發送了一條無線電消息,要求警衛趕往隊長所在位置支援)。
游戲AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html
