AI代理如何访问有关其环境的信息?


9

这可能是一个琐碎的问题,但是我很难理解。非常感谢您的帮助。

在使用面向对象设计的游戏开发中,我想了解AI代理如何从游戏世界访问他们需要的信息以执行其动作。

众所周知,在游戏中,人工智能代理商经常需要“感知自己的环境”并根据周围发生的事情采取行动。例如,如果一个代理人足够靠近他,他可能被编程为追逐该玩家,在移动时避开障碍物(使用避障转向行为),等等。

我的问题是我不确定该怎么做。AI代理如何获取有关游戏世界所需的信息?

一种可能的方法是,代理简单地直接从游戏世界中请求他们需要的信息。

有一个叫做GameWorld的类。它处理重要的游戏逻辑(游戏循环,碰撞检测等),并保留对游戏中所有实体的引用。

我可以让这个班成为单身人士。当代理商需要游戏世界的信息时,它直接从GameWorld实例中获取信息。

例如,Seek当玩家靠近时,可以将一个坐席编程给玩家。为了做到这一点,经纪人必须获得球员的位置。因此,它可以直接直接请求:GameWorld.instance().getPlayerPosition()

代理商也可以只获取游戏中所有实体的列表,并根据需要对其进行分析(以找出,或其他附近有哪些实体): GameWorld.instance().getEntityList()

这是最简单的方法:代理商直接联系GameWorld类并获取他们所需的信息。但是,这是我所知道的唯一方法。有更好的吗?

有经验的游戏开发人员将如何设计?天真的“获取所有实体的列表并寻找所需的东西”的方法吗?有哪些方法和机制可以使AI代理访问他们所需的信息以执行其动作?


如果您有权访问GDCVault,则在2013年有一个精彩的演讲,名为“为AI创造生活,呼吸世界的生命创造呼吸的AI”,详细介绍了他们的AI知识模型。
DMGregory

Answers:


5

您所描述的是查询世界的经典“拉”模型。在大多数情况下,这非常有效,特别是对于具有基本AI的游戏(最多)。但是,有几个点,你应该考虑到可能是缺点:

  • 您可能要加倍缓冲。请参阅有关该主题的游戏编程模式。通过始终直接从世界请求数据,您可以得到奇怪的比赛条件,其中的结果取决于AI被调用的顺序。这是否对您的游戏很重要,需要您确定。一个可能的结果是,它将游戏偏向“先”或“后”的任何人,从而使多人游戏变得不公平。

  • 批处理请求,尤其是某些数据结构的批处理,通常效率更高。在这里,您可能会让每个要搜索障碍物的AI代理都创建一个“查询对象”,并将其注册到一个中心障碍物单例中。然后,在主AI循环之前,所有查询都针对数据结构运行,从而使障碍数据结构更多地保留在缓存中。然后在AI阶段,每个代理都会处理其查询结果,但不允许直接进行查询。在框架的结尾,AI对象使用其新位置更新查询,或添加或删除查询。这类似于面向数据的设计

    请注意,这基本上是通过将查询结果存储在缓冲区中来进行双重缓冲的。它还要求您预先确定是否需要对框架进行查询。这是一个“推送”模型,因为代理声明了它们感兴趣的更新类型(通过创建相应的查询对象),并将这些更新推送到了它们。请注意,您还可以使查询对象包含回调,而不是存储框架的所有结果。

  • 最后,您可能希望对可搜索对象使用接口或组件,而不要使用继承,这在其他地方都有详细记录。遍历列表Entities检查instanceOf是可能是相当脆弱的代码食谱,你想同时得到分钟StaticObject,并MovingObjectHealable。(除非instanceOf适用于您选择的语言的界面。)


5

AI成本高昂,性能通常是架构的驱动因素。

为了减轻您对数据访问模型的担忧,让我们考虑游戏行业内外的一些不同的AI示例,从人类导航最远的示例到我们最熟悉的示例。

(每个示例均假设单个全局逻辑更新。)

  • A *最短路径每个AI都会计算地图的状态以进行寻路。A *要求每个AI已经知道必须进行寻路的整个(局部)环境,因此我们必须将有关地图障碍物和空间(通常是2D布尔数组)的信息交给它。A *是Dijkstra算法的一种特殊形式,它是最短路径开放图搜索算法;这种方法返回代表路径的列表,并且在每个步骤中,AI只需选择该列表中要移至的下一个节点,直到达到其目标或需要重新计算(例如,由于地图障碍物更改)。没有这些知识,就找不到现实的最短路径。A *是RTS游戏中的AI总是知道如何从A点到达B点的原因-如果存在路径的话。它将重新计算每个AI的最短路径,因为路径有效性基于先前移动(并可能阻塞了某些路径)的那些AI的位置。A *在寻路过程中计算像元值的迭代过程是数学收敛之一。可以说最终结果类似于嗅觉和视觉,但是总的来说,这与我们的思维方式有些不同。

  • 协同扩散也可以在游戏中找到,这种扩散与基于气体和微粒扩散的嗅觉最为相似。CD解决了A *中昂贵的重新处理问题:相反,存储单个地图状态,对所有AI每次更新处理一次,然后由每个AI依次访问结果,以进行各自的移动。搜索算法不再返回单个路径(单元格列表);而是 相反,每个AI将在处理完地图后检查地图,然后移动到值最高的任何相邻像元。这就是爬山。不过,地图处理阶段必须已经可以事先访问地图信息,此处还包含所有AI实体的位置。因此,地图引用AI,然后AI引用地图。

  • 计算机视觉和光线投射+最短路径在漫游者和无人机机器人技术中,这已成为确定机器人导航空间范围的规范。这使机器人可以构建其环境的完整体积模型,就像我们通过视觉,甚至声音或触摸(对于盲人或聋人)一样,然后机器人可以将其简化为最小的地形图(有点像路标图)在带有A *)的游戏中使用,然后可以应用最短路径算法。在这种情况下,尽管“视觉”可以为当前环境提供线索,但仍然会导致最终提供通往目标的图搜索。这接近于人类的思想:为了从卧室到达厨房,我必须经过客厅。我已经看过他们并知道他们的空间和他们的门户的事实,这就是这种有计划的行动的原因。这是一种图拓扑,虽然最短路径算法已嵌入软蛋白而非硬硅中,但已对其应用了最短路径算法。

