前言
学习如何应用 AI 至关重要,它可以在开发传统、教育或任何其他类型的游戏时将乐趣因素提升到新的水平。虚幻引擎是一个强大的游戏开发引擎,允许你创建 2D 和 3D 游戏。如果你想使用 AI 来延长游戏寿命并使其更具挑战性和趣味性,这本书就是为你准备的。
本书从将人工智能(AI)分解为简单概念开始,以获得对其的基本理解。通过各种示例,你将实际操作实现,旨在突出与 UE4 中游戏 AI 相关的关键概念和功能。你将学习如何通过内置 AI 框架构建每个流派(例如 RPG、策略、平台、FPS、模拟、街机和教育游戏)的可信角色。你将学习如何为你的 AI 代理配置导航、环境查询和感官系统,并将其与行为树结合,所有这些都有实际示例。然后,你将探索引擎如何处理动态人群。在最后一章中,你将学习如何分析、可视化和调试你的 AI 系统,以纠正 AI 逻辑并提高性能。
到本书结束时,你对虚幻引擎内置 AI 系统的 AI 知识将深入且全面,这将使你能够在项目中构建强大的 AI 代理。
本书面向对象
如果你是一位在虚幻引擎中有些经验的游戏开发者,现在想要理解和实现虚幻引擎中的可信游戏 AI,这本书就是为你准备的。本书将涵盖蓝图和 C++,让不同背景的人都能享受阅读。无论你是想构建你的第一款游戏,还是想作为游戏 AI 程序员扩展你的知识,你都会找到大量关于游戏 AI 的概念和实现方面的精彩信息和示例,包括如何扩展这些系统的一些内容。
本书涵盖内容
第一章,《在 AI 世界迈出第一步》,探讨了成为 AI 游戏开发者的先决条件以及 AI 在游戏开发流程中的应用。
第二章,《行为树和黑板》,介绍了在虚幻 AI 框架中使用的两种主要结构,这些结构用于控制游戏中的大多数 AI 代理。你将学习如何创建行为树以及它们如何在黑板中存储数据。
第三章,《导航》,教你如何让代理在地图或环境中导航或找到路径。
第四章,《环境查询系统》,帮助你掌握制作环境查询,这是虚幻 AI 框架的空间推理子系统。掌握这些是实现在虚幻中可信行为的关键。
第五章,代理意识,处理 AI 代理如何感知世界和周围环境。这包括视觉、听觉,以及通过扩展系统可能想象到的任何其他感官。
第六章,扩展行为树,通过使用蓝图或 C++扩展行为树,带你完成 Unreal 的任务。你将学习如何编程新的任务、装饰器和服务。
第七章,人群,解释了如何在提供一些功能性的 Unreal AI 框架内处理人群。
第八章,设计行为树 – 第 I 部分,专注于如何实现行为树,以便 AI 代理可以在游戏中追逐我们的玩家(在蓝图和 C++中)。本章,连同下一章,从设计到实现探讨了这一示例。
第九章,设计行为树 – 第 II 部分,是上一章的延续。特别是,在我们下一章构建最终的行为树之前,我们将构建最后缺失的拼图(一个自定义的服务)。
第十章,设计行为树 – 第 III 部分,是上一章的延续,也是设计行为树系列的最后一部分。我们将完成我们开始的工作。特别是,我们将构建最终的行为树并使其运行。
第十一章,AI 调试方法 – 记录日志,检查我们可以用来调试 AI 系统的一系列方法,包括控制台日志、蓝图中的屏幕消息等等。通过掌握日志记录的艺术,你将能够轻松跟踪你的值以及你正在执行的代码的哪个部分。
第十二章,AI 调试方法 – 导航、EQS 和性能分析,探讨了 Unreal 引擎内集成的 AI 系统的一些更具体的工具。我们将看到更多与 AI 代码相关的性能分析工具,以及可视化环境查询和导航信息的工具。
第十三章,AI 调试方法 – 游戏调试器,带你探索最强大的调试工具,也是任何 Unreal AI 开发者的最佳朋友——游戏调试器。本章将更进一步,通过教授如何扩展这个工具来定制它以满足你的需求。
第十四章,超越,以一些关于如何探索本书中提出(以及其他)概念的建议以及一些关于 AI 的想法作为总结。
要充分利用这本书
熟练使用 Unreal Engine 4 是一个重要的起点。本书的目标是将那些使用这项技术的人带到他们能够足够舒适地掌握所有方面,成为项目中的技术领导者和推动者的水平。
下载示例代码文件
github.com/PacktPublishing/
下载彩色图像
www.packtpub.com/sites/default/files/downloads/9781788835657_ColorImages.pdf
使用的约定
本书中使用了多种文本约定。
CodeInTextOnBecomRelevant()
代码块设置如下:
void AMyFirstAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
AUnrealAIBookCharacter* Character = Cast<AUnrealAIBookCharacter>(InPawn);
if (Character != nullptr)
{
UBehaviorTree* BehaviorTree = Character->BehaviorTree;
if (BehaviorTree != nullptr) {
RunBehaviorTree(BehaviorTree);
}
}
}
当我们希望您注意代码块的特定部分时,相关的行或项目将以粗体显示:
void AMyFirstAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
AUnrealAIBookCharacter* Character = Cast<AUnrealAIBookCharacter>(InPawn);
if (Character != nullptr)
{
UBehaviorTree* BehaviorTree = Character->BehaviorTree;
if (BehaviorTree != nullptr) {
RunBehaviorTree(BehaviorTree);
}
}
}
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从行为树变量中的下拉菜单中选择 BT_MyFirstBehaviorTree。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
copyright@packt.com
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下您的评价。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!
第一部分:虚幻引擎框架
在本节中,我们将深入探讨内置的虚幻引擎 AI 框架。我们将从行为树和黑板是什么开始,然后继续学习关于导航系统、环境查询系统和感知系统的内容。在本节的最后,我们还将了解虚幻引擎如何处理大量人群,以及我们如何通过创建自定义任务、装饰器和服务来扩展(在蓝图和 C++中)我们的行为树。
本节将涵盖以下章节:
-
第一章,在 AI 世界迈出第一步
-
第二章,行为树和黑板
-
第三章,导航
-
第四章,环境查询系统
-
第五章,代理意识
-
第六章,扩展行为树
-
第七章,人群
第一章:在 AI 世界的第一步
从青铜巨人塔洛斯到符号系统和神经网络:AI 是如何在视频游戏中被塑造和使用的。
欢迎读者,欢迎来到我们人工智能之旅的开始,或者简称为 AI。你有没有想过那些在《魔兽世界》中辛勤工作的平民是如何探索复杂的地图的?或者,那些在《吃豆人》中活泼的幽灵是如何把你带到任何地方的?或者,也许你的对手在《最终幻想》中是如何优化攻击来屠杀你的团队的?
来自《最终幻想 XV》[Square Enix, 2016]的战斗截图。
那你就来对地方了!
在本章中,我们将探讨成为 AI 游戏开发者的先决条件以及 AI 在游戏开发流程中的应用。然后,我们将回顾 AI 的一般历史和在视频游戏中的历史,了解众多杰出人士的联合努力是如何构建出我们今天所知道的 AI。之后,我们将讨论 Unreal 引擎下的 AI 框架,因为本书将专注于 Unreal。
最后,我们将规划我们的旅程,并对本书不同章节所涉及的主题有一个总体了解。
在开始之前…
…我想回答一些你们中的一些人可能已经有的问题。
这本书考虑了 Blueprint 和 C++吗?
本书将解释这两者,所以请放心。
如果你不知道 C++,你可以跟随本书的 Blueprint 部分,如果你愿意,也可以尝试 C++部分。
如果,另一方面,你是一个更喜欢 C++的程序员,那么请不要担心!本书将解释如何在 Unreal 中使用 C++处理 AI。
关于 AI 的书有很多,我为什么应该选择这本书?
不同的书解释了 AI 的不同方面,而且它们往往不是相互排斥的,而是相互补充的。
然而,这本书的主要亮点在于它很好地平衡了 Unreal 中存在的不同 AI 系统的理论以及实际应用,因为整本书都充满了具体的例子。
这本书提供测试项目/材料来工作吗?
hog.red/AIBook2019ProjectFiles
我已经在使用 Unreal Engine 进行人工智能开发,这本书对我有帮助吗?
这一切都取决于你的知识水平。实际上,本书的第一部分,我们将主要讨论内置在虚幻引擎中的 AI 框架以及如何使用它。如果你在虚幻引擎 AI 方面有一些经验,这可能是你更熟悉的部分。然而,本书将深入探讨这些主题,即使是专家也能找到一些有用的提示。相反,第二部分将讨论游戏 AI 的一些调试方法,并解释如何扩展它们(主要使用 C++)。请随意查看大纲,并决定这本书是否适合你。
我已经在使用另一个游戏引擎,这本书对我还有用吗?
好吧,尽管我很想说我写的是一本关于 AI 的通用书籍,但这并不是——至少不是完全如此。尽管主要焦点仍然是 AI 的主要概念,但我们将探讨如何在虚幻引擎中实现它们。然而,这本书将大量依赖虚幻引擎内置的 AI 框架。因此,我鼓励你阅读更多关于 AI 的通用书籍,以获得更好的理解。另一方面,你总是可以尝试。也许,通过理解这里的一些概念,其他书籍会更容易阅读,你将能够将这种知识转移到你选择的任何游戏引擎中。
我是一个学生/教师,这本书适合在课堂上教学吗?
hog.red/AIBook2019LearningMaterial
这本书是否会涵盖关于虚幻引擎及其所有系统的 AI 的方方面面?
尽管我尽力详细描述每个系统,但涵盖所有内容是不可能的任务,这也归因于如此大型引擎的复杂性。然而,我有信心地说,这本书涵盖了与虚幻引擎中每个 AI 系统相关的几乎所有方面,包括如何扩展内置系统以及如何高效地进行调试。因此,我确实可以说这本书非常全面。
前置条件
由于本书面向的是刚开始在游戏开发中使用 AI 的人群,因此我不会假设读者有任何关于 AI 的先验/背景知识。然而,请考虑以下几点:
-
蓝图用户:你应该熟悉蓝图编程,并了解蓝图图的一般工作原理。
-
C++ 用户:您应该熟悉编程,特别是 C 家族语言(如 C、C#、C++ 或甚至 Java),尤其是 C++,因为这是 Unreal Engine 使用的语言。熟悉 Unreal Engine C++ API 是一个很大的加分项,尽管不是强制性的。所以,即使您不是专家,也不要担心——跟随步骤,您将会学到。
此外,如果您对矢量数学和物理运动学原理有所了解——至少是视频游戏中常用的那些——那就太好了。无论如何,如果您对这些内容不太熟悉,也不要过于担心,因为这本书并不要求您必须掌握;然而,如果您在寻找人工智能开发者的工作,这将是一个加分项。
安装和准备软件
在您继续阅读之前,让我们安装我们需要的软件。特别是,我们需要 Unreal Engine 和 Visual Studio。
Unreal Engine
让我们来谈谈 Unreal Engine。毕竟,这是一本关于如何在这样一个优秀的游戏引擎中开发游戏人工智能的书。
Unreal Engine 是由 Epic Games 开发的一款游戏引擎。它最初于 1998 年发布,如今由于它强大的功能,它是使用最广泛的(开源)游戏引擎之一(与 Unity 并列)。下面的截图显示了 Unreal Engine 的主界面:
Unreal Engine 主界面截图
www.unrealengine.com/en-US/what-is-unreal-engine-4docs.unrealengine.com/en-us/Programming/Development/BuildingUnrealEngine
在安装引擎时,如果您使用的是 C++,您需要检查一些选项。特别是,我们需要确认我们既有 “Engine Source” 也有 “Editor symbols for debugging”,如下面的截图所示:
通过这样做,我们能够导航 C++ 引擎源代码,并在发生崩溃时拥有完整的调用栈(这样您就会知道出了什么问题)。
Visual Studio
如果您使用的是 Blueprint,您不需要这样做——这仅适用于 C++ 用户。
www.visualstudio.comvisualstudio.microsoft.com/vs/
docs.unrealengine.com/en-us/Programming/Development/VisualStudioSetup
一旦你安装好所有东西并准备就绪,我们就可以继续本章的其余部分。
如果你是一名MacOS用户,有一个适用于MacOS的Visual Studio版本。你可以使用那个版本。或者,你可能能够使用 XCode。
成为 AI 游戏开发者
你曾梦想过成为一名 AI 游戏开发者吗?或者只是能够编写"智能"程序?那么这本书就是为你准备的!
然而,我必须建议你,这并不是一件容易的任务。
游戏开发和设计是周围最广泛的艺术作品之一。这是由于将游戏带到生命中所需要的专业知识量很大。你只需看看游戏中的最终字幕就能得到这个想法。它们是无穷无尽的,包含了许多人在各种角色上为游戏投入了大量时间的名字。AI 开发是这个大过程中的一个核心部分,它需要多年的时间来掌握,就像生活中的大多数事情一样。因此,迭代是关键,这本书是一个开始的好地方。
成为 AI 游戏开发者意味着什么
首先,你需要掌握数学、物理和编程。此外,你很可能会在一个跨学科团队中工作,这个团队包括艺术家、设计师和程序员。实际上,你可能需要与现有的专有软件技术合作,并且需要你能够构建新技术以满足项目的技术要求。你将被要求研究编码技术和算法,以便你能够跟上游戏行业的技术发展和进步,并识别技术和发展风险/障碍,并生成解决方案来克服已识别的风险。
另一方面,你将能够赋予视频游戏中的角色和实体生命。在你可能经历的所有挫折之后,你将是第一个提供帮助的人,或者更好的是,在游戏中生成智能行为。这需要时间,而且相当具有挑战性,所以在早期阶段不要对自己太苛刻。一旦你在游戏中实现了可以独立思考的真正 AI,这将是一个值得奖励自己的成就。
对于 AI 的初学者来说,这本书将帮助你为那个目标奠定第一块基石。对于专家来说,这本书将提供一份有用的指南,帮助你刷新 Unreal 中的不同 AI 系统,并深入探索可能有助于你工作的功能。
游戏开发过程中的 AI
游戏开发流程可能会因你访问的哪个工作室而大不相同,但它们都指向了视频游戏的创作。这不是一本关于流程的书,所以我们不会探索它们,但了解 AI 大致的位置是很重要的。
事实上,AI 与游戏开发流程的许多部分相交。以下是一些主要的部分:
-
动画:可能会让一些人感到惊讶,但关于这个主题正在进行很多研究。有时,动画和 AI 会重叠。例如,开发者需要解决的一个问题是如何以程序化的方式为角色生成数百个动画,这些动画可以表现得非常逼真,以及它们如何相互交互。实际上,解决逆运动学(IK)是一个数学问题,但选择无限多解中的哪一个来实现目标(或者只是提供一个逼真的外观)是一个 AI 任务。在这本书中,我们不会面对这个具体问题,但最后一章将提供一些指向你可以了解更多信息的地点。
-
关卡设计:如果一个游戏能够自动生成关卡,那么 AI 在这个游戏中就扮演着重要的角色。程序性内容生成(PCG)在游戏中是一个热门话题。有些游戏完全基于 PCG。不同的工具可以用来程序化生成高度图,帮助关卡设计师实现看起来逼真的景观和环境。这确实是一个值得深入探讨的广泛话题。
-
游戏引擎:当然,在游戏引擎内部,有很多 AI 算法在发挥作用。其中一些是针对代理的特定算法,而另一些则只是改进了引擎的功能和/或任务。这些构成了最广泛的类别,它们可以从简单的调整贝塞尔曲线的算法到实现用于动画的行为树或有限状态机。在底层,这里有很多事情在进行。在这本书中,我们将探讨一些这些概念,但要记住的是,一个算法可以被调整来解决不同领域中的类似问题。实际上,如果有限状态机(FSMs)被用来做出决策,为什么不用它们来“决定”播放哪个动画?或者为什么不甚至处理整个游戏逻辑(即 Unreal 引擎中的蓝图可视化脚本)?
-
非玩家角色(NPCs):这是在游戏中使用 AI 的最明显例子,也是玩家最明显的 AI(我们将在第十四章“超越”中探讨 AI 与玩家之间的关系)。这本书的大部分内容都集中在这一点上;也就是说,从移动角色(例如,使用寻路算法)到做出决策(即使用行为树),或者与其他 NPC 合作(多代理系统)。
很遗憾,我们在这本书中没有足够的空间来处理所有这些主题。因此,我们将只关注最后一部分(NPCs),并探索内置在 Unreal 中的 AI 框架。
一点历史
在我们开始这段旅程之前,我相信对人工智能和游戏人工智能的历史有一个大致的了解可能会很有益。当然,如果你是一个更倾向于动手操作、迫不及待想要开始编程人工智能的人,你可以跳过这部分内容。
什么是人工智能?
这是一个非常有趣的问题,它没有唯一的答案。实际上,不同的答案会引导我们了解人工智能的不同方面。让我们探索一些(众多)不同学者(按时间顺序)给出的定义。
实际上,在他们的书中,Russell 和 Norvig 将这些特定的定义组织成了四个类别。以下是他们的框架:
Russell 和 Norvig 的四个类别。左上角:“像人类一样思考的系统”。右上角:“理性思考的系统”。左下角:“像人类一样行动的系统”。右下角:“理性行动的系统”。
我们没有时间详细探讨“什么是人工智能?”这个问题,因为这可以单独填满一本书,但本书的最后一章也将包括一些哲学参考,你可以在这里扩展你对这个主题的了解。
回顾过去
对于一些人来说,人工智能的故事始于计算机之前。事实上,甚至古希腊人也假设了智能机器的存在。一个著名的例子是青铜巨人塔洛斯,它保护克里特城免受入侵者。另一个例子是赫菲斯托斯的金色助手,它们帮助上帝在火山锻造中工作,还有独眼巨人。在 17 世纪,勒内·笛卡尔写下了关于能够思考的自动机,并相信动物与机器不同,可以用滑轮、活塞和凸轮复制。
然而,这个故事的核心始于 1931 年,当时奥地利逻辑学家、数学家和哲学家库尔特·哥德尔证明了所有一阶逻辑中的真命题都是可推导的。另一方面,这在高阶逻辑中并不成立,其中一些真(或假)命题是无法证明的。这使得一阶逻辑成为自动推导逻辑后果的良好候选者。听起来很复杂吗?嗯,你可以想象这对他的传统主义同代人来说听起来是怎样的。
阿兰·图灵 16 岁时的照片
在 1937 年,英国计算机科学家、数学家、逻辑学家、密码分析家、哲学家和理论生物学家艾伦·图灵,通过停机问题指出了“智能机器”的一些局限性:除非实际运行,否则无法预先判断一个程序是否会终止。这在理论计算机科学中产生了许多后果。然而,根本性的步骤发生在十三年后的 1950 年,当时艾伦·图灵撰写了他的著名论文“计算机与智能”,在其中他讨论了模仿游戏,现在通常被称为“图灵测试”:一种定义智能机器的方法。
在 20 世纪 40 年代,一些尝试试图模拟生物系统:1943 年,麦克洛奇和皮茨为神经元开发了一个数学模型,1951 年,马文·明斯基创建了一台机器,能够用 3000 个真空管模拟 40 个神经元。然而,他们陷入了黑暗。
从 20 世纪 50 年代末到 20 世纪 80 年代初,大量的人工智能研究致力于“符号系统”。这些系统基于两个组件:由符号组成的知识库和一个推理算法,该算法使用逻辑推理来操纵这些符号,以扩展知识库本身。
在这个时期,许多杰出的思想者取得了显著的进步。值得提及的名字是麦卡锡,他在 1956 年在达特茅斯学院组织了一次会议,在那里首次提出了“人工智能”这个术语。两年后,他发明了高级编程语言LISP,在其中编写了第一个能够自我修改的程序。其他引人注目的成果包括 1959 年盖尔伦特的几何定理证明器,1961 年纽厄尔和西蒙的通用问题求解器(GPS),以及由维齐纳鲍姆开发的著名聊天机器人Eliza,这是 1966 年第一款能够用自然语言进行对话的软件。最后,在 1972 年,法国科学家阿兰·科梅拉乌尔发明了PROLOG,标志着符号系统的顶峰。
符号系统导致了众多人工智能技术的产生,这些技术至今仍被用于游戏,如黑板架构、路径查找、决策树、状态机和转向算法,我们将在本书中探讨所有这些内容。
这些系统的权衡在于知识和搜索之间。你拥有的知识越多,你需要的搜索就越少,你搜索得越快,你需要的知识就越少。这甚至已经在 1997 年由沃尔珀特和麦克雷德通过数学证明了。我们将在本书的后面有机会更详细地考察这种权衡。
在 20 世纪 90 年代初,符号系统变得不适用,因为它们证明难以扩展到更大的问题。此外,一些哲学论点反对它们,认为符号系统是有机智能的不兼容模型。因此,开发了受生物学启发的旧技术和新技术。旧的神经网络被从架子上取下来,1986 年 Nettalk 的成功,这个程序能够学会如何朗读,以及同年 Rumelhart 和 McClelland 出版的书"并行分布式处理"。事实上,"反向传播"算法被重新发现,因为它们允许神经网络(NN)真正地学习。
在过去 30 年的 AI 研究中,研究方向发生了新的变化。从 Pearl 在"智能系统中的概率推理"上的工作开始,概率被采纳为处理不确定性的主要工具之一。因此,人工智能开始使用许多统计技术,如贝叶斯网络、支持向量机(SVMs)、高斯过程和马尔可夫隐模型,后者被广泛用于表示系统状态的时态演变。此外,大型数据库的引入为人工智能解锁了许多可能性,并出现了一个名为"深度学习"的新分支。
然而,重要的是要记住,即使人工智能研究人员发现了新的和更先进的技巧,旧的技巧也不应该被丢弃。事实上,我们将看到,根据问题和其规模的不同,特定的算法可以大放异彩。
游戏中的 AI
视频游戏中人工智能的历史与我们之前讨论的内容一样有趣。我们没有时间详细地回顾并分析每一款游戏以及它们如何为该领域做出贡献。对于最好奇的你们,在本书的结尾,你们将找到其他讲座、视频和书籍,你们可以更深入地了解视频游戏中人工智能的历史。
视频游戏中人工智能的第一种形式是原始的,并用于像**《乒乓》[Atari, 1972],《太空侵略者》**[Midway Games West, Inc., 1978]等游戏。事实上,除了移动球拍试图捕捉球,或者移动外星人向玩家移动之外,我们并没有做更多的事情:
**《太空侵略者》**的一个截图[Midway Games West, Inc., 1978],其中使用了一种原始形式的人工智能来控制外星人
第一款使用显著人工智能的著名游戏是**《吃豆人》[Midway Games West, Inc., 1979]。四个怪物**(后来因为 Atari 2600 中的闪烁端口而被称为幽灵)使用有限状态机(FSM)来追逐(或逃离)玩家:
**《吃豆人》**游戏的一个截图[Midway Games West, Inc., 1979],其中四个怪物使用有限状态机试图捕捉玩家
在 20 世纪 80 年代,游戏中的 AI 并没有太大变化。直到**《魔兽世界:兽人 vs 人类》**[Blizzard Entertainment, 1994]的引入,路径查找系统才在视频游戏中成功实现。我们将在第三章,导航中探讨导航系统:
《魔兽世界:兽人 vs 人类》[Blizzard Entertainment, 1994]的截图,其中单位(本截图中的兽人步兵和士兵)使用路径查找算法在地图上移动
可能是开始让人们关注 AI 的游戏是**《007 黄金眼》[Rare Ltd., 1997],它展示了 AI 如何提升游戏体验。尽管它仍然依赖于 FSM,但创新之处在于角色可以看到彼此,并相应地行动。我们将在第五章,代理意识中探讨代理意识。这当时是一个热门话题,一些游戏将其作为主要游戏机制,例如《盗贼:暗影项目》**[Looking Glass Studios, Inc., 1998]:
《007 黄金眼》[Rare Ltd., 1997]的截图,它改变了人们对于视频游戏 AI 的看法
和**《合金装备固体》**[Konami Corporation, 1998]:
《合金装备固体》[Konami Corporation, 1998]的截图,
另一个热门话题是在战斗中模拟士兵的情绪。最早实现情感模型的游戏之一是**《战锤:黑暗预兆》[Mindscape, 1998],但直到《全面战争:幕府将军》**[The Creative Assembly, 2000],这些模型才在大量士兵中使用并取得了极大的成功,而没有性能问题:
**《战锤:黑暗预兆》**的截图,这是最早使用士兵情感模型的游戏之一
和
**《全面战争:幕府将军》**的截图。士兵的情感模型比《战锤:黑暗预兆》中的更复杂,但仍然成功地用于许多士兵
一些游戏甚至将 AI 作为游戏的核心。尽管最早这样做的一款游戏是**《Creatures》[Cyberlife Technology Ltd., 1997],但这一概念在《模拟人生》[Maxis Software, Inc., 2000]或《黑与白》**[Lionhead Studios Ltd., 2001]等游戏中更为明显:
**《模拟人生》**的截图。一个模拟者(角色)正在烹饪,这是游戏中由 AI 驱动的复杂行为的一部分。
在过去的 20 年里,许多 AI 技术已被采用和/或开发。然而,如果游戏不需要高级 AI,你可能会发现仍然广泛使用的有限状态机(FSMs),以及我们将很快在第二章中探讨的行为树,黑板。
游戏中的 AI – 行业与学术界
当涉及到比较应用于视频游戏的 AI,无论是在学术界还是在工业界,都存在很大的差异。我可以说,两者之间几乎有一场斗争。让我们看看背后的原因。事实上,它们的目标非常不同。
学术界希望创建能够智能思考并在环境中行动以及与玩家互动的游戏 AI 代理。
另一方面,游戏行业希望创建看起来能够智能思考并在环境中行动以及与玩家互动的游戏 AI 代理。
我们可以清楚地注意到,前者导致更真实的 AI,而后者导致更可信的 AI。当然,商业游戏更担心后者而不是前者。
我们将在第十四章中更详细地探讨这个概念,超越,当我们讨论创建游戏 AI 系统所涉及的心理学和游戏设计时。实际上,为了实现可信的行为,你通常需要尝试并使其尽可能真实。
然而,在更正式的术语中,我们可以这样说,游戏 AI 属于弱 AI(与强 AI相对)的范畴,它专注于以智能的方式解决特定任务或问题,而不是在其背后发展意识。无论如何,我们不会进一步探讨这个问题。
规划我们的旅程
现在是时候开始规划我们的旅程了,在跳入下一章之前。
技术术语
由于对于一些人来说,这是他们第一次进入 AI 领域,因此了解这本书(以及在 AI 中通常使用的)中使用的术语的小型词汇表很重要。我们在过去几页中已经遇到了其中的一些:
-
代理是能够自主推理以解决特定目标集的系统。
-
反向链式推理是通过向后工作来追踪问题原因的过程。
-
黑板是不同代理之间交换数据以及有时甚至在代理本身内部(特别是在虚幻引擎中)交换数据的架构。
-
环境是代理生活的世界。例如,游戏世界是同一游戏中 NPC 的环境。另一个例子是棋盘,它代表了一个与人类(或其他系统)下棋的系统的环境。
-
正向链式推理,与反向链式推理相反,是通过向前工作来找到问题解决方案的过程。
-
启发式是一种解决问题的实用方法,它不保证是最优的,也不足以满足即时目标。当寻找问题的最优解不切实际(甚至不可能)时,使用启发式方法来找到令人满意的解决方案。它们可以被视为在决策过程中减轻认知负担的心理捷径。有时,它可以代表基于代理过去经验的认知(尽管这通常是在先验的基础上给出的)。术语"启发式"源自古希腊,其意义为"找到"或"发现"。
en.wikipedia.org/wiki/Glossary_of_artificial_intelligence
自下而上的方法
通常,当一个系统被构建或研究时,有两种主要的方法:自上而下和自下而上。前者从系统的较高层次结构开始,逐渐进入系统的颗粒度细节。后者从基础开始,逐步创建依赖于前者的更复杂结构。两种方法都是有效的,但出于个人偏好,我选择了自下而上的方法来介绍本书的主题。
事实上,我们将从代理如何移动开始,然后理解它如何感知,最后使用这些数据来做出信息化的决策,甚至制定一个计划。这一点反映在这本书的结构和各部分中。
代理模式
由于在这本书中,我们将探讨人工智能代理如何感知、移动、规划和与周围环境交互的不同部分,因此绘制一个为此目的的方案将是有用的。当然,可能会有许多其他方案,它们都是同样有效的,但我相信这个方案对于开始AI 游戏开发特别有用:
本书将要使用的代理模型
由于我们选择了自下而上的方法,我们应该从底部读取模式。我们将更正式地称这个为我们的代理模型。
首先,我们可以看到代理总是与游戏世界交换信息,这包括几何、物理和动画,以及它们的抽象。这些信息被用于我们代理模型的所有层级。
从底层来看,我们首先关注的是如何在环境中移动。这是一个可以分解为运动和路径查找的过程(第三章*,导航*)。沿着链向上,我们可以看到代理感知世界(第四章,环境查询系统和第五章,代理意识*),并且基于这种感知,代理可以做出决策(第二章,行为树和黑板)。有时,在那一刻做出最佳决策可能不会在长期内带来更好的结果,因此代理应该能够提前规划。通常,在视频游戏中,一个 AI 系统(不一定是 NPC)可以控制多个角色,因此它应该能够协调一组角色。最后,代理可能需要与其他代理协作。当然,我们无法在这本书中深入探讨每个主题,但你可以自由地在网上查看,以便更深入地了解某些主题。
最后一点:通常,游戏中的 AI 不会一次性使用所有这些层级;有些只实现其中之一,或者混合使用。然而,在开始操作之前,了解事物的结构是很重要的。
虚幻引擎 AI 框架
尽管其他游戏引擎只提供渲染能力,但虚幻引擎自带了许多实现(并通过插件扩展)。这并不意味着制作游戏更容易,而是我们拥有更多开发游戏所需的工具。
docs.unrealengine.com/en-us/Gameplay/Framework
存在一个控制器类,它可以分为两个子类。第一个是玩家控制器;正如其名所示,它为游戏和玩家之间提供了一个接口(当然,这本书中并未涵盖,因为我们将会关注 AI 而不是通用的游戏玩法)。第二个类是 AIController,它提供的是我们的 AI 算法和游戏本身之间的接口。
以下图表展示了这些工具以及它们如何相互作用:
这两种控制器都可以拥有一个 Pawn,这可以被认为是一个虚拟化身。对于玩家来说,这可能就是主要角色;对于 AIController 来说,Pawn 可以是被玩家想要击败的敌人。
在这本书中,我们将只关注 AIController,以及所有围绕和在其下为我们的 AI 带来生命力的工具(我们不会涵盖前图中省略的部分)。我们将在稍后的阶段理解我的意思,但关键概念是我们将操作在AIController的层面。
如果你已经对 C++和 Unreal 有些熟悉,你可以查看其类,该类定义在AIController.h文件中,以了解更多关于这个控制器的信息。
我们旅程的草图
既然我们已经对将要使用的架构有了大致的了解,让我们按我们将要面对的主题的顺序(我说的是大致的顺序,因为有些主题会跨越多个章节,并且在我们对 AI 的了解扩展后需要迭代)来分解这本书将要涵盖的内容。
然而,你可以将这本书视为分为三个部分:
-
第 2-7 章:对不同内置 AI 系统的描述
-
第 8-10 章:如何使用我们在前几章中探索的 AI 系统的具体示例
-
第 11-13 章:对游戏 AI 的不同调试方法的描述(因为我相信这部分与了解系统本身同等重要)
让我们详细谈谈这本书将要涵盖的内容。
使用行为树进行决策(第 2、6、8、9 和 10 章)
一旦智能体能够感知其周围的世界,它就需要开始做出决策,这些决策会有后果。某些决策过程可能会变得非常复杂,以至于智能体需要制定一个适当的计划才能成功实现目标。
内置的 Unreal Engine 框架围绕行为树旋转,这占据了本书的大部分内容。当然,这并不排除你在 Unreal 中自行实现其他 AI 系统进行决策的可能性,但通过选择行为树,你将拥有一套强大的工具集,我们将在本书中详细探讨。
导航(第 3 和 7 章)
除非游戏是离散的或回合制的,否则每个 AI 智能体都需要以连续的方式在其自己的环境中移动。Unreal 提供了一个强大的导航系统,允许你的 AI 智能体在环境中轻松导航,从坠落到跳跃,从蹲伏到游泳,到不同类型的区域和不同类型的智能体。
这个系统如此庞大,要全部涵盖它将很困难,但我们将尽力涵盖你开始学习第三章,“导航”所需的所有内容。
环境查询系统(第 4 和 12 章)
环境查询系统 (ESQ) 可以从代理周围的环境中收集信息,从而允许代理据此做出决策。本书专门用一章来介绍这个系统。实际上,它位于第五章,代理意识和决策制定之间,并且是已经内置到 Unreal 中的宝贵资源。
代理意识(第五章和第十二章)
代理意识(或感知)涉及赋予 AI 代理感官的能力。特别是,我们将涵盖视觉,这是最常见和最广泛使用的,但也会涉及听觉和嗅觉。
此外,我们将开始探讨这些数据如何被用于高级结构中,以便代理可以相应地行动。
人群(第七章)
当你在地图中拥有许多 AI 代理时,环境会变得容易过于拥挤,各种代理可能会相互干扰。人群系统允许你控制大量 AI 代理(同时它们可以保持个体行为),以便它们可以避免彼此。
设计行为树(第 8、9 和 10 章)
对于 AI 开发者来说,仅仅了解行为树的工作原理是不够的:他们需要知道如何设计它们。实际上,你的大部分工作都是关于创建一个抽象系统来协调所有 AI 代理,然后你才会花剩下的时间来实现它。因此,我们将涵盖一个单一且庞大的示例,展示如何从头开始设计、创建单个部分,并构建一个完整的 行为树。
游戏 AI 的调试方法(第 11、12 和 13 章)
一旦你了解了所有不同的 AI 系统,你就可以开始对这些系统进行实验或编写游戏,但如何理解你的 AI 是否按照你的计划执行并且/或者表现良好?调试方法在任何软件中都是关键,但在游戏 AI 中,你还需要视觉调试。因此,Unreal Engine 提供了许多调试方法(包括一些专门针对 AI 的方法),我坚信了解这些方法非常重要。你不仅会学习工具,还会学习如何根据你的需求扩展它们。
超越(第十四章)
本书最后一部分将探讨一些目前在 AI 领域正在进行的激动人心的想法和创新,并为你继续美妙旅程提供灵感。我将介绍一些正在进行的 AI 研究,这些研究被应用于游戏,以及这最终如何对你的游戏产生益处。在这个领域,了解新技术和算法是关键,这样你才能始终保持最新。
为 C++用户启用 AI
.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", *"GameplayTasks", "AIModule"* });
摘要
在本章中,我们看到了视频游戏中的 AI 世界是多么奇妙。我们探索了视频游戏背后的历史,无论是在学术界还是在工业界。我们在本书中规划了我们的旅程,并解释了它将是什么样子。
现在,是我们准备的时候了,因为从下一章开始,我们将深入实践,直接进入 Unreal Engine。
第二章:行为树和黑板
一个决定我们如何行动的树,一个黑板来记住它!
欢迎来到第二章Chapter 2,行为树和黑板。从这里开始,事情开始变得更有趣,因为我们将会学习如何使用两个主要的虚幻 AI 框架结构。首先,我们将查看行为树并了解它们的主要组件,如任务、装饰器和服务。接下来,我们将学习关于黑板的内容以及如何将其与行为树集成。在完成这些之后,我们将能够设置使用行为树的 AI 控制器,这对于实现本书中其余的技术至关重要。
如你所注意到的,我们首先学习一点理论,然后直接进入实践来理解它是如何运作的。这是我们将在每一章中遵循的模式。所以,让我们开始吧。
在决策领域,有许多可以使用的数学结构。有限状态机(FSM)是一个简单而强大的例子,它能够做出复杂的决策。然而,在游戏人工智能的世界中,还有一个结构可以被非 AI 专家使用:行为树。
因此,虚幻引擎的设计选择之一是它内置了对行为树的支持,并且实际上是 AI 框架的主要核心部分。这并不意味着你不能实现其他决策过程或结构,但使用内置的行为树支持将大大有利于你团队的预算(从时间角度考虑)。所以,在你实现在虚幻引擎中的不同决策结构之前,请三思是否这是一个好的决定(当然,行为树可能不是你游戏的最佳选择,但请记住它们有内置支持,可能是一个节省时间的好方法)。然而,你仍然可以在行为树中实现子结构来扩展其功能,但不要急于求成;首先,让我们来了解一下行为树的基础知识。
特别是,在本章中,我们将学习以下主题:
-
行为树是什么,从更广泛的角度和虚幻引擎的上下文中来看。
-
在虚幻引擎中,行为树是如何工作的,包括其不同的组件以及它们如何与树交互
-
什么是黑板以及它如何用于存储行为树的数据
-
如何通过使用 AI 控制器启动运行行为树,无论是在蓝图还是 C++中
那么,让我们深入探讨!
行为树是如何工作的
考虑到行为树在我们 AI 代理中的作用,最简单的方法是将其想象成一个大脑。它做出决定,并相应地采取行动。它是我们代理中人工智能的处理器。在我们开始之前,如果你在其他环境中对行为树有任何经验,重要的是要理解它们在虚幻引擎中的不同。
docs.unrealengine.com/en-US/Engine/AI/BehaviorTrees/HowUE4BehaviorTreesDiffer
然而,在这里强调一个关键的区别是很重要的:虚幻引擎的行为树是从上到下读取的,节点将从左到右执行。在其他环境中,你可能发现顺序相反,即树是从左到右读取的,节点是从上到下执行的。
如果这是你第一次遇到行为树,那么当你阅读下一节时,这将会变得有意义。
数学树的结构
好的,现在是时候了解一个行为树是如何工作的了。首先,正如其名所示,它是一个树,从数学的角度来说。
zh.wikipedia.org/wiki/树 _(图论)mathworld.wolfram.com/Tree.html
需要明确的是,一个(数学)树表达了节点之间的关系。在这个意义上,描述家庭(例如,父母、子女、兄弟姐妹)的技术术语中采用了相同的关系。为了简化对树的了解,你可以想象你的家谱树:每个节点是一个人,连接人们的分支(即关系)是各种人之间的关系。然而,结构还是略有不同。
那么,什么是树?它是一个描述不同节点之间关系的图。
特别是,有一个根节点,它是唯一没有父节点的节点。从那里,每个节点可以有一个或多个子节点,但只有一个父节点。没有子节点的终端节点被称为叶子节点。以下是一个简单的图表,帮助你理解一般数学树的基本结构:
这可能听起来很复杂,但实际上并不复杂。当我们继续前进并讨论行为树时,事情将会变得有趣起来。
行为树组件
如果你查看官方文档,你会发现有五种类型的节点(任务、装饰器、服务、组合和根)可供使用,具体取决于你试图创建的行为类型(以及随后 AI 在世界上应该如何行动)。然而,我想以更易于理解的方式重新表述这一点,并希望它更实用。
除了根节点之外,唯一的一种不是叶子的节点是组合节点。叶子被称为任务。装饰器和服务是组合节点或任务叶子的附加功能。尽管虚幻引擎允许你将组合节点作为叶子使用,但你不应该这样做,因为这意味着你可以移除该节点,而行为树仍然会以相同的方式工作。以下是一个显示所有不同类型节点的树形结构的示例(实际上,我们将在本书的后面部分构建这个行为树):
当树在执行时,你需要从根节点开始,沿着树向下,从左到右读取节点。你以特定的方式遍历所有不同的分支(组合节点),直到我们达到一个叶子,即任务。在这种情况下,AI 将执行那个任务。重要的是要注意,任务可能会失败,例如,如果 AI 无法完成它。任务可能会失败的事实将有助于理解组合节点的工作原理。毕竟,决策过程只是选择执行哪个任务以更好地实现目标(例如,杀死玩家)。因此,根据哪个任务未能执行(或者,正如我们将看到的,装饰器可以使任务或整个分支失败),组合节点将确定树中的下一个任务。
此外,当你创建你的行为树时,每个节点都可以被选中,你可以在详细面板中找到一些调整节点/叶子的行为设置的选项。此外,由于顺序很重要,行为树中的节点有数字(在右上角)来帮助你理解节点的顺序(尽管它始终是从上到下,从左到右)。以下截图显示了你可以找到这些数字的位置:
“-1”的值意味着节点将不会以任何顺序执行,节点周围的色彩会略暗。这可能是由于节点以某种方式未连接到根,因此它是孤立的:
让我们详细看看这些组件,并特别注意组合节点。
根
对于根节点,没有太多可说的。树需要从某个地方开始,所以根节点就是树开始执行的地方。下面是这个节点的样子:
请注意,根节点只能有一个子节点,并且这个子节点必须是组合节点。您不能将任何装饰器或服务附加到根节点。如果您选择根节点,它没有任何属性,但您将能够分配一个黑板(我们将在本章后面介绍),如下面的屏幕截图所示:
任务
当我们想到一棵树时,我们通常会想象一个粗大的树干和树枝,树枝上长着叶子。在 UE4 的上下文中,那些“叶子”就是我们所说的“任务”。这些是执行各种动作的节点,例如移动 AI,并且可以附加装饰器或服务节点。然而,它们没有输出,这意味着它们本身不参与决策过程,这个决策完全由组合节点负责。相反,它们定义了如果该任务需要执行,AI 应该做什么。
请注意,任务可以像您喜欢的那样复杂。它们可以像等待一段时间那样简单,也可以像在射击玩家的同时解决谜题那样复杂。大任务难以调试和维护,而小任务可能会使行为树变得过于拥挤和庞大。作为一名优秀的 AI 设计师,您应该尝试在任务的大小之间找到平衡,并以一种方式编写它们,以便它们可以在树的各个部分(甚至在其他树中)重复使用。
一个任务可以失败(报告失败)或成功(报告成功),并且它不会停止执行直到报告这两个结果之一。组合节点负责处理这个结果并决定下一步要做什么。因此,一个任务可能需要几个帧来执行,但它只有在报告了失败或成功时才会结束。当您继续到第六章,扩展行为树时,请记住这一点,在那里您将创建自己的任务。
任务可以有参数(一旦选择了一个任务,您就可以在详细信息面板中设置这些参数),通常它们是硬编码的值或黑板键引用(关于黑板的更多内容将在本章后面介绍)。
在行为树编辑器中,任务以紫色框的形式出现。在下面的屏幕截图中,您可以查看一些任务的示例以及它们在编辑器中的外观:
Unreal 自带一些内置的任务,可以直接使用。它们是通用的,涵盖了您可能需要的最基本的情况。显然,它们不能针对您的游戏特定,因此您需要创建自己的任务(我们将在第六章,扩展行为树)中查看这一点)。
这里是 Unreal 内置任务的列表:
-
完成并返回结果:强制任务立即返回一个完成结果(无论是失败还是成功)。
-
制造噪音:产生一个噪音刺激,由感知系统(这将在第五章,代理意识)使用。
-
直接朝向移动:与下面的节点类似,但它忽略了导航系统。
-
移动到:使用导航系统(我们将在第三章,导航)将 Pawn 移动到从黑板中指定的位置(我们将在本章后面介绍黑板)。
-
播放动画:正如其名所示,此节点播放动画。然而,除了例外情况(这也是此节点存在的原因)之外,将动画逻辑和行为逻辑分开是良好的实践。因此,尽量不使用此节点,而是改进你的动画蓝图。
-
播放声音:正如其名所示,此节点播放声音。
-
推送 Pawn 动作:执行一个Pawn 动作(不幸的是,我们不会在本章中介绍它们)。
-
旋转以面对 BB 条目:将 AI Pawn 旋转以面对在 Blackboard 中记住的特定键(我们将在本章后面介绍黑板)。
-
运行行为:作为一个整体子树运行另一个行为树。因此,可以嵌套行为树以创建和组合非常复杂的行为。
-
运行行为动态:与前面的节点类似,但在运行时可以更改要执行的(子)行为树。
-
运行 EQS 查询:执行一个EQS 查询(我们将在第四章中看到它们,环境查询系统)并将结果存储在黑板中。
-
设置标签冷却时间:通过使用标签为特定的冷却时间节点设置计时器(我们将在本章后面介绍装饰器)。
-
等待:停止行为一段时间。可以指定一个随机偏差,使等待的时间每次都不同。
-
黑板时间等待:与上一个节点类似,但时间是从黑板中获取的(关于黑板的更多内容将在本章后面介绍)。
现在我们已经了解了任务节点的工作方式,让我们来探索组合节点,这些节点根据任务返回的是失败还是成功来做出决策。
组合
组合节点是 Unreal 中行为树决策能力核心,理解它们的工作方式是关键。
有三种组合节点:选择器、序列和简单并行。最后一种最近被添加,你会发现通过使用选择器和序列的组合,你将能够覆盖大多数情况。以下是它们的工作方式:
选择器:这种节点会尝试找到其子节点中的一个来执行,这意味着它会尝试找到一个分支(因此作为子节点附加的另一个复合节点)或一个任务(另一个子节点,但它是一个叶子)。因此,选择器从最左边的子节点开始尝试执行它。如果它失败了(无论是任务未能执行,还是整个分支失败了),那么它将尝试第二个最左边的,依此类推。如果一个子节点返回成功,这意味着任务已经完成或整个分支已经完成,那么选择器将向其父节点报告成功,并停止执行其他子节点。另一方面,如果选择器的所有子节点都报告失败,那么选择器也将向其父节点报告失败。在下面的屏幕截图中,你可以看到选择器节点的外观:
序列:这种节点的工作方式有点像选择器的反面。为了向其父节点报告成功,序列的所有子节点都必须报告成功。这意味着序列将开始执行最左边的子节点。如果它成功了,它将继续执行第二个最左边的,依此类推,如果也成功了。如果所有子节点直到最右边的都是成功,那么序列将向其父节点报告一个成功。否则,如果只有一个子节点失败,那么序列将停止执行其子节点,并向父节点报告一个失败。在下面的屏幕截图中,你可以看到序列节点的外观:
简单并行:这是一种特定的复合节点,用于特定情况。实际上,它只能有两个子节点。最左边的子节点必须是一个任务,而最右边的子节点可以是任务或复合(从而产生一个子树)。简单并行开始并行执行其所有子节点,尽管最左边的一个被认为是主要的。如果主要的一个失败了,它将报告一个失败,但如果主要的一个成功了,那么它将报告一个成功。根据其设置,简单并行一旦完成了主要任务的执行,可以选择等待子树执行结束,或者直接向父节点报告主要任务的成功或失败,并停止执行子树。在下面的屏幕截图中,你可以看到简单并行节点的外观。请注意,只能拖动两个子节点,其中最左边的一个必须是一个任务(紫色块是可拖动区域):
以这种方式,复合 节点可以根据其子节点报告的内容(失败或成功)来“决定”执行哪些任务,并且 复合 节点会向其父节点报告(要么失败要么成功)。即使根节点(也是一个 复合 节点)的唯一子节点向 根节点 报告成功,那么整个树已经成功执行。一个好的 行为树 设计应该始终允许成功。
装饰器
装饰器 节点(也称为条件)附加到 复合 或 任务 节点上。装饰器 节点决定 行为树 中的某个分支,甚至单个节点是否可以执行。本质上,它们是一个条件;它们检查是否应该发生某事。换句话说,一个 装饰器 可以检查是否值得继续该分支,并且如果根据条件我们确定 任务(或子树)将失败,它可以报告一个预防性的 失败。这将避免装饰器尝试执行一个不可能的任务(或子树)(由于任何原因:信息不足,目标不再相关等…)。
通常,装饰器节点可以充当父节点和其余子树之间的 门。因此,装饰器有权力循环子树直到满足某个条件,或者直到特定计时器到期才在子树中执行,甚至可以改变 子树 的返回结果。
例如,想象有一个专门用于杀死玩家的子树(它将做出决策,使代理尝试杀死玩家)。检查玩家是否在范围内(并且不是来自地图的另一侧),或者甚至玩家是否仍然存活,可能会在没有执行该子树的情况下给我们一个预防性的失败。因此,树可以继续执行其他事件或树的其余部分,例如,在另一个子树中,该子树将负责游荡行为。
装饰器 可以有参数(一旦选择了一个 装饰器,你将在 详情面板 中能够设置这些参数),通常它们是硬编码的值或 黑板键引用(关于 黑板 的更多内容将在本章后面介绍)。
几乎每个 装饰器 都有一个复选框在其参数中,允许你反转条件(因此,你将拥有更多的自由,并且可以在树的两个不同部分使用相同的装饰器来执行不同的条件)。
以下截图展示了如何将装饰器附加到 复合 节点上。请注意,每个节点可以有多个装饰器:
docs.unrealengine.com/en-us/Engine/AI/BehaviorTrees/HowUE4BehaviorTreesDiffer
与任务类似,Unreal 自带一些内置的装饰器,它们可以立即使用。它们是通用的,涵盖了你可能需要的最基本的情况,但显然,它们不能针对你的游戏或应用程序进行特定化,因此你需要创建自己的装饰器(我们将在第六章扩展行为树中详细讨论)。
这里是 Unreal 内置任务列表:
-
黑板:检查黑板上的特定键是否设置(或未设置)。
-
检查演员游戏标签:正如其名所示,它检查是否有由黑板值指定的特定游戏标签(或多个标签)在指定的演员上。
-
比较 BB 条目:比较两个黑板值,并检查它们是否相等(或不相等)。
-
组合:这允许你使用布尔逻辑一次性组合不同的装饰器。一旦放置了此装饰器,你可以通过双击它来打开其编辑器。从那里,你将能够使用布尔运算符和其他装饰器构建一个图。
-
条件循环:只要条件得到满足(无论黑板键是否设置或未设置),它将不断循环通过子树。
-
锥形检查:这检查一个点(通常另一个演员)是否在从另一个点(通常为 AI 代理)开始的锥形内;锥形角度和方向可以更改。其使用的一个例子是,如果你想检查玩家是否在敌人前方——你可以使用此代码来确定此条件。
-
冷却时间:一旦执行从包含此装饰器的分支退出,将启动冷却计时器,并且此装饰器不允许执行在此计时器过期之前再次进入(它立即报告失败)。此节点用于确保你不频繁地重复相同的子树。
-
路径是否存在:这使用导航系统(关于这一点,请参阅第三章导航)来确定(并检查)是否存在特定点的路径。
-
强制成功:正如其名所示,它强制子树成功,无论是否从下面报告了失败(或成功)。这对于在序列中创建可选分支非常有用。
注意,强制失败不存在,因为这没有意义。如果将其放置在选择上,这将使其成为一个序列,如果将其放置在序列上,它将只执行一个子节点。
-
位于位置:正如其名所示,它检查 Pawn 是否(靠近或)位于特定位置(可选地,使用导航系统)。
-
是类的 BB 条目:正如其名所示,它检查特定的黑板条目是否属于特定的类。当黑板条目是 Object 类型,并且需要检查黑板内的引用是否属于特定类(或继承自一个类)时,这很有用。
-
保持圆锥内:与圆锥检查类似,这个装饰器(持续地)检查观察者是否在圆锥内。
-
循环:正如其名所示,它会在特定子树中循环特定次数(甚至无限次数;在这种情况下,需要其他东西来停止子树的行为,例如另一个装饰器)。
-
设置标签冷却时间:与同名的任务类似,当这个装饰器变得相关(或者如果你将其想象为一个门,当它被穿越时),它将改变特定标签的冷却时间计时器(参见以下节点)。
-
标签冷却时间:这与冷却时间节点相同,但它与一个标签相关联的计时器。因此,这个计时器可以通过"设置标签冷却时间" 任务和"设置标签冷却时间" 装饰器来改变。
-
时间限制:正如其名所示,它为子树完成其执行提供时间限制。否则,这个装饰器将停止执行并返回失败。
现在我们已经了解了装饰器节点的工作方式,让我们探索行为树中的最后一种节点类型,服务节点,这些节点将连续更新并提供实时信息。
服务
服务节点连接到组合或任务节点,并且如果它们的分支正在执行,它们将执行。这意味着只要节点下方有节点连接,无论父-子级别有多少层正在执行——服务也会运行。以下截图将帮助您可视化这一点:
这意味着服务节点是行为树执行的眼睛。实际上,它们会持续运行(如果子树是活跃的),并且可以实时执行检查和/或更新黑板(稍后介绍)的值。
服务节点 确实是为你的 行为树 应用程序量身定制的,因此只有两个默认节点。它们的一个用法示例可能是向子树提供/更新信息。例如,想象一个场景,子树(敌人)试图杀死玩家。然而,即使玩家没有向敌人射击,追求这个目标也是愚蠢的(好吧,这取决于敌人的类型,巨魔可能并不那么聪明)。因此,当子树试图杀死玩家时,子树需要找到掩护来减少敌人受到的伤害。然而,敌人可能在地图上移动,或者玩家可能摧毁了我们 AI 藏身的掩护。因此,子树需要有关最近且最安全的掩护位置的信息,这个位置仍在玩家的射程内(一个 EQS 查询 可以计算出这个信息)。服务可以实时更新这些信息,以便当子树需要使用有关掩护的数据时,它们已经准备好了。在这个特定的例子中,为了找到掩护,在服务上运行 环境查询 是处理这个任务的动态方式(我们将在 第四章,环境查询系统)中探讨这个话题)。否则,服务 可能会检查地图上设计师放置的某些指定点,并评估哪个最适合其给定的动作。
如你所见,服务节点 可以非常强大,但它们也特定于你使用它们的应用程序。因此,它们确实取决于你为你的游戏编写的 AI。
下面的屏幕截图显示了几个服务示例。请注意,服务 可以与 装饰器 一起使用,并且一个 组合节点 可以有多个 服务:
可用的两个默认 服务(因为你将需要为你的游戏编写自己的服务,我们将在 第六章*,扩展行为树*)在下面的屏幕截图中显示:
-
设置默认焦点:当这个节点变为活动状态时,它会自动为 AI 控制器 设置 默认焦点。
-
运行 EQS (定期查询):正如其名所示,它定期运行 环境查询(有关更多信息,请参阅 第四章,环境查询系统),以检查特定的位置或演员。这是我们例子中寻找掩护所需的这种服务。
你将在第四章“环境查询系统”中了解更多关于环境查询的内容。然而,目前你需要知道的是,这是一个用于空间推理的系统,运行这些查询可以在空间中找到具有特定属性的位置(或演员)(在寻找掩护敌人的例子中,最大化这些属性的那个:最近的、最安全的,并且仍然在射程内可以射击玩家)。
现在,我们已经了解了组成行为树的不同类型的节点。现在,是时候探索黑板了!
黑板及其与行为树集成
将行为树视为大脑,我们可以将黑板视为其记忆——更具体地说,是 AI 的记忆。黑板存储(并设置)用于行为树使用的键值。
它们被称为黑板,因为在教室里,黑板是一个传递大量信息的地方,但其中大部分信息是学生之间共享的;分发给学生的单独笔记是私人的。你可以将学生想象为不同的任务(和节点),而黑板则是一个共享的数据空间。
黑板相对简单易懂,因为它们只比数据结构复杂一点。唯一的区别在于可以将一个黑板分配给特定的行为树,这个黑板被树中的每个节点共享。因此,每个节点都可以读取和/或写回黑板。
对于那些熟悉黑板设计模式的人来说,在虚幻引擎中,它们只是承担了为行为树保存记忆的角色。
它的工作方式就像一个字典(数据结构),其中键对应一个特定的值类型(例如,一个向量、一个浮点数、一个演员等……,甚至是另一个黑板键)。因此,通过使用或回忆键,可以写入或读取相关的值。
黑板的另一个酷炫特性是它们可以通过继承来扩展。这意味着另一个黑板可以作为一个父类,子类将继承所有父类的键值对,再加上子类本身包含的一些特定键值对。
现在我们已经涵盖了理论部分,让我们看看如何创建一个行为树并让它运行。要做到这一点,让我们先创建一个新的项目。
创建我们的 AI 项目
从现在起,我们将通过创建项目来实践,并了解我们关于行为树所学的知识。在本节中,我们将创建一个简单的树,但随着我们在下一章学习更多其他主题,我们将迭代行为树的工具。这将为你提供更好的理解,了解创建出色的行为树所需的工具。然后,在第八章,设计行为树 - 第一部分,第九章,设计行为树 - 第二部分,和第十章,设计行为树 - 第三部分中,我们将专注于如何从头开始创建和设计一个追逐玩家的行为树,这将为你提供关于行为树的实用方法。
为了能够测试本书将要探索的技术,我们需要创建一个项目。通过这样做,你将能够跟随本书中将要涵盖的实践方面。
你可以从模板创建一个新项目。第三人称模板特别适用。实际上,它已经内置了一个角色,可以很容易地被 AI 控制。这意味着你不必过多担心与 AI 无关的细节,例如动画。你可以选择蓝图版本或 C++版本。我将在整个过程中用蓝图和 C++术语解释我们将要覆盖的概念,但请注意,本书中的一些技术如果用 C++编写将运行得更好。因此,我选择了第三人称模板的 C++版本,尽管这个初始选择对我们影响不大(我们是在编写 AI,而不是玩家或游戏玩法)。
UnrealAIBookhog.red/AIBook2019ProjectFiles
从 AI 控制器开始行为树
现在我们已经了解了行为树的基本概念及其构成,让我们来创建自己的行为树。回顾前一章,负责拥有并控制棋子的类是 AI 控制器。因此,我们的行为树应该在AI 控制器上运行。
我们有两种方法可以做到这一点。第一种是使用蓝图。通常,即使你是程序员,最好也使用蓝图来创建一个行为树,因为逻辑非常简单,控制器也很简单。另一方面,如果你是 C++爱好者,并且想尽可能多地使用它,即使是对于小任务,不用担心——我会再次重构我们在蓝图中所做的相同逻辑,但这次是在 C++中。无论如何,行为树资产应该在编辑器中创建和修改。你最终要编写的程序节点将不同于默认可用的节点(我们将在本书的后面看到这一点),但树本身始终是在编辑器中制作的。
创建行为树和黑板
要开始,我们需要创建四个蓝图类:AI 控制器、角色、行为树和黑板。我们将在后面介绍 AI 控制器。如果你选择了两个第三人称模板之一,你应该已经有一个角色准备好了。因此,你只需要创建一个行为树和一个黑板。
Chapter2AI
创建黑板
AI
BB_MyFirstBlackboardBB_
由于在同一行为树上无法拥有多个黑板,您可以在黑板详情面板中使用继承,父级和子级,如下所示(右边的截图):
创建行为树
让我们通过转到内容浏览器并选择添加新项 > 人工智能 > 行为树来添加一个行为树,如下面的截图所示:
BT_MyFirstBehaviorTreeBT_
当你打开行为树窗口时,你会看到一个名为根的单个节点,如下所示:
根节点是您的行为树执行开始的地方(从上到下,从左到右)。根节点本身只有一个引用,那就是黑板,因此它不能连接到其他任何东西。它是树的顶端,所有后续的节点都在其下方。
如果您从根节点拖动,您将能够添加组合节点:
对于此,行为树编辑器非常直观。您可以从节点拖动以添加组合或任务节点。要添加装饰器或服务,您可以在节点上右键单击并选择“添加装饰器…”或“添加服务…”,如图所示:
最后,如果您单击一个节点,可以在详细信息面板中选择其参数(以下截图显示了一个移动到节点的示例):
运行行为树的 AI 控制器
下一步是从AI 控制器运行行为树。通常,这是一个简单的任务,在蓝图(其中可以直接引用特定的行为树)中实现。即使我们有复杂的C++ AI 控制器,我们也可以在蓝图扩展控制器并从蓝图运行行为树。在任何情况下,如果硬引用不起作用(例如,您正在使用 C++或因为您想要有更多的灵活性),那么您可以将行为树存储在需要运行该特定行为树的角色/单位中,并在AI 控制器拥有单位时检索它。
让我们探索如何在蓝图(我们将在一个变量中引用行为树,我们可以决定其默认值)和 C++(我们将把行为树存储在角色中)中实现这一点。
蓝图中的 AI 控制器
我们可以通过单击添加新 | 蓝图类 | AI 控制器来创建蓝图 AI 控制器。您必须单击所有类并搜索AI 控制器来访问它。您可以在以下截图中看到一个示例:
BP_MyFirstAIController
首先,我们需要创建一个变量,以便我们可以存储我们的行为树。尽管保留对行为树的引用不是必需的,但这是一个好的实践。要创建变量,我们需要在我的蓝图面板中按下***+ 变量 按钮,位于变量***标签旁边,如图所示(请注意,您的光标需要位于变量标签上,按钮才会显示):
然后,作为一个变量类型,你需要选择行为树并给它一个名称,例如BehaviorTreeReference。这就是你的变量应该看起来像的样子:
然后,在详细面板中,我们将设置默认值(记住,为了设置默认值,蓝图需要编译):
然后,我们需要重写On Possess函数,如下面的截图所示:
最后,在AI 控制器的事件拥有中,我们需要开始运行/执行行为树。我们可以通过使用以下简单的节点,命名为运行行为树来实现这一点:
结果,你的 AI 控制器将能够执行存储在BehaviorTreeReference中的行为树。
C++中的 AI 控制器
docs.unrealengine.com/en-us/Programming/QuickStartdocs.unrealengine.com/en-us/Programming/Development/CodingStandard
.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", **"GameplayTasks", "AIModule"** });
这将确保你的代码可以无问题编译。
让我们创建一个新的 C++类,如下面的截图所示:
这个类需要从AIController类继承。你可能需要检查右上角的显示所有类复选框,然后使用搜索栏,如下面的截图所示:
Chapter2AI
现在,点击创建并等待你的编辑器加载。你可能看到如下内容:
UnrealAIBookCharacter.h
//** Behavior Tree for an AI Controller (Added in Chapter 2)
UPROPERTY(EditAnywhere, BlueprintReadWrite, category=AI)
UBehaviorTree* BehaviorTree;
对于那些仍然不熟悉的人来说,这里有一段更大的代码块,以便你可以理解如何在类中放置前面的代码:
public:
AUnrealAIBookCharacter();
/** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
float BaseTurnRate;
/** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
float BaseLookUpRate;
*//** Behavior Tree for an AI Controller (Added in Chapter 2)*
*UPROPERTY(EditAnywhere, BlueprintReadWrite, category=AI)*
*UBehaviorTree* BehaviorTree;*
.generated
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BehaviorTree.h" #include "UnrealAIBookCharacter.generated.h"
关闭角色类,因为我们已经完成了它。因此,每次我们在世界中放置该角色的实例时,我们都能在详细信息面板中指定一个行为树,如下面的截图所示:
.hPossess()
UCLASS()
class UNREALAIBOOK_API AMyFirstAIController : public AAIController
{
GENERATED_BODY()
protected:
//** override the OnPossess function to run the behavior tree.
void OnPossess(APawn* InPawn) override;
};
.cppUnrealAIBookCharacter
#include "MyFirstAIController.h"
#include "UnrealAIBookCharacter.h" #include "BehaviorTree/BehaviorTree.h"
Possess()ifnullptr
void AMyFirstAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
AUnrealAIBookCharacter* Character = Cast<AUnrealAIBookCharacter>(InPawn);
if (Character != nullptr)
{
UBehaviorTree* BehaviorTree = Character->BehaviorTree;
if (BehaviorTree != nullptr) {
RunBehaviorTree(BehaviorTree);
}
}
}
RunBehaviorTree()
一旦我们编译了我们的项目,我们就能使用这个控制器。从层级中选择我们的 AI 角色(如果您没有,您可以创建一个),这次,在详细信息面板中,我们可以设置我们的 C++控制器,如下所示:
此外,别忘了在详细信息面板中将行为树分配好,我们总是这样做:
因此,一旦游戏开始,敌人将开始执行行为树。目前,树是空的,但这给了我们所需的架构,以便我们可以开始使用行为树。在接下来的章节中,我们将更详细地探讨行为树,特别是在第八章、第九章和第十章,我们将探讨设计和构建行为树的更实际的方法。
摘要
在本章中,我们介绍了什么是行为树以及它们包含的一些内容,包括任务、装饰器和服务。接下来,我们学习了黑板以及如何将其与行为树集成。然后,我们创建了一个行为树并学习了如何从AI 控制器(在蓝图和 C++中)启动它。通过这样做,我们建立了一个坚实的基础,为我们提供了关键知识,以便我们可以处理这本书的其他部分。
因此,在这本书中,我们将遇到更多的行为树,您将有机会掌握它们。但在那之前,我们首先需要了解一些特定的主题。一旦我们有了导航和感知(包括 EQS)的坚实基础,我们就可以迭代行为树来理解复合节点的作用,以及装饰器和任务。此外,我们还将能够创建自己的。第八章、第九章和第十章将指导您从头开始创建行为树的过程,从设计阶段到实现阶段。
但在那之前,让我们继续到下一章,我们将讨论导航和路径查找!
第三章:导航
路径查找背后的问题是与克诺索斯迷宫一样古老:如何使用最短路径从点 A 到点 B,同时避开所有中间的障碍物?
已经开发了许多算法来解决路径查找问题,包括与 A*算法相关的问题,该算法最早在 20 世纪 60 年代计算机科学中引入(第二部分)。
路径查找例程是许多视频游戏的典型组件,非玩家角色(NPC)的任务是在游戏地图上找到最优路径,这些路径可以不断变化。例如,通道、门或门在游戏过程中可以改变其状态。
在路径查找方面存在许多问题,不幸的是,我们并没有一个通用的解决方案。这是因为每个问题都会有其特定的解决方案,这取决于问题的类型。不仅如此,它还取决于你正在开发的游戏类型。例如,AI 的最终目的地是一个静态建筑(静止的),还是他们需要跳上漂浮的筏子(动态的)?你还需要考虑地形——它是平坦的还是多岩石的,等等?为了增加额外的复杂性,我们还需要考虑是否存在障碍物,以及这些物体是静态的(如消防栓)还是可以移动的(例如箱子)。然后,我们需要考虑实际的路径本身。例如,沿着道路旅行可能更容易,但穿越屋顶会更快地到达目的地。遵循同样的思路,AI 可能甚至没有最终目的地,从某种意义上说,他们不需要去某个特定的地方。例如,他们可能只是像村庄里的人一样四处闲逛。然而,我仅仅指出了与路径查找相关的一些问题和考虑因素。随着你经历使用路径查找的不同情况,你可能会遇到其他问题。请记住要有耐心,并考虑我提到的所有变量以及其他特定于你情况的因素。
幸运的是,虚幻引擎已经集成了可以用于最常见情况的导航系统。因此,我们不需要从头开始重新实现一切。本章的主要目标是确保你了解如何使用它,并确保你对如何扩展它有一些想法。
在本章中,我们将涵盖以下主题:
-
对导航系统的期望
-
虚幻导航系统及其工作原理
-
如何生成导航网格以及其可用设置
-
如何修改导航网格,以下方法:
-
导航区域,以改变与导航网格一部分相关的权重
-
导航链接,以连接原本分开的导航网格的两部分
-
导航过滤器,在执行对导航系统的特定查询时对导航网格进行轻微的更改
-
让我们深入探讨吧!
对导航系统有什么期望
首先,在我们探索虚幻导航系统之前,定义一下我们对一个通用的导航系统的期望是有用的。以下是从导航系统中所需的内容:
-
需要确定在地图上的两个通用点之间是否存在一条路径(可以由执行查询的代理穿越)
-
如果存在这样的路径,则返回对代理最方便的路径(通常是最近的路径)
然而,在搜索最佳路径时,有许多方面需要考虑。一个好的导航系统不仅应该考虑这些方面,而且应该在相对较短的时间内执行查询。以下是一些这些方面的例子:
-
执行查询的人工智能代理能否通过地图上的特定部分?例如,可能有一个湖,AI 角色可能不知道如何游泳。同样,代理能否蹲下并进入通风隧道?
-
人工智能代理可能想要避免(或偏好)某些路径,这些路径不一定是最近的。例如,如果一座建筑着火了,代理应该尽量避免这种情况,否则可能会被烧伤。作为另一个例子,假设有两条路径:一条被敌人的火力覆盖,但路程较长,而另一条路程较短但暴露在敌人的火力下;AI 应该选择哪一条?虽然这可能是决策过程的一部分,但在路径查找的层面上可以实施一些启发式方法,并且导航系统应该支持它们。
-
地图可能是动态的,这意味着障碍物、物体、道路、悬崖等在游戏过程中会发生变化。导航系统能否在实时处理这些变化的同时,并纠正生成的路径?
现在,是时候看看虚幻是如何实现所有这些功能的了。
虚幻导航系统
虚幻导航系统基于一个导航网格(简称Nav Mesh)。它包括将可导航空间划分为区域——在这种情况下,多边形——这些区域被细分为三角形以提高效率。然后,为了到达某个地方,每个三角形被视为图中的一个节点,如果两个三角形相邻,则它们的相应节点相连。在这个图上,你可以执行路径查找算法,如带有欧几里得距离启发式的 A算法,甚至更复杂的算法(例如 A的变体或考虑不同成本的系统)。这将在这几个三角形之间产生一条路径,AI 角色可以行走。
在现实中,这个过程要复杂一些,因为将所有三角形视为一个巨大图的网络节点会产生一个好的结果,但这是低效的,尤其是当我们能够访问存储在多边形中的信息以及这些多边形是如何连接的时候。此外,你可能还需要关于特定三角形的一些额外信息,这些三角形可能具有不同的成本、穿越它们所需的不同能力等。然而,除非你需要改变导航系统的底层结构,否则你不需要在这个细节级别上进行工作/操作。能够理解所有三角形以某种方式形成一个图,路径查找算法可以在其中运行,就足以掌握这个工具本身。
要能够使用导航系统,让我们了解设置导航系统的主过程。在这个阶段,我们不再担心系统是如何构建的,而是如何使用其所有功能。系统会完成剩下的工作。同样,我们需要向导航系统提供有关地图的信息(例如,指定特殊区域)。通常,这是你的团队中的 AI 程序员负责这项工作,但如果你的团队规模较小,关卡设计师可能会负责这项任务。尽管没有特定的流程,而是一个迭代过程,但让我们探索你可以用来在 Unreal 中定义导航网格的不同步骤——或者如果你更喜欢,工具。我们将在本章中详细检查它们。
-
导航网格的生成:这是第一步。在你能够使用以下工具之前,生成一个导航网格非常重要。这一步骤包括定义如何生成多边形、三角形、导航网格的精度,甚至确定哪些类型的代理将穿越这个特定的导航网格。
-
导航网格修改器:导航网格的各个部分并不都是相同的,这是一个工具,用于指定导航网格的哪些部分应该有不同的行为。实际上,正如我们之前所看到的,可能有一个含有毒气体的区域,代理会希望避开这部分,除非他们真的不得不穿越它。导航网格修改器允许你指定包含气体的区域是特殊的。然而,区域内的行为类型(例如,这条路径不应该穿越,或者只有具有游泳能力的代理才能穿越)是在导航区域内指定的。
-
导航区域:这允许你指定特定类型的区域应该如何行为,是否应该避开等。在执行导航过滤以确定代理可以穿越哪些区域时,这些信息是关键的。
-
导航链接:这些可以连接导航网格的两个不同部分。假设你有一个平台边缘。默认情况下,AI 代理会找到另一条路。如果你考虑的是第三人称地图模板,需要从平台上下来的代理会绕过该区域使用楼梯,而不是直接从平台上掉落/跳跃。一个导航链接允许你连接平台上的导航网格部分与下面的部分。结果,代理将能够从平台上掉落。然而,请注意,导航链接可以连接导航网格的两个通用部分,从而允许路径查找通过跳跃、传送等方式找到路径。
-
导航过滤:我们并不一定希望每次都以相同的方式找到路径。导航过滤允许我们定义针对特定实例(在路径查找被调用以寻找路径的特定时间)如何执行路径查找的特定规则。
让我们逐一分析这些点,并更详细地讨论它们。
生成导航网格
在虚幻引擎中生成简单的导航网格相当直接。让我们看看我们如何能完成它。从模式面板,在体积选项卡中,你可以找到导航网格边界体积,如下截图所示:
将其拖入世界。你会注意到与地图相比,体积相当小。该体积内的所有内容都将被考虑以生成导航网格。当然,导航网格有许多参数,但为了简单起见,我们现在保持简单。
如果你按下键盘上的P按钮,你将能够在视口中看到导航网格,如下截图所示:
如你所见,它限制在导航网格边界体积所包含的区域。让我们调整导航网格边界体积以适应我们拥有的所有关卡。你的关卡应该看起来像这样:
你注意到当你调整体积时,导航网格会自动更新吗?这是因为,在虚幻引擎中,每次影响导航网格的任何东西移动时,都会生成导航网格。
在更新过程中,受影响的导航网格部分(即更新的部分)应变为红色,如下截图所示:
这就是生成导航网格有多简单。然而,为了能够掌握这个工具,我们需要了解更多关于如何细化导航网格以及它是如何被 AI 使用的。
为导航网格设置参数
如果你点击导航网格边界体积,你会注意到没有生成导航网格的选项。事实上,一些参数是在项目级别,而另一些是在地图级别。
让我们导航到世界轮廓图,在那里你会发现场景中已经放置了一个RecastNavMesh-Default演员,如下面的截图所示:
实际上,当你拖动导航网格边界体积时,如果地图中没有RecastNavMesh-Default,则会创建一个。如果我们点击它,我们将在详细信息面板中能够更改其所有属性。
如你所见,有很多默认值。这些可以在项目设置(在导航网格选项卡下)中更改。让我们逐个分析每个部分,并尝试掌握它们的主要概念。
显示设置
正如名称所示,这些是与如何详细可视化我们生成的导航网格相关的设置。特别是,我们将能够看到生成的多边形、三角形以及多边形是如何连接的。我们将在第十二章*,AI 调试方法 - 导航、EQS 和性能分析*中更详细地介绍这些内容,届时我们将讨论调试工具:
生成设置
这些设置涉及导航网格的生成。通常,默认值已经足够好,可以开始使用,因此只有在你了解自己在做什么的情况下才应该更改这些值。以下截图显示了这些设置:
了解这些设置的最好方法是调整它们的参数,首先在一个示例地图中,然后在自己的地图中进行。之后,你需要检查这样做的结果(特别是第十二章中介绍的视觉调试工具,AI 调试方法 - 导航、EQS 和性能分析)。为了帮助你入门,让我们看看主要的设置:
-
瓦片大小 UU:此参数定义了生成的多边形的精细程度。较低的值意味着更精确的导航网格,具有更多的多边形,但生成时间会更长(并且可能使用更多的内存)。你可以通过在显示设置中打开绘制三角形边缘来查看此参数的效果,如前一个截图所示。
-
单元格高度:这决定了生成的单元格从地板的高度(这可能会导致连接不同高度的区域,所以请小心)。
-
代理设置(半径、高度、最大高度、最大坡度、最大步高):这些设置针对您的代理,应适当指定。特别是,这些是代理穿越此Nav Mesh所需的最小值。因此,Nav Mesh将无法导航具有比这些值更小值的代理,因为Nav Mesh只为满足这些要求的代理生成。这些设置有助于为您的代理生成适当的Nav Mesh,而不会在代理永远无法导航的区域浪费资源。
-
最小区域面积:这消除了Nav Mesh 生成中的一些过于微不足道的瑕疵。
许多剩余的设置都是关于优化的,它们可能会让人感到不知所措,尤其是对于 AI 编程的新手来说。因此,我决定不将这些细节包含在本书中。然而,一旦您对使用导航系统有信心,您就可以检查这些设置的提示,并尝试它们,以便了解它们的作用。
项目设置
值得注意的是,即使我们不详细讨论,相同的导航设置也可以从项目设置中更改;有一个专门的选项卡,如下图所示:
有趣的是,最后一个选项卡是关于代理的。在这里,您可以创建一个支持代理数组,以便不同的代理可以以不同的方式在Nav Mesh中导航。例如,鼠标可能有一个与巨魔非常不同的导航网格。事实上,鼠标还可以进入小洞,而巨魔则不能。在这里,您将能够指定您拥有的所有不同类型的代理:
您不能直接指定角色将跟随哪种类型的代理,但基于角色移动组件(或一般意义上的移动组件),会将一种代理分配给角色/AI 代理。
角色移动组件上的设置
如前节所述,代理的能力、形状等对其在Nav Mesh中的导航有很大影响。您可以在角色移动组件中找到所有这些设置。
然而,这个组件超出了本书的范围,我们不会看到它。
修改导航网格
到目前为止,我们已经看到了如何生成导航网格。然而,我们希望对其进行修改,使其更好地满足我们的需求。正如我们之前提到的,可能会有一些区域穿越成本较高,或者Nav Mesh中两个点之间的连接似乎被隔开(例如,由悬崖隔开)。
因此,本节探讨了 Unreal 中用于修改Nav Mesh的不同工具,以便它可以适应关卡。
Nav Modifier Volume
好的——现在是时候看看我们如何开始修改 Nav Mesh 了。例如,可能有一些我们不希望 AI 可以穿越的 Nav Mesh 部分,或者我们希望另一部分具有不同的属性。我们可以通过使用 Nav Modifier Volume 来实现这一点。
您可以通过转到 Mode 面板,在 Volumes 选项卡下,然后转到 Nav Mesh Bounds Volume 来找到此设置:
一旦这个体积被放置到地图中,默认值是移除体积内的 Nav Mesh 部分,如下面的截图所示:
当你有不想让 AI 进入的区域,或者修复导航网格的瑕疵时,这很有用。尽管 Nav Modifier Volume 指定了地图的一部分,但其行为是在 Nav Mesh Areas 中指定的。这意味着,如果我们查看 Nav Mesh Modifier Volume 的设置,我们只能找到一个与 Navigation 相关的设置,名为 Area Class:
因此,本卷只能指定应用了特定 区域类别 的地图的一部分。默认情况下,区域类别 是 NavArea_Null,它将地图中与该体积重叠的部分的 Nav Mesh “移除”。我们将在下一节中探讨 Nav Mesh Areas 的工作原理。
Nav Mesh Areas
在上一节中,我们讨论了地图的可导航区域并非所有部分都同等重要。如果有一个被认为是危险区域的区域,AI 应该避开它。虚幻引擎内置的导航系统能够通过使用成本来处理这些不同的区域。这意味着 AI 将通过计算路径上的所有成本来评估要采取的路径,并选择成本最低的那条路径。
此外,还值得指出的是,存在两种类型的成本。对于每个区域,都有一个进入(或离开)区域的基本成本和穿越区域的成本。让我们通过几个例子来澄清这两种成本之间的区别。
想象一下有一个森林,但在森林的每个入口处,AI 都需要向森林中的土著居民支付通行费。然而,一旦进入森林,AI 可以自由移动,就像他们在外面一样。在这种情况下,进入森林有成本,但一旦进入,就没有成本需要支付。因此,当 AI 需要评估是否穿越森林时,这取决于是否有其他路线以及他们这样做需要多长时间。
现在,想象一下有一个区域充满了毒气。在这个第二种情况下,进入该区域的成本可能是零,但穿越该区域的成本很高。事实上,AI 在该区域停留的时间越长,它的健康值损失就越多。是否值得进入不仅取决于是否有替代路线以及穿越替代路线需要多长时间(就像在先前的例子中那样),还取决于一旦进入,AI 需要穿越该区域多长时间。
在 Unreal 中,成本是在类中指定的。如果你点击一个 Nav Modifier Volume,你会注意到你需要指定一个 Area Class,如下面的截图所示:
如你所猜,默认值是 NavArea_Null,进入该区域的成本是无限的,导致 AI 从不进入该区域。导航系统足够智能,甚至不会生成该区域,将其视为不可导航区域。
然而,你可以更改 Area 类。默认情况下,你将能够访问以下 Area Classes:
-
NavArea_Default:这是默认生成的区域。如果你想在同一个位置有多个这些修饰符,那么它很有用。
-
NavArea_LowHeight:这表明该区域不适合所有代理,因为高度降低了(例如,在通风隧道的情况下,并非所有代理都能适应/蹲下)。
-
NavArea_Null:这使得该区域对所有代理都不可导航。
-
NavArea_Obstacle:这会给区域分配更高的成本,因此代理会想要避开它:
你会注意到,如果你创建一个新的蓝图,或者甚至当你在 Visual Studio 中打开源代码时,都会有一个 NavArea_Meta 以及它的一个子项,NavArea_MetaSwitchingActor。然而,如果你查看它们的代码,它们主要有一些过时的代码。因此,我们在这本书中不会使用它们。
然而,你可以通过扩展 NavArea 类 来扩展不同区域列表(并且可能添加更多功能)。让我们看看我们如何在蓝图和 C++ 中做到这一点。当然,就像我们在上一章中做的那样,我们将创建一个名为 Chapter3/Navigation 的新文件夹,我们将把所有的代码放在这个文件夹中。
在蓝图中创建 NavArea 类
在蓝图中创建一个新的 NavArea 类相当简单;你只需要创建一个新的蓝图,它继承自 NavArea 类,如下面的截图所示:
按照惯例,类的名称应该以 “*NavArea_” 开头。在这里我们将将其重命名为 NavArea_BPJungle(我添加了 BP 来表示我们是用蓝图创建的,因为我们同时在蓝图和 C++ 中重复执行相同的任务)。这是它在 内容浏览器 中的样子:
然后,如果你打开蓝图,你将能够为该区域分配自定义成本。你还可以为你的区域指定一个特定的颜色,以便在构建导航网格时易于识别。这是默认情况下详细信息面板的外观:
现在,我们可以根据我们的需求进行自定义。例如,我们可能想要为进入丛林设置一个成本,并且穿越它时设置一个略高的成本。我们将使用明亮的绿色作为颜色,如下面的截图所示:
编译并保存后,我们可以将这个新创建的区域分配给Nav Modifier Volume,如下面的截图所示:
这是我们级别中完成后的类的外观(如果导航网格可见):
在 C++中创建 NavArea 类
在 C++中创建一个NavArea类同样简单。首先,你需要创建一个新的 C++类,该类从NavArea类继承,如下面的截图所示:
按照惯例,名称应该以"NavArea_“开头。因此,你可以将其重命名为NavArea_Desert(只是为了改变 AI 可能遇到的哪种地形,因为我们之前创建了一个丛林),并将其放置在"Chapter3/Navigation”:
一旦创建了类,你只需要在构造函数中分配参数。为了方便起见,以下是类定义,其中我们声明了一个简单的构造函数:
#include "CoreMinimal.h"
#include "NavAreas/NavArea.h"
#include "NavArea_Desert.generated.h"
/**
*
*/
UCLASS()
class UNREALAIBOOK_API UNavArea_Desert : public UNavArea
{
GENERATED_BODY()
UNavArea_Desert();
};
然后,在构造函数的实现中,我们可以分配不同的参数。例如,我们可以为进入设置一个高成本,为穿越设置一个更高的成本(相对于默认或丛林)。此外,我们可以将颜色设置为黄色,以便我们记住这是一个沙漠区域:
#include "NavArea_Desert.h"
UNavArea_Desert::UNavArea_Desert()
{
DefaultCost = 1.5f;
FixedAreaEnteringCost = 3.f;
DrawColor = FColor::Yellow;
}
你可以随时调整这些值以查看哪个最适合你。例如,你可以创建一个进入成本非常高但穿越成本很低的区域。结果,如果只穿越一小段时间,该区域应该被避免,但如果代理穿越它的时间较长,它可能比较短路线更方便。
一旦创建了类,你可以将其设置为Nav Modifier Volume的一部分,如下面的截图所示:
因此,你将能够在导航网格(在这种情况下,带有黄色)中看到你的自定义区域:*
导航链接代理
默认情况下,如果有一个悬崖,AI 不会从悬崖上掉下去,即使这是它们到达目的地的最短路径。实际上,悬崖上的“导航网格”并没有(直接)与底部的“导航网格”连接。然而,“虚幻导航系统”提供了一种通过所谓的“导航链接代理”连接“导航网格”中任意两个三角形的方法。
尽管区域是连接的,路径查找器也会找到正确的道路,但 AI 不能违反游戏规则,无论是物理规则还是游戏机制。这意味着如果 AI 无法跳跃或穿越魔法墙,角色会卡住,因为路径查找器返回了一条路径,但角色无法执行它。
让我们更详细地探索这个工具。
创建导航链接代理
要通过链接连接两个区域,我们需要进入“模式”面板,在“所有类”选项卡中并选择“导航链接代理”,如图所示:
或者,你可以在“模式”面板中搜索它以更快地找到它:
一旦链接被放置在层级中,你将看到一个“箭头/链接”,并且你可以修改链接的起始点和终点。它们被称为“左”和“右”,设置它们位置的最简单方法是拖动(并放置)它们在“视口”中。结果,你将能够连接“导航网格”的两个不同部分。正如我们在以下截图中可以看到的,如果“导航网格”是可见的(通过按“P”键启用),你将看到一个连接“右”和“左”节点的箭头。这个箭头指向两个方向。这将导致链接是双向的:
你可能会注意到有两个箭头,一个带有较深的绿色阴影。此外,这个第二个“箭头/弧/链接”可能并不完全在你放置的“右”端点处,而是附着在“导航网格”上。你可以在以下截图中更清楚地看到这个第二个箭头:
这实际上是“导航网格”是如何通过“链接”的“投影设置”连接起来的。我们将在下一节中探讨这个设置。
如果你想让链接只向一个方向走,我们可以在“详情面板”中更改这个设置。然而,要探索这些设置,我们首先需要理解存在两种不同的“链接”类型:“简单”和“智能”。
简单链接和智能链接
当我们创建一个“导航链接代理”时,它附带一系列“简单链接”。这意味着我们可以使用单个“导航链接代理”将“导航网格”的不同部分连接在一起。然而,“导航链接代理”还附带一个默认禁用的单个“智能链接”。
让我们了解简单链接和智能链接之间的相似之处和不同之处。
简单链接和智能链接
简单链接和智能链接的行为方式相似,即在意义上将导航网格的两个部分连接起来。此外,这两种类型的链接都可以有方向(从左到右,从右到左,或双向)和导航区域(链接所在的导航区域类型;例如,您可能希望在通过此链接时使用自定义成本)。
简单链接
简单链接存在于导航代理链接中的点链接数组中,这意味着在单个导航代理链接中可以存在多个简单链接。要创建另一个简单链接,您可以从详细信息面板中向简单节点数组添加一个额外的元素,如下所示:
一旦我们有了更多的简单链接,我们可以设置起始和结束位置,就像我们为第一个链接所做的那样(通过选择它们并在视口内移动它们,就像其他任何代理一样)。以下截图显示了我在同一导航代理链接旁边放置的两个简单链接的位置:
每次我们创建一个导航链接代理时,它都会在数组中包含一个简单链接。
对于我们在点链接数组中的每个简单链接,我们可以通过展开项目来访问其设置。以下截图显示了第一个简单链接的设置:
让我们了解这些不同的设置:
-
左和右:链接左和右端的位置,分别。
-
左投影高度和右投影高度:如果此数字大于零,则链接将分别投影到链接的左和右端导航几何形状上(使用最大长度由此数字指定的跟踪)。您可以在以下截图中看到此投影链接:
-
方向:这指定了链接工作的方向。此外,视口中的箭头将相应更新。此选项的可能如下:
-
双向:链接是双向的(请记住,AI 需要能够以两个方向穿越链接;例如,如果我们正在越过悬崖,代理需要能够从它上掉落(链接的一个方向)和跳跃(链接的另一个方向)。
-
从左到右:链接只能从左端向右端穿越(代理仍然需要具备在该链接方向行进的能力)。
-
从右到左:链接只能从右端向左端穿越(代理仍然需要具备在该链接方向行进的能力)。
-
-
吸附半径和高度半径:您可能已经注意到连接每个链接末端的圆柱体。这两个设置控制该圆柱体的半径和高度。查看吸附到最便宜的区域以获取有关该圆柱体使用的更多信息。以下截图显示第一个链接有一个更大的圆柱体(更大的半径和更高的高度):
-
描述:这只是一个字符串,您可以在其中插入方便的描述;它对导航或链接没有影响。
-
吸附到最便宜的区域:如果启用,它将尝试将链接端连接到由吸附半径和高度半径指定的圆柱体内的最便宜的三角形区域。例如,如果圆柱体同时与默认导航区域和BPJungle导航区域(我们之前创建的)相交,链接将直接连接到默认导航区域,而不是丛林。
-
区域类:链接可能具有穿越成本,或属于特定的导航区域。此参数允许您定义链接穿越时是哪种类型的导航区域。
这就结束了所有关于简单链接的可能性。然而,这是一个非常强大的工具,让您能够塑造导航网格并实现惊人的 AI 行为。现在,让我们深入了解智能链接。
智能链接
智能链接可以通过使用“智能链接相关”布尔变量在运行时启用和禁用。您还可以通知周围的演员这一变化。默认情况下,它是不相关的(它没有被使用,即链接不可用),并且每个导航代理链接只有一个智能链接。
请注意,不要混淆:智能链接可以处于两种状态:启用和禁用。然而,如果链接实际上是“存在/存在”(对于导航网格),这又是另一个属性(智能链接相关),换句话说,这意味着链接对于导航系统来说是“活动”的(但它仍然可以处于启用或禁用状态)。
不幸的是(至少对于当前版本的引擎),这些在编辑器中是不可见的,这意味着需要手动设置起始和结束位置。
然而,让我们来看看智能链接的设置:
-
启用区域类:这是链接启用时假设的导航区域。默认为NavArea_Default。
-
禁用区域类:这是链接禁用时假设的导航区域。这意味着当链接禁用时,如果分配了可穿越的区域(例如,当链接禁用时,我们可能希望有非常高的成本来穿越,但我们仍然希望它能够穿越。当然,默认为NavArea_Default,这意味着它不可穿越。
-
链接相对起始:这表示链接的起始点,相对于其导航链接代理的位置。
-
链接相对结束:这表示链接的结束点,相对于其导航链接代理的位置。
-
链接方向:这指定了链接工作的方向。可能的选项如下:
-
双向:链接是双向的(记住 AI 需要能够双向穿越链接;例如,在悬崖上,代理需要能够从上面掉落(链接的一个方向)和跳跃(链接的另一个方向)。
-
从左到右:链接只能从左端穿越到右端(代理仍然需要在该链接方向上移动的能力)。
-
从右到左:链接只能从右端穿越到左端(代理仍然需要在该链接方向上移动的能力)。
-
虽然此参数的选项将链接的端点标记为左和右,但它们指的是链接的起始点和结束点。或者(这可能更好,因为链接可以是双向的),链接相对起始和链接相对结束指的是左和右。
-
链接启用:这是一个布尔变量,用于确定智能链接是否启用。此值可以在运行时更改,并且链接可以"通知"对这种信息感兴趣的周围代理/演员(见后文了解更多信息)。默认值是 true。
-
智能链接相关:这是一个布尔变量,用于确定智能链接是否实际上是"活动状态",即它是否相关,或者我们应该忽略它。默认值是 false。
这些是关于智能链接的主要设置。
NavLinkProxy.h
/** called when agent reaches smart link during path following, use ResumePathFollowing() to give control back */
UFUNCTION(BlueprintImplementableEvent)
void ReceiveSmartLinkReached(AActor* Agent, const FVector& Destination);
/** resume normal path following */
UFUNCTION(BlueprintCallable, Category="AI|Navigation")
void ResumePathFollowing(AActor* Agent);
/** check if smart link is enabled */
UFUNCTION(BlueprintCallable, Category="AI|Navigation")
bool IsSmartLinkEnabled() const;
/** change state of smart link */
UFUNCTION(BlueprintCallable, Category="AI|Navigation")
void SetSmartLinkEnabled(bool bEnabled);
/** check if any agent is moving through smart link right now */
UFUNCTION(BlueprintCallable, Category="AI|Navigation")
bool HasMovingAgents() const;
之前我们提到,智能链接可以在运行时向附近的代理/演员广播有关其状态变化的信息。您可以通过以下广播设置更改智能链接广播此信息的方式,这些设置位于智能链接下方:
这些设置相当直观,但让我们快速浏览一下:
-
启用时通知:如果为真,链接将在启用时通知代理/演员。
-
禁用时通知:如果为真,链接将在禁用时通知代理/演员。
-
广播半径:这指定了广播应该延伸多远。所有位于此半径之外的代理都不会收到关于链接变化的通知。
-
广播间隔:这指定了链接应该在多长时间后重复广播。如果值为零,则广播只重复一次。
-
广播频道:这是用于广播变化的跟踪频道。
这就结束了我们对 智能链接 的讨论。
其他 Nav Link Proxy 设置
最后,值得一提的是,当生成 Nav Mesh 时,Nav Link Proxy 可以创建一个 障碍盒。你可以在 Nav Link Proxy 的 详细信息面板 中找到这些设置,如下面的截图所示:
这些设置允许你决定是否激活/使用 障碍盒,其 尺寸/范围 和偏移量,以及 导航区域 的类型。
扩展 Nav Link Proxy
如果你想知道是否可以扩展 链接 或在更复杂的演员中包含它们,答案是“当然可以!但你只能用 C++ 扩展它们”。
由于这本书不能涵盖所有内容,我们没有时间详细处理这部分。然而,你可能想要扩展 Nav Link Proxy 的原因之一是更好地控制进入你的链接的角色。例如,你可能想要有一个 跳跃垫 将角色推过链接。这并不复杂,如果你在网上搜索,你会找到很多关于如何使用 导航链接 来实现这一点的教程。
只需记住,要成为一名优秀的 Unreal AI 程序员,你最终需要掌握 Nav Links 的这部分内容,但就目前而言,我们已经涵盖了足够的内容。
导航规避
导航规避是一个非常广泛的话题,Unreal 有一些子系统为我们处理这个问题。因此,我们将把这个话题放在 第六章*,人群* 中讨论。
导航过滤
我们不希望每次都以相同的方式找到特定的路径。想象一下,我们的 AI 代理使用了一个增强效果,它能够以两倍的速度穿越丛林。在这种情况下,导航系统没有意识到这种变化,这也不是对 Nav Mesh 形状或权重的永久性更改。
导航过滤 允许我们定义在特定时间段内如何执行路径查找的具体规则。你可能已经注意到,每次我们在蓝图或 C++ 中执行导航任务时,都有一个可选参数用于插入一个 导航过滤器。以下是一些具有此可选过滤器参数的蓝图节点(C++ 函数也是如此)的示例:
即使是 行为树 中的 移动到 节点也有 导航过滤器 选项:
当然,一旦您插入了一个过滤器,路径查找将相应地表现。这意味着使用导航过滤器非常简单。然而,我们如何创建导航过滤器?让我们在蓝图和 C++中找出答案。
在蓝图创建导航过滤器
在本章之前,我们在蓝图中创建了一个丛林区域。因此,这似乎是一个很好的例子,我们可以用它来创建一个允许 AI 代理更快地穿越丛林——甚至比穿越导航网格的默认区域还要快——的导航过滤器。让我们想象 AI 代理有一些力量或能力,允许它在关卡中的丛林类型区域中更快地移动。
要在蓝图创建一个导航过滤器,我们需要开始创建一个新的蓝图,该蓝图继承自NavigationQueryFilter,如下面的截图所示:
按照惯例,类的名称应该以"NavFilter_"开头。我们将将其重命名为NavFilter_BPFastJungle**(我添加了 BP,以便我可以记住我是用蓝图创建的,因为我们正在蓝图和 C++中重复相同的任务)。这是它在内容浏览器中的样子:
一旦我们打开蓝图,我们将在详细信息面板中找到其选项:
如您所见,有一个区域数组和两个用于包括和排除(导航)标志的集合。不幸的是,我们没有涵盖导航标志,因为它们超出了本书的范围,并且在撰写时只能在 C++中分配。然而,区域数组非常有趣。让我们添加一个新的区域,并使用我们的NavArea_BPJungle作为区域类,如下面的截图所示:
现在,我们可以覆盖丛林区域的旅行成本和进入成本,如果使用此过滤器,则将使用这些成本代替我们在区域类中指定的成本。请记住勾选选项旁边的复选框以启用编辑。例如,我们可以将旅行成本设置为0.6(因为我们可以快速通过丛林而不会遇到任何问题),并将进入成本设置为零:
现在,我们一切都准备好了。过滤器已准备好供您在丛林中旅行时使用!
为 导航区域 更改 旅行成本 并不会使 AI 代理在该区域更快或更慢,它只是使路径查找更倾向于该路径而不是另一条路径。代理在该区域变得更快是实现,被排除在导航系统之外,因此您需要在 AI 角色在丛林中时实现这一点。
如果你同时也跟随了Nav Areas的 C++部分,那么你应该在你的项目中也有沙漠区域。作为一个可选步骤,我们可以向过滤器添加第二个区域。想象一下,通过使用在丛林中移动更快的加成或能力,我们的角色对阳光变得非常敏感,很容易晒伤,这会显著降低他们的健康。因此,如果使用此过滤器,我们可以为沙漠区域设置更高的成本。只需添加另一个区域,并将区域类设置为NavArea_Desert。然后,覆盖成本;例如,一个旅行成本为2.5和进入成本为10:
一旦你完成了设置编辑,保存蓝图。从现在起,你将能够在导航系统中使用此过滤器。这标志着如何在蓝图中创建Nav Filter的方法结束。
在 C++中创建导航过滤器
以类似蓝图的方式,我们可以创建一个 C++的Nav Filter。这次,我们可以创建一个稍微降低沙漠区域成本的过滤器。你可以将此过滤器用于某些生活在沙漠中的动物,使其不太容易受到其影响。
首先,我们需要创建一个新的 C++类,它继承自NavigationQueryFilter,如下面的截图所示:
按照惯例,类的名称应该以"NavFilter_“开头。因此,我们将将其重命名为NavFilter_Desert Animal并将其放置在"Chapter3/Navigation”:
.h
#include "CoreMinimal.h"
#include "NavFilters/NavigationQueryFilter.h"
#include "NavFilter_DesertAnimal.generated.h"
/**
*
*/
UCLASS()
class UNREALAIBOOK_API UNavFilter_DesertAnimal : public UNavigationQueryFilter
{
GENERATED_BODY()
UNavFilter_DesertAnimal();
};
.cpp#include
#include "NavArea_Desert.h"
Desert
UNavFilter_DesertAnimal::UNavFilter_DesertAnimal() {
//Create the Navigation Filter Area
FNavigationFilterArea Desert = FNavigationFilterArea();
*// [REST OF THE CODE]*
}
Desert
UNavFilter_DesertAnimal::UNavFilter_DesertAnimal() {
*// [PREVIOUS CODE]*
//Set its parameters
Desert.AreaClass = UNavArea_Desert::StaticClass();
Desert.bOverrideEnteringCost = true;
Desert.EnteringCostOverride = 0.f;
Desert.bOverrideTravelCost = true;
Desert.TravelCostOverride = 0.8f;
*// [REST OF THE CODE]*
}
最后,我们需要将此过滤器区域添加到Areas数组中:
UNavFilter_DesertAnimal::UNavFilter_DesertAnimal() {
*// [PREVIOUS CODE]*
//Add it to the the Array of Areas for the Filter.
Areas.Add(Desert);
}
.cpp
#include "NavFilter_DesertAnimal.h"
#include "NavArea_Desert.h"
UNavFilter_DesertAnimal::UNavFilter_DesertAnimal() {
//Create the Navigation Filter Area
FNavigationFilterArea Desert = FNavigationFilterArea();
//Set its parameters
Desert.AreaClass = UNavArea_Desert::StaticClass();
Desert.bOverrideEnteringCost = true;
Desert.EnteringCostOverride = 0.f;
Desert.bOverrideTravelCost = true;
Desert.TravelCostOverride = 0.8f;
//Add it to the the Array of Areas for the Filter.
Areas.Add(Desert);
}
编译此代码,你将能够在下次需要使用导航系统时使用此过滤器。这标志着我们对导航过滤器的讨论结束。
覆盖导航系统
从模式面板中,你可以将一个名为Nav System Config Override的特殊演员拖入级别。
此演员允许你通过使用另一个来覆盖内置的导航系统。当然,你将不得不首先开发它,这将需要大量的努力。
你应该替换默认的导航系统(或者可能与其他系统一起使用)的原因主要是为了克服限制。那么空中单位呢?它们如何进行 3D 路径查找?蜘蛛如何进行表面路径查找?
摘要
在本章中,我们探讨了如何设置导航系统,以便我们的 AI 角色可以在地图上移动。特别是,我们学习了如何使用修改体积、导航链接代理和导航网格区域来塑造导航网格。
因此,我们的 AI 代理可以平滑地穿越地图,高效地找到两点之间的路径,该路径基于他们的能力进行了优化(例如,使用导航过滤器),同时尊重地图上各种类型的"地形"(例如,使用导航区域)。此外,它们可以翻过悬崖或跳过平台(例如,通过使用导航链接代理和一点跳跃的编码)。
在下一章中,我们将学习关于 Unreal 框架中更高级的 AI 功能,即环境查询系统,它允许代理"查询"环境,以便他们可以找到具有特定要求的地点(或演员)。
第四章:环境查询系统
一位优秀的领导者知道哪里是好的,而 EQS 知道得更好!
欢迎来到第四章,环境查询系统。Chapter 4。在本章中,我们将使用虚幻 AI 框架中的一个特定且非常强大的系统。我指的是 环境查询系统(EQS)。我们将探索这个系统,并不仅了解其工作原理,还将了解如何在我们的游戏中有效地使用它。
再次强调,EQS 属于 决策制定 的领域,特别是评估哪个位置(或虚幻中的演员)最适合满足某些条件。我们将通过本章详细了解其工作原理,但作为对我们将要涵盖内容的预览,请记住,系统过滤器提供不同的可能性,剩余的则分配一个分数。得分最高的选择将被选中。
尤其是我们将涵盖以下主题:
-
如何启用 环境查询系统 (EQS)?
-
理解 EQS 的工作原理
-
了解 Generators、Tests 和 Contexts
-
探索 EQS 内置的 Generators、Tests 和 Contexts
-
使用自定义的 Generators、Tests 和 Contexts 来扩展 EQS
那么,让我们深入探讨吧!
启用环境查询系统
EQS 是一个在 Unreal 4.7 中引入的功能,在 4.9 中得到了很大的改进。然而,在版本 4.22 中,EQS 被列为实验性功能,尽管它在许多游戏中得到了成功应用,这表明 EQS 是稳健的。
因此,我们需要从 实验性功能设置 中启用它。从顶部菜单,转到 编辑 | 编辑器首选项…,如下面的截图所示:
请注意,不要与 项目设置 混淆。从顶部菜单,在 视口 之上,您只能访问 项目设置。然而,从整个编辑器的顶部菜单中,您将能够找到 编辑器首选项。前面的截图应有助于您找到正确的菜单(即 编辑 下拉菜单)。
从侧边菜单中,您将能够看到一个名为 Experimental(在 General 类别下)的部分,如下面的截图所示:
如果您滚动浏览设置,您将找到 AI 类别,其中您可以启用 环境查询系统:
勾选此选项旁边的框,结果,整个项目中的 环境查询系统 将被激活。现在,您将能够为其创建资产(以及扩展它),并从 行为树 中调用它。
如果你在AI 类别中看不到环境查询系统复选框,那么你很可能正在使用一个较新的引擎版本,其中(终于)EQS不再是实验性的,因此在你的项目中始终是启用的。如果这是你的情况,那么请跳过这一部分,继续下一部分。
理解环境查询系统
当人们第一次面对 EQS 时,可能会感到不知所措,尤其是因为它不清楚系统的不同部分是如何工作的以及为什么。本节的目标是通过让你熟悉 EQS 的底层工作流程来提高你对系统的理解,这将有助于你在创建查询时的实际工作流程。
EQS 的一般机制
想象一下,在某个时刻,我们的 AI 代理正在遭受火力攻击,并且需要评估不同的掩护地点。一个地方可能很远但保护得很好,而另一个地方可能很近但保护得不好。我们应该怎么办?
解决这个问题的方法之一是使用效用函数并在时间上解决方程(我们将在第十四章,超越)中详细讨论)。实际上,这会产生非常好的结果,并且已经在许多游戏中成功实施。然而,Unreal 提供了另一种可能性:EQS。也就是说,使用 EQS 而不是效用函数不是强制性的,但作为 AI 框架的一部分,EQS 使得评估此类决策变得容易,因为它是一个内置的系统。
因此,回到我们那个需要掩护的代理,一个行为树将运行一个 EQS 查询,这将给出代理应该获得掩护的最终位置。现在,环境查询是如何工作的呢?
首先,一个组件(称为生成器,我们稍后将讨论)将根据在测试中指定的某些标准生成一系列位置(或代理,我们将在本章稍后讨论)。例如,我们可以在一个均匀的网格上取不同的位置,这在不知道预先(在评估之前)要寻找哪种位置时非常有用。
然后,有一个过滤过程用于可能的地点(或演员),其中它会消除所有不符合特定标准的。在我们的掩护示例中,任何仍然暴露在直接火力下的地方都应该被丢弃。
剩余的地方将根据其他标准进行评估(系统会为它们分配一个分数)。再次,在我们的掩护示例中,这可能是代理的距离、它们提供的掩护程度,或者地点离敌人的距离。系统通过考虑所有这些因素(当然,其中一些因素可能比其他因素更重要;例如,从火力的保护可能比从敌人位置的距离更重要)来分配分数。
最后,从查询中给出得分最高的位置(或演员)到 行为树,这将决定如何处理它(例如,快速逃离到那个地方以躲避)。
环境查询的组件
基于我们在上一节中描述的机制,让我们深入了解 Unreal 中 EQS 的实际实现方式。
在高层次上,我们有 环境查询、上下文、生成器 和 测试。
环境查询
如其名所示,环境查询 是一种数据结构(类似于 行为树),它包含有关如何执行查询的信息。实际上,它是一个您可以在您的 内容浏览器 中创建和找到的资产。
您可以通过在您的 内容浏览器 上右键单击,然后选择 人工智能 | 环境查询 来创建一个新的 环境查询,如下面的截图所示:
请记住,如果 EQS 未启用,此选项将不会出现。
这是在 内容浏览器 中的样子:
如果我们双击它以打开它,Unreal 将打开一个特定且 专用 的编辑器用于 环境查询。这就是编辑器的样子:
编辑器视图
如您所见,它与 行为树 非常相似,但您只能将一个生成器节点附加到 根节点(只有一个),这将使其也成为叶子节点。因此,整个 “树” 将只是 根节点 和一个 生成器。实际上,通过使用类似 行为树 的编辑器,您可以轻松设置一个 环境查询。在(唯一的)生成器 节点上,您可以附加一个或多个 测试节点*——无论是生成器本身还是上下文。以下是一个示例:
编辑器视图
我们将在下一节中了解这意味着什么。
上下文
上下文是特定且方便的类,用于检索信息。您可以通过蓝图或使用 C++ 来创建/扩展一个 上下文。
它们被称为上下文的原因是,它们为生成器或测试提供了一个上下文。通过拥有上下文,生成器(或测试)能够从那个点开始执行所有计算。如果您愿意,可以将上下文视为一个特殊(并且非常详细的)变量,能够程序化地传递一组有趣的演员和/或位置。
让我们通过一个例子来看看上下文是什么。在进行测试时,您通常知道查询者(例如,需要掩护的智能体)的位置(在引擎盖下,即使查询者是默认上下文)。然而,我们的测试可能需要我们敌人的位置(例如,检查掩护点是否受到攻击,因为这取决于我们智能体的敌人的位置)。上下文可以提供所有这些信息,并且可以以程序化的方式做到这一点:例如,智能体可能不知道地图上的每个敌人,因此上下文可能只返回智能体当前意识到的敌人,因此它只从那些地方找到掩护。因此,如果有一个隐藏的敌人被选为掩护的地方,那么对我们智能体来说就是不幸的事情!
理解上下文并不容易,所以请坚持这一章,也许在您对生成器和测试以及如何在我们的项目中构建EQS有更好的了解之后,再重新阅读上一段。
生成器
如同其名,生成器生成一个初始位置集(或数组)或演员。这个集合将由测试进行过滤和评估。
生成初始集的方法完全自由。如果您在评估阶段之前有关您正在寻找的地方的重要信息,那么您可以创建一个自定义的生成器(例如,如果智能体不能游泳,则不要检查有水的地方;如果唯一可用的攻击是近战,则不考虑飞行敌人)。
与上下文一样,生成器是特定类别的子类。您可以在蓝图以及 C++中创建生成器。
通常,最常用的生成器是网格生成器,它会在一个上下文周围(例如,在智能体周围)生成一个均匀的网格。通过这样做,智能体将检查其周围的大部分区域。
测试
测试负责对由生成器生成的不同位置(或演员)进行过滤和评分(评估)。单个测试可以在同一标准上过滤和评分,也可以只进行其中之一。
在使用过滤的测试的情况下,它们试图确定哪些位置(或演员)不符合我们的标准。EQS 进行了优化,因此它以特定的顺序执行测试,以尽早检测不合适的地方。这样做是为了避免分配不会使用的分数。
一旦所有位置(或演员)都被过滤掉,剩下的位置将被评估。因此,每个能够分配分数的测试都会在位置(或演员)上执行(执行),以报告评估结果,形式为分数(可以是正数或负数)。
作为旁注,测试需要(至少)一个上下文来正确地进行过滤和评估。
让我们通过一个简单的测试例子来了解它们是如何工作的。最常见的测试之一是距离,即这个地点(我们正在评估的生成地点)距离上下文有多远?上下文可以是查询者,或它正在攻击的敌人,或任何其他东西。因此,我们可以(例如)过滤距离高于或低于某个距离阈值的地点(例如,如果它们离玩家太远,我们可能不想有完美的掩护地点)。相同的距离测试可以根据距离分配得分,如果上下文远离(或接近),得分可以是正的(或负的)。
此外,一个测试有一个得分因子,它代表测试的权重:测试的重要性以及此测试在计算当前评估位置(或演员)的最终得分时需要产生的影响。实际上,您将在由生成器生成的位置上运行许多不同的测试。得分因子允许您轻松权衡它们,以确定哪个测试对位置的最终得分(或演员)有更高的影响。
每个测试在其详细信息面板中的选项具有以下结构:
-
测试:在这里,您可以选择测试目的是要过滤和得分,还是仅其中之一,并添加描述(对测试没有影响,但您可以将其视为注释来回忆这个测试的内容)。此外,可能还有其他选项,例如可以与导航系统一起使用的投影数据(对于依赖于导航系统的测试)。
-
特定测试:这是存放测试特定选项的地方。这因测试而异。
-
过滤:在这里,您可以选择如何设置过滤的行为。这因测试而异,但通常您可以选择一个过滤类型,如果测试将返回值评估为浮点数,则可以是范围(或最小值或最大值);否则,在条件测试的情况下,可以是布尔值。如果测试目的设置为仅得分,则此选项卡不会显示。
-
得分:在这里,您可以选择如何设置得分的行为。这因测试而异。对于测试的浮点返回类型,您可以选择一个得分方程,以及一个归一化。此外,还有得分因子,这是与其他测试相比此测试的权重。对于布尔返回值,只有得分因子。如果测试目的设置为仅过滤,则此选项卡不会显示。
-
预览:这为您提供了过滤和得分函数的预览。
如您所见,这些选项非常容易理解,如果您使用 EQS 进行练习,您将更好地理解它们。
组件的视觉表示
这些组件一开始可能不太直观,但一旦你习惯了 EQS,你就会意识到它们是如何有意义的,以及为什么系统会以这种方式设计。
为了总结组件及其重要性,以及提供一个视觉表示,这里有一个你可以参考的图表:
在行为树中运行环境查询
最后,要完全理解环境查询是如何工作的,最后一步是看看它如何在行为树中运行。
幸运的是,我们有一个名为“运行 EQS”的节点,这是一个内置的行为树任务。在假设的行为树编辑器中看起来如下:
可能在详细信息面板中找到的可能设置如下:
如你所见,许多已经过时(所以只需忽略它们),但我已经突出显示了最重要的那些。以下是对它们的解释:
-
黑板键:这是引用包含 EQS 结果的黑板变量的黑板键选择器。
-
查询模板:对我们要运行的特定 EQS 的引用。否则,我们可以取消激活此选项以激活一个EQSQuery 黑板键。
-
查询配置:这是查询的可选参数(不幸的是,我们在这本书中不会详细讨论它们)。
-
EQSQuery 黑板键:一个黑板键选择器,它引用包含EQS的黑板变量。如果激活,包含在黑板变量中的EQSQuery将被执行,而不是查询模板。
-
运行模式:这显示了我们将要检索的查询结果。可能的选项如下:
-
最佳单个项目:这会检索得分最高的点(或演员)
-
从最佳 5%中随机选择单个项目:这会从得分最高的 5%的位置(或演员)中随机检索一个点
-
从最佳 25%中随机选择单个项目:这会从得分最高的 25%的位置(或演员)中随机检索一个点
-
所有匹配项:这会检索所有与查询匹配的位置(或演员)(它们尚未被过滤掉)
-
这就完成了我们如何运行 EQS 以及如何检索其结果,以便在行为树中使用。
当然,还有其他触发 EQSQuery 的方法,这些方法不一定是在行为树中完成的,尽管这是 EQS 最常见的使用方式。不幸的是,我们在这本书中不会涵盖运行 EQSQuery 的其他方法。
不仅位置,还包括演员!
当我说“……评估一个位置**(或演员)……”时,我强调了这一点。
实际上,EQS 最酷的功能之一是能够评估不仅位置,还可以评估演员!
再次强调,您可以将 EQS 用作决策过程。想象一下,您需要先选择一个敌人进行攻击。您可能需要考虑各种参数,例如该敌人的剩余生命值、它的强度以及它在未来立即被视为威胁的程度。
通过仔细设置 EQS,您可以为每个敌人分配一个分数,取决于哪个敌人最方便攻击。当然,在这种情况下,您可能需要做一些工作来创建适当的生成器,以及上下文和适当的测试,但从长远来看,这使得 EQS 在代理需要做出这类决策时成为一个非常好的选择。
探索内置节点
在我们创建自己的生成器、上下文和测试之前,让我们先谈谈内置节点。虚幻引擎自带了一些有用的通用内置节点。我们将在本节中探讨它们。
请记住,本节将分析性地解释 EQS 中每个内置节点的工作原理,就像文档一样。因此,如果您愿意,请将本节用作参考手册,如果您不感兴趣,请跳过这些部分。
内置上下文
由于我们是通过查看上下文来解释 EQS 的,所以让我们从内置上下文开始。当然,制作通用的上下文几乎是一个悖论,因为上下文非常具体于"上下文"(情境)。
然而,虚幻引擎自带了两个内置上下文:
- EnvQueryContext_Querier:这代表发起查询的 Pawn(精确地说,这并不是发起查询的 Pawn,而是运行行为树并发起查询的控制器,并且这个上下文返回受控 Pawn)。因此,通过使用这个上下文,所有内容都将相对于查询器。
如我之前所述,在底层,查询器确实是一个上下文。
- EnvQueryContext_Item:这返回由生成器生成的所有位置。
内置生成器
有许多内置的生成器,大多数情况下,这些将足够您完成大多数所需的 EQS。您只有在有特定需求或希望优化 EQS 时才会使用自定义生成器。
大多数这些生成器都很直观,所以我将简要解释它们,并在必要时提供截图,以展示它们生成点的方式。
以下截图使用了一个能够可视化环境查询的特殊 Pawn。我们将在本章的后面学习如何使用它。
这是可用的内置生成器列表,正如您在环境查询编辑器中找到的那样:
为了组织这些信息,我将每个生成器分成一个子节,并将它们按先前的截图中的顺序(按字母顺序)排列。
当我提到 Generator 的设置时,我的意思是,一旦选择了特定的 Generator,在Details Panel中就会显示它的可用选项。
类别演员
这个 Generator 会获取特定类别的所有演员,并将它们的所有位置作为生成的点返回(如果这些演员在 Context 的一定半径内)。
这是在Environmental Query Editor中的样子:
可能的选项包括Searched Actor Class(显然)和从Search Center来的Search Radius(这被表达为Context)。可选地,我们可以检索特定类别的所有演员,并忽略它们是否在Search Radius内:
在前面的截图中,我使用了Querier作为Search Center,Search Radius为50000,以及ThirdPersonCharacter作为Searched Actor Class,因为它们已经在项目中可用。
通过使用这些设置(并放置几个ThirdPersonCharacter演员),我们得到以下情况:
注意围绕三个ThirdPersonCharacter演员的(蓝色)球体。
当前位置
Current Location Generator简单地从Context中检索位置(或它们),并使用它(或它们)来生成点。
这是在Environmental Query Editor中的样子:
对于这个Generator,唯一可用的设置是Query Context:
因此,如果我们使用Querier作为Query Context,那么我们只有Querier自己的位置,如下面的截图所示:
复合
Composite Generator允许你混合多个 Generator,以便有更广泛的选择点。
这是在Environmental Query Editor中的样子:
在Settings中,你可以设置一个Generators数组:
由于我们没有时间详细地查看所有内容,所以不会进一步介绍这个 Generator。
点:圆形
如其名所示,Circle Generator会在指定半径的圆周上生成点。此外,还提供了与Navmesh交互的选项(这样就不会在Navmesh之外生成点)。
这是在Environmental Query Editor中的样子:
这是一个非常复杂的 Generator,因此这个 Generator 有各种设置。让我们来看看它们:
理想情况下,为每个设置提供一张截图会很好,这样我们可以更好地了解每个设置如何影响点的生成。不幸的是,这本书已经有了很多截图,仅为了这些复杂的生成器的不同设置而专门写一章将花费很多时间和空间。“书空间*”。然而,有一种更好的方法让你获得同样的感觉:自己尝试!是的——一旦你学会了如何设置EQSTestingPawn,你就可以自己尝试,看看每个设置如何影响生成过程。这是你学习和真正理解所有这些设置的最佳方式。
-
圆半径:正如其名所示,它是圆的半径。
-
空间间隔:每个点之间应该有多少空间;如果将圆上点间隔方法设置为按空间间隔。
-
点数数量:应该生成多少个点;如果将圆上点间隔方法设置为按点数。
-
圆上点间隔方法:确定要生成的点数是否应根据固定数量的点(按点数)计算,还是根据固定间隔的点数来计算,如果点之间的空间是固定的(按空间间隔)。
-
弧方向:如果我们只生成圆的弧,此设置确定这个方向应该是怎样的。计算方向的方法可以是两点(它需要两个上下文并计算两点之间的方向)或旋转(它需要一个上下文并检索其旋转,然后根据该旋转决定弧的方向)。
-
弧角度:如果这与360不同,它定义了点停止生成的地方的切割角度,从而创建一个弧而不是圆。这种弧的方向(或旋转)由弧方向参数控制。
-
圆心:正如其名所示,它是圆的中心,表示为上下文。
-
生成圆时忽略任何上下文演员:如果选中,它将不会考虑用作圆的上下文的演员,从而跳过在这些位置生成点。
-
圆心 Z 偏移:正如其名所示,它是圆心沿 z 轴的偏移。
-
追踪数据:在生成圆时,如果有障碍物,通常我们不想在障碍物后面生成点。此参数确定进行"水平"追踪的规则。这些选项如下:
-
无:将没有痕迹,所有生成的点都将位于圆上(或弧上)。
-
导航:这是默认选项。NavMesh结束的地方就是生成点的地方,即使中心距离小于半径(在某种程度上,如果遇到边界,圆会假设NavMesh的形状)。
-
几何形状:与导航相同,但使用几何形状而不是NavMesh作为边界,追踪将使用级别的几何形状(如果你没有NavMesh,这可能非常有用)。
-
导航过边缘:与导航相同,但现在追踪是“过边缘”。
-
-
投影数据:这与 Trace Data 类似,但通过从上方投影点进行“垂直”追踪。其余部分,概念与Trace Data完全相同。选项是无,导航,和几何形状,它们在Trace Data中的含义相同。“Navigation Over Ledges”不存在,因为它没有意义。
通过使用前一个屏幕截图中显示的相同设置(我在使用Trace Data与Navigation,并且在级别中有NavMesh),这就是它的样子(我使用P键激活了 NavMesh,所以你也能看到它):
通过使用几何形状代替Trace Data,我们得到一个非常相似,但略有不同的形状:
如果你有一个结束的 NavMesh,但没有级别的几何形状,效果会更加明显。
点:圆锥
正如其名所示,圆锥生成器在特定上下文(如聚光灯)的圆锥内生成点。此外,还有与Navmesh交互的选项(这样你就可以将点投影到Navmesh上)。
重要的是要理解,其形状是由许多圆生成的,我们总是取相同的弧。所以,如果我们取整个圆,我们基本上是在生成单个切片的区域中的点。
这个生成器也可以用来生成覆盖整个圆区域的点。
这是在环境查询编辑器中的样子:
其设置主要与圆锥的形状有关,所以让我们来探索它们:
再次强调,最好为每种设置组合都有一张截图,这样你就能感受到每个设置如何影响点的生成。由于我们在这本书中没有足够的空间这样做,我鼓励你使用EQSTestingPawn进行实验,以便你有一个更清晰的理解。
-
-
对齐点距离:这是生成点之间的弧距离(从中心相同角度的点之间的距离)。较小的值生成更多的点,考虑到的区域将更加密集。
-
圆锥度数:这决定了每个圆的弧度大小(我们考虑切片的宽度)。360 的值考虑了整个圆的面积。
-
角度步长:这是相同弧线点之间的距离,以度为单位。较小的值意味着更多的点,考虑到的区域将更加密集。
-
范围:这决定了圆锥可以延伸多远(以聚光灯为例,它可以照亮多远)。
-
中心演员:这是生成的圆的中心,用于确定圆锥。它是中心,并以上下文的形式表示。
-
包含上下文位置:正如其名所示,如果选中,还会在圆锥/圆的中心生成一个点。
-
投影数据:通过从上方投影点(考虑几何形状或 导航网格)执行 “垂直” 追踪。实际上,可能的选择是 无, 导航,和 几何。
-
使用默认设置,圆锥在关卡中可能看起来是这样的:
点:甜甜圈
正如其名所示, 甜甜圈生成器 以甜甜圈形状(或对那些喜欢数学的人来说是"圆环")生成点,从一个特定的中心开始,该中心作为一个上下文给出。此外,还有各种选项,以便你可以与 导航网格 交互(这样你就可以将点投影到 导航网格 上)。
此生成器还可以用于生成螺旋形状。就像圆锥形状一样,此生成器可以用于生成点来覆盖整个圆的面积。你可以通过将其内半径设置为零来实现这一点。
这是在 环境查询编辑器 中的样子:
可用的以下设置:
-
内半径:这是甜甜圈的"洞"的半径;在此半径内不会生成任何点(因此它离中心的距离更远)。
-
外半径:这是整个甜甜圈的半径;点将在内半径和 外半径之间生成环。这也意味着在此半径之外不会生成任何点(因此,它离中心的距离更远)。
-
环数:在内半径和外半径之间应生成多少个点环。这些环总是均匀分布的,这意味着它们的距离由这个变量控制,以及内半径和外半径。
-
每环点数:这决定了每个生成的环应该有多少个点。点沿环均匀分布。
-
弧方向:如果我们只生成甜甜圈的弧(精确地说,只生成将生成甜甜圈的圆的弧),此设置确定这个方向。计算方向的方法可以是 两点(它需要两个 上下文并计算两点之间的方向)或 旋转(它需要一个 上下文并检索其旋转,然后根据该旋转决定弧的方向)。
-
弧度角:如果这不是360,它定义了点停止生成的地方的切割角度,从而创建一个弧而不是圆。这种弧的方向(或旋转)由弧方向参数控制。
-
使用螺旋模式:如果选中,每个环中的点略有偏移以生成螺旋图案。
-
中心:这是生成的环的中心(以及用内半径和外半径指定的甜甜圈的最小和最大延伸)。它被表示为上下文。
-
投影数据:这通过从上方投影点来执行一个"垂直"追踪,考虑了几何形状或导航网格。可能的选项是无,导航,和几何。
要理解这些设置,请看以下截图:
通过使用这些略微修改的设置(请注意我如何增加了内半径,提高了环数和每环点数,并且还使用了导航来投影数据),可以轻松地可视化甜甜圈。以下是使用的设置:
这是它们产生的结果:
通过使用相同的设置,并检查使用螺旋模式,你可以看到不同环中的点略有偏移,从而创建一个螺旋图案:
点:网格
如其名所示,网格生成器在网格内生成点。此外,还有与导航网格交互的选项(这样你就不必在导航网格外生成点)。
这是在环境查询编辑器中的样子:
这个生成器的设置相当简单:
-
网格半尺寸:网格应从其中心延伸多远(这意味着它是完整网格大小的一半)。网格的尺寸完全由这个参数以及行间距决定。
-
行间距:网格的每一行和每一列之间的空间大小。网格的尺寸完全由这个参数以及网格半尺寸决定。
-
生成区域:这是网格的中心(生成开始的地方),它被表示为上下文。
-
投影数据:这通过从上方投影点来执行一个"垂直"追踪。它是通过考虑几何形状或导航网格来做到这一点的。可能的选项是无,导航,和几何。
通过查看设置,你可以看到这个生成器相当简单,但功能强大且非常常用。使用默认设置,在关卡中的样子如下(在 Navmesh 中启用了投影,并在地图中存在):
点:路径网格
正如其名所示,路径网格生成器在网格内生成点,就像网格生成器一样。然而,这个生成器的不同之处在于,路径网格生成器会检查点是否可以通过在生成周围设置(通常为查询者)中指定的上下文(通常为查询者),在指定距离内到达。
这是在环境查询编辑器中的样子:
这个生成器的设置几乎与点:网格生成器相同:
-
项目路径:如果选中,则在查询者的设置中排除所有从上下文不可达的点。
-
导航过滤器:正如其名所示,它是用于执行路径查找的导航过滤器。
-
网格半尺寸:这表示网格应从其中心延伸多远(这意味着它是完整网格大小的一半)。网格的尺寸完全由这个参数确定,以及空间间隔。
-
空间间隔:这表示网格的每一行和每一列之间的空间大小。网格的尺寸完全由这个参数确定,以及网格半尺寸。
-
生成周围:这是网格的中心(它是开始生成的地方),并以上下文的形式表示。
-
投影数据:通过从上方投影点来执行一个“垂直”追踪。它通过考虑几何形状或导航网格来完成此操作。可能的选项是无、导航和几何。
这就是它在环境中的样子(我稍微调整了级别以阻断楼上的路径。这清楚地表明,那些在楼梯之后的不可达的点甚至不是由这个生成器生成的):
内置测试
现在我们已经探索了所有生成器,是时候探索引擎内可用的不同测试了。通常,返回值可以是布尔值或浮点值。
返回浮点值的测试通常用于评分,而返回布尔值的测试则更常用于过滤。然而,每个测试可能都有不同的返回值,这取决于测试是用于过滤还是评分。
这是可能的内置测试列表;让我们来探索它们:
-
距离:计算项目(生成的点)与特定上下文(例如查询者)之间的距离。它可以在3D、2D、沿 z 轴或沿 z 轴(绝对)计算。返回值是一个浮点数。
-
点积:计算线 A和线 B之间的点积。这两条线都可以表示为两个上下文之间的线或作为特定上下文的旋转(通过取旋转的前向方向)。计算可以在3D或2D中进行。
-
游戏标签:在游戏标签上执行一个查询。
-
重叠:与一个盒子执行重叠测试;可以指定一些选项,例如偏移量或扩展,或重叠通道。
-
路径查找:在正在评估的生成点和上下文之间执行路径查找。特别是,我们可以指定返回值是一个布尔值(如果路径存在)或一个浮点数(路径成本或甚至路径长度)。此外,我们可以指定路径是否从上下文到点或相反,并且可以使用导航过滤器。
-
路径查找批量处理:与路径查找相同,但以批量的形式。
-
项目:执行一个投影,可以通过不同的参数进行自定义。
-
追踪:执行一个追踪测试,提供所有可能的选项以在引擎的其他地方执行追踪。这意味着它可以追踪一条线、一个盒子、一个球体或一个胶囊;无论是在可见性或相机追踪通道上;无论是复杂还是简单;无论是从上下文到点,还是相反。
这就结束了我们对内置节点的探索。
可视化环境查询
如我们之前提到的,有一个简单内置的方法可以在游戏世界中可视化环境查询,直接从视口进行;游戏甚至不需要运行。事实上,有一个特殊的 Pawn 能够做到这一点。然而,这个 Pawn 不能直接带入关卡,因为它已被在代码库中声明为虚拟,以确保它不会被误用。这意味着要使用它,我们需要创建自己的蓝图 Pawn,该 Pawn 直接从这个特殊的 Pawn 继承。
幸运的是,经过这一步,Pawn 就完全功能化了,不再需要任何更多的代码,只需要与参数一起工作(即你想要可视化的环境查询)。
要开始,创建一个新的蓝图。要继承的类是EQSTestingPawn,如下面的截图所示:
然后,你可以将其重命名为MyEQSTestingPawn。
如果你将其拖入地图,从详细信息面板,你可以更改EQS设置,如下面的截图所示:
最重要的参数是 查询模板,在其中你指定要可视化的查询。如果你想要深入了解参数,请查看 第十二章*,调试 AI 的方法——导航、EQS 和性能分析*。
为环境查询系统创建组件
在本节中,我们将学习需要扩展哪个类来在 环境查询系统 中创建我们的自定义组件。
创建上下文
创建自定义 上下文 对于在环境查询过程中需要正确引用时至关重要。特别是,我们将创建一个简单的上下文来检索单个玩家的引用。
让我们探索如何在 C++ 和蓝图 中创建这个 上下文。
在蓝图中创建玩家上下文
要创建上下文,我们需要从 EnvQueryContext_BlueprintBase 类继承。在蓝图的情况下,在其创建时,只需选择突出显示的类,如下截图所示:
至于名称,惯例是保留前缀 “EnvQueryContext_”。我们可以将我们的上下文命名为 “EnvQueryContext_BPPlayer”。
对于蓝图上下文,你可以选择实现以下函数之一:
每个生成器都将为 环境查询 提供一个上下文。
我们可以重写“提供单个演员”函数,然后返回玩家 Pawn,就这么简单:
因此,我们现在有一个能够获取玩家引用的上下文。
在 C++ 中创建玩家上下文
在创建 C++ 上下文的情况下,从 EnvQueryContext 类继承,如下截图所示:
惯例相同,即用 “*EnvQueryContext_” 前缀来命名上下文。我们将把我们的类命名为 “EnvQueryContext_Player”。
ProvideContext().h
#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryContext.h"
#include "EnvQueryContext_Player.generated.h"
/**
*
*/
UCLASS()
class UNREALAIBOOK_API UEnvQueryContext_Player : public UEnvQueryContext
{
GENERATED_BODY()
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
};
.cpp
#include "EnvQueryContext_Player.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include "Runtime/Engine/Classes/Engine/World.h"
void UEnvQueryContext_Player::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
if (GetWorld()) {
if (GetWorld()->GetFirstPlayerController()) {
if (GetWorld()->GetFirstPlayerController()->GetPawn()) {
UEnvQueryItemType_Actor::SetContextHelper(ContextData, GetWorld()->GetFirstPlayerController()->GetPawn());
}
}
}
}
因此,我们能够在 C++ 中检索玩家上下文。
创建生成器
与创建上下文的方式类似,我们可以创建自定义生成器。然而,我们不会详细介绍这一点,因为它们超出了本书的范围。
在蓝图的情况下,从 EnvQueryGenerator_BlueprintBase 类继承,如下截图所示:
在 C++中,您需要从EnvQueryGenerator类继承:
由于您已经拥有了所有投影,您可能希望直接从EnvQueryGenerator_ProjectedPoints开始。通过这样做,您只需关注其生成。
创建测试
在当前版本的虚幻引擎中,您无法在蓝图(Blueprint)中创建测试——我们只能用 C++来实现。您可以通过扩展EnvQueryTest类来完成这项工作:
不幸的是,这也不在本书的范围之内。然而,探索虚幻引擎的源代码将为您提供大量的信息和几乎无穷无尽的学习资源。
摘要
在本章中,我们探讨了环境查询系统如何使决策领域的空间推理成为可能。
具体来说,我们了解了整个系统的一般工作原理,然后我们逐一了解了系统的内置节点。我们还看到了如何通过一个特殊的棋子来可视化查询。最后,我们探讨了如何扩展系统。
在下一章中,我们将探讨代理意识以及内置的感知系统。
第五章:智能体意识
你可以跑,但你无法藏!
哦,你回来了?太好了,因为这意味着你的眼睛捕捉到了一些光线信息,你的大脑正在感知,这通常被称作阅读。我们做的每一件事,我们做的每一个决定,都是基于我们的感知,从生物学角度来看,我们做出快速的决定过程,因为时间是至关重要的(例如,你看到一条蛇,你的杏仁核处理这些信息比你的视觉皮层要快得多和快!)
同样的概念,AI 需要通过收集他们需要感知的信息来基于事实做出决策。本章全部关于感知,以及 AI 如何从环境中获取这些信息,以便它能够意识到其周围的环境。我们在上一章中探讨了 EQS,它收集了大量关于周围环境的信息并进行处理。在这里,我们将仅限于简单的感知行为。AI 将如何使用这些信息是其他章节(以及我们已经讨论过的章节)的主题。
下面是本章我们将要讨论的主题的简要概述:
-
现有视频游戏中的感知和意识
-
Unreal 中感知系统的概述
-
感知组件
-
视觉和听觉感知
-
感知刺激
-
在智能体中实现视觉(在蓝图和 C++中)
-
在智能体中实现听觉(在蓝图和 C++中)
那么,让我们先看看一些关于视频游戏中 AI 意识的例子,然后我们将看到如何在 Unreal 中设置感知系统。
游戏中的人工智能感知
这一切都是感知的问题,对吧?但是当涉及到人工智能——特别是游戏中的 AI——感知可以决定胜负。换句话说,AI 角色在游戏过程中如何感知玩家可以创造一系列不同的体验,从而在你缓慢、试探性地转弯时,营造出充满紧张和悬念的环境。
声音
你有没有尝试在游戏中悄悄绕过守卫,试图不发出声音或被发现?这是 AI 感知玩家并相应反应(通常不是对你有利)的最常见方式之一!然而,使用声音来影响 AI 对玩家的感知的好处是,它给了玩家发起突袭的机会(例如,《杀手》,《刺客信条》)。例如,玩家可以悄悄地接近敌人,从后面击晕或攻击他们,从而为玩家提供优势。这在敌人难以击败或玩家资源不足(例如,弹药、医疗包/药水等)时尤其有用。
脚步声
正如前面的例子所暗示的,AI 通过声音感知角色的最常见方式之一是通过脚步声。这里没有关于如何做到的惊喜,但检测的邻近性可能取决于许多因素。例如,一些角色可以蹲着走以避免被检测到,或者简单地通过潜行(例如 Abe’s Oddyssey);其他游戏允许某些角色在移动时不可检测,除非被敌人视觉上发现(例如 Resident Evil: Revelations 2 中的 Natalia)。使用脚步声作为 AI 感知的触发器的另一个关键因素是玩家行走的地面材料类型。例如,一个在森林中行走的玩家,踩在树叶和树皮上,会比在沙地上行走的玩家更明显(也更响亮)。
撞倒物体
当你在关卡中潜行或蹲着走,甚至是在卧姿(例如 Battlefield)时,不会触发敌人,但如果你撞倒了某个东西(例如瓶子、箱子、随机物品),它很可能会引起他们的注意。在这种情况下,环境物体在 AI 通过玩家在环境中的笨拙动作来感知玩家位置方面发挥着重要作用。在某些情况下,某些物体可能比其他物体更容易吸引注意力,这取决于它们产生的噪音大小。当然,作为游戏设计师,你有权决定这一点!
位置
类似于声音,AI 可以根据你与它们之间的距离看到你。当你被敌人直接看到时,这种情况会更明显,也更难以避免。想象一下,你正在悄悄地绕过敌人,一旦你足够接近他们,那就完了,你已经被发现!这是许多玩家面临的悲惨危险,但也是一个有很多回报的事情,尤其是在战胜敌人的满足感方面。
让我们通过一些例子进一步探讨这个概念。首先,我们有像 Assassin’s Creed、Hitman: Absolution 和 Thief 这样的游戏,在这些游戏中,通过操纵来避开敌人的艺术对于玩家完成任务的成败至关重要。通常,这要求玩家利用环境周围的环境,如 NPC、墙壁、干草堆、植物(树木、灌木)、屋顶,以及利用惊喜元素。
距离区域
在其他情况下,有一个明确的距离区域,玩家可以在被检测到之前保持在这个区域之外。在游戏中,这通常通过光源,如手电筒来体现,迫使玩家在阴影和光线之间穿梭以避免被检测到。采用这种方法的优秀游戏例子有 Monaco: What’s Yours Is Mine 和 Metal Gear Solid,其中某些 AI 角色通过火炬或长时间面对玩家来获得可见性。
你可以在下面的屏幕截图中看到这个例子:
游戏截图来自《摩纳哥:你的就是我的》
在这里(在《摩纳哥:你的就是我的》中),你可以看到手电筒的半径,一旦玩家进入,他们就有有限的时间来吸引警卫的注意。
由于《摩纳哥:你的就是我的》完全基于这种机制,让我们看看更多截图,以更好地了解这款游戏中视觉感知的工作方式。
在下面的截图中,我们可以看到当玩家改变房间时感知是如何变化的:
游戏截图来自《摩纳哥:你的就是我的》
在下面的截图中,我们看到了玩家的感知特写:
游戏截图来自《摩纳哥:你的就是我的》
然后,我们看到了一名警卫手电筒的特写:
游戏截图来自《摩纳哥:你的就是我的》
改变游戏,在《合金装备》中,感知与敌人(红色圆点)在玩家(白色圆点)周围巡逻环境的方式相似。在下面的截图中,你可以看到一个摄像头(在小地图中以红色圆点表示)在小地图中有一个黄色的视野锥(警卫有一个蓝色的视野锥):
游戏截图来自《合金装备》
《合金装备》游戏系列完全基于感知,如果你对使用这种机制开发游戏 AI 感兴趣,那么探索更多并了解这款游戏是值得的。
总结一下,如果你离 NPC(例如,在他们的可视范围内)太近,你会被发现,他们会尝试与你的人物互动,无论是好是坏(例如《刺客信条》中的乞丐或敌人攻击你),这解锁了许多基于感知的有趣机制。
与其他敌人的互动
一个 AI 对你的位置的感知并不一定与你进入他们的可视区域的时间有关。在其他情况下(例如第一人称射击游戏),这可能会在你开始射击敌人时发生。这会在你的初始近距离内的许多 AI 中产生连锁反应,它们会以你为目标(例如《合金装备》、《双雄》、《战地》等)。
并非所有都与“敌人”有关
在许多体育游戏中,AI 必须具有感知能力才能相应地做出反应,例如防止进球、击球或投篮。在体育游戏中,AI 在与你对抗时必须具有感知能力(和竞争力)。他们需要知道你的位置和球的位置(或任何其他物体),以便他们可以做出反应(例如将球踢离球门柱)。
感知 AI 不仅仅是人形或动物性的
感知 AI 也可以包括机器,例如汽车和其他车辆。以游戏《侠盗猎车手》、《赛车手》和《逃离》为例,这些游戏要求玩家在车内某个时刻在 3D 世界空间中导航。在某些情况下,车内有 NPC,但大部分情况下,汽车本身会对你驾驶做出反应。这种情况也适用于更多以运动为导向的游戏,如《极品飞车》、《速度与激情》和《 Ridge Racer》(仅举几个例子)。
玩家的影响
正如我们所看到的,AI 检测玩家的方式有很多种。但在所有这些中,游戏设计师必须考虑的是这将对游戏体验产生怎样的影响;它将如何驱动游戏玩法?虽然感知 AI 的使用对任何游戏来说都是一个很好的补充,但它也会影响游戏玩法。例如,如果你想有一个高度关注技能、玩家敏捷性和更多环境意识的玩法,那么 AI 的感知需要非常敏感,玩家将更加脆弱(例如,盗贼)。但另一方面,如果你想有一个快节奏的动作游戏,你需要有一个平衡的感知 AI,允许玩家相应地做出反应。例如,他们有一个与 AI 对抗的公平竞技场。
感知系统概述
回到虚幻引擎,正如你所预期的那样,AI 框架中有一个子系统实现了 AI 感知。再次强调,你可以自由地实现自己的系统,尤其是如果你有特殊需求的话…
在感知与感知方面,我们处于比决策(如行为树和 EQS)更低的层次。实际上,这里没有需要做出的决策,没有需要选择的地方,而只是信息的流动/流程。
如果感知系统感知到某些“有趣”的东西(我们稍后会定义这是什么意思),那么它会通知 AI 控制器,AI 控制器将决定如何处理收到的刺激(这在虚幻引擎术语中是其感知)。
因此,在本章中,我们将重点介绍如何正确设置感知系统,以便我们的 AI 能够感知,但我们不会处理收到刺激后的操作(例如,玩家在视线中,开始追逐他们)。毕竟,如果你已经有了准备好的行为(例如,追逐玩家的行为树;我们将在本书后面构建这样的树),感知背后的逻辑简单到“如果玩家在视线中(AI 控制器从感知系统中收到刺激),则执行追逐行为树”。
在实际应用中,虚幻引擎内置的感知系统主要基于两个组件的使用:AIPerceptionComponent和AIPerceptionStimuliSourceComponent。前者能够感知刺激,而后者能够产生刺激(但产生刺激的方式不止这一种,我们很快就会看到)。
虽然听起来可能有些奇怪,但系统认为 AIPerceptionComponent 是附加到 AI 控制器上的(而不是它们所控制的 Pawn/Character)。实际上,是 AI 控制器将根据接收到的刺激做出决定,而不是单纯的 Pawn。因此,AIPerceptionComponent 需要直接附加到 AI 控制器上。
AIPerceptionComponent
让我们分解一下 AIPerceptionComponent 的工作原理。我们将同时在蓝图和 C++ 中进行这一操作。
蓝图中的 AIPerceptionComponent
如果我们打开蓝图 AI 控制器,我们就可以像添加任何其他组件一样添加 AIPerceptionComponent:从组件选项卡,点击 添加组件 并选择 AIPerceptionComponent,如下面的截图所示:
当你选择组件时,你将看到它在 详细信息 面板中的样子,如下面的截图所示:
它只有两个参数。一个定义了主要感官。实际上,AIPerceptionComponent 可以拥有多个感官,当涉及到检索已感知的目标位置时,AI 应该使用哪一个?主要感官通过给予一个感官相对于其他感官的优先权来消除歧义。另一个参数是一个感官数组。当你将不同的感官填充到数组中时,你将能够自定义每一个,如下面的截图所示:
请记住,你可以拥有每种类型超过一个感官。假设你的敌人有两个头,朝向不同的方向:你可能想要有两个视觉感官,一个对应每个头。当然,在这种情况下,需要更多的设置来确保它们正确工作,因为你需要修改视觉组件的工作方式,比如说,AI 总是从其前向矢量观察。
每个感官都有自己的属性和参数。让我们来看两个主要的:视觉和听觉。
感官 – 视觉
视觉感官的工作方式正如你所期望的,并且它几乎可以直接使用(这可能不适用于其他感官,但视觉和听觉是最常见的)。它看起来是这样的:
让我们分解一下控制视觉感官的主要参数:
-
视线半径:如果一个目标(一个可以看到的对象)进入这个范围内,并且没有被遮挡,那么目标就会被检测到。在这种情况下,它就是“最大视线距离以注意到目标”。
-
失去视线半径:如果目标已经被看到,那么如果未被遮挡,目标仍然会在这个范围内被看到。这个值大于 视线半径,这意味着如果目标已经被看到,AI 能够在更远的距离上感知到目标。在这种情况下,它就是“最大视线距离以注意到已经看到的目标”。
-
外围视野半角度数:正如其名所示,它指定了 AI 可以看多远(以度为单位)。90 的值意味着(因为这个值只是角度的一半)AI 能够看到其前方直到 180 度的所有事物。180 的值意味着 AI 可以朝任何方向看;它有 360 度的视野。此外,重要的是要注意,这个半角是从前进向量测量的。以下图表说明了这一点:
- 从上次看到的位置自动成功范围:默认情况下,它设置为无效值(-1.0f),这意味着它没有被使用。这指定了从目标上次看到的位置的范围,如果它在这个范围内,则目标总是可见的。
有其他一些更通用的设置,可以应用于许多感官(包括听觉,因此它们将在下一节中不再重复):
-
通过隶属关系检测:参见不同的团队部分。
-
调试颜色:正如其名所示,这是在视觉调试器中显示此感官的颜色(参见第十一章[de51b2fe-fb19-4347-8de9-a31b2c2a6f2f.xhtml],AI 调试方法 – 记录,获取更多信息)。
-
最大年龄:它表示刺激被记录的时间(以秒为单位)。想象一下,一个目标从 AI 的视野中消失;它的最后位置仍然被记录,并分配了一个年龄(这些数据有多久)。如果年龄大于最大年龄,则刺激将被删除。例如,一个 AI 正在追逐玩家,玩家逃离了他的视野。现在,AI 应该首先检查玩家最后被看到的位置,试图将其带回视野中。如果失败,或者位置记录了许多分钟之前,那么这些数据就不再相关,可以将其删除。总之,这指定了由这个感官产生的刺激被遗忘后的年龄限制。此外,0 的值表示永不。
感官 – 听觉
听觉感官只有一个合适的参数,即听觉范围。这设置了 AI 能够听到的距离。其他的是我们已经看到的通用参数(例如最大年龄、调试颜色和通过隶属关系检测)。它看起来是这样的:
为了使这本书完整,值得提一下,还有一个选项,称为LoSHearing。据我所知,通过查看虚幻引擎源代码(版本 4.20),这个参数似乎不影响任何事物(除了调试)。因此,我们将其设置为未启用。
在任何情况下,都有其他选项来控制声音的产生。实际上,听觉事件需要使用特殊的功能/蓝图节点手动触发。
AIPerceptionComponent 和 C++中的感官
如果你跳过了前面的部分,请先阅读它们。实际上,所有概念都是相同的,在这个部分,我只是将要展示组件在 C++ 中的使用,而不会重新解释所有概念。
#include
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISense_Sight.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISense_Hearing.h"
#include "Perception/AISenseConfig_Hearing.h"
.hinerith
UPROPERTY(VisibleDefaultsOnly, Category = AI)
UAIPerceptionComponent* PerceptionComponent;
.h
.cppCreateDefaultSubobject()
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("SightPerceptionComponent"));
此外,你将需要额外的变量,每个你想要配置的感官都需要一个。例如,对于视觉和听觉感官,你需要以下变量:
UAISenseConfig_Sight* SightConfig;
UAISenseConfig_Hearing* HearingConfig;
要配置一个感官,你首先需要创建它,你可以访问它的所有属性,并设置你需要的内容:
//Create the Senses
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
HearingConfig = CreateDefaultSubobject<UAISenseConfig_Hearing>(FName("Hearing Config"));
//Configuring the Sight Sense
SightConfig->SightRadius = 600;
SightConfig->LoseSightRadius = 700;
//Configuration of the Hearing Sense
HearingConfig->HearingRange = 900;
最后,你需要将感官绑定到 AIPerceptionComponent:
//Assigning the Sight and Hearing Sense to the AI Perception Component
PerceptionComponent->ConfigureSense(*SightConfig);
PerceptionComponent->ConfigureSense(*HearingConfig);
PerceptionComponent->SetDominantSense(SightConfig->GetSenseImplementation());
如果你需要调用回事件,你可以通过绕过回调函数(它必须具有相同的签名,不一定是相同的名称)来这样做:
//Binding the OnTargetPerceptionUpdate function
PerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(this, &ASightAIController::OnTargetPerceptionUpdate);
这就结束了在 C++ 中使用 AIPerceptionComponent 和 Senses 的过程。
不同团队
就内置在 Unreal 中的 AI 感知系统而言,AI 和任何可以被检测到的内容都可以有一个团队。有些团队是相互对立的,而有些则是中立的。因此,当 AI 来感知某物时,那物可以是友好的(它在同一个团队中),中立的,或者敌人。例如,如果一个 AI 在巡逻营地,我们可以忽略友好的和中立的实体,只专注于敌人。顺便说一句,默认设置是只感知敌人。
你可以通过 Sense 的 Detecting for Affiliation 设置来改变 AI 可以感知的实体类型:
这提供了三个复选框,我们可以选择我们希望 AI 感知的内容。
总共有 255 个团队,默认情况下,每个实体都在团队 255(唯一的特殊团队)中。在团队 255 中的实体被视为中立(即使两个实体都在同一个团队中)。否则,如果两个实体在同一个团队中(不同于 255),它们“看到”对方是友好的。另一方面,两个不同团队(不同于 255)的实体“看到”对方是敌人。
现在的问题是,我们如何更改团队?目前,这只能在 C++ 中完成。此外,我们已经讨论了实体,但谁实际上可以成为团队的一员?所有实现了 IGenericTeamAgentInterface 的内容都可以成为团队的一部分。AIControllers 已经实现了它。因此,在 AI 控制器上更改团队很容易,如下面的代码片段所示:
// Assign to Team 1
SetGenericTeamId(FGenericTeamId(1));
GetGenericTeamId()
AIStimuliSourceComponent
我们已经看到了 AI 如何通过感知来感知,但刺激最初是如何生成的?
所有 Pawns 都会自动检测。
在视觉感知的情况下,默认情况下,所有 Pawns 已经是刺激源。实际上,在本章的后面,我们将使用玩家角色,该角色将由 AI 检测,而无需AIStimuliSourceComponent。如果你有兴趣禁用此默认行为,你可以通过进入你的项目目录,然后进入Config文件夹来实现。在那里,你会找到一个名为DefaultGame.ini的文件,你可以在其中设置一系列配置变量。如果你在文件末尾添加以下两行,Pawns 将默认不产生视觉刺激,并且它们还需要AIStimuliSourceComponent以及所有其他内容:
[/Script/AIModule.AISense_Sight]
bAutoRegisterAllPawnsAsSources=false
在我们的项目中,我们不会添加这些行,因为我们想要 Pawns 被检测,而无需添加更多组件。
AIStimuliSourceComponent 在蓝图中的使用
和其他组件一样,它可以添加到蓝图:
如果你选择它,你将在详细信息面板中看到它只有两个参数:
-
自动注册为源:正如其名所示,如果选中,源将自动在感知系统中注册,并且它将从一开始就开始提供刺激。
-
注册为感知源:这是一个数组,包含该组件提供的所有感知刺激。
关于这个组件没有太多可说的。它非常简单易用,但很重要(你的 AI 可能无法感知任何刺激!)。因此,当你想要它们生成刺激(这可能只是被 AI 看到)时,记得将其添加到非 Pawns 实体中。
C++中的 AIStimuliSourceComponent
在 C++中使用此组件很容易,因为你只需创建它、配置它,它就准备好了。
#include
#include "Perception/AIPerceptionStimuliSourceComponent.h"
.h
UAIPerceptionStimuliSourceComponent* PerceptionStimuliSourceComponent;
CreateDefaultSubobject()
PerceptionStimuliSourceComponent = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("PerceptionStimuliComponent"));
TSubClassOf()
PerceptionStimuliSourceComponent->RegisterForSense(TSubclassOf<UAISense_Sight>());
自动注册为源布尔值是受保护的,默认为 true。
实践感知系统 – 视觉 AI 控制器
了解某物的最佳方式就是使用它。所以,让我们先创建一个简单的感知系统,当有东西进入或离开 AI 的感知区域时,我们在屏幕上打印出来,同时显示当前看到的对象数量(包括/不包括刚刚进入/离开的对象)。
再次强调,我们将这样做两次,一次使用蓝图,另一次使用 C++,这样我们就可以了解两种创建方法。
一个蓝图感知系统
首先,我们需要创建一个新的 AI 控制器(除非你想要继续使用我们之前一直在使用的那个)。在这个例子中,我将称它为“SightAIController”。打开蓝图编辑器,添加 AIPerception 组件,并且如果你喜欢的话,可以将其重命名为“SightPerceptionComponent”。
选择这个组件。在详细信息面板中,我们需要将其添加为视野的一个感知,如下面的截图所示:
我们可以将视野半径和失去视野半径设置为合理的值,例如600和700,这样我们就可以得到类似这样的效果:
我们可以保持角度不变,但我们需要更改通过归属检测。实际上,在蓝图上无法更改团队,所以玩家将处于相同的 255 号团队,这是一个中立团队。由于我们只是刚开始了解这个系统的工作原理,我们可以勾选所有三个复选框。现在,我们应该有类似这样的效果:
在组件的底部,我们应该有所有不同的事件。特别是,我们需要目标感知更新,每次目标进入或退出感知区域时都会调用它——这正是我们所需要的:
点击图中的“+”符号来添加事件:
这个事件将为我们提供导致更新并创建刺激的演员(值得记住的是,感知组件可能同时有多个感知,这个变量告诉你哪个刺激导致了更新)。在我们的例子中,我们只有视野,所以它不可能是其他任何东西。下一步是了解我们有多少目标在视野中,以及哪个目标离开了或进入了视野。
因此,将SightPerceptionComponent拖入图中。从那里,我们可以拖动一个引脚来获取所有的“当前感知到的演员”,这将给我们一个演员数组。别忘了将感知类设置为视野:
通过测量这个数组的长度,我们可以得到当前感知到的演员的数量。此外,通过检查从事件传递过来的演员是否在当前“可见演员”数组中,我们可以确定这样的演员是否已经离开或进入了视野:
最后一步是将所有这些信息格式化成一个漂亮的格式化字符串,以便可以在屏幕上显示。我们将使用 Append 节点来构建字符串,以及一个用于选择 “进入” 或 “离开” 实体的选择器。最后,我们将最终结果连接到 Print String:
Print String 仅用于调试目的,在发布游戏时不可用,但我们现在只是在测试和理解感知系统的工作原理。
此外,我知道当感知到的实体数量为一个是,字符串将产生 “1 objects”,这是不正确的,但修正复数(尽管可能,无论是使用 if 语句还是以更复杂的方式处理语言结构)超出了本书的范围。这就是为什么我使用这个表达式的理由。
保存 AI 控制器并返回到关卡。如果你不想在 C++ 中做同样的事情,请跳过下一节,直接进入 “测试一切”。
C++ 感知系统
再次,如果你更倾向于 C++ 方面,或者想要实验如何用 C++ 构建相同的 AI 控制器,这部分就是为你准备的。我们将遵循完全相同的步骤(或多或少),而不是图片,我们将有代码!
AIControllers
SightAIController.h.h#include
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISense_Sight.h"
#include "Perception/AISenseConfig_Sight.h"
#include "SightAIController.generated.h"
AIPerception
//Components Variables
UAIPerceptionComponent* PerceptionComponent;
UAISenseConfig_Sight* SightConfig;
ConstructorOnTargetPerceptionUpdateUFUNCTION()
//Constructor
ASightAIController();
//Binding function
UFUNCTION()
void OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus);
.cppAIPerception
ASightAIController::ASightAIController() {
//Creating the AI Perception Component
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("SightPerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
}
然后,我们可以使用相同的参数配置 Sight Sense:视野半径 为 600 和 失去视野半径 为 700:
ASightAIController::ASightAIController() {
//Creating the AI Perception Component
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("SightPerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
//Configuring the Sight Sense
SightConfig->SightRadius = 600;
SightConfig->LoseSightRadius = 700;
}
接下来,我们需要检查 DetectionByAffiliation 的所有标志,以便检测我们的玩家(因为,目前,他们都在第 255 个团队中;查看 练习 部分了解如何改进这一点):
ASightAIController::ASightAIController() {
//Creating the AI Perception Component
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("SightPerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
//Configuring the Sight Sense
SightConfig->SightRadius = 600;
SightConfig->LoseSightRadius = 700;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
}
AIPerceptionOnTargetPerceptionUpdateAIPerceptionComponent
ASightAIController::ASightAIController() {
//Creating the AI Perception Component
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("SightPerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
//Configuring the Sight Sense
SightConfig->SightRadius = 600;
SightConfig->LoseSightRadius = 700;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
//Assigning the Sight Sense to the AI Perception Component
PerceptionComponent->ConfigureSense(*SightConfig);
PerceptionComponent->SetDominantSense(SightConfig->GetSenseImplementation());
//Binding the OnTargetPerceptionUpdate function
PerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(this, &ASightAIController::OnTargetPerceptionUpdate);
}
OnTargetPerceptionUpdate()
因此,我们的数组将填充感知到的演员:
void ASightAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus)
{
//Retrieving Perceived Actors
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetPerceivedActors(TSubclassOf<UAISense_Sight>(), PerceivedActors);
}
通过测量数组的长度,我们可以得到当前感知到的演员数量。此外,通过检查从事件传递过来的演员(函数的参数)是否在当前"可见演员"数组中,我们可以确定这样的演员是否已经离开或进入了视野:
void ASightAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus)
{
//Retrieving Perceived Actors
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetPerceivedActors(TSubclassOf<UAISense_Sight>(), PerceivedActors);
//Calculating the Number of Perceived Actors and if the current target Left or Entered the field of view.
bool isEntered = PerceivedActors.Contains(Actor);
int NumberObjectSeen = PerceivedActors.Num();
}
最后,我们需要将此信息打包到一个格式化的字符串中,然后将其打印到屏幕上:
void ASightAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus)
{
//Retrieving Perceived Actors
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetPerceivedActors(TSubclassOf<UAISense_Sight>(), PerceivedActors);
//Calculating the Number of Perceived Actors and if the current target Left or Entered the field of view.
bool isEntered = PerceivedActors.Contains(Actor);
int NumberObjectSeen = PerceivedActors.Num();
//Formatting the string and printing it
FString text = FString(Actor->GetName() + " has just " + (isEntered ? "Entered" : "Left") + " the field of view. Now " + FString::FromInt(NumberObjectSeen) + " objects are visible.");
if (GEngine) {
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Turquoise, text);
}
UE_LOG(LogTemp, Warning, TEXT("%s"), *text);
}
一次又一次,我知道“1 objects”是不正确的,但修正复数(尽管可能)超出了本书的范围;让我们保持简单。
测试所有这些
现在,你应该已经实现了一个带有感知系统的 AI 控制器(无论是蓝图还是 C++——这并不重要,它们应该表现相同)。
ThirdPersonCharacter
在详细信息面板中,我们让它由我们的 AI 控制器控制,而不是玩家(这个流程现在应该对你来说很容易):
或者,如果你使用 C++设置,选择以下设置:
在按下播放之前,创建一些可以被检测到的其他对象会很好。我们知道所有的 Pawns 都可以被检测到(除非被禁用),所以让我们尝试一个不是 Pawn 的对象——也许是一个移动平台。因此,如果我们想检测它,我们需要使用AIPerceptionStimuliSourceComponent。
首先,让我们创建一个浮动平台(可以被我们的角色轻易推动)。如果你在ThirdPersonCharacter Example的默认关卡中,你可以通过Alt + 拖动复制以下截图中的大网格,否则,如果你使用自定义关卡,一个可以被压扁的立方体将工作得很好):
到目前为止,它太大,所以让我们将其缩小到(1, 1, 0.5)。此外,为了保持一致,你可以将其移动到(-500, 310, 190)。最后,我们需要将移动性更改为可移动,因为它需要移动:
接下来,我们希望能够推动这样的平台,因此我们需要启用物理模拟。为了保持我们的角色可以推动它,让我们给它一个质量为100 Kg(我知道,这看起来很多,但考虑到摩擦很小,并且平台可以漂浮,这是正确的数量)。此外,我们不希望平台旋转,因此我们需要在约束中阻止所有三个旋转轴。如果我们想让平台漂浮,也是同样的道理——如果我们锁定 z 轴,平台只能沿着XY 平面移动,没有旋转。这将确保平台易于推动。这就是物理部分应该看起来像的:
最后,我们需要添加一个AIPerceptionStimuliSourceComponent,从 Actor 名称附近的添加组件绿色按钮:
一旦添加了组件,我们就可以从前面的菜单中选择它。结果,详细信息面板将允许我们更改AIPerceptionStimuliSourceComponent设置。特别是,我们想要添加视觉感知,并自动将组件注册为源。这就是我们应该如何设置它:
作为可选步骤,你可以将其转换为蓝图,以便可以重用,并可能赋予一个更有意义的名称。此外,如果你想让多个对象被视觉感知系统跟踪,你可以复制它几次。
最后,你可以点击播放并测试我们迄今为止所取得的成果。如果你通过了我们的AI 控制角色,你将在屏幕顶部收到通知。如果我们推动平台进入或离开 AI 的视野,我们会得到相同的结果。以下截图显示了 C++实现,但它与蓝图实现非常相似(只是打印的颜色不同):
此外,作为预期,你可以使用可视化调试器查看 AI 的视野,我们将在第十三章中探讨,即AI 调试方法 - 游戏调试器。以下截图是我们创建的 AI 角色的视野参考。关于如何显示它以及理解所有这些信息的详细说明,请耐心等待第十三章的介绍,即AI 调试方法 - 游戏调试器:
是时候给自己鼓掌了,因为这可能看起来你只做了一点点,但实际上,你成功地了解了一个复杂的系统。此外,如果你尝试了一种方法(蓝图或 C++),如果你想掌握蓝图和 C++中的系统,尝试另一种方法。
摘要
难道这不是一个感知到的信息量很大吗?
我们首先理解了内置感知系统中的不同组件如何在 Unreal 的 AI 框架内工作。从那里,我们探索了如何实际使用这些组件(无论是在 C++ 中还是在蓝图),并学习了如何正确配置它们。
我们通过一个设置 视觉 感知系统的实际例子来总结,这次我们既在蓝图(Blueprint)中,也在 C++ 中实现了这一点。
在下一章中,我们将看到我们如何模拟大型 人群。
