游戏AI之感知(1)

视觉感知


视觉感知是一种常见的感知。
在许多即时战略游戏或者类DOTA游戏里,一个单位的视觉感知每每是圆形范围的。算法

扇形视野

固然在其余大部分俯视角游戏里,一个智能体的视觉感知应该是相似现实人眼观看的扇形范围。数组

对于横板游戏,能够把视野“竖”起来,检测方式无多少差异。
对于空间更加复杂的3D游戏,可能须要视锥体(立体)检测。性能

一个更快的技巧是照样作成扇形检测,只是再额外增长高度差检测(即看做2.5D处理)。优化

可是视野实际还需考虑阻挡问题,这里提供2种解决视野遮挡的思路:this

1. 在前方扇形范围每隔一段弧度发出一条射线进行检测,若检测到某个射线第一个碰到的物体是目标物体,则感知到该目标。spa

2. 在所在区域的全部潜在目标进行遍历,每次遍历 先判断是否在扇形范围内,
再作一条智能体到目标的射线,若射线碰到的第一个物体是该目标,则感知到该目标。
code

第一个思路比较容易实现,第二个则算法效率比较高。orm

第二个思路的进一步优化是,预先“规划”好区域,构建潜在可视集(PVS),尽量过滤没必要要的目标,缩小所在区域的潜在目标数量
(例如屋外看不到房内的人,也就能够过滤掉房内的人),那么检测速度就很是快。htm

示例(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

相关文章
相关标签/搜索