因此,您可以看到前两个依赖于已经完全了解环境。由于从零开始评估环境的成本,这在游戏中很常见。显然,最后一个功能最强大。以此方式配备的机器人(例如,每帧读取深度缓冲区的游戏AI)可以在任何环境中充分导航,而无需事先知道。您可能已经猜到了,它也是上述三种方法中最昂贵的,在游戏中,我们通常无法以AI为基础来做到这一点。当然,2D的成本远低于3D。

建筑要点

上面已经很清楚了,我们不能只为AI假设一种正确的数据访问模式。选择取决于您要实现的目标。GameWorld直接访问该类是绝对标准的:它只是为您提供世界信息。本质上,这就是您的数据模型,这就是数据模型的作用。辛格尔顿对此很好。

“获取所有实体的列表,并寻找所需的东西”

一点也不幼稚。唯一可能幼稚的事情是执行比所需次数更多的列表迭代。在碰撞检测中,我们通过使用例如四叉树来减少搜索空间来避免这种情况。类似的机制可以应用于AI。而且,如果您可以共享同一个循环来执行多项操作,请这样做,因为分支的成本很高。


谢谢回答。因为我是游戏开发者的初学者,所以我认为我现在将继续使用简单的“从游戏世界中获取列表”方法:)一个问题:在我的问题中,我将GameWorld类描述为包含以下内容的类:所有游戏实体,还包含大多数重要的“引擎”逻辑:主游戏循环,碰撞检测等。它基本上是游戏的“主类”。我的问题是:这种方法在游戏中很常见吗?有“主班”吗?还是应该将其分成较小的类,并具有一个类作为“实体数据库”对象可以轮询?
Aviv Cohn 2014年

@Prog不客气。同样,上述AI方法(或其他任何方法)都没有任何东西表明您的“从游戏世界中获取列表”在任何方面,形状或形式在架构上都是错误的。该AI架构必须满足AI的需求; 但是,正如您建议的那样,应该将逻辑进行模块化,封装(在其自己的类中),以使其与更广泛的应用程序体系结构分离。是的,一旦出现此类问题,应始终将子系统分解为单独的模块。您的指导原则应该是SRP
工程师

2

基本上,我将有两种查询信息的方式。

  1. 当AIState因检测到冲突或高速缓存而发生更改时,对重要对象的引用。这样,您就知道需要什么参考。当其他系统必须每帧都进行大型搜索时,我建议您将它们放回原处,这样您就不必执行多次搜索。因此,检测到与使敌人“警报”的区域发生“碰撞”,从而向他发送消息/事件,如果他/他尚未存在,则将其注册到该对象,并将游戏状态更改为让他根据自己的状态进行交易的状态该游戏状态。您需要某种类型的事件来告诉您进行更改,我只是将引用传递给您用来提供该信息的任何回调。这比只需要与播放器打交道更具可扩展性。也许您想要一个敌人追击另一个敌人或其他物体。这样,您只需更改标识它的标签。

  2. 利用这些信息,您将对使用A *或其他算法为您提供路径的路径查找系统执行查询,或者您可以将其用于某些操纵行为。也许两者兼而有之。基本上,通过两者的转换,您应该能够查询您的节点系统或navmesh并为其提供路径。您的游戏世界可能除了寻路之外还有很多事情。我只将您的查询提交给寻路。同样,如果您有很多查询,则批处理这些东西可能是最好的,因为这可能会非常密集并且批处理将有助于提高性能。

    Transform* targetTransform = nullptr;
    EnemyAIState  AIState = EnemyAIState::Idle;
    void OnTriggerEnter(GameObject* go)
    {
       if(go->hasTag(TAG_PLAYER))
       {
       //Cache important information that will be needed during pursuit
       targetTransform = go->getComponent<Transform>();
       AIState = EnemyAIState::Pursue;
       }
    }
    
    void Update()
    {
       switch(AIState)
       {
          case EnemyAIState::Pursue:
           //Find position to move to
           Vector3 nextNode = PathSystem::Seek(
                              transform->position,targetTransform->position);
           /*Update the position towards the target by whatever speed the unit moves
             Depending on how robust your path system is you might want to raycast
             against obstacles it can't take into account or might clip the path.*/
            transform->Move((nextNode - transform->position).unitVector()*speed*Time::deltaTime());
            break;
        }
     }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.