虚幻-5-人工智能-全-
虚幻 5 人工智能(全)
原文:
zh.annas-archive.org/md5/4ebbfc33056814d5b91c40dd3abc9e8d译者:飞龙
前言
自从其诞生之初,人工智能(AI)就改变了游戏开发的格局,为玩家提供了曾经被认为是科幻领域的丰富和沉浸式体验。与其他游戏开发方法不同,在这些方法中,脚本事件决定了更严格的玩家交互,而 AI 引入了不可预测性和响应性,使虚拟世界栩栩如生。
近年来,AI 技术的进步使其更加易于获取和实施,导致各种类型的智能游戏激增。这种演变使 AI 成为现代游戏开发的核心,每天数百万玩家都能从他们最喜欢的游戏中受益于更智能、更吸引人的交互。
Unreal Engine 作为一个高级平台,对于希望在其项目中利用 AI 力量的开发者来说脱颖而出。凭借一套针对 AI 开发量身定制的强大工具和功能——例如行为树和导航系统——Unreal Engine 使创作者能够构建复杂的 AI 系统,从而增强游戏玩法和玩家参与度。
如果您准备好探索 AI 开发的世界及其改善游戏的可能性,那么现在是最好的时机来深入其中!
本书面向对象
如果你是一位游戏程序员,或者更具体地说是一位 Unreal Engine 开发者,对视频游戏 AI 系统知之甚少或一无所知,并希望深入探讨这个主题,那么这本书就是为你准备的。
精通其他游戏引擎并对理解 Unreal AI 框架原理感兴趣的开发者也将从这本书中受益;然而,强烈建议您具备基本的 Unreal Engine 和 C++知识。
对游戏逻辑的热情将帮助您充分利用这本书。
本书涵盖内容
第一章 ,开始 AI 游戏开发,温和地引导您进入 AI 游戏开发的领域,从理解 AI 行为的基本原理开始。
第二章 ,介绍 Unreal Engine AI 系统,向您介绍 Unreal Engine 游戏框架中包含的主要 AI 元素,如行为树、导航系统和感知系统。
第三章 ,展示 Unreal Engine 导航系统,向您介绍 Unreal Engine 中强大的导航功能,包括导航网格生成和路径查找算法等关键概念。
第四章 ,设置导航网格,通过一个具体的项目开始,涵盖了使用 Unreal Engine 实现导航网格的必要实用技术。
第五章 ,改进智能体导航,向您介绍优化 AI 智能体在复杂环境中移动和交互的集成算法。
第六章 ,优化导航系统,介绍了一些策略和技术,以最大化虚幻引擎中导航系统的性能和效率。
第七章 ,介绍行为树,向您介绍虚幻引擎框架内强大且通用的行为树系统。
第八章 ,设置行为树,指导您通过创建和配置行为树以在虚幻引擎中驱动 AI 代理的基本步骤。
第九章 ,扩展行为树,深入探讨了扩展行为树功能的高级技术,以创建更复杂的 AI 行为和交互。
第十章 ,使用感知系统改进代理,展示了如何利用虚幻引擎感知系统的力量来增强虚拟环境中 AI 代理的响应性。
第十一章 ,理解环境查询系统,对虚幻引擎框架中的环境查询系统进行了全面和详细的解释。
第十二章 ,使用带有状态树的状态机,向您介绍用于在虚幻引擎中实现分层状态机的 StateTree 系统。
第十三章 ,使用质量实现面向数据的计算,介绍了 MassEntity 框架,通过该框架您将能够实现高效且可扩展的面向数据计算。
第十四章 ,使用智能对象实现交互元素,介绍了智能对象,并展示了如何在虚幻引擎环境中集成它们。
附录 - 在虚幻引擎中理解 C++,深入探讨了在虚幻引擎框架中使用 C++编程语言的基本概念和原则。
为了充分利用本书
为了充分利用本书,强烈建议您对虚幻引擎及其主要功能有良好的理解。一些 C++编程经验也将是一个优势。对游戏的强烈热情——特别是游戏逻辑——将帮助您更好地理解最先进的话题。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| 虚幻引擎 5.4 | Windows、macOS 或 Linux |
| Visual Studio 2019 或 2022 和 JetBrains Rider 2023+ |
由于本书专注于 AI 编程而不是图形,您不需要高性能的计算机来跟随所有章节。然而,为了正确运行虚幻引擎,推荐使用配置良好的 PC 和良好的显卡。
如果您使用的是本书的数字版,我们建议您自己输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包可供在github.com/PacktPublishing/获取。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“一旦项目打开,请检查内容文件夹中有什么。”
代码块设置如下:
#pragma once UENUM(BlueprintType)enum class EBatteryStatus : uint8 { EBS_Empty = 0 UMETA(DisplayName = "Empty"), EBS_Low = 1 UMETA(DisplayName = "Low"), EBS_Medium = 2 UMETA(DisplayName = "Medium"), EBS_Full = 3 UMETA(DisplayName = "Full")};
任何命令行输入或输出都按以下方式编写:
$ mkdir css
$ cd css
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在详细信息面板中,在演员|高级类别中查找标签属性,然后按+按钮创建一个新的标签。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《Artificial Intelligence in Unreal Engine 5》,我们很乐意听到您的想法!请点击此处直接进入本书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但又无法随身携带您的印刷书籍吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 图书,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的邮箱。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781836205852
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱。
第一部分:介绍游戏中的人工智能
本书的第一部分,您将获得一个面向初学者的游戏人工智能(AI)开发领域的介绍。一旦您对其关键概念有了扎实的理解,您就可以开始实施利用这些主题的项目了。
本部分包括以下章节:
-
第一章 ,开始 AI 游戏开发
-
第二章 ,介绍虚幻引擎 AI 系统
第一章:AI 游戏开发入门
欢迎来到虚幻引擎中人工智能(AI)开发的迷人世界!我非常高兴您选择了我和我的书作为您在这个有时令人畏惧的 AI 编程领域的向导。请放心,我致力于让这次体验尽可能轻松愉快。
在本书中,您将获得创建涉及 AI 技术使用的虚幻引擎游戏所需的所有技能,并学习如何在运行时处理它们。我们将从基础知识开始,例如在游戏关卡内移动代理,并逐步过渡到更高级的主题,例如创建复杂的行为和管理多个 AI 实体(甚至数十个或数百个)。通过这次旅程的结束,您将能够制作出能够挑战玩家的强大对手;更重要的是,您将对 AI 开发的潜在陷阱以及如何避免它们有深刻的理解。
在本章中,我将向您介绍一些关于 AI 开发的常用关键词;这些概念将作为本书的温和引言,为您深入探索虚幻引擎中 AI 编程的迷人世界打下基础。
在本章中,我们将涵盖以下主题:
-
AI 介绍
-
游戏开发中的 AI 理解
-
解释视频游戏中的 AI 技术
技术要求
我想您已经知道,虚幻引擎编辑器在硬件要求方面可能相当苛刻。然而,您无需感到害怕,因为本书主要关注游戏编程,而不是实时视觉效果。
在本节中,我们将探讨遵循本书所需的硬件和软件要求。此外,我们还将讨论一些对您的旅程有益的先决知识。
先决知识
在我们深入游戏开发中 AI 的精彩世界之前,我想善意地提醒您,这本书是为那些已经具备一些使用虚幻引擎知识的人设计的。因此,您必须已经熟悉以下主题:
-
虚幻引擎:了解这个软件界面、工具和工作流程的基本知识是必不可少的。
-
游戏开发基础:掌握一般的游戏开发原则和术语将极大地帮助您理解本书中讨论的概念。
-
编程知识:由于本书侧重于游戏开发,我们假设您有一些编程经验。理想情况下,您至少应该熟悉虚幻引擎的可视化脚本系统(蓝图),并在一定程度上熟悉 C++。
注意
如果你刚开始接触虚幻引擎,我强烈建议探索一些入门书籍或资源,以便熟悉其基础知识。一个惊人的起点是《虚幻引擎 5 蓝图视觉脚本编程》,由Marcos Romero,Packt Publishing编写,它将指导你了解使用蓝图在虚幻引擎中进行编程的主要功能。
在这本书中,只要可能,我会通过使用蓝图和 C++两种技术来展示技术。如果你需要 C++的温和介绍,你将在本书末尾找到一个宝贵的附录,该附录深入探讨了在虚幻引擎环境中 C++编程的复杂性。这个快速指南也旨在帮助你了解 C++在虚幻引擎框架中是如何工作的。
硬件要求
在撰写本书时,Epic Games 官方推荐以下基本要求。如果你的硬件至少满足这些规格,你可以在阅读章节时获得愉快的体验:
-
Windows 操作系统:
-
操作系统:Windows 10 或 11 64 位版本
-
处理器:四核 Intel 或 AMD,2.5 GHz 或更快
-
内存:8 GB RAM
-
显卡:DirectX 11-或 12 兼容的显卡
-
-
Linux:
-
操作系统:Ubuntu 22.04
-
处理器:四核 Intel 或 AMD,2.5 GHz 或更快
-
内存:32 GB RAM
-
显卡:配备最新 NVIDIA 二进制驱动程序的 NVIDIA GeForce 960 GTX 或更高版本
-
视频内存:8 GB 或更多
-
-
macOS:
-
操作系统:最新的 macOS Ventura
-
处理器:四核 Intel,2.5 GHz
-
内存:8 GB RAM
-
显卡:兼容 Metal 1.2 的显卡
-
我使用以下硬件编写了这本书:
-
台式机:
-
操作系统:Windows 10 64 位版本
-
处理器:Intel Core i9 9900K
-
内存:64 GB RAM
-
显卡:NVIDIA GeForce RTX 3090ti
-
-
笔记本电脑:
-
操作系统:Windows 11 64 位版本
-
处理器:Intel Core i7 13650HX
-
内存:8 GB RAM
-
显卡:NVIDIA GeForce RTX 4060
-
软件要求
本书假设你已经安装并完全在计算机上运行了Epic Games Launcher和虚幻引擎 5。
注意
在撰写本书时,虚幻引擎的最新版本是 5.4,但你将能够跟随任何比 5.4 更新的版本。
此外,由于我们还将使用 C++,你需要一个支持此语言和虚幻引擎的 IDE。如果你已经有了一些经验,那么你很可能已经安装了 Visual Studio 2019/2022 或 JetBrains Rider;如果没有,你需要安装其中之一来跟随 C++编码部分。
设置 Visual Studio 以进行 C++虚幻引擎开发
一旦安装了 Visual Studio,你还需要以下额外组件才能使其与虚幻引擎正常工作:
-
C++ 性能分析工具
-
C++ AddressSanitizer
-
Windows 10 SDK
-
虚幻引擎安装程序
要包含这些工具,请按照以下步骤操作:
-
打开 Visual Studio 安装程序。
-
从你的 Visual Studio 安装中选择 修改,选择你将要使用的版本:

图 1.1 – 选择 Visual Studio 安装程序版本
-
一旦 修改 模态窗口打开,在顶部栏中,确保你处于 工作负载 部分。
-
然后,通过点击复选标记激活 使用 C++ 进行游戏开发 选项。
-
接下来,如果它已关闭,请从右侧边栏打开 安装详情 | 使用 C++ 进行游戏开发 | 可选。
-
选择以下字段,如图 图 1 .2 所示:
-
C++ 性能分析工具
-
可用的最新 Windows 11 SDK 版本
-
C++ AddressSanitizer
-
IDE 对 Unreal Engine 的支持(可选)
-
Unreal Engine 安装程序
-

图 1.2 – 工作负载部分
- 点击 下载时安装 按钮(或 下载全部,然后安装)以开始安装过程。
备注
Unreal Engine 的 IDE 支持 集成是 Visual Studio 2022 中引入的一个扩展,它为 Unreal Engine 的类、函数和属性添加了一些实用的功能,例如蓝图引用、蓝图资源和 CodeLens 提示。尽管不是强制性的,但我强烈推荐使用它,因为它将使你的开发者生活变得更加轻松!
完成下载和安装过程后,你将完全准备好开始使用 Unreal Engine 开发自己的 C++ 游戏。
现在你已经成功设置了系统,是时候熟悉 AI 环境中的关键术语了。这将为你提供一个坚实的基础,以便更有效地理解和导航 AI 世界。
介绍 AI
人工智能已成为各个行业的变革力量;在广义上,人工智能涉及在机器中模拟人类智能,这些机器被编程为像人类一样思考和(有时)学习。
因此,开发 AI 意味着研究使机器能够感知其环境、从数据中学习、推理并做出决策以实现既定目标的方法和软件。
人工智能包括各种子领域和应用,包括以下内容:
-
机器人技术:开发能够与物理世界交互的智能机器
-
自然语言处理:计算机理解和生成人类语言的能力
-
机器学习:使用算法和统计模型使计算机能够从数据中学习并做出预测或决策,而无需明确编程
-
深度学习:机器学习的一个分支,利用神经网络来模拟人类大脑中观察到的决策能力
-
计算机视觉:计算机理解和解释图像或视频中的视觉信息的能力
更重要的是,AI 在娱乐行业中取得了显著的进步,正在改变内容创作、消费和个性化的方式。以下是关于 AI 对娱乐行业影响的几个关键点:
-
生成式 AI:这种技术可以创建故事、剧本和图像等输出,并有可能彻底改变娱乐行业的内容创作。
-
个性化推荐:AI 赋能的工具正被用于帮助用户发现符合他们偏好的内容,通过提供个性化的建议。这些推荐基于用户行为、观看历史和其他数据,从而提升用户体验。
-
数据驱动洞察:通过分析用户行为,AI 使娱乐行业能够从诸如偏好和趋势等数据中获得宝贵的见解,并有助于公司更好地了解他们的受众,并在内容制作、分发和营销方面做出基于数据的决策。
然而,正如古老的谚语所说,“权力越大,责任越大。”AI 的采用带来了许多挑战和考虑。关于 AI 生成内容的知识产权和版权问题的讨论已经发生,并且仍在继续。此外,人们对 AI 对各个行业就业的影响越来越担忧,因为某些角色可能会因 AI 技术的进步而面临中断或转变。
利用 AI 的伦理是一个重要的主题,它深入探讨了与这些系统利用相关的道德考虑和影响。随着 AI 技术以越来越快的速度发展,它对社会、个人和环境的影响引发了深刻的伦理担忧。
虽然不可否认 AI 有潜力提高效率和生产力,但它也可能导致特定行业的就业岗位减少;因此,有些人认为实施减轻对工人和社会不利影响的措施是强制性的。
您刚刚接触到了一些关于人工智能(AI)的常见术语。现在,让我们将焦点转向理解人工智能在游戏开发领域的具体应用。在接下来的这一节中,我们将深入探讨其基础,为您提供对其原则和运作方式的基本理解。
理解游戏开发中的 AI
当应用于游戏开发时,AI 被用于创建能够执行任务而无需明确编程的智能系统。这些系统根据经验进行适应和改进其性能,从而提升整体的游戏体验。例如,游戏角色已经使用了多年的 AI,使它们能够表现出看似智能的行为。甚至四个标志性的吃豆人鬼魂也被编程为具有独特和不同的行为!
在游戏开发中,人工智能的应用远远超出了对非玩家角色(NPC)或敌人的控制。它包括一系列多样化的应用,这些应用正在革新游戏设计、开发和玩家体验。通过利用人工智能,游戏开发者可以引入创新和沉浸式的游戏元素,使玩家在游戏过程中着迷。
如果你选择了这本书,那么你很可能渴望掌握游戏人工智能编程的基本原理,并将这些知识应用到创造你的下一个大作中。作为一名人工智能程序员,你将拥有创造令人惊叹的对手、创建帮助玩家实现目标的 NPC,或者仅仅发明新的和吸引人的行为,使你的游戏达到前所未有的愉悦程度;这将是一项极具回报的挑战!
然而,重要的是要注意,人工智能视频游戏编程可能带来重大挑战,需要长时间工作,并可能导致压力。在开始这条职业道路之前,意识到这些潜在陷阱至关重要。为了避免这样的挫折,必须对人工智能的工作方式有一个坚实的理解,使玩家能够享受到无缝和愉快的游戏体验。更重要的是,理解这个主题还涉及解决可能出现的计算机问题并有效地解决它们。请放心,这些问题迟早会浮出水面!
在接下来的部分,你将得到对游戏中使用的主要人工智能技术的温和介绍,以及使它们与众不同的独特特征。
解释视频游戏中的 AI 技术
人工智能在提升游戏体验方面发挥着关键作用,使游戏更加沉浸和刺激。因此,对人工智能发展背后的基本原理及其工作方式有一个全面的理解至关重要。这种知识将赋予游戏开发者有效利用人工智能潜力的能力,创造出丰富和吸引人的游戏玩法,使玩家保持参与和着迷。
想想《刺客信条》系列,它以其开放世界的游戏玩法而闻名,其中使用了复杂的 AI 行为来控制 NPC。在更高级的水平上,像《反恐精英》这样的游戏引入了由 AI 控制的玩家角色——称为机器人(bots)——可以创建和管理来代替真实玩家。
最后,人工智能在游戏开发中的未来充满了令人兴奋的可能性和创新,因为人工智能正在被用来创造动态和自适应的游戏叙事。通过观察玩家的行为和偏好,人工智能算法可以构建独特的叙事分支、挑战和奖励,这些内容将针对每个玩家量身定制。
在本节中,我将简要概述在游戏中常用的一些 AI 技术。在本书中,你将有机会探索这些技术,并了解它们在 Unreal Engine 中的应用。对于那些本书中不会涉及的技术,你将有很多机会自己探索和深入研究。游戏中的 AI 世界广阔且不断演变,为实验和创新提供了无限的可能性。所以,如果这里没有涵盖某个特定技术,请不要气馁 – 探索之旅仍在继续,有无数资源可以帮助你开启 AI 游戏开发的全新视野。
寻路
寻路对于游戏环境中的高效导航至关重要,它指的是在模拟从一个点到另一个点的移动过程中确定最佳路径的过程。它可以由自主代理,如 NPC 或对手使用,但在点选游戏中也很有用,其中你的角色需要到达特定位置。寻路涉及在避开障碍物的同时找到从一个位置到另一个位置的最佳路径;在这些情况下,通常使用 A 算法等算法。NPC 可以使用这种技术来规划他们的移动,无论是为了避免敌方单位、找到捷径还是跟随航点。
在游戏开发中最常见的寻路技术是通过使用 导航网格 – 或 nav mesh,这是一种表示关卡可通行表面的数据结构。图 1.3 展示了 AI 在 nav mesh 中的移动示例:

图 1.3 – AI 在导航网格中的移动
基于规则
基于规则的系统指的是一种基于一组预定义规则运作的 AI。这些规则由人类编写,并决定了系统的行为和决策;这意味着根据某种输入遵循规则以产生预定的结果。简单来说,这些规则通常被称为 if 语句,因为它们通常遵循 如果某事为真,则执行另一件事 的结构。尽管有限,但这些系统相对容易实现和管理,因为规则中编码的知识是模块化的,规则可以按任何顺序编写。这为编码和修改系统提供了很大的灵活性。
有限状态机
有限状态机(FSM)是 AI 开发中常用的一种技术,它涉及将对手或 NPC 的行为分解为不同的状态,其中每个状态代表特定的行为或动作。当满足某些条件或事件时,状态之间的转换被触发。例如,哨兵角色可能具有巡逻、警报或追逐等状态,当角色发出噪音或被发现时(因为他们处于视线范围内),就会发生转换。FSMs 提供了一种清晰和有序的方式来控制 NPC 的行为,尤其是在具有预定义动作序列的游戏中。
图 1.4展示了简单有限状态机(FSM)的示例,包括状态和条件:

图 1.4 – 有限状态机
行为树
行为树是用于控制 AI 行为的分层结构。它由代表特定动作或条件的节点组成。树结构允许根据条件本身进行动作排序和决策。系统将从根节点遍历到叶节点,在途中执行动作或评估条件。行为树提供了一种灵活和模块化的 NPC 行为方法,允许进行复杂和动态的决策。行为树可以包括选择器、序列、条件或动作节点。图 1.5展示了行为树,其中选择器决定将执行树的哪个部分,序列节点将按照预定义的顺序执行一系列任务。

图 1.5 – 行为树
如果你对这些术语不熟悉,不要担心!我将在第七章,“介绍行为树”中解释它们。
机器学习 AI
机器学习涉及使用数据和算法训练 AI 模型,以使 NPC 能够随着时间的推移学习和改进其行为。这种技术允许 NPC 根据之前游戏中的模式和经验来适应、做出决策和应对不可预测的情况。机器学习可以为 NPC 提供更动态、更真实和更有吸引力的交互,因为他们的行为通过迭代和学习玩家的行为而演变。
一个使用机器学习的游戏示例是 DeepMind 开发的AlphaGo(deepmind.google/technologies/alphago/),它通过使用机器学习技术计算概率并在游戏中做出战略决策,掌握了古老的中文游戏围棋。
强化学习
强化学习是一种机器学习系统,其中 NPC 通过试错来学习,根据其行为获得反馈或奖励。NPC 探索游戏环境,采取行动,并从后果中学习。强化学习使 NPC 通过最大化奖励和最小化惩罚来优化其行为。这种技术可以使 NPC 表现出适应性和战略性的决策,增强游戏的挑战性和沉浸感。强化学习通常在开发过程中使用,以便在游戏发布时创建一个功能性的系统。由于强化学习的性质,有时结果可能不符合预期,NPC 可能会表现出奇怪或古怪的行为。
生成式 AI
如前所述,生成式 AI 正在越来越多地应用于游戏开发中,提供了新的可能性,并正在改变游戏开发的各个方面。其中一些方面包括创建更真实的 NPC,其行为超越了固定模式,以及使决策系统对玩家更具适应性和吸引力。
尽管生成式 AI 在游戏开发中仍处于初期阶段,其全部潜力仍有待挖掘,但它已经显示出在改变游戏行业各个方面的有希望的能力。
摘要
在本章中,我们探讨了 AI 发展的基本原理,并看到了它在游戏行业中的应用。在下一章中,我将向您介绍虚幻引擎提供的令人难以置信的潜力以及如何利用其框架在游戏中创建智能和沉浸式的 AI。准备好进行一次激动人心的探索,让我们带着您最喜欢的游戏引擎一起深入 AI 游戏编程的迷人领域!
致谢
本章中的示例是在 Flaticon 的Basic Miscellany Lineal图标(www.flaticon.com/)的帮助下创建的。
第二章:介绍虚幻引擎 AI 系统
欢迎来到使用虚幻引擎进行 AI 编程的激动人心的世界!在本章中,我将向您介绍虚幻引擎的强大工具,这些工具将为您的虚拟世界带来生命和智能。通过探索虚幻引擎 AI 系统的各个方面,例如使用导航系统移动代理、通过行为树和黑板实现半智能行为,以及整合智能对象和大量实体等功能,您将全面了解这个强大框架提供的卓越能力。
掌握这些技能将使你跻身精英游戏程序员的行列——谁不想成为其中的一员呢?
到本章结束时,你将清晰地了解使用虚幻引擎 AI 系统可以完成什么,这将赋予你在项目中创建高级 AI 角色的能力。
在本章中,我们将涵盖以下主题:
-
了解虚幻引擎游戏框架
-
展示虚幻引擎 AI 系统
-
理解高级 AI 功能
技术要求
本章没有技术要求需要遵循。
了解虚幻引擎游戏框架
如您可能已经知道,虚幻引擎提供了一个现成的系统,称为游戏框架(GF),它包含了许多开发游戏所需的功能;这包括从拥有先进的输入系统到常见的入口点,这些入口点将允许您轻松访问数据或游戏状态。
以下是一些关键点,解释为什么 GF 如此重要:
-
结构和组织:GF 提供了一种结构化和组织化的游戏开发方法。它提供了一套系统、类和接口,它们协同工作以创建游戏的核心结构。
-
游戏逻辑和进度:该框架包含预定义的概念,有助于定义游戏的逻辑、进度和组织结构。
-
玩家和 AI 控制:GF 包括处理玩家输入和游戏世界中角色决策的系统。这包括玩家和 AI 控制,这对于创建交互性和沉浸式游戏体验至关重要。
-
实用函数:该框架提供了一组实用函数库,这些函数有助于处理常见的游戏操作和交互。这些函数可以简化游戏逻辑,并提高实现各种功能的效率。
-
灵活性和集成:GF 非常灵活,与虚幻引擎深度集成。它使用常见的游戏编程模式,并执行大量工作,使开发者能够专注于构建游戏,而不是创建自己的游戏框架。
作为个人反思,我发现多年来使用和理解 GF 显著提高了我对游戏编程最佳实践的总体理解。
显然,管理 AI 系统也是 GF 工作的一部分,因此,在接下来的小节中,我将为您提供一个关于 GF 中可用的关键 AI 功能的简要介绍,使您能够为使用它们做好准备。
演员(Actors)和组件(components)
我很确定您已经熟悉 Unreal Engine 中的演员和组件,但以防万一,让我们对这两个概念进行快速复习。
在 Unreal Engine 中,Actor类指的是可以放置在关卡中的任何实体,无论是摄像机、静态网格还是玩家的角色。演员可以经历变换,如平移、旋转和缩放。
演员充当着称为组件的专用类的容器,这些组件在控制移动、渲染等方面发挥着各种作用。在演员内部有三种类型的组件,它们各自发挥着不同的作用:
-
演员组件(Actor components): 这些主要包含演员的代码逻辑。它们处理各种功能和交互,但没有任何视觉表示。
-
场景组件(Scene components): 这些用于在演员内定位和定位其他组件。它们作为变换(如平移、旋转和缩放)的参考点,但没有任何可见的存在,主要用于组织目的。
-
原始组件(Primitive components): 这些负责在关卡内表示演员的视觉表现。它们可以被玩家或其他对象渲染和交互。
通过组合这些组件,游戏开发者可以创建具有功能和视觉方面的复杂和交互式演员。
主要 GF 元素(Main GF elements)
Unreal Engine 的 GF 是一个综合性的类集合,它作为构建游戏体验的模块化基础。在这个框架中,游戏开发者可以自由选择最适合游戏的特定元素,同时确保这些类被精心设计,可以无缝协作并相互增强。
在接下来的小节中,我们将介绍涉及的主要元素,以便您对事物的工作原理有一个清晰的了解。
游戏实例(GameInstance)
GameInstance类充当后台操作的管理器(即,它不是一个 Unreal Engine 演员);当引擎启动时创建一个实例,并且实例在引擎关闭前保持活动状态。其主要目的是跟踪数据和按需执行代码。
游戏实例提供了一个方便的中心枢纽,用于管理持久数据,例如保存游戏系统,并作为其他子系统的管理器,提供对游戏流程的便捷控制。
游戏模式(GameMode)
与 GameInstance 类不同,GameModeBase 或其直接后代 GameMode 实例仅在单个关卡中存在,并在关卡本身加载和世界构建后立即创建。这个类作为管理器来处理游戏会话,每个关卡都可以有自己的不同游戏模式逻辑。其主要作用是创建剩余的框架角色。
GameState 和 PlayerState
GameState 和 PlayerState 是专门的角色,在跟踪游戏状态和参与玩家的状态中扮演着关键角色。游戏状态负责存储和处理游戏中所有玩家相关的数据,而玩家状态则专注于特定玩家。鉴于它们固有的特性,这些类在多人游戏中找到其主要应用,无论这些游戏是在线上还是本地进行。
物品和角色
pawn 指的是所有可以在游戏世界中由玩家或 AI 实体控制的角色的基类。它作为实体的物理表示,处理其实体在游戏世界中的参与,包括碰撞和其他物理交互。它还通常用于确定实体的视觉外观。
Pawn 类通过更高级的 Character 类获得了额外的功能。角色类专门设计用来以垂直方式表示玩家,使他们能够在关卡内执行广泛的动作,如行走、奔跑、跳跃和游泳。顺便提一下,角色类还包含了用于多人处理的基本功能。
控制器
Controller 类负责管理决定玩家在游戏世界中行为的逻辑。两种广泛使用的控制器类是 PlayerController 和 AIController;第二个选项是我们热切期待的,原因很明显。
玩家控制器类作为一个管理实体,能够处理来自人类玩家的输入,使他们能够与游戏环境互动,并促进他们的整体游戏体验。另一方面,AI 控制器通过使用行为树、状态树、导航等来管理 AI 实体的行为。
玩家控制器和 AI 控制器类可以在运行时通过拥有角色或 pawn 来管理它们。
GameplayStatics
Unreal Engine 提供了一个非常有用的函数库,称为 GameplayStatics,它为游戏相关的任务提供了各种实用函数。这些函数可以用于在引擎内执行常见的游戏操作和交互。
这些函数的一些例子包括生成和销毁演员、检索有关游戏世界的信息、管理游戏标签、操作游戏实例等。这些函数可以从蓝图可视化脚本和 C++编程中访问和使用,可以简化游戏逻辑,并在运行时作为管理和操作游戏元素的有价值工具。
现在我已经分享了一些虚幻引擎 GF 知识,准备好进入本书中最精彩的部分(至少在这个书的背景下):AI 如何深入引擎的复杂运作,为你提供踏上创造自己游戏逻辑的奇妙旅程的知识!
展示虚幻引擎 AI 系统
考虑到您可利用的先前描述框架的强大功能,虚幻引擎提供全面且强大的 AI 系统可能不会令人惊讶。
在本节中,我们将展示可用于虚幻引擎 AI 程序员的工具全面系列,以及它们主要功能的简要描述。首先,让我们检查导航系统及其功能。
导航系统
虚幻引擎导航系统允许 AI 实体,称为代理,通过路径查找算法在关卡中移动。
导航系统将通过使用碰撞来从关卡中现有的几何形状创建一个导航网格。这个网格随后被分割成瓦片,这些瓦片进一步被分割成多边形,从而形成一个图。系统内的代理使用这个图来导航到它们的目标地点。多边形有一个指定的成本,这有助于代理根据最低的总成本确定最优化路径。此外,导航系统包括一系列可调整的组件和设置,可以修改导航网格生成过程。这些修改可能包括对多边形成本的调整,影响关卡内代理的导航行为。最后,系统允许连接导航网格中的非连续区域,例如平台和桥梁,从而在这些空间元素之间实现无缝导航。图 2.1 .1 展示了在 Epic Games Launcher 上免费提供的内容示例项目中的一个关卡;绿色区域是导航网格,左侧的角色是 AI 代理。

图 2.1 – 导航系统
本书第二部分将致力于理解虚幻引擎导航系统以及如何对其进行优化和调试。
行为树
在 Unreal Engine 中,行为树是创建游戏中 NPC AI 的有价值工具。行为树资产的主要功能是执行包含逻辑指令的分支。在 Unreal Engine 中,行为树以与蓝图非常相似的方式创建——这意味着您将使用某种类型的可视化脚本方法——在这里,将具有特定功能附加的节点序列添加并连接起来,形成一个行为树图。
图 2.2描绘了来自 Epic Games Launcher 上可用的Lyra 入门游戏项目的一部分行为树:

图 2.2 – 行为树示例
为了确定哪些分支应该执行,行为树依赖于另一个称为黑板的资产,它作为行为树本身的大脑。图 2.3显示了与之前行为树相对应的黑板:

图 2.3 – 黑板示例
行为树和黑板在 AI 游戏编程中非常重要;这就是为什么我将本书的第三部分专门用于这个主题。
质量实体
质量实体系统是一个以游戏玩法为中心的计算框架,为游戏中的行为元素提供了一种范式;它旨在处理大量实体并便于对骨骼和静态网格进行行为控制。
图 2.4 显示了来自城市样本项目(可在 Epic Games Launcher 上获取)的屏幕截图,该项目利用质量实体进行人群和交通控制:

图 2.4 – 质量实体应用
注意
在撰写本书时,质量实体仍被标记为实验性;因此,应谨慎使用,因为随着时间的推移,事物可能会出现故障或发生变化。
本书第四部分将介绍质量实体。
状态树
状态树是一种多功能的分层状态机,它将行为树的一些功能与状态机的一些功能相结合。使用这个系统(以树状结构组织)开发者将能够创建高度可执行的逻辑,同时保持结构化和适应性。图 2.5显示了上述城市****样本项目中的状态树:

图 2.5 – 状态树示例
我将在本书的第四部分中向您展示状态树是如何工作的。
智能对象
在 Unreal Engine 中,智能对象代表可以在级别中通过预留系统使用的一组活动,该系统确保一次只有一个 AI 代理可以使用智能对象,防止其他代理使用它,直到它再次可用。这些对象放置在级别上,可以被 AI 代理和玩家交互。智能对象包含进行这些交互所需的所有信息,并且可以在运行时使用专用过滤器查询。图 2.6显示了来自City Sample项目的智能对象资产:

图 2.6 – 智能对象示例
智能对象将在本书的第四部分中介绍。
环境查询系统
环境查询系统(EQS)从环境中收集数据,使 AI 能够通过各种测试来查询数据。这个过程导致选择一个与提出的问题最匹配的项目。
查询可以从行为树中调用,并用于根据执行测试的结果做出如何继续的决定。图 2.7展示了来自Lyra Starter Game项目的环境查询。

图 2.7 – 环境查询示例
注意
在撰写本书时,EQS 仍被标记为实验性,因此您应谨慎使用,因为随着开发进程的进行,事物可能会出错或改变。
我将在本书第四部分的末尾向您介绍 EQS,就在您对行为树的工作原理有了扎实理解之后。
AI 感知系统
AI 感知系统为 pawns 提供了从环境中接收数据的另一种方式,例如声音从哪里传来或 AI 是否看到了什么。它通过为 AI 提供感官数据来允许 AI 产生意识。该系统允许数据源创建刺激,以便数据监听器可以定期更新它们。该系统用于在游戏中启用 AI 感知,并可以响应一系列可定制的传感器。
图 2.8显示了一个带有刺激源组件的Lyra Starter Game中的角色:

图 2.8 – AI 感知示例
本书第四部分将介绍 AI 感知,以及行为树,使您的 AI 角色能够意识到其周围的环境。
AI 调试
没有一个严肃的框架会没有调试系统。调试是软件开发的一个基本方面,允许开发者识别和修复其代码中的错误或缺陷。它在确保框架的可靠性和功能方面发挥着关键作用。
正因如此,虚幻引擎提供了一套完整的工具和功能,以帮助开发者调试 AI,包括可视化调试工具、行为树可视化以及 AI 模拟模式。这些工具允许开发者实时检查和修改 AI 行为,识别诸如路径查找错误或决策异常等问题,并在游戏环境中对 AI 性能进行必要的调整。
图 2.9 显示了启用 AI 调试工具时的城市样本项目操作:

图 2.9 – 在关卡中启用的调试工具
在整本书中,我将展示根据你将使用的工具,调试游戏的不同技术。这些技术将赋予你高效追踪和解决游戏代码逻辑中错误、bug 和其他问题的能力。
在本节中,我向您展示了虚幻引擎 GF 中可用的主要 AI 功能;在下一节中,我将介绍一些在引擎中实现的新技术,这些技术涉及机器学习(ML)系统。
理解高级 AI 功能
现在你已经对虚幻引擎中可用的主要 AI 功能有了基本的了解,我想向你介绍一些最实验性和在一定程度上与游戏玩法无关的功能。
注意
请记住,这些功能目前仍处于实验或 beta 版本,因此需要谨慎处理。
学习代理
学习代理是一个专门设计的实验性插件,旨在使你能够使用机器学习训练 AI 角色。此插件提供了一个独特的机会,可以增强或甚至取代传统的游戏 AI 系统,如行为树或状态机。通过学习代理,你可以利用强化学习和模仿学习的方法来创建智能和自适应的 AI 角色。
此插件的主要目标是提供一套强大的解决方案,用于在虚幻引擎中实现角色决策。然而,其潜在应用范围远超游戏开发。例如,学习代理可以通过创建执行特定动作和场景的 AI 角色来自动化测试流程。这有助于识别潜在问题并确保游戏的健壮性。
尽管仍在开发中,这个插件已经相当令人印象深刻,随着时间的推移,你应当期待更多改进。
神经网络引擎
神经网络引擎(NNE)插件为开发者提供了一个 API,允许统一访问不同的神经网络推理引擎。这使得程序员可以根据需要无缝地在推理运行时之间切换,优化他们的用例并有效地针对特定平台。
如果你熟悉虚幻引擎的渲染硬件接口(RHI),你可以将 NNE 视为类似;它是一个主要目的是从不同的推理运行时抽象出来的工具。
ML Deformer
ML Deformer 是一个插件,它提供了一个 API 来访问不同的 ML 推理运行时实现,允许开发者近似复杂的变形模型并提高角色网格变形的质量。
ML Deformer 专门设计用于在实时游戏引擎中为角色创建精确的非线性变形系统。它利用一些内部虚幻引擎工具在 GPU 上执行计算,优化性能。一个名为ML Sample Project的示例项目可在虚幻引擎市场上找到,其结果相当惊人;图 2.10展示了我的同事 Giovanni Visai 从上述示例项目开始的照明测试:

图 2.10 – ML Deformer 插件在行动中
ML 布料模拟
ML 布料模拟系统为开发者提供了一个高保真和高性能的实时布料模拟解决方案。该系统在产生与预模拟数据相当质量的服装网格的同时,保持快速高效的性能,并使用最少的内存。
总之,将 ML 功能集成到虚幻引擎中为开发者打开了无限可能。通过利用这些技术,开发者将能够在他们的项目中创建更沉浸式、智能和动态的体验。
摘要
在本章中,我向你介绍了虚幻引擎 GF 中可用的关键功能。之后,我概述了主要的 AI 系统,从导航系统开始,逐步到行为树。此外,我还讨论了更高级的系统,如 Mass Entity 和状态树。最后,我向你介绍了实验性功能,如学习代理和 NNE 插件。
恭喜!你已经到达了本书第一部分的结尾。在接下来的章节中,准备好深入探索导航系统以及如何创建基本的 AI 角色,它们将在这个系统中导航。所以,卷起袖子,让我们开始创造一些惊人的东西吧!
第二部分:理解导航系统
在本书的第二部分,你将深入了解虚幻引擎导航系统的基本功能。从那里,你将创建自己的项目并学习如何实现一个由 AI 代理可导航的完整工作环境。
本部分包括以下章节:
-
第三章 ,展示虚幻引擎导航系统
-
第四章 ,设置导航网格
-
第五章 ,改进代理导航
-
第六章 ,优化导航系统
第三章:展示虚幻引擎导航系统
虚幻引擎导航系统是一个复杂的框架,它使受人工智能控制的实体能够在游戏关卡中无缝导航和交互。它提供了一套工具和算法,允许游戏开发者定义和创建路径、障碍物和移动行为。通过使用导航系统,您将能够模拟受人工智能控制的实体的真实移动和行为模式,增强虚拟环境的沉浸感和可信度。由于该系统集成了诸如路径查找算法、碰撞避免和动态障碍物处理等高级功能,因此理解其全部潜力对于有抱负的人工智能程序员来说是一项关键技能。
到你读完本章的时候,你将深刻理解这个特定部分的游戏框架是如何运作的。有了这些知识,你将完全准备好开始与系统本身进行积极的工作。
在本章中,我们将涵盖以下主题:
-
介绍人工智能移动
-
理解路径查找
-
使用项目模板测试导航系统
技术要求
本章没有技术要求需要遵循。
介绍人工智能移动
当涉及到在虚拟环境中移动人工智能实体时,我们面临着众多挑战,并且没有通用的解决方案。解决每个问题的方法取决于将要面对的每种独特特征,这取决于正在开发的游戏类型。例如,人工智能的目标是一个静止的物体——比如一个拾取物——还是它是一个不可预测移动的物体,比如玩家角色?此外,人工智能是否只需要在没有特定目的地的情况下四处游荡,或者它是否有一个预定义的模式——比如作为巡逻哨兵?
此外,作为一名开发者,你还需要考虑不同的地形、障碍物和危险区域等因素。在更容易和更危险——但更快捷——的路径之间做出选择,在运行时可能会产生重大影响。这些只是涉及在关卡内移动人工智能所涉及的一些考虑因素,随着你遇到不同的场景,你可能会面临不同的问题。理解和正确处理所有相关变量对于提供最佳玩家体验至关重要。
那么,在人工智能运动中,特别是路径查找中,涉及的主要实体有哪些?它们如何合作以使玩家的体验完美无瑕?我将在几秒钟内告诉你所有关于它的事情!
理解导航网格
在虚幻引擎中,导航系统基于导航网格——或nav mesh——通过将可导航空间划分为多边形来实现,这些多边形随后被划分为三角形以提高效率。每个三角形随后被视为到达特定位置的图的一个节点,当两个三角形相邻时,它们的相应节点相连。图 3 .1 描述了一个带有上述网格的游戏关卡,该网格由三角形划分:

图 3.1 – 导航网格示例
使用此图表,您可以应用任何类型的路径查找算法——例如A 星(A*),我将在本章后面解释——并且生成的过程将在这些三角形之间生成一条路径,AI 角色可以穿越。
幸运的是,除非您真正需要深入了解修改导航系统核心结构的复杂性,否则没有必要立即深入这种细节。了解生成的三角形集合形成一个连贯的图,该图是路径查找算法的基础,就足以充分利用导航系统。
要在虚幻引擎中生成导航网格,您只需在关卡中添加一个或多个导航网格边界体积演员,并更改其大小以适应您的需求。
注意
在虚幻引擎中,体积类指的是一种可以影响其作用区域内其他演员行为的特殊演员。体积用于定义各种效果,如照明,并可以修改玩家或其他对象与游戏世界的交互方式。虚幻引擎中常见的体积类型包括触发体积、光子重要性体积、后期处理体积,以及显然的导航网格边界体积。
图 3 .2 展示了一个添加到关卡中的导航网格边界体积演员;黄色线条标记了该体积本身。

图 3.2 – 导航网格边界体积
此演员相当简单,您唯一可以采取的操作是调整其扩展。从上一张图片中,您可能已经注意到一个由两个三角形组成的绿色网格;这个相当简单的形状是由另一个演员生成的:Recast Nav Mesh,通常在第一次将导航网格边界体积添加到关卡时自动生成。
此演员负责生成 AI 实体可使用的可通行区域,它将使用此区域进行自己的高效和准确的路径查找计算;通常,一旦您在关卡中添加导航网格边界体积,就会自动生成其实例。
应该注意的是,大多数可用的RecastNavMesh演员设置都可以在您的编辑器项目设置中使用预定义值设置——这可以通过文件菜单打开——通过选择引擎 - 导航网格部分,如图图 3 .3 所示:

图 3.3 – 导航网格设置部分
现在你已经了解了在虚幻引擎中创建导航网格的方法,重要的是要知道你可以通过使用可用的修改器系统来调整它,以增强其趣味性和现实感。
修改导航网格
导航系统由各种演员和组件组成,这些组件会改变导航网格的生成,例如穿越多边形的成本。这些调整会影响 AI 代理在你的级别中的移动方式。
导航修改器体积
最简单的一个是导航修改器体积演员,其任务是……好吧,就是修改导航网格!一旦你在你的级别中定位了这个体积,你将可以选择如何修改 AI 代理对其路径搜索的感知 – 你可以将其指定为不可通行的地形、困难地形,甚至障碍物。图 3.4 显示了设置了三种不同成本设置的三个修改器体积:

图 3.4 – 三个导航修改器在行动
如果你正在考虑创建自己的导航网格修改器,那么,这是可能的;你只需要扩展UNavArea类,设置其参数,然后你就可以出发了!
导航查询过滤器
作为调整 AI 代理生成路径时导航系统行为的额外方法,你可以利用导航查询过滤器。这种方法包括与一个或多个特定区域相关的信息,并提供覆盖分配给这些区域的成本值的灵活性。通过实现查询过滤器,你将能够根据 AI 代理在游戏世界中穿越各个区域时的导航模式进行定制,这将让你能够微调和优化 AI 实体的移动。
导航链接代理
当你开始设计可通行地形时,你很可能会引入缺口或不同海拔的区域;我想你可能需要你的 AI 角色从一个侧面跳到另一个侧面。这正是导航链接代理被创建的原因;这个演员将连接导航网格中缺少直接导航路径的两个区域。图 3.5 显示了这样一个链接,连接了两个不同高度的区域:

图 3.5 – 导航链接代理
在你的工具箱中有这样一款有用的工具,你将能够让你的角色跳跃、坠落,并执行令人叹为观止的杂技,无缝地从一种违反重力的动作过渡到另一种。
运行时导航网格生成
默认情况下,虚幻引擎设置为静态生成导航网格 – 这意味着网格是在离线生成的,无法在运行时更改。然而,如果你需要一个更灵活的生成导航网格的方式,你可以选择动态网格生成系统,这将允许你在不同情况下更新网格 – 例如,通过添加移动实体。可以通过打开项目设置,然后转到引擎 - 导航网格部分,并在运行时类别中选择运行时生成选项来为整个项目启用运行时生成:

图 3.6 – 启用运行时生成
或者,你也可以只为单个关卡启用它,通过在Recast Nav 网格演员中更改运行时生成属性。
调用者
导航调用者是一个演员组件,它将在运行时为人工智能代理生成导航网格。它用于消除预先计算网格的需求,并允许在游戏世界中动态导航。这个特性在拥有广阔地形 – 例如,一个开放世界 – 需要大量时间来生成导航网格时特别有用;通过调用者,系统将在运行时自行生成网格,但仅限于演员周围的一小片区域。
现在你已经了解了导航网格的创建和调整方法,那么让我们来探索谁或什么会通过它移动。
人工智能代理
在虚幻引擎的导航系统中,代理是一个能够通过导航网格在游戏世界中导航的人工智能角色或实体。代理将使用导航网格数据来计算路径、避开障碍物,并在环境中智能地移动。一个关卡中的每个代理通常代表一种特定的角色类型,例如玩家角色 – 例如,在一个点击式游戏中 – 敌人 AI,或任何需要在游戏世界中移动的实体。
要在关卡内移动一个代理,你通常会使用Pawn或Character演员。
将代理移动到位置或演员的最简单方法之一是使用简单移动到位置或简单移动到演员蓝图节点。

图 3.7 – 简单移动节点
或者,你可以使用相应的 C++ 方法,分别称为SimpleMoveToLocation()和SimpleMovetoActor()。
显然,在虚幻引擎中,你可以创建比仅仅将代理移动到单个点更复杂的动作;这一点我们将在从第八章,“设置行为树”开始时进行探讨。
避免碰撞
基本路径查找算法对于寻找绕过静止物体的路线是有效的;然而,当涉及到移动障碍物——如玩家角色或其他 AI 代理——时,需要一个更合适的系统。这就是为什么虚幻引擎提供了两个避免系统,以防止移动实体之间的碰撞:
-
互逆速度障碍(RVO)系统计算每个代理的速度向量,考虑附近的代理,并假设它们在计算的每个时间步中以恒定速度移动。选择的最优速度向量是与代理期望速度方向上最接近的代理速度。此系统包含在角色移动组件中。RVO 不使用导航网格进行避免,因此它可以与导航系统分开用于任何角色。
-
绕路人群管理器计算一个倾向于代理方向的粗略速度样本,与标准 RVO 方法相比,在避免质量上有了显著提升。任何通过ADetourCrowdAIController类扩展Pawn类的演员都可以使用此系统。
你将在第五章中了解到避免,提高 代理导航。
在本节中,你已经了解了路径查找中涉及的主要元素以及它们与环境之间的交互。
在下一节中,我将提供有关路径查找工作原理的更多详细信息。
理解路径查找
如你所知,虚幻引擎使用路径查找在级别中移动代理;在本节中,我将深入探讨内部的工作原理。虚幻引擎利用了 A*算法的通用版本,这是计算机科学中广泛使用的图遍历和路径查找算法。以其完备性、最优性和效率而闻名,其主要目标是确定加权图中指定源节点和指定目标节点之间的最短路径。
此图是级别的基于节点的表示,其中节点代表相互连接并可通行的区域,并且包含有关相邻节点和到达它们的通行成本的信息。
A*使用启发式函数来估计从每个节点到目标位置的成本;这种试错系统有助于引导搜索走向最有希望的路径,提高效率。
在路径查找过程中,算法维护两个列表:其中一个包含尚未评估的节点,而另一个包含已经评估过的节点。算法通过考虑其成本以及从上一个节点到达它的成本来评估每个节点。它从开放列表中选择总成本最低的节点进行进一步评估。一旦到达目标节点,算法通过从目标节点回溯到起始节点,沿着节点之间的连接重建路径。
虚幻引擎的版本通常包括后平滑操作,以提高生成路径的质量。后平滑调整路径,使其更加自然,并更有效地避开障碍。
如果你想要深入了解导航网格生成的工作原理以及路径搜索是如何计算的,我的建议是查看 GitHub 上可用的虚幻引擎源代码(github.com/EpicGames/UnrealEngine);特别是,你应该查找位于Engine/Source/Runtime文件夹中的NavigationSystem和NavMesh模块。
注意
要访问虚幻引擎源代码,你需要成为 Epic Games GitHub 组织的一部分。订阅是免费的,没有理由你不应该参与其中。
例如,通过检查虚幻引擎源代码中的DetourNavMeshQuery类,你可以了解 A* 寻路算法的使用方式以及成本是如何计算的,或者如何在路径上找到一个瓦片。
看来你已经对在虚幻引擎中如何处理路径搜索有了一些了解,所以我认为是时候通过探索一个真实案例场景来深入一个实际示例了;我们将从创建一个基于模板的项目开始。
使用项目模板测试导航系统
在本节中,我们将查看一个使用虚幻引擎导航系统的项目,并使用项目模板来完成它——使用预制的项目,如模板,为你提供了一个在特定主题上获得实践经验的有价值的机会,节省了你从头开始构建项目所需的时间和精力。
一旦项目创建完成,我们将快速分析导航网格生成系统以及模板如何处理运行时的人物移动。
在这里,你将开始通过使用俯视项目,这是在Unreal 项目浏览器 GAMES类别中可用的模板之一,来创建一个游戏原型。
设置项目
一旦你准备好了,你可以启动 Epic Games Launcher 并按照以下步骤操作:
-
从可用的模板中选择GAMES | Top Down。
-
根据个人喜好将项目设置为蓝图或C++。
-
为你的项目命名——任何名字都可以。
-
将其他设置保留为默认值。
-
点击创建按钮。

图 3.8 – 项目设置
一旦项目创建并打开,你就可以开始分析它了。
分析导航网格
我们现在将简要浏览生成的关卡以及有助于导航网格生成的演员。
在大纲视图中,你会注意到有一个名为导航的文件夹,包括三个演员:
-
导航网格 边界体积
-
重构 导航网格
-
导航 链接代理
让我们详细分析每个元素。
导航网格边界体积
如您从本章前面的部分中已经了解的那样,NavMeshBoundsVolume 实体负责定义导航网格将被计算的区域。通过选择它,您将注意到一个黄色边缘的区域被显示出来,包围了所有游戏级别,如图 3.9* 所示:

图 3.9 – 导航网格边界体积
您无法对此实体做太多;只需调整其大小,导航网格将重新计算。
重新构建导航网格
RecastNavMesh 实体将负责导航网格的生成;默认情况下,它在 Unreal Engine 编辑器中没有可见的表示。但是,如果您按键盘上的 P 键,导航网格将在编辑器界面中变得可见并可访问。图 3.10* 显示了在此实体可见后级别的样子:

图 3.10 – 重新构建导航网格
在第六章,“优化导航系统”中,我将向您展示一些使用此实体优化网格生成的技术。现在,您只需检查详细信息窗口中的显示类别;在这里,您将能够访问一系列可视化工具,这些工具将在本书后面的内容中非常有用。例如,在图 3.11* 中,我展示了级别的各个部分——称为瓦片——以及它们的标签和生成的多边形:

图 3.11 – Recast Nav Mesh 实体的某些显示设置
您可能已经注意到,级别上的蓝色立方体实体以任何方式都没有影响导航网格。这是因为它们已被配置为不会影响导航;由于它们是可移动对象,我们不希望它们在周围创建不可导航区域。
作为简单的测试,在详细信息面板中,您可以查找始终影响导航属性并启用它;导航网格将立即重新计算,立方体将在其中挖一个洞,如图 3.12* 所示:

图 3.12 – 蓝色立方体实体雕刻导航网格
上述属性表示该对象将是导航区域中的障碍物,并且生成的洞将是网格中的不可导航区域。
请注意,让这样的可移动对象雕刻导航网格可能会产生不期望的结果;默认情况下,导航网格是静态的,无法在运行时进行更改。这意味着即使对象被移动,不可导航区域也将保持固定,尽管看不见,但会阻碍玩家角色进入或穿越它。
导航链接代理
在此示例中,最后一个导航网格实体是一个 NavLinkProxy 实体,在我们的级别中,它将允许玩家角色从平台上跳下来。

图 3.13 – 级别中的导航链接代理
现在您已经理解了导航网格的结构,让我们来检查角色控制器,以了解玩家角色是如何操纵的。
分析角色控制器
根据您在创建项目时的选择——蓝图或 C++——您将拥有两种略有不同的角色控制器版本。
蓝图角色控制器
移动控制器角色的代码相当简单,可以在BP_TopDownController中找到——位于Content/TopDown/Blueprints文件夹中。
一旦打开蓝图类,在函数选项卡中找到MoveTo函数并打开它;您将找到用于使玩家角色通过导航网格移动的简单移动到位置节点。

图 3.14 – MoveTo 函数
这就是您需要用来将玩家角色移动到级别中预定义位置的所有内容。
C++角色控制器
同样的逻辑可以在 C++生成的项目中找到;只需打开为玩家控制器生成的.cpp类,查找OnSetDestinationReleased()方法;您将找到以下代码行:
UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, CachedDestination);
这个辅助函数将启动您的代理的导航过程。
测试项目
现在您已经了解了参与此项目的所有演员,您可以简单地按播放按钮开始测试导航系统的工作方式。特别是,您会注意到角色将通过选择最短路径移动到目标点。
此外,一旦在左侧的升高平台上,它将能够跳下,这要归功于在该位置添加的导航链接代理。
摘要
在本章中,我向您介绍了虚幻引擎导航系统的关键组件。我们首先讨论了生成导航网格的过程,这对于 AI 代理在环境中导航至关重要。然后,我简要解释了路径查找算法的工作原理,使 AI 角色能够高效地找到路径。最后,我强调了使用自上而下项目模板的好处,该模板有效地利用了导航系统。
到现在为止,您应该已经很好地理解了虚幻引擎 AI 导航系统提供的功能。我想您一定渴望开始编码,这正是我们在下一章将要做的!
第四章:设置导航网格
我相当确信,到现在为止,你已经意识到在 AI 游戏开发中,一个关键要素是建立一个完全功能化的导航网格。这个实体作为基石,用于精确有效地引导 AI 控制的代理穿越游戏世界。
在本章中,我们将开始将这种理解付诸实践,通过启动一个新项目。到本章结束时,你将在自己的项目中开发和完善导航系统的实际经验。
这项知识将成为塑造你作为 AI 程序员道路上的一个关键里程碑,推动你朝着创造颠覆游戏行业的突破性游戏迈进!
在本章中,我们将涵盖以下主题:
-
介绍 Unreal 敏捷竞技场
-
创建 AI 代理
-
设置基本关卡
-
添加导航修改器
-
与导航链接代理一起工作
技术要求
要跟随本章内容,你应该已经按照第一章中解释的,设置了 Visual Studio(或 JetBrains Rider)以及所有 Unreal 依赖项,入门 AI 游戏开发。
你将使用本书配套仓库中可用的某些入门内容,该仓库的网址为github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。通过此链接,找到本章的相应部分,并下载以下.zip文件:Unreal 敏捷竞技场 – 入门内容。
如果你在这章中迷路了,在仓库中,你还可以在这里找到最新的项目文件:Unreal 敏捷竞技场 – 第四章 结束。此外,为了完全理解本章内容,在我引导你了解设置 AI 代理的关键特征的同时,你需要具备一些关于 Blueprint 视觉脚本的基本知识。
介绍 Unreal 敏捷竞技场
要启动一个成功的项目,拥有坚实的基础至关重要。想象一下,你开始阅读一本短篇小说,开头是这样的:
在一个隐藏在不起眼的建筑下的秘密地下实验室中,一位名叫马克斯博士的古怪科学家在他的最新发明上辛勤工作:AI 木偶。这些木偶并非普通的木偶;它们配备了先进的 AI 技术,使它们能够以最 意想不到的方式与环境互动。*
马克斯博士以其古怪个性和疯狂的想法而闻名。他相信这些木偶是理解人类行为和改善社会互动的关键。在他的忠实伙伴,维多利亚教授的陪伴下,他开始了一系列 令人捧腹的实验。
嗯,看起来你已经找到了创建视频游戏行业下一个大热门的完美起点,你的任务是制作出令人惊叹的 AI 智能体,它们将通过与虚拟环境的无缝交互震撼游戏世界!
解释项目概述
你将要工作的项目将是一组健身房级别,在那里你将为你的 AI 智能体创建不同的行为。
注意
在游戏开发中,健身房通常指的是一个开发者可以测试和训练他们的 AI 算法和模型的训练环境。这个术语在强化学习的背景下也经常被使用,在那里 AI 智能体通过在模拟环境中试错来学习玩游戏。为了这本书的目的,我们将坚持第一种定义。
在我看来,在健身房工作是一个游戏原型阶段中最有趣的部分之一,因为你不需要过多担心事物是否完美运行;你可以尝试各种各样的事情,最终你很可能会想出富有创意和非常规的解决方案!
因此,首先,我提供了一个项目模板——称为 Unreal Agility Arena——你将在本书的其余部分使用它。在下载并在虚幻引擎中打开它之后,我们的主要焦点将是创建自包含的级别来实验到目前为止获得的知识。这将涉及处理小任务并有效地解决它们。
一旦你达到第七章,“介绍行为树”,你将准备好迎接更具挑战性的内容,事情会变得有点困难,但我也向你保证——这将更加吸引人和有趣!
注意
由于这本书是关于 AI 游戏编程而不是游戏设计,因此平衡游戏机制不会是游戏玩法的主要焦点。相反,重点将在于确保事物有效运行。
第一步是打开项目,深入挖掘我为你准备的丰富资源。那么,让我们开始吧!
开始项目
虽然这个项目将主要关注健身房级别,但我们追求的是视觉上吸引人的,或者正如我们游戏开发者喜欢说的——诱人的外观和感觉。我明白你们中的许多人可能没有 3D 建模的背景(我也没有!)。这就是为什么我们将使用凯·卢斯伯格(kaylousberg.com/)提供的某些出色的资产,这些资产可用于个人和商业用途。

图 4.1 – 凯·卢斯伯格的网站
注意
在这个项目中,我主要使用了Prototypes Bits Extra包,一旦购买了许可证,这个包就可以免费分发。如果你打算将模型用于商业项目,请考虑也从凯的网站上购买许可证。
在本章开头提供的链接中下载文件后,解压它,通过双击 UnrealAgilityArena.uproject 文件来打开项目。
一旦项目打开,请检查 Content 文件夹中的内容。您将看到以下子文件夹:
-
一个包含我用 Unreal Engine 建模工具 创建的一些额外模型的 _GENERATED 文件夹
-
一个包含 Kay 所有模型的 KayKit 文件夹
-
一个包含一些预制关卡,这些关卡已准备好使用的 Maps 文件夹
-
一个包含项目资产所需的某些材质的 Materials 文件夹
-
一个包含项目材料使用的某些纹理的 Textures 文件夹
-
一个包含我们将要使用的某些 Niagara 效果的 Vfx 文件夹
在我们可利用的所有资源下,我们准备开始为项目创建元素,首先是一个将在我们的关卡中导航的代理。
创建 AI 代理
作为测试路径查找系统的第一步,我们将创建一个代理,其唯一目标是到达关卡内的一个目标演员;它不会有什么特别之处,只是一个能够到达关卡内目标点的演员。
让我们从在 Content Drawer 内创建一个新的文件夹并命名为 Blueprints 开始。双击新创建的文件夹以打开它并执行以下步骤:
-
右键单击 Content Drawer,然后从打开的菜单中选择 Blueprint Class 。
-
从将打开的 Pick Parent Class 窗口中,选择 Character ,如图 Figure 4 .2 所示:

Figure 4.2 – Character creation
-
将新创建的资产命名为 BP_NavMeshAgent 。
-
双击它以打开它。
如您可能已经知道,Character 类指的是一种特定的 pawn 类型,它被设计用来以垂直方式表示玩家或 AI 代理,允许他们在游戏世界中行走、跳跃、飞行和游泳。
我们将首先给它一个视觉表示,并设置主要值。
创建代理
在 Blueprint 类打开且选择 Viewport 选项卡的情况下,找到 Details 面板并按照以下步骤操作:
-
打开 Skeletal Mesh Asset 属性下拉菜单并选择 Dummy 资产。
-
打开 Anim Class 属性下拉菜单并选择 ABP_Dummy 资产。
-
在 Character Movement (Rotation Settings) 类别中,找到 Max Walk Speed 属性并将其值设置为 500.0 cm/s 。
-
在 Character Movement (Rotation Settings) 类别中,找到 Rotation Rate 属性并将 Z 值设置为 640.0° 。
-
在同一类别中,勾选 Orient Rotation to Movement 复选框。
-
在 Shape 类别中,将 Capsule Half Height 属性设置为 120.0,将 Capsule Radius 属性设置为 50.0 。
-
在 Pawn 类别中,取消勾选 Use Controller Rotation Yaw 属性。
之前的步骤相当直接,它们将仅设置角色网格,分配一个动画蓝图(该蓝图已经为您创建)以及,最后,设置胶囊组件大小和移动旋转设置。代理的最终结果显示在图 4 .3 中:

图 4.3 – 代理蓝图
代理几乎准备好了,我们只需要添加一些简单的代码逻辑,使其能够到达关卡中的目标。
添加导航逻辑
打开事件图选项卡并按照以下步骤操作:
-
在图中添加一个获取 AIController节点,并将其输入的受控演员引脚连接到一个指向****自我节点。
-
从Get AIController节点的输出返回值引脚点击并拖动,并将一个移动到演员节点添加到图中。
-
将Event BeginPlay节点的输出执行引脚连接到移动到演员节点的输入执行引脚。
-
从Move to Actor的输入目标引脚,点击并拖动,并在释放后选择提升为变量选项;为新创建的变量命名为TargetActor。
-
在我的蓝图选项卡中,选择TargetActor变量,并在其详细面板中检查实例****可编辑属性。
这相当简单;在游戏开始时,代理将尝试导航到目标演员;将变量设置为实例可编辑将使其在关卡中可见,以便拾取代理目的地。您刚刚创建的视觉脚本代码显示在图 4 .4 中:

图 4.4 – 蓝图图
我们可能不是那么聪明的代理现在已经完全装备好,可以导航到指定的目标位置。在接下来的部分中,我们将为代理创建一个合适的环境,使其可以自由移动和探索。
设置基本关卡
我们现在将创建我们的第一个关卡并开始测试代理的路径查找系统。项目模板包含我为您创建的一些预制件,特别是用于快速原型设计地图的打包关卡演员集和一个关卡实例用于设置照明系统。您完全可以创建自己的游戏关卡,但在这一阶段,我的建议是跟随我将要进行的操作。
注意
在 Unreal Engine 中,关卡实例允许您创建关卡或关卡部分的可重用实例;这样,您可以有效地复制和重用关卡设计的一部分,而无需从头开始重新创建。打包关卡演员是一种优化的关卡实例类型,只能包含静态网格。关卡实例和打包关卡演员在您需要在关卡中重复使用复杂或重复元素时特别有用。
创建关卡
要创建我们的第一个健身房,请按照以下步骤操作:
-
从主菜单中选择文件 | 新建级别。
-
导航到Maps/LevelInstances文件夹,并将LI_Lighting的实例拖动到您的级别中;将其变换位置设置为(0, 0, 0)。
-
导航到Maps/PackedLevelActors文件夹,并将PLA_Lab_01的实例拖动到你的级别中;将其变换位置设置为(0, 0, 0)。
-
从KayKit/PrototypeBits/Models文件夹中,将一些障碍物拖动到级别中,以使您的代理更加有趣;我的级别如图图 4 .5 所示:

图 4.5 – 基础级别
- 将级别保存在Maps文件夹中,并命名为Gym_NavMesh_01。
添加导航网格
现在,您可以为级别添加导航网格:
-
从工具栏中的快速添加到项目按钮,选择Nav Mesh Bounds Volume并将实例拖动到级别中;将自动添加Recast Nav Mesh演员以及体积。
-
将体积的位置设置为(0, 0, 0)和缩放设置为(20, 20, 1)。
-
在级别内点击并按下键盘上的P键以显示生成的导航网格,如图图 4 .6 所示:

图 4.6 – 导航网格
如您所见,您添加的障碍物将雕刻导航网格,并为即将添加的代理增添更多趣味。
添加代理
作为最后一步,我们需要添加代理和要到达的目标点。所以,让我们先做这个:
-
从Vfx文件夹中,拖动NS_Target Niagara 系统的实例并将其放置在导航网格上的任何位置。
-
从Blueprints文件夹中,拖动BP_NavMeshAgent Blueprint 的实例并将其放置在级别中蓝色的瓷砖上;位置值应大约为(-1650, 30, 180)。
-
在选择代理后,在细节面板中找到目标演员属性,并从下拉菜单中将其值设置为NS_Target,这是之前添加的 Niagara 系统。最终级别应类似于图 4 .7:

图 4.7 – 最终级别
现在级别已经完成,我们可以开始测试它。
测试健身房
现在健身房已经完成,您可以从测试代理开始,看看它在级别中的行为如何。
您可以直接在工具栏中点击播放按钮,或者使用模拟按钮,它不会进入播放模式,但会显示级别如何工作的模拟。在这个上下文中,我个人更喜欢第二种选择,因为它会保持导航网格可见。
一旦开始模拟,您将看到代理通过最短路径到达目标演员。您可以自由地实验障碍物,以检查代理在不同场景下的行为。
在本节中,你已经开始亲手实验路径查找系统的运作方式。你通过创建一个智能体和一个简单的健身房环境来实现这一点,以便智能体进行导航。
在下一节中,你将向导航网格添加修饰符,以给你的智能体增加一些挑战。
添加导航修饰符
在本节中,我们将创建另一个健身房,以便我们可以测试导航网格修饰符——这些修饰符可以用来定义进入区域成本不同于常规导航网格的区域。
我们将首先创建关卡,然后添加修饰符。
创建关卡
要创建我们的第二个健身房,请按照以下步骤操作:
-
从主菜单选择 文件 | 新建关卡 。
-
导航到 Maps/LevelInstances 文件夹,并将 LI_Lighting 实例拖动到你的关卡中;将它的变换 位置 设置为 (0, 0, 0) 。
-
导航到 Maps/PackedLevelActors 文件夹,并将 PLA_Lab_04 实例拖动到你的关卡中;将它的变换 位置 设置为 (0, 0, 0) 。
-
将关卡保存在 Maps 文件夹中,并命名为 Gym_NavMesh_02 。
现在,通过以下步骤重复你为之前健身房所做的相同步骤:
-
添加 Nav Mesh Bounds Volume 行为,并设置其边界以覆盖整个可通行区域。
-
在关卡蓝色瓷砖的对侧添加 NS_Target Niagara 系统。
-
在蓝色瓷砖上添加 BP_NavMeshAgent 蓝图,并将 目标行为者 属性值设置为 NS_Target 。现在关卡应该看起来像 图 4.8 :

图 4.8 – 更新的导航网格
到目前为止,一切与之前的健身房相当相似;我们现在将插入一个修饰符,看看它的表现如何。
添加修饰符
我们现在准备通过添加修饰符来改变路径查找系统的行为。为此,请按照以下步骤操作:
-
从工具栏中的 快速添加到项目 按钮选择 Nav Modifier Volume 并将实例拖动到关卡中。
-
将体积的 位置 设置为 (0, 0, 0) 和 缩放 设置为 (5, 20, 1) 。
你会注意到导航网格已经被修改,并且在放置棕色(让我们说泥泞的)瓷砖的地方被雕刻出来。

图 4.9 – 添加了修饰符的关卡
如果你现在测试关卡,你会看到智能体向目标点移动,但在泥泞地形旁边停止;这是因为修饰符体积改变了导航网格,现在智能体无法到达其目标。
通过选择 Nav Modifier Volume 行为并在 细节 面板中检查 区域类 属性,你会注意到它已被设置为等于 NavArea_Null 的值。此值将该区域应用于无限成本,使其对智能体来说无法穿越。

图 4.10 – 区域类属性
如果您尝试将此值设置为NavArea_Default,您会注意到导航网格的行为将像没有修饰符一样,这正是此值所做的那样;遍历此部分的成本与常规网格的成本相同。通过现在测试健身房,您会注意到智能体会穿过泥泞地形并到达目标点。
如果您需要检查导航网格中每个多边形的遍历成本,可以通过选择Recast 导航网格演员,并在详细信息面板中检查绘制多边形成本属性来完成。图 4.11 显示了在关卡中的成本可视化:

图 4.11 – 导航网格遍历成本
让我们使关卡更有趣,并添加一些自定义修饰体积。
改进关卡
现在让我们为我们的智能体创建一个安全路径,以免它在泥泞区域弄脏脚。我们将通过使用一些额外的模型来创建一条通道。为此,请按照以下步骤操作:
-
在KayKit/PrototypeBits/Models文件夹中,找到Pallet_Large_Pallet_Large模型。
-
将此模型的三个实例拖放到关卡中,以在泥泞区域创建通道,如图 4.12 所示:

图 4.12 – 桥梁
是时候为桥梁添加另一个修饰符了:
-
修改关卡中之前添加的导航修饰体积演员的大小,使其覆盖河流左侧的泥泞通道。
-
在关卡中添加另一个导航修饰体积演员,使其覆盖河流右侧的泥泞通道。
-
在关卡中添加一个第三个导航修饰体积演员,并将其放置以创建覆盖整个桥梁的区域。
在 Unreal Engine 中,导航修饰体积是一个用于更改导航网格生成方式的演员,可以添加到关卡中以指定导航网格本身的某些区域。
您现在应该有三个修饰符创建的完全无法通行的区域,如图 4.13 所示:

图 4.13 – 带有修饰符的桥梁
现在我们已经添加了一条安全通道,我们将创建自定义修饰符以进行不同的设置实验。
创建自定义修饰符
修饰符是扩展NavArea类的蓝图;这意味着您可以通过子类化此类型并设置自己的参数来创建自己的修饰符。
我们现在将创建一个用于泥泞表面的和一个用于桥梁的修饰符。让我们从第一个开始:
-
打开内容抽屉,在蓝图文件夹中,右键单击并选择蓝图类。
-
从所有类下拉列表中选择NavArea类型。
-
将新创建的资产命名为NavArea_Mud并双击它以打开。
-
按以下方式更改其值:
-
将默认成本更改为10.0。
-
将固定区域进入成本更改为2.0。
-
将绘制颜色更改为你选择的易于识别的颜色。
-

图 4.14 – 污泥导航区域的成本设置
虽然绘制颜色值几乎不言自明——它将被用来为体积覆盖的导航网格区域着色——默认成本是应用于穿越该区域整体成本的一个乘数。这意味着路径查找系统将计算穿越成本乘以默认成本的值。另一方面,固定区域进入成本是一次性应用的成本——当代理进入体积覆盖的区域时。在这种情况下,我们选择了进入泥泞区域的固定成本和穿越它的高成本。
现在,让我们为桥梁区域执行相同的步骤:
-
打开内容抽屉,在蓝图文件夹中,右键单击并选择蓝图类。
-
从所有类下拉列表中,选择NavArea类型。
-
将新创建的资产命名为NavArea_Bridge并双击它以打开。
-
按以下方式更改其值:
-
将默认成本更改为5.0。
-
将固定区域进入成本更改为0.0。
-
将绘制颜色更改为你选择的易于识别的颜色。
-

图 4.15 – 桥梁导航区域的成本设置
在这种情况下,我们希望为代理创建一条更简单的路径,因此我们将固定成本设置为0.0,并将穿越成本设置为更低的值。
我们现在需要将这些类应用到关卡中的修饰符上。
应用自定义修饰符
我们现在准备好获取新创建的类并将它们应用到关卡修饰符上。为此,请按照以下步骤操作:
-
选择两个泥导航修饰体积,在详细信息面板的区域类属性下拉列表中,选择NavArea_Mud。
-
选择桥梁导航修饰体积,在详细信息面板的区域类属性下拉列表中,选择NavArea_Bridge。
导航网格应该已更新,并应看起来像图 4.16中所示:

图 4.16 – 修改后的导航网格区域
注意
根据你创建桥梁的方式,你可能会有比我更不规则的修饰区域;此外,导航网格可能生成得不够完全可通行。为了解决这些问题,你需要稍微调整模型面板的Z值和修饰区域的大小。
我们最终准备好测试这个关卡。
测试关卡
要测试你的健身房,只需开始关卡模拟;你应该看到你的代理走向桥梁,穿过它,并到达目标点。尽管泥泞的地形是可通行的,但穿过它的成本比穿越桥梁要高。
为了双重检查,请在桥梁上添加一些不可通行的障碍物,就像我在图 4.17中做的那样:

图 4.17 – 被阻挡的桥梁
如果你开始模拟,你会看到代理直接走向目标点;尽管泥泞的地形有很高的通过成本,但没有其他可行的解决方案,所以代理将选择它。
这部分内容到此结束,你学习了如何使用修饰符。在下一节中,我将向你展示另一种使用链接代理修改导航网格的方法。
与导航链接代理一起工作
如我们已在第三章中看到的,介绍虚幻引擎导航系统,导航链接代理是一个用于定义代理可以导航的特定区域的演员,即使关卡的一部分无法穿越。导航链接代理放置在游戏世界中,以标记起点和终点,创建一个导航链接。此链接将在两个可能无法直接到达的区域之间提供连接——可以是单向或双向的。
为了检查这个链接是如何工作的,我们将创建一个新的健身房。
创建关卡
要创建这个新的健身房,请按照以下步骤操作:
-
从主菜单中选择文件|新建关卡。
-
导航到Maps/LevelInstances文件夹,并将LI_Lighting实例拖动到你的关卡中;将其变换的位置设置为(0, 0, 0)。
-
导航到Maps/PackedLevelActors文件夹,并将PLA_Lab_03实例拖动到你的关卡中;将其变换的位置设置为(0, 0, 0)。
-
将关卡保存在Maps文件夹中,并命名为Gym_NavMesh_03。
这个健身房中间有一个宽阔的水道,还有一个桥梁,如图 4.18所示:

图 4.18 – 健身房
现在,让我们添加几个导航网格——每个可通行区域一个——以及代理和目标点:
-
添加两个导航网格边界体积演员,并设置它们的边界,以便覆盖河流两侧的可通行区域。
-
在关卡蓝色瓷砖的对侧添加NS_Target Niagara 系统。
-
在蓝色瓷砖上添加BP_NavMeshAgent蓝图,并将目标演员属性值设置为NS_Target。现在关卡应该看起来像图 4.19:

图 4.19 – 带有导航网格的健身房
如果你现在测试你的训练场,你会注意到你的智能体走向目标点,但在水道附近停下。嗯,我想这就像一只企鹅穿着燕尾服参加一个盛大的派对一样出人意料——没有联系,所以不可能有成功的路径搜索!
现在让我们让智能体穿过这座桥。
添加导航链接代理
要添加一个连接水道两边的链接,请按照以下步骤操作:
-
在工具栏中的快速添加到项目按钮中选择导航链接代理,并将一个实例拖到关卡中。
你会注意到这个演员有几个菱形的小工具,分别称为PointLinks[0].Left和PointLinks[0].Right;这些是形成你的链接代理的连接点。

图 4.20 – 两个点链接之一
- 选择每个点链接并将它们移动到桥梁的两侧,如图 图 4.21 所示:

图 4.21 – 一个导航网格代理
点链接属性是一个可以用来在不可达位置之间创建连接的元素列表。默认情况下只有一个,但你可以添加任意多个。
测试训练场
既然这个训练场也完成了,你可以测试它来看看你的智能体表现如何。一旦模拟开始,你会看到智能体通过桥梁到达目标点,就像上面存在导航网格一样。
注意
如果你尝试在桥上放置一些障碍物,你会注意到你的智能体直接冲过去然后停下;这是因为桥上实际上没有导航网格,而是一个直接的链接。这意味着导航链接代理是一个强大的工具,但需要明智地使用。
在本节中,你看到了如何使用导航链接代理连接导航网格中不可达的部分;有了这些新知识,你就可以让你的 AI 朋友们做一些史诗般的悬崖跳水,并完成各种古怪的花样了!
摘要
在本章中,你开始使用导航系统;从简单的可导航区域开始,你添加了一些障碍物并检查了智能体的行为。接下来,你学习了如何通过添加不可穿越区域或困难地形来修改可通行区域。最后,你了解了一些如何连接导航网格不连续部分的方法。
所有这些知识都是至关重要的,因为它帮助你的 AI 代理确定他们可以去哪里而不会撞到墙壁或陷入混乱的迷宫。你可以把导航网格想象成你数字伙伴的 GPS,确保他们不会在虚拟荒野中迷路!
在下一章中,我们将更深入地探讨导航网格的领域;准备好在 Unreal Engine 中创建更多高级和迷人的内容吧!
第五章:改进智能体导航
现在你已经对 Unreal Engine 路径查找系统的基本原理有了牢固的掌握,是时候深入挖掘并开始研究增强智能体导航系统复杂运作的方法了。
在本章中,你将了解如何改进你的导航网格生成和智能体移动:从动态生成的网格开始,经过查询环境,直到有效地避开其他智能体。
到本章结束时,你将掌握一些全新的技能,让你的关卡更加引人入胜和有趣。这些知识将成为引导你走向创建复杂游戏的基础构建块,最终提升你作为 AI 游戏程序员的技能。
在本章中,我们将涵盖以下主题:
-
运行时生成导航网格
-
使用查询过滤器影响导航
-
实现智能体避让
技术要求
要跟随本章中介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。
此外,如果你希望从本书的配套仓库中的代码开始,你可以下载本书配套项目仓库中提供的.zip项目文件,网址为github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。
你可以通过点击Unreal Agility Arena – 第四章 - 结束链接下载与上一章结尾对应的文件。
运行时生成导航网格
让我们继续我们在第四章中开始的短篇故事,设置导航网格:
在秘密研究实验室中,一项突破性的实验正在进行。由马克斯博士和维克托利亚教授开发的 AI 木偶,准备开始一项任务:配备了先进的路径查找系统,木偶被巧妙地放置在实验室复杂的走廊和 相连的房间 网络中。
他们面对模拟的建筑工地、意外的障碍,甚至模拟的干扰,这些干扰阻碍了他们的路径。然而,凭借他们最先进的 AI 能力,木偶迅速适应了不断变化的情况,运用他们的独创性找到替代路线,并巧妙地穿过了 实验室 错综复杂的布局。
如果你正在使用导航网格,那么你迟早会遇到移动的物体,这将导致智能体到达目标的路径发生变化。
这就是为什么静态导航网格生成将变得毫无用处;你需要某种类型的系统来在运行时更新导航网格。这就是为什么 Unreal Engine 提供了多种生成此类网格的方法。
如第三章中所述,介绍虚幻引擎导航系统,生成方法可以从项目设置或层级中的Recast Nav Mesh演员中更改;由于我们只需要更改此层级的导航网格生成,我们将选择第二个选项。
但首先,我们需要为我们的木偶代理创建一个可以行走的层级!
创建层级
要创建这个健身房,请按照以下步骤操作:
-
从主菜单中选择文件 | 新建层级。
-
导航到Maps/LevelInstances文件夹,并将LI_Lighting实例拖动到你的层级中;将其变换的位置值设置为(0, 0,0)。
-
导航到Maps/PackedLevelActors文件夹,并将PLA_Lab_02实例拖动到您的层级中;将其变换的位置值设置为(0, 0,0)。
-
在Maps文件夹中保存层级,并将其命名为Gym_NavMesh_04。
这个健身房有一个楼梯块和一个分离的平台,如图图 5.1所示:

图 5.1 – 健身房
现在,让我们添加导航网格以及代理和目标点:
-
添加一个导航网格边界体积演员,并设置其边界,使其覆盖整个层级。
-
在隔离平台的顶部添加NS_Target Niagara 系统。
-
在蓝色瓦片上添加BP_NavMeshAgent蓝图,并将目标演员属性值设置为NS_Target。现在层级应该看起来如下:

图 5.2 – 带有导航网格的健身房
由于我们使用的是一个比上一章中展示的层级稍微复杂一些的层级,您可能会遇到与我的截图中描述的略有不同的网格生成;楼梯导航网格可能看起来与其它部分分离,或者您可能会遇到一些其他问题。图 5.3展示了您可能遇到的典型场景:

图 5.3 – 破坏的导航网格
要解决此类问题,您需要稍微调整Recast Nav Mesh演员的设置。例如,您可以尝试以下操作:
-
将默认单元格高度属性值设置为40.0。
-
将代理半径设置为70.0。
第一个值将使楼梯的可导航区域连接到整个层级,而第二个值将为网格的边缘添加一些额外的填充,以避免代理走得太近。只需稍微调整一下这些值,直到您得到满意的结果。
如果您现在测试层级,您会注意到代理会尝试到达目标点,但它会在分离平台的底部卡住;显然,没有通往目的地的路线,您的小代理可能无法到达那里,但它会尽力靠近。
现在,尝试在层级周围添加一些障碍物,迫使您的代理爬楼梯 – 就像图 5.4所示:

图 5.4 – 带有某些障碍的健身房
如果你测试关卡,智能体将在楼梯的第一步停下;这是由于这个可怜的家伙的步高太高了!为了解决这个问题,打开BP_NavMeshAgent蓝图,并在详细信息面板中查找角色移动:行走类别,并将最大步高值设置为55.0。
这将允许智能体采取更高的步幅并爬上楼梯。再次测试关卡,你应该看到智能体正在爬楼梯并在其边缘处停下。到达最终目的地的旅程似乎对我们这位小友友来说是一个相当大的挑战!
让我们通过添加一条通往孤立平台(目标点所在位置)的路径来帮助它。
添加移动平台
为了让我们的智能体到达目标点,我们将添加一个移动平台;这将允许我们创建一个动态生成的导航网格。
创建蓝图
要做到这一点,我们将从一个静态网格开始,并将其转换为蓝图:
-
在内容抽屉中,打开KayKit/ProtorypeBits/Models文件夹,并将一个Primitive_Cube_Primitive_Cube实例拖动到关卡中。
-
将其位置属性设置为(380.0, 50.0, 60.0),以便连接楼梯和孤立黄色平台。
-
将此演员的移动性设置为可移动。
-
选择此演员后,通过点击转换为 蓝图按钮将其转换为蓝图。

图 5.5 – 转换为蓝图按钮
-
将蓝图保存在蓝图文件夹中,并命名为BP_MovingPlatform。
-
通过双击资产来打开蓝图。
我们现在将添加一些代码逻辑,以便使其移动。
添加代码
在BP_MovingPlatform蓝图打开的情况下,开始执行以下步骤:
-
创建一个新的Vector类型变量,并将其命名为StartLocation。
-
创建一个新的Float类型变量,并将其命名为VerticalOffset。从详细信息面板,检查实例 可编辑属性。
这两个变量将分别存储平台初始位置和移动时覆盖的垂直距离。第二个变量也已被从关卡中设置为可编辑。
现在,在事件图中,执行以下步骤:
-
添加一个Set Start Location节点,并将其输入执行引脚连接到Event Begin Play节点的输出执行引脚。
-
添加一个Get Actor Location节点,并将其返回值输出引脚连接到之前添加的设置节点中的Start Location输入引脚。
-
从Set Start Location节点的输出执行引脚,添加一个时间轴节点,并双击它以开始编辑。
这部分代码存储平台的初始位置并初始化一个时间轴来设置平台动画。到目前为止的代码在图 5 .6中显示:

图 5.6 – 图表的第一部分
时间线将是一个简单的正弦曲线,它将控制平台的垂直偏移。要创建曲线,请执行以下步骤:
-
点击+ 轨道按钮,从下拉菜单中选择添加****浮点轨道。
-
将轨道命名为Alpha。
-
将曲线的长度值设置为15.00。
-
点击循环按钮使时间线无限重复。
-
在曲线图中,通过右键单击并选择添加关键点选项添加三个关键点。分别将关键点的值设置为以下:
-
时间设置为0.0,值设置为0.0。
-
时间设置为7.5,值设置为1.0。
-
时间设置为15.0,值设置为0.0。
-
-
右键单击每个关键点,并将关键点插值值设置为自动;这将使曲线呈正弦波形而不是线性。结果曲线如图5.7所示:

图 5.7 – 时间线曲线
返回到事件图,你应该会注意到时间线节点现在有一个Alpha输出引脚;我们将使用它来控制平台的垂直位置。开始执行以下步骤:
-
添加一个获取垂直偏移节点。
-
添加一个获取起始位置节点;右键单击其输出引脚,并选择拆分结构引脚以暴露结构中的X、Y和Z引脚。
-
添加一个乘法节点,并将其两个输入引脚连接到垂直偏移输出引脚和时间线的Alpha输出引脚。
-
添加一个添加节点,并将其两个输入引脚连接到乘法节点的输出引脚和获取起始位置节点的起始位置 Z输出引脚。
-
在图中添加一个设置演员位置节点,将其输入执行引脚连接到时间线节点的输出执行引脚,并执行以下操作:
-
右键单击新位置输入引脚,并选择拆分结构引脚以暴露结构中的X、Y和Z引脚。
-
将新位置 X输入引脚连接到获取起始位置节点的起始位置 X输出引脚。
-
将新位置 Y输入引脚连接到获取起始位置节点的起始位置 Y输出引脚。
-
将新位置 Z输入引脚连接到添加节点的输出引脚。此部分图表的结果如图5.8所示:
-

图 5.8 – 图表的第二部分
尽管代码相当长,但它相当直观;它只是简单地使用时间线节点来计算随时间变化的偏移量,并将其应用于平台的Z位置。在运行时,平台将不断上下移动,每 15 秒就会到达目标点一次——即循环的持续时间。
如果你现在模拟健身房 – 导航网格可见 – 会发生一些奇怪的事情;尽管平台会上下移动,但导航网格将保持最初生成的状态,留下一个漂浮的通道。

图 5.9 – 静态生成
这个问题发生是因为导航网格是静态生成的,在运行时不会改变;这个可怜的小家伙在试图到达目标点时会掉落,因为它相信存在一条通道!
让我们立即通过使导航网格动态化来解决这个问题。
使导航网格动态化
如本节开头所述,我们将只为这个级别设置网格运行时生成活动,因此我们需要更改Recast 导航网格演员。为此,选择Recast 导航网格演员,在详细信息面板中找到运行时类别。将运行时生成下拉值设置为动态。
如果你测试健身房,你现在将看到导航网格以固定间隔更新,通道将被中断。

图 5.10 – 动态生成
你可能会注意到代理会在楼梯平台的边缘停下来,当移动平台创建通道时不会继续前进。为了修复这个小问题,请执行以下操作:
-
打开BP_NavMeshAgent并断开事件 BeginPlay节点。
-
将移动到演员的输入执行引脚连接到事件 Tick节点的输出执行引脚。
-
打开类默认值选项卡,将Tick 间隔(秒)属性设置为0.5,使更新更稀疏。更新后的蓝图如图 5.11 所示。

图 5.11 – 更新后的蓝图
我们在这里所做的是相当简单的;我们不是在游戏开始时一次性执行代码,而是将其设置为每帧执行一次 – 设置为半秒 – 以保持设置最终目的地。这可能不是最性能高效的解决方案,但应该适用于我们的小型原型。
注意
请记住,在运行时更新导航网格计算量很大;这意味着你应该只在必要时使用此功能,并且大多数时候坚持静态生成。
现在你已经掌握了创建动态导航网格,你的小代理在向目标点导航时将面临挑战。生活 – 或者更确切地说,人工生活 – 对它来说将不再那么简单了!
在下一节中,我将向您介绍导航过滤器,这是一个功能,将允许你改变代理在导航网格上的行为方式。
使用查询过滤器影响导航
如同在第三章中提到的,展示虚幻引擎导航系统,使用查询过滤器,你可以自定义调整 AI 代理的导航路径,使你能够增强和优化它们在环境中的移动。
你可能已经注意到,在之前创建的图中,Move to Actor方法有一个名为Filter Class的属性(见图 5.11);这将允许我们自定义我们的代理在导航网格上的行为方式。
通过扩展NavigationQueryFilter类并设置一些适当的值来创建一个过滤器,因此让我们先创建这样一个类并看看它的工作方式。
创建级别
作为第一步,我们需要一个用于测试过滤器的健身房;在这种情况下,我们将复制一个现有的一个并稍作调整:
-
复制Gym_NavMesh_02地图,将其重命名为Gym_NavMesh_05,然后打开它。
-
从桥梁上移除所有障碍物(如果有)。
-
复制代理并将其放在第一个代理旁边,在蓝色瓷砖上。
-
为了使代理易于识别,你可能希望更改复制的材质;在我的情况下,我选择了MI_Prototype_B。最终级别应该像图 5.12中描述的那样:

图 5.12 – 基础健身房
如果你测试这个级别,不出所料,两个代理都会走向桥梁,穿越它,并到达目标点。
让我们稍微增加一些趣味性,怎么样?
创建查询过滤器类
我们现在将创建一个查询过滤器,将覆盖导航修改器的考虑方式;我们希望泥泞区域被视为成本较低且易于穿越,但仅针对单个代理。为此,请按照以下步骤操作:
-
打开内容抽屉,在蓝图文件夹中创建一个新的导航查询****过滤器类型的蓝图类。
-
将资产命名为NavFilter_MudWalker,双击它以打开它。
-
在细节面板中,你会看到一个区域数组属性;点击+按钮添加一个元素。
-
打开元素并执行以下操作:
-
从区域类下拉菜单中选择NavArea_Mud。
-
打开旅行成本覆盖复选框并将其值设置为1.0。
-
打开进入成本覆盖复选框并将其值设置为0.0。
-

图 5.13 – 查询过滤器设置
我们基本上在创建我们级别泥泞区域的不同的成本 – 也就是说,NavArea_Mud修改器放置的地方。
我们现在需要稍微修改一下 AI 代理,以便它能够接受这种修改器。
修改代理
我们需要对代理进行一些轻微的修改,因此打开BP_NavMeshAgent蓝图类并执行以下操作:
-
从Move to Actor节点,点击并拖动Filter Class输入引脚,并在释放后选择提升到变量。
-
将新创建的变量命名为FilterClass,并在细节面板中检查相应的属性使其可实例编辑。
-
编译蓝图,并在细节面板中再次确认默认值设置为无。
无论何时你告诉智能体移动到目标点,它都会使用FilterClass变量——如果设置为任何值——来覆盖寻路网格成本规则。
让我们在实际操作中测试一下。打开你的健身房,选择第二个智能体;然后,从详细信息面板中,从过滤器类别下拉菜单中选择NavFilter_MudWalker。
测试健身房,你现在会注意到第二个智能体直接移动到目标点,穿过泥地。这个顽皮的 AI 智能体已经叛变,决定玩弄手段,不是吗?
在本节中,你发现了覆盖寻路网格成本的能力,这为你创建高度可定制的 AI 角色提供了显著的力量。有了这项新能力,你可以让你的 AI 角色表现出独特的个性,并从人群中脱颖而出。我相当确信你会理解这为创建独特和动态的游戏体验开辟了一个全新的世界。
在下一节中,我将向你展示 AI 寻路中的另一个重要技术,那就是如何让你的 AI 智能体避免彼此。
实现智能体避免
毫不奇怪,大多数时候,你将在一个关卡中与多个智能体一起工作,这意味着他们很可能会有交叉的寻路规则;这意味着你的 AI 实体有很大的风险相互碰撞。虽然这可能很有趣,但我猜这并不是你在游戏中的预期行为。
正因如此,Unreal Engine 提供了一个开箱即用的——但默认情况下是禁用的——避免系统。在本节中,我们将考虑如何让 AI 智能体避免彼此。
和往常一样,我们将从一个全新的健身房开始。
创建关卡
作为第一步,我们需要一个带有一些障碍物的健身房。为了开始,请执行以下操作:
-
从主菜单中选择文件 | 新建关卡。
-
导航到Maps/LevelInstances文件夹,并将LI_Lighting实例拖动到你的关卡中;将其变换的位置值设置为(0, 0, 0)。
-
导航到Maps/PackedLevelActors文件夹,并将PLA_Lab_05实例拖动到你的关卡中;将其变换的位置值设置为(0, 0, 0)。
-
在Maps文件夹中保存关卡,并将其命名为Gym_NavMesh_06。
这个健身房比之前的要大一些,并有一些障碍物使事情更有趣。此外,还有八个蓝色方块——我们将使用八个智能体——如图 5.14所示:

图 5.14 – 健身房
添加智能体
如前所述,我们将添加几个智能体——总共八个——以检查它们在拥挤环境中的行为。因此,请按照以下步骤操作:
-
在关卡中添加八个BP_NavMeshAgent蓝图实例,并将每个实例放入关卡中的一个蓝色方块中。
-
添加八个NS_Target Niagara 系统实例,并将它们放在每个蓝色方块后面。
-
对于每个代理,将目标演员属性值设置为位于健身房另一侧的NS_Target Niagara 系统。现在关卡应该看起来像图 5.15:

图 5.15 – 完成的健身房
如果你尝试测试健身房,很可能会发现一些代理会与图中5.16所示的反方向移动的其他代理相撞:

图 5.16 – 哎呀,这很疼!
幸运的是,我们的代理配备了安全头盔,而你的游戏中的角色可能没有任何保护地四处游荡!这就是为什么我们要使代理的路径寻找稍微聪明一点。
激活避障
一旦你打开了BP_NavMeshAgent蓝图,你就可以启用避障系统。为此,请按照以下步骤操作:
-
在详细信息面板中,找到角色移动:避障类别,并检查使用 RVOAvoidance属性。
-
将避障考虑半径设置为2000.0。
如第三章所述,介绍虚幻引擎导航系统,RVO 指的是一个功能,它使 AI 代理能够避免相互碰撞。
当为角色或代理启用使用 RVOAvoidance属性时,它允许它们动态调整其移动以避免与环境中的其他代理发生碰撞。避障考虑半径属性用于定义代理考虑其他代理进行避障的半径。
在此测试健身房将让你看到避障系统在工作;代理会在到达目标点的同时避免彼此。
测试最坏情况
让我们测试一些不同的事情;我们将创建一个对代理来说空间更具挑战性的健身房。新的健身房将是上一个的副本。首先按照以下步骤操作:
-
复制Gym_NavMesh_06地图,并将其命名为Gym_NavMesh_07。
-
添加一些障碍物,将创造一个狭窄的路径,几乎迫使角色遵循单一、特定的路线。我的健身房在图 5.17中显示:

图 5.17 – 最坏情况
测试游戏,你会看到所有代理都在努力避免相互碰撞。你可以进行反测试,取消选中使用 RVOAvoidance属性;你会注意到所有代理最初聚集在一起,然后相互碰撞,最终解决路径并到达各自的目标点。

图 5.18 – 大规模碰撞
在本节中,你学习了如何为在导航网格上移动的代理实现避障,确保它们可以在积极避免与其他代理碰撞的同时成功导航。
摘要
在本章中,我们介绍了虚幻引擎寻路系统的一些更高级的功能。首先,我们看到了如何创建一个可以在运行时更新的导航网格。接下来,我们看到了如何覆盖代理解释导航网格部分成本的方式。最后,我们看到了如何使用内置的避障系统,以便 AI 代理不会相互碰撞。
在即将到来的章节中,我们将探讨寻路系统的最后一个组成部分:调试和优化技术。我们将探讨如何识别和解决可能出现的任何问题,并优化系统以提高性能。准备好迎接一个既激动人心又富有信息量的会议吧!
第六章:优化导航系统
随着项目的复杂性增加,确保导航网格精细调整以实现平滑高效的 AI 代理移动变得至关重要。这就是为什么在本章中,我们将深入探讨优化和调试导航系统。
在本章中,我们将探讨各种技术和策略来优化导航网格,并讨论简化路径查找计算、减少计算开销和提升整体性能的方法。
你还将获得调试导航系统内可能出现的任何问题的工具和知识。从解决路径查找错误到识别导航网格的不一致性,我们将涵盖一系列调试技术,帮助你克服任何阻碍代理平滑导航的障碍。
在本章中,我们将涵盖以下主题:
-
理解导航网格调试工具
-
分析导航网格分辨率
-
精炼导航网格生成
-
进一步改进
技术要求
要跟随本章介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。
此外,如果你希望从本书的配套仓库开始,你可以下载本书配套项目仓库中提供的.zip项目文件,网址为github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。
要下载最后一章末尾的文件,请点击虚幻敏捷竞技场 – 第五章 – 末尾链接。
理解导航网格调试工具
驱使他不懈追求完美的动力,马克斯博士深入调试和改进人工智能木偶的任务。凭借不懈的精神和充满创新思想的头脑,他着手微调木偶的能力,并解决他们在实验中遇到的任何故障或不足。
在他的忠实助手,维多利亚教授的陪伴下,马克斯博士仔细分析了来自木偶之前探险收集的数据,审查每一行代码,寻找任何潜在的错误,并寻求提升他们表现的机会。
作为游戏程序员,你知道优化游戏中的代码和追踪错误对于创建成功的游戏至关重要。AI 系统也不例外。幸运的是,虚幻引擎提供了一套功能 – 调试工具,这些工具为你提供了宝贵的洞察力,让你能够实时可视化并分析导航网格。在本节中,我们将开始探索这些工具,以检查系统的行为以及它是否运行良好。
注意
在本章中,我们将考虑调试工具的一部分,这将让你分析导航网格系统,但你应该知道 AI 调试系统覆盖了 Unreal Engine 中所有可用的 AI 功能。这就是为什么我会在本书的后面部分回到调试工具,当我将涵盖其他 AI 主题时。
让我们首先检查如何启用 AI 调试工具以及如何开始使用导航网格调试功能。
启用 AI 调试工具
要启用调试工具,你只需要按下键盘上的撇号键(‘)。请注意,在某些键盘布局中——比如我的——这个键可能不可用;你可以通过以下步骤添加自己的快捷键:
-
从主菜单中选择编辑 | 编辑器首选项。
-
选择通用 – 键盘快捷键类别,并在搜索栏中输入show ai debug。
-
在显示 AI 调试字段中,插入你喜欢的快捷键。在我的情况下,我选择了数字键盘上的/字符。

图 6.1 – AI 调试器快捷键
-
启用 AI 调试工具后,你可以使用以下数字键盘键来切换 AI 信息:
-
数字键盘 0:显示或隐藏当前可用的导航网格数据信息
-
数字键盘 1:显示或隐藏一般 AI 调试信息
-
数字键盘 2:显示或隐藏行为树调试信息
-
数字键盘 3:显示或隐藏 EQS 调试信息
-
数字键盘 4:显示或隐藏 AI 感知调试信息
-
数字键盘 5:显示或隐藏 AI 感知系统调试信息
-
让我们开始检查这些工具,使用我们在前几章中创建的健身房级别。
检查 AI 调试工具
要开始使用 AI 调试工具,你需要一个级别。所以,首先打开Gym_NavMesh_01级别并执行以下操作:
-
通过按下P键禁用导航网格可视化工具——如果已启用——以避免混乱 AI 信息显示。
-
启动级别模拟并立即暂停。
-
通过按下撇号键启用 AI 调试工具;这将打开一个侧边栏,你将看到一些显示消息,如图图 6.2所示:

图 6.2 – AI 调试工具在实际应用中
- 启用调试工具后,请确保导航网格和AI类别已启用,并且所有其他类别都禁用,使用相应的数字键盘键。启用的类别将以绿色突出显示,如图图 6.3所示:

图 6.3 – 启用类别
游戏暂停状态下,请执行以下操作:
-
查找BP_NavMeshAgent,你会注意到它现在有一个红色图标与之关联。
-
点击演员以选择它;这将显示一些关于它的信息。其中最重要的是相关的 AI 控制器。

图 6.4 – 人工智能代理
此外,在显示中,你将看到有关人工智能代理的附加信息,如图图 6.5所示:

图 6.5 – 人工智能代理信息
如您从图中所见,这种显示提供了大量信息,包括控制者、控制器和代理正在行走的网格。
最后,你可能会得到关于导航网格和寻路系统最有趣的信息之一;如果你在关卡中移动,你会看到组成通往目标点的路径的多边形的突出显示。

图 6.6 – 寻路多边形
作为额外的练习,你可以打开所有其他关卡,看看你可以从工具中获得哪些信息。例如,你可以查看Gym_NavMesh_04关卡,并检查当移动平台上下移动时会发生什么。
在本节中,你已经了解了将提供有关人工智能系统有意义信息的工具;在下一节中,我将向你展示如何从你的关卡中提取关于导航网格生成的见解,以便优化它们。
分析导航网格分辨率
Unreal Engine 提供了一个导航网格分辨率系统,允许开发者在单个导航网格内创建三个不同细节级别的网格瓦片。这意味着你可以生成具有高、中(默认选项)或低精度设置的瓦片集。通过选择不同的精度级别,用户可以在游戏进行时实现动态导航网格的更快生成(从计算时间来看)。
注意
当我们谈论分辨率时,我们指的是为了绘制特定导航区域而产生的单元格的精度和数量。
高分辨率瓦片可能会将给定区域划分为更多的多边形,以更精确地近似其形状。相反,低分辨率瓦片将包含相同区域,但多边形数量较少。这种权衡使得瓦片生成更快,但可能会在过程中牺牲一些精度。
作为第一次测试,我们可以开始分析我们的一个健身房:
-
打开Gym_NavMesh_01关卡。
-
为了更好地查看关卡,选择俯视图。

图 6.7 – 选择俯视图
-
在大纲中,选择重铸导航****网格演员。
要获取关于你的导航网格成本的总体信息,请执行以下操作:
-
在仍然选择重铸导航网格演员的情况下,检查细节面板中的绘制瓦片边界属性。
-
检查绘制瓦片边界次数属性。你应该得到如图图 6.8所示的视图:

图 6.8 – 瓦片和瓦片生成时间
绘制瓦片边界时间 属性显示了处理特定瓦片的时间成本;您会发现更复杂的区域需要更多时间来计算。如果您想要更直观的成本表示,可以检查 绘制瓦片构建时间热图,这将启用成本的热图可视化,如图 图 6.9 所示:

图 6.9 – 热图可视化
注意
如果你对这个功能不熟悉,热图是一种使用颜色编码系统来显示特定属性在定义区域或数据集中体积或强度的视觉表示。热图具有适应性,并在多种场景中得到应用,包括数据分析、用户行为分析和地理表示。在我们的场景中,通过将颜色与不同级别的强度相关联——蓝色代表成本较低的区域,较浅的颜色代表成本较高的区域——热图可以照亮成本增加或减少的区域,使观察者能够识别模式和关键情况。
这种视图在处理大型区域并希望获得资源密集型区域概览时特别有用。在前面的图中,红色区域是导航成本较高的区域。
在本节中,您已经掌握了分析导航网格以识别潜在问题的过程;在下一节中,我们将探讨解决和改进其生成的策略。
精细化导航网格生成
环境的复杂性直接影响系统生成导航网格所需的时间,这不足为奇。相反,如果生成时间较短,生成的导航网格可能不够精确。在大多数情况下,您的最终系统将涉及计算速度和精度之间的权衡。这意味着作为游戏开发者,了解如何正确设置您的导航网格生成是强制性的。
在本节中,我将为您提供一些关于如何优化导航网格生成方式的建议。
影响导航网格分辨率
下一个测试我们将执行的是修改导航网格的分辨率。首先按照以下步骤操作:
-
打开 Gym_NavMesh_02 级别。
-
如果它尚未启用,选择 顶视图。
-
在 大纲 面板中,选择 Recast Nav Mesh 对象。
-
在 细节 面板中,检查以下属性:
-
绘制 瓦片边界
-
绘制瓦片 构建时间
-
绘制瓦片构建时间 热图
您应该得到类似于 图 6.10 的视图:
-

图 6.10 – 热图可视化
如您从热图中所见,中心部分成本较高。然而,整体几何形状相当简单;我们有泥泞区域和一座桥,它们基本上是矩形、未旋转的区域。让我们降低我们的网格成本。开始执行以下步骤:
-
在大纲面板中,选择三个导航修改体积 – 两个泥泞区域和桥梁区域。
-
在详细面板中,搜索导航网格分辨率属性,并在下拉菜单中选择低。
你会注意到地图中央部分的时间成本会立即下降,如图图 6.11所示:

图 6.11 – 改进的导航网格
当你查看热图可视化时,你可能会看到一些未被前一次修改触及的瓦片 – 那些在边缘的瓦片 – 改变颜色并显得更昂贵。然而,不要被这个现象迷惑 – 系统只是突出显示在整体导航网格中成本更高的区域。实际上,如果你检查这些区域,你会发现成本根本没有改变;它们只是比中央地图的对应区域更昂贵。这意味着特定区域的成本相对于系统分析的所有其他区域是相对的。
作为另一个实验,你可以将桥梁的导航网格修改器演员设置为导航网格分辨率值为高,看看结果。剧透一下!包括桥梁在内的瓦片将成为级别中最昂贵的部分!
改变导航网格分辨率
如果在这个时候,你还在想是否可以调整你级别的导航网格分辨率,答案是出人意料的肯定!
通过选择重铸导航网格演员,在详细面板中,你会找到一个导航网格分辨率参数选项,有三个字段 – 低,默认和高 – 这将让你决定你级别的单元格大小。

图 6.12 – 导航网格分辨率参数
作为一条经验法则,只需记住,单元格大小越高,区域成本越低。
改变瓦片大小
你已经知道导航网格被划分为瓦片,这些瓦片用于重建导航网格的特定部分。由于每个瓦片由单元格组成,重建瓦片意味着用更新的信息重新创建所有单元格。
较大的瓦片包含更多的单元格,因此重建成本比较小的瓦片更高。然而,在处理瓦片时,系统也会处理瓦片边缘的相邻单元格。在确定瓦片大小时,考虑这种额外的开销成本是很重要的。在某些情况下,处理多个较小瓦片的累积开销成本可能会超过重建单个大型瓦片的成本。因此,在选择合适的瓦片大小时,应仔细考虑以优化性能。
为了在运行时重建瓦片时实现最佳性能,Epic Games 的建议是将每个单元格大小属性(低、默认和高)设置为彼此的倍数,并将Tile size UU属性设置为可以除以所有单元格大小值的数值。例如,在图 6.13中,我为Gym_NavMesh_02关卡设置了以下值:
-
Tile Size UU为960.0
-
低单元格大小为60.0
-
默认单元格大小为30.0
-
高单元格大小为15.0

图 6.13 – 单元格大小示例
如你所见,低单元格大小值可以除以默认单元格大小和高单元格大小,得到一个整数值。同样,对于Tile Size UU,它可以除以低单元格大小、默认单元格大小和高单元格大小。
作为额外的示例,图 6.14显示了具有这些值的相同关卡:
-
Tile Size UU为1280.0
-
低单元格大小为80.0
-
默认单元格大小为40.0
-
高单元格大小为20.0

图 6.14 – 单元格大小替代示例
在第二个示例中,使用了更大的瓦片大小。然而,很明显,优化并不那么有效,导致空间利用效率低下。
在本节中,我向你提供了一些关于如何分析你的导航网格生成以及如何通过更改单元格分辨率和瓦片大小来有效优化的宝贵建议。在下一节中,我将提供一些额外的建议,帮助你使导航更加出色。
进一步改进
调整导航网格可能需要很长时间——这取决于你想要达到的目标——有时,这更多是试错和个人经验的问题。在本节中,我将为你提供一些额外的建议,帮助你改进地图,使其更加实用。
调整分辨率
选择合适的导航网格分辨率不仅是一个计算性能的问题;有时它甚至可能影响你的智能体导航。
例如,考虑一下图 6.15显示的Gym_NavMesh_07关卡的一部分:

图 6.15 – 导航网格分辨率
在这种情况下,默认单元格大小值已设置为20.0,如你所见,障碍物之间没有可通行区域。然而,如果你将值降低到5.0,你将得到一个完全不同的场景,如图 6.16所示:

图 6.16 – 改进的导航网格分辨率
如你清晰可见,障碍物之间现在有了开放通道;硬币越高,酒越精致!
禁用网格影响
有时,你的导航网格可能会因为可能导致无法到达路径的网格而变得杂乱,但它们被包含在导航网格生成过程中。在这种情况下,建议隐藏它们以防止它们影响生成时间。例如,考虑图 6.17中描述的情况:

图 6.17 – 无法到达的路径
如你所见,红色矩形区域是无法到达的,但中央的立方体在导航网格生成过程中将被计算。这意味着,尽管它不会影响任何路径查找解决方案,但它仍然必须被计算。在这种情况下,你可以通过完成以下步骤来排除它:
-
选择网格。
-
在详细信息面板中,查找始终影响导航属性并取消选中复选框。
网格现在将不会被排除在导航网格生成之外,但整体结果将保持不变,正如你从图 6.18中可以看到:

图 6.18 – 改进后的区域
你可以通过以下方式更进一步:
-
选择其他两个框,并取消选中两个的始终影响导航属性。
-
添加一个导航修改器体积演员,并将其缩放以包含整个区域。
-
将体积区域类属性设置为NavArea_Null。
图 6.19显示了最终结果:

图 6.19 – 优化后的区域
如你所见,我们现在使用单个修改器来避免三个网格对最终导航网格生成的影响。想象一下,一旦你的关卡充满了各种道具和障碍物,你将节省多少时间!
摘要
在本章中,我们介绍了一些更高级的技术来帮助你改进导航网格生成。从调试工具开始,我们学习了如何分析和调整网格生成过程。最后,我们学习了几个技巧,这将帮助我们制作更有效的关卡。
有了这个,本书的第二部分到此结束;从下一章开始,准备好面对在游戏开发中讨论 AI 时可能最有趣的话题之一:行为树。
准备好。你的 AI 伙伴即将进行一次重大更新,它们将永远不再相同!
第三部分:决策制作
在本书的第三部分,你将全面了解 Unreal Engine 框架内强大且通用的行为树系统。此外,你还将了解高级功能,以便你可以实现自己的复杂游戏 AI 逻辑。
本部分包括以下章节:
-
第七章 ,“介绍行为树”
-
第八章 ,“设置行为树”
-
第九章 ,“扩展行为树”
-
第十章 ,通过感知系统改进智能体
-
第十一章 ,理解环境查询系统
第七章:介绍行为树
在游戏开发的世界里,行为树是控制 AI 角色决策过程的分层结构,决定他们在游戏中的行动和反应。作为一名游戏程序员,深入研究行为树的复杂性至关重要,因为它将赋予你制作动态、智能和吸引人的虚拟实体的能力,从而增强玩家的游戏体验。
本章旨在提供对行为树和黑板以及它们在虚幻引擎中的应用的温和介绍。
在本章中,我们将涵盖以下主题
-
解释行为树
-
理解虚幻引擎中的行为树
-
理解黑板
技术要求
遵循本章没有技术要求。
解释行为树
在更广泛的意义上,行为树是一种在计算机科学的许多领域使用的数学模型,包括视频游戏。它以模块化的方式概述了有限任务集之间的转换。行为树的力量在于它们能够从简单的组件中创建复杂的任务,而不必深入了解每个组件的实现细节。虽然行为树与分层状态机有一些相似之处——状态以分层组织,允许更好地重用行为——但主要区别在于任务,而不是状态,是行为的基本元素。主要优势在于它们的直观性,使得它们更不容易出错;这就是为什么它们在游戏开发行业中非常受欢迎。
今天的视频游戏越来越复杂,导致 AI 角色的复杂性成比例增加。因此,这些角色或代理的维护至关重要。与状态数量增加时难以维护的有限状态机系统不同,行为树为决策过程提供了实际且可扩展的解决方案。当代理执行行为树时,它会进行深度优先搜索以定位和执行最低级别的叶节点。
行为树相较于其他系统的关键优势在于它们的可扩展性、表达性和可扩展性。与其他系统不同,行为树不涉及状态之间的显式转换;相反,树中的每个节点都指定如何运行其子节点。这种无状态的性质消除了跟踪先前执行节点以确定下一组行为的需求。行为树的表达性源于对各种抽象级别、隐式转换和复合节点复杂控制结构的运用。
此外,在行为树中,转换是通过树节点之间交换的调用和返回值发生的,促进了双向控制转移机制。
行为树结构
行为树以树结构的形式直观表示,节点分为根节点、控制流和执行——或任务。在这个表示中,每个节点可能有一个父节点和一个或多个子节点。特别是以下几点值得注意:
-
根节点没有父节点,只有一个子节点
-
控制流节点有一个父节点和至少一个子节点
-
执行节点有一个父节点,没有子节点
行为树的执行从根节点开始,它向其子节点发送执行触发器。
每当达到控制流节点时,它将控制树内的执行和决策流程,根据某些条件或规则确定应该执行哪些任务或子树。
每次触发执行节点时,它将执行一个特定的任务,如果任务正在进行,则向其父节点报告状态为运行,如果目标实现,则报告状态为成功,如果任务未成功,则报告状态为失败。
图 7.1 显示了一个行为树执行示例,从根节点开始,到控制流节点,最后执行一个任务:

图 7.1 – 行为树示例
注意
行为树节点从上到下、从左到右执行;这也是它们通常编号的方式。
不言而喻,实现行为树的方式不止一种。这就是为什么在下一节中,我将深入探讨虚幻引擎系统的所有细节。
虚幻引擎中的行为树是什么?
在虚幻引擎中,行为树是类似蓝图——即,通过添加和链接具有特定功能的节点集来形成行为树图的资产。在行为树中执行逻辑时,使用一个称为黑板的单独资产——关于这一点将在本章后面提供更多详细信息——来保留行为树需要做出明智决策的信息。
行为树由一个BehaviorTreeComponent实例处理,该实例由AIController实例持有。需要注意的是,组件不是自动附加到控制器上的;您需要通过 C++或蓝图来添加它。如果没有组件,它将在运行时自动创建。
当比较虚幻引擎的行为树与其他行为树系统时,需要记住的一个关键区别是它们的事件驱动特性,这防止了代码的持续执行。而不是持续检查相关变化,虚幻引擎的行为树会监听可以触发树修改的事件。使用事件驱动架构提供了性能提升和调试能力的好处——这一点我将在接下来的章节中展示。
行为树节点实例化
需要注意的是,行为树在您的项目中作为共享对象存在;这意味着所有使用行为树的代理将共享相同的实例,并且所有共享对象都无法存储特定于代理的数据。使用共享节点的主要优势是提高 CPU 速度和减少内存使用。
可以通过多种方式利用特定于代理的数据 – 其中一种是我们将在本章后面看到的黑板 – 以提供更多关于如何使用行为树的灵活性。
另一种方法是实例化单个节点;这将使每个使用行为树的 AI 代理获得节点的唯一实例,但代价是更高的性能和内存使用。使用此方法的一个节点示例是播放动画任务。
执行顺序
如前所述,行为树节点是从上到下、从左到右执行的,虚幻引擎也不例外。节点按照此惯例编号,以便轻松跟踪执行顺序。图 7.2展示了来自Lyra 入门游戏项目的行为树,显示了带有相应序列号的节点:

图 7.2 – 行为树序列
备注
在虚幻引擎中,根节点不会被编号,因为它不被视为序列的一部分。
在本节中,您已经得到了对行为树及其执行方式的温和介绍。下一节将深入探讨虚幻引擎系统,帮助您更好地理解如何有效地将行为树融入您的游戏中。
理解虚幻引擎中的行为树
理解行为树及其构成对于在虚幻引擎中设计有效的 AI 系统至关重要;在本节中,我将向您介绍与行为树相关的关键概念,以帮助您开始开发自己的 AI 角色。
在虚幻引擎中,行为树有五种类型的元素:
-
根节点
-
任务节点
-
组合节点
-
装饰器
-
服务
为了让您对每种类型有一个全面的理解,我将分别介绍它们,确保清晰地展示它们各自的功能。
根节点
根节点作为行为树的起始点;它在树中占据一个独特的位置,并受一套特殊规则的约束:
-
树结构中只能有一个此类节点
-
它只能有一个连接,如果此连接被移除,则整个树将禁用
-
它不支持附加装饰器或服务节点

图 7.3 – 根节点
任务节点
任务节点负责执行诸如移动 AI 或调整黑板值等操作。任务将不会停止其执行,直到报告失败或成功的结果。
任务节点也可以附加一个或多个装饰器或服务,允许在游戏环境中实现更复杂的行为和交互。
任务节点
任务以紫色标识。

图 7.4 – 任务示例
Unreal Engine 包含一组预构建的任务,这些任务可供使用。这些任务解决了开发者可能需要的大多数通用场景。然而,任务可以被扩展,以便您创建自己的自定义节点。
这里是部分任务列表,这些任务将作为标准功能提供:
-
完成结果:一旦执行,此节点将立即以定义的结果完成 - 成功、失败、中止或进行中
-
移动到:一旦执行,它将通过使用导航系统将 AI 代理移动到目标位置
-
直接移动:一旦执行,它将不使用导航系统将 AI 代理移动到目标位置
-
等待:一旦执行,它将使行为树在此节点上等待,直到经过指定的时间
-
播放动画:一旦执行,此节点将播放指定的动画资源
-
播放声音:一旦执行,此节点将播放指定的声音
正如您所看到的,任务代表 AI 代理可以执行的单个动作或操作;您可以使用它们创建简单的动作,或将多个任务组合以创建更复杂的行为。
复合节点
复合节点定义了分支的根并设置其执行规则;此外,它们是唯一可以应用于行为树根节点的节点。
复合节点也可以应用装饰器和服务,从而使其逻辑更加复杂。一旦应用了服务,它将在复合节点的子节点执行期间保持活跃。
复合节点
复合节点以灰色标识。
有三种复合节点可用:
-
选择器
-
简单并行
-
序列
让我们逐一检查它们。
选择器
选择器节点按从左到右的顺序执行其子节点,并且它们会在任何一个子节点成功时立即停止执行。当一个选择器节点的子节点成功时,选择器本身被认为是成功的。另一方面,如果选择器的所有子节点都失败,选择器节点本身将被标记为失败。

图 7.5 – 选择器节点
简单并行
简单并行节点允许在完整树的同时执行单个主任务节点。主任务完成后,您可以通过完成模式属性决定节点是否应立即完成,停止次要树,还是等待次要树完成后再完成。

图 7.6 – 简单并行节点
序列
序列节点从左到右依次运行其子节点。当子节点失败时,将停止执行。如果子节点失败,序列也会失败。与选择器不同,序列的成功只有在所有子节点都成功时才会实现。

图 7.7 – 序列节点
装饰器
装饰器——有时也被称为条件——用于确定树中的分支,甚至单个节点是否可以执行。它们必须附加到复合节点或任务节点上。
装饰器在确定行为树中分支的执行路径方面起着至关重要的作用;它们本质上充当决策者,评估特定分支或单个节点是否应该继续执行。它们作为条件,评估继续沿着特定分支前进的可行性,如果任务——或子树——注定要失败,则发出预防性失败的信号。这种预防性操作有助于防止装饰器尝试执行注定要失败的任务——或子树——这可能由于各种原因(如信息不足或目标过时)而失败。
装饰器
装饰器以蓝色标识。

图 7.8 – 应用到选择器的装饰器
Unreal Engine 包含一组预构建的装饰器,这些装饰器可以随时使用,但可以扩展以允许您创建自己的自定义装饰器。
这里是一个部分列表,列出了一些您将作为标准功能可用的任务:
-
黑板:将检查在给定的黑板键上是否设置了值——或者没有设置值
-
复合:通过使用AND、OR和NOT节点,可以创建比内置节点更高级的逻辑
-
冷却:将锁定节点或分支的执行,直到经过预定义的时间
-
路径存在:将检查两点之间是否存在路径
-
循环:将无限期地循环节点或分支——或者如果设置了次数,则循环指定次数
大多数装饰器都包括一个逆条件属性,这将让您……反转条件,从而提供更多灵活性。例如,您可以在行为树中使用相同的装饰器,在相反的条件下执行不同的任务。
例如,您可以使用路径存在将 AI 代理移动到目标点,并在另一个路径存在实例上使用逆条件来寻找替代目标点。
总之,装饰器作为决策点,确定行为树中的特定动作或分支是否应该执行。
服务
服务可以附加到复合节点或任务节点上,并在其分支活动时以特定间隔运行——在间隔属性中定义。它们通常用于执行检查和更新黑板。
一旦由任务或组合触发,服务将保持执行,无论在所属节点下执行的父级-子级级别数量如何。
服务
服务以绿色标识。

图 7.9 – 应用到选择器的服务
服务对于您正在开发的行为树非常具体;这意味着,很可能会需要创建自己的自定义服务。然而,虚幻引擎提供了两个预先构建的服务,它们可以随时使用:
-
默认焦点:这允许从 AI 控制器快速访问一个演员,而不是使用黑板键。
-
运行 EQS:这可以用来定期执行 EQS – 更多内容请参阅第十一章 ,理解环境查询系统 – 在指定的间隔。它还可以更新指定的黑板键。
在介绍了构成行为树的各种节点类型之后,现在是时候进入下一节,以便深入了解黑板资产。
理解黑板
在虚幻引擎中,黑板是行为树的关键组件;它充当一个内存空间 – 类似于大脑 – AI 代理可以在其决策过程中读取和写入数据。这意味着开发者将能够查询和更新其中存储的信息。
黑板作为黑板数据资产创建,将被分配给行为树,并包含一组变量 – 命名为键 – 存储预定义类型的特定信息。这些键可以在运行时访问和操作,以影响 AI 角色的决策。

图 7.10 – 黑板示例
键可以设置为实例同步;在这种情况下,键本身将在所有黑板实例之间同步。这种同步确保对键值的任何更改都将一致地反映在共享相同行为树和黑板的 AI 代理的所有实例中。

图 7.11 – 实例同步属性
黑板可以存储多达 255 个键,并支持以下数据类型:
-
FVector
-
FRotator
-
布尔值
-
Int32
-
浮点数
-
UClass
-
UObject
-
FName
-
UEnum
-
FString
注意
黑板不能存储数组。
BlackboardComponent实例将允许您从黑板查询数据并将数据存储在黑板本身中。创建系统遵循与BehaviorTreeComponent类似的模式,如本章前面所述。
尽管它表面上看起来很简单,但理解黑板的工作原理对于确保您的 AI 代理有效运行至关重要。
摘要
在本章中,我们介绍了行为树系统。从简要的理论概述开始,我们了解了行为树在虚幻引擎中的工作方式,并学习了构成整个系统的关键组件。最后,我们讨论了黑板资产,这是行为树有效运行的关键元素。
准备迎接下一章,我们将引导你重新投入行动,并为你制作一个专门为我们的虚拟角色构建的行为树。具体来说,你将创建自己的自定义服务和任务,以便为即将到来的 AI 代理提供一个合适的 AI 大脑。做好准备吧,因为事情即将变得有趣而狂野——有时甚至令人愉快地混乱!
第八章:设置行为树
正确设置行为树对于在您的游戏中开发有效的 AI 系统至关重要。正如您在前一章所看到的,行为树作为定义 AI 角色逻辑和决策过程的有力工具,允许开发者以结构化和可管理的方式创建复杂的行为。本章将提供关于行为树实现的基本原理及其在虚幻引擎中内部工作的宝贵见解。
在本章中,我们将涵盖以下主题:
-
扩展 Unreal Agility Arena
-
创建行为树
-
实现行为树任务和服务
-
在代理上设置行为树
技术要求
要跟随本章内容,您需要使用本书配套仓库中提供的起始内容,该仓库位于github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5 。通过此链接,找到本章的相应部分并下载Unreal Agility Arena – 起始内容ZIP 文件。
如果您在阅读本章的过程中迷路了,在仓库中,您还可以找到最新的项目文件,位于Unreal Agility Arena – 第 08 章 结束。
此外,为了完全理解本章内容,您需要具备一些关于蓝图视觉脚本和 C++的基本知识;作为额外建议,您可能想浏览一下附录 A,在虚幻引擎中理解 C++,以获得对虚幻引擎中 C++语法的温和介绍(或复习)。
扩展 Unreal Agility Arena
要开始,让我们继续探索我们在第四章中介绍的短篇小说,设置导航网格:
随着马克斯博士和他的忠实助手维多利亚教授继续完善他们的 AI 木偶,他们遇到了一个有趣的挑战:木偶电池的有限电源供应。似乎先进的 AI 技术以惊人的速度消耗能量,导致木偶意外地关闭了电源。
尽管遭遇了这次挫折,马克斯博士看到了将这一限制转化为木偶行为独特方面的机会。他提出理论,认为当木偶的互动由日益减少的电池寿命提供动力时,将模仿人类的疲劳和疲惫。马克斯博士和维多利亚教授充满热情地制定了一个计划,围绕木偶有限的电源供应开展一系列新的实验。
在上一章学习了所有这些信息之后,现在是时候深入其中,开始制作您自己的 AI 代理,配备完整功能的行为树。为了保持简单和整洁,我将从一个全新的项目开始,但您也可以自由地继续开发之前章节中开始的工作。
首先,我将给你一些简要信息,告诉你将要创建的内容。
更新项目摘要
作为起点,你需要创建一个新的傀儡角色(前几章的傀儡功能过于有限),它需要实现一些基本逻辑。这将使我们能够在创建更高级的角色时扩展其基本功能。
主要要求如下:
-
AI 代理将使用 C++实现
-
它将具有两种不同的移动速度——步行和跑步
-
它将配备一个电池系统,在行走时会消耗能量,在静止时充电
-
它需要由一个自定义的AIController类来控制,该类将使用行为树
一旦角色创建完成,我们就可以开始创建新的健身房级别,以创建和测试新的 AI 代理行为。所以,让我们先创建项目。
创建项目
项目创建过程基本上与我们之前在第四章中介绍的过程相同,设置导航网格,所以我就不多详细介绍了;我们将做出的区别(这当然不是微不足道的)是包含 C++类。
幸运的是,这将是一个无缝的过渡,因为当你第一次创建新的 C++类时,虚幻引擎会为你设置整个系统。一旦生成了 C++项目文件,你应该能在你的内容浏览器窗口中看到C++类文件夹,以及内容文件夹。

图 8.1 – C++文件夹
注意
如果你的内容浏览器窗口中没有出现C++类文件夹,你可能需要打开设置窗口并勾选显示 C++类选项,如图图 8 .2 所示。

图 8.2 – 启用 C++文件夹
让我们开始创建角色类。
创建角色
我们将要做的第一件事是为我们预期的 AI 傀儡创建基础角色类。为此,请按照以下步骤操作:
- 从虚幻引擎编辑器的主菜单中,选择工具 | 新建 C++类 。

图 8.3 – 创建 C++类
- 从添加 C++类弹出窗口,选择角色选项并点击下一步 。

图 8.4 – 类选择
- 在以下窗口中,将BaseDummyCharacter输入到名称字段,其余保持不变。

图 8.5 – 类创建
- 点击创建类按钮开始类创建过程。
由于这是你创建的第一个类,虚幻引擎将开始生成 C++项目;之后,你的 IDE——很可能是 Visual Studio 或 Rider——将打开,你将能够开始开发你的类。
处理电池状态
在实现角色之前,我们需要定义如何跟踪其电池状态;这就是为什么我们将创建一个简单的枚举类来列出电池的充电量。为此,请按照以下步骤操作:
-
在你的 IDE 中,定位到UnrealAgilityArena/Source/UnrealAgilityArena文件夹。
-
在这个文件夹中,创建一个新的文本文件,并将其命名为BatteryStatus.h。
-
打开文件进行编辑。
在文件中添加以下代码:
#pragma once
UENUM(BlueprintType)
enum class EBatteryStatus : uint8
{
EBS_Empty = 0 UMETA(DisplayName = "Empty"),
EBS_Low = 1 UMETA(DisplayName = "Low"),
EBS_Medium = 2 UMETA(DisplayName = "Medium"),
EBS_Full = 3 UMETA(DisplayName = "Full")
};
我猜你已经熟悉枚举的作用,但在这里简要解释是必要的;我们正在定义一个enum类,在虚幻引擎中需要是uint8类型,并列出四个电池充电级别——Empty(空)、Low(低)、Medium(中)和Full(满)。UENUM()宏为虚幻引擎框架定义了一个enum类,BlueprintType指定器将使其对蓝图系统可用,使其可用于变量。DisplayName元数据定义了在蓝图系统中如何显示值;如果你愿意,可以在这里使用自己的约定。
最后,注意名称定义中的E前缀;这是虚幻引擎中枚举类型的命名约定,是强制性的。
在定义了电池状态后,我们就可以开始实现虚拟角色了。
实现角色
要开始实现 AI 代理,打开BaseDummyCharacter.h文件——即代理类的头文件——并开始添加类定义。
作为第一步,在文件顶部添加电池状态定义,那里设置了所有的#include定义:
#include "BatteryStatus.h"
注意
你将要添加的所有#include声明都需要放在.generated.h定义之前——在本例中是BaseDummyCharacter.generated.h。.generated.h定义始终需要在声明列表的末尾;这个约定确保所有必要的依赖项在编译时都得到正确设置。
声明属性
第二步是添加所有将在扩展蓝图类中公开的属性。要做到这一点,在文件的public部分添加以下声明:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float MaxBatteryLevel = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float BatteryCostPerTick = 5.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float BatteryRechargePerTick = 1.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float RunSpeed = 650.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float WalkSpeed = 500.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dummy Character")
float MovementRandomDeviation = 5.f;
在这里,我们声明了一系列变量来处理代理:
-
MaxBatteryLevel:表示代理电池可以达到的最大值
-
BatteryCostPerTick:表示代理在移动时消耗的电池电量
-
BatteryRechargePerTick:表示当代理休息时恢复的电池电量
-
RunSpeed:表示跑步时可以达到的最大速度
-
WalkSpeed:表示行走时可以达到的最大速度
-
MovementRandomDeviation:一个将被随机添加或减去以使移动节奏更不可预测的值
UPROPERTY()宏用于声明具有附加功能和元数据的类属性。它允许轻松集成到虚幻引擎编辑器中,提供了一个可视化界面来修改和配置这些属性。EditAnywhere属性指定符表示可以从虚幻引擎编辑器的属性窗口中编辑该属性,而BlueprintReadWrite指定符表示该属性将以读/写模式从扩展的蓝图类中访问。最后,我们希望所有属性都在同一个类别中——即Dummy Character——这就是为什么我们设置了Category属性指定符。
我们只需要一个额外的变量,但这个变量不需要是公共的,因为它将用于角色的内部逻辑。在受保护部分,让我们添加以下声明:
UPROPERTY()
float BatteryLevel;
这个一目了然的属性将用于跟踪代理的实际电池级别。
添加委托
现在我们需要创建一个用于电池状态更改通知的事件分发器;在 C++中,最好的方法是使用委托。
注意
如果你不太熟悉委托,我的建议是查看本书末尾的附录 A,Understanding C++ in Unreal Engine。
定位到公共部分,并添加以下代码片段:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBatteryStatusChanged, EBatteryStatus, NewBatteryStatus);
UPROPERTY(BlueprintAssignable, Category = "Dummy Character")
FOnBatteryStatusChanged OnBatteryStatusChanged;
我们已经声明了一个带有单个参数的动态多播委托——电池的新状态——每次电池改变充电级别时都会分发。
声明函数
我们需要在头文件中添加的最后一件事情是函数声明。作为第一步,删除文件末尾的以下代码行:
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
这个角色将由 AI 控制,所以我们不需要设置玩家输入。接下来,在Tick()声明之后,添加以下代码行:
UFUNCTION(BlueprintCallable, Category="Dummy Character")
void SetWalkSpeed();
UFUNCTION(BlueprintCallable, Category="Dummy Character")
void SetRunSpeed();
UFUNCTION(BlueprintCallable, BlueprintGetter, Category="Dummy Character")
EBatteryStatus GetBatteryStatus() const;
我们刚刚声明了两个函数——SetWalkSpeed()和SetRunSpeed()——这将允许我们在运行时更改角色速度。此外,我们还添加了一个用于代理电池状态的获取器函数。
在虚幻引擎中,UFUNCTION()宏用于声明由虚幻引擎反射系统识别的函数;这意味着函数在虚幻引擎框架内变得可访问和可用。所有三个函数都添加了BlueprintCallable指定符,这意味着这些函数将在蓝图图中可访问。此外,GetBatteryStatus()函数添加了const关键字;这将移除相应的蓝图节点中的执行引脚,因为我们只需要这个函数作为一个获取器,在执行过程中不改变任何数据。
实现函数
现在所有类声明都已经完成,我们可以开始实现函数。要做到这一点,你需要做的第一件事是打开BaseDummyCharacter.cpp文件。
您首先需要做的是移除SetPlayerInputComponent()函数,因为头文件中的相应声明之前已被移除。
接下来,我们需要在文件的非常开始处添加#include声明。只需添加以下三行代码:
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "BatteryStatus.h"
和往常一样,记得在.generated.h声明之前添加这些#include声明。
接下来,定位到ABaseDummyCharacter()构造函数,因为我们需要设置一些角色属性和组件。这个函数应该已经有一行代码将bCanEverTick属性设置为true。在其后添加以下代码行:
PrimaryActorTick.TickInterval = .25f;
由于我们将使用Tick()事件仅用于更新电池状态,我们不需要它在每一帧都执行;我们已设置了一个四分之一的秒的时间间隔——这应该足够满足我们的需求。
接下来,添加以下代码行来设置角色的偏航、俯仰和翻滚行为:
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
接下来,我们需要初始化骨骼网格组件以显示虚拟木偶模型。添加以下代码行:
GetMesh()->SetRelativeLocation(FVector(0.f, 0.f, -120.f));
GetMesh()->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
static ConstructorHelpers::FObjectFinder<USkeletalMesh>
SkeletalMeshAsset(TEXT("/Game/KayKit/PrototypeBits/Character/Dummy.Dummy"));
if (SkeletalMeshAsset.Succeeded())
{
GetMesh()->SetSkeletalMesh(SkeletalMeshAsset.Object);
}
GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
static ConstructorHelpers::FObjectFinder<UAnimBlueprint>
AnimBlueprintAsset(TEXT("/Game/KayKit/PrototypeBits/Character/ABP_Dummy.ABP_Dummy"));
if (AnimBlueprintAsset.Succeeded())
{
GetMesh()->SetAnimClass(AnimBlueprintAsset.Object->GeneratedClass);
}
在这里,我们设置网格位置和旋转以适应虚拟木偶模型。之后,我们通过硬编码资产路径来分配虚拟木偶骨骼网格资产;我们只使用这个资产,因此没有必要从扩展蓝图类中分配它。我们也会以相同的方式处理动画蓝图资产;我在项目文件中提供了这样一个资产,路径已在声明中给出。
现在,我们将设置胶囊组件的大小以匹配虚拟木偶模型。为此,添加以下代码行:
GetCapsuleComponent()->InitCapsuleSize(50.f, 120.0f);
最后,通过添加以下代码行来设置运动组件:
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->RotationRate = FRotator(0.f, 640.f, 0.f);
GetCharacterMovement()->bConstrainToPlane = true;
GetCharacterMovement()->bSnapToPlaneAtStart = true;
GetCharacterMovement()->AvoidanceConsiderationRadius = 2000.f;
GetCharacterMovement()->bUseRVOAvoidance = true;
注意到bUseRVOAvoidance的使用,设置为true;我们将同时使用几个代理,因此一个基本的避障系统几乎是强制性的,以确保事情能够正常工作。
构造函数方法已完成,因此我们现在可以开始实现所有其他函数。
定位到BeginPlay()方法,并在Super::BeginPlay()声明之后,添加以下代码行:
BatteryLevel = MaxBatteryLevel * FMath::RandRange(0.f, 1.f);
OnBatteryStatusChanged.Broadcast(GetBatteryStatus());
当游戏开始时,我们将 AI 代理的电池电设为随机值以使事情更有趣,然后,我们将此状态广播给所有已注册的监听器。
之后,在BeginPlay()函数的括号关闭后,添加以下代码段:
void ABaseDummyCharacter::SetWalkSpeed()
{
const auto Deviation = FMath::RandRange(-1.f * MovementRandomDeviation, MovementRandomDeviation);
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed + Deviation;
}
void ABaseDummyCharacter::SetRunSpeed()
{
const auto Deviation = FMath::RandRange(-1.f * MovementRandomDeviation, MovementRandomDeviation);
GetCharacterMovement()->MaxWalkSpeed = RunSpeed + MovementRandomDeviation;
}
这里没有什么特别的地方;我们只是实现了两个函数来改变代理的运动速度,使其行走或奔跑。
现在,我们需要实现电池状态获取函数,因此添加以下代码行:
EBatteryStatus ABaseDummyCharacter::GetBatteryStatus() const
{
const auto Value = BatteryLevel / MaxBatteryLevel;
if (Value < 0.05f)
{
return EBatteryStatus::EBS_Empty;
}
if (Value < 0.35f)
{
return EBatteryStatus::EBS_Low;
}
if (Value < 0.95f)
{
return EBatteryStatus::EBS_Medium;
}
return EBatteryStatus::EBS_Full;
}
如您所见,我们只是简单地检查电池电量并返回相应的状态枚举。
我们需要实现的是最后的 Tick() 函数,其中我们将不断检查根据角色移动速度消耗了多少电池电量。定位到 Tick() 函数,并在 Super::Tick(DeltaTime); 之后添加以下代码行:
const auto CurrentStatus = GetBatteryStatus();
if(GetMovementComponent()->Velocity.Size() > .1f)
{
BatteryLevel -= BatteryCostPerTick;
}
else
{
BatteryLevel += BatteryRechargePerTick;
}
BatteryLevel = FMath::Clamp<float>(BatteryLevel, 0.f, MaxBatteryLevel);
if (const auto NewStatus = GetBatteryStatus();
CurrentStatus != NewStatus)0
{
OnBatteryStatusChanged.Broadcast(NewStatus);
}
在这段代码中,我们使用 GetBatteryStatus() 函数计算当前电池状态。然后,如果角色的移动速度大于一个非常小的数值 – 即 0.1 – 这意味着代理正在移动,因此我们将电池电量减少 BatteryCostPerTick 的值。否则,代理处于静止状态 – 因此正在充电 – 所以我们将电池电量增加 BatteryRechargePerTick。之后,我们将电池电量限制在零和 MaxBatteryLevel 之间。最后,我们检查起始电池状态是否与新的电池状态不同,并最终使用 OnBatteryStatusChanged 代理广播新的电池状态。
BaseDummyCharacter 类已经完成。很明显,我们还没有将任何 AI 代理行为集成进去;这是故意的,因为我们计划通过 AIController 类来管理一切,这项任务我们将在下一节中完成。
创建行为树
在本节中,我们将为之前创建的代理创建一个完整功能的行为树。我们将遵循以下步骤:
-
创建 AI 控制器
-
创建黑板
-
创建行为树
让我们先创建一个 AIController 类的子类来控制我们的虚拟木偶。
创建 AI 控制器
现在,我们将创建一个扩展 AIController 的类,它将作为行为树的起点。要开始,打开 Unreal Engine 编辑器并执行以下步骤:
-
从主菜单选择 工具 | 新建 C++ 类。
-
点击 所有类 选项卡部分,查找 AIController。

图 8.6 – 创建 AI 控制器类
-
点击 下一步 按钮。
-
将类命名为 BaseDummyAIController 并点击 创建 类 按钮。
一旦创建了类文件并且你的 IDE 已经打开,查找 BaseDummyAIController.h 头文件并打开它。
编辑头文件
作为第一步,在 # include 声明之后添加对 BehaviorTree 类的前向声明:
class UBehaviorTree;
然后,在头文件的保护部分添加以下代码行:
UPROPERTY(EditAnywhere, BlueprintReadOnly,
Category = "Dummy AI Controller")
TObjectPtr<UBehaviorTree> BehaviorTree;
这个属性声明了一个指向行为树的指针,因此它可以从 类默认 蓝图面板 – 使用 EditAnywhere 指定符 – 进行赋值,并且可以从任何扩展蓝图中进行读取。
现在,就在这些代码行之后,添加以下内容:
virtual void OnPossess(APawn* InPawn) override;
当控制器拥有一个 Pawn 实例 – 以及我们的虚拟角色扩展了它 – 时,会调用 OnPossess() 函数,这是一个运行行为树的好地方。
实现控制器
控制器的实现相当简单;我们只需要在 AI 智能体被控制时运行行为树。要做到这一点,打开 内容抽屉 文件,并添加以下代码行:
void ABaseDummyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (ensureMsgf(BehaviorTree, TEXT("Behavior Tree is nullptr! Please assign BehaviorTree in your AI Controller.")))
{
RunBehaviorTree(BehaviorTree);
}
}
在这个函数中,首先调用基类 Super::OnPossess()。然后,我们使用 ensureMsgf() 宏确保 BehaviorTree 变量不是一个空指针。如果已经设置了行为树,我们使用 RunBehaviorTree() 函数运行它。
在设置好 AI 控制器后,我们可以开始实现实际的 AI 行为,从黑板开始。
创建黑板
创建黑板资产是一个简单的任务,一旦你知道你将跟踪哪些键。在我们的例子中,我们希望以下值对行为树可用:
-
一个目标位置向量,智能体将用它来在关卡中行走
-
一个布尔标志,当电池电量危险低时将警告智能体
-
一个布尔标志,将指示电池已耗尽
要开始,我们需要创建一个黑板资产。要做到这一点,请按照以下步骤进行:
-
打开 内容抽屉 并创建一个新文件夹,命名为 AI 。
-
打开文件夹,在 内容抽屉 上右键单击,选择 人工智能 | 黑板 以创建黑板资产。
-
将资产命名为 BB_Dummy 并双击它以打开它。

图 8.7 – 黑板面板
一旦打开黑板,你会看到已经有一个名为 SelfActor 的键;在本章中我们不会使用它,但我们会保留它,因为它通常用于存储对拥有者的引用。
根据本小节开头所述,我们需要创建三个键,因此我们将首先按照以下步骤进行:
- 单击 新建键 按钮,然后从下拉列表中选择 Vector 类型,如图 8.8 所示:

图 8.8 – 键创建
-
将新键命名为 TargetLocation 。
-
再次单击 新建键 按钮,选择 Bool 类型,并将新键命名为 IsLowOnBattery 。
-
再次单击 新建键 按钮,选择 Bool 类型,并将新键命名为 IsBatteryDepleted 。
完成这些步骤后,你的黑板应该与图 8.9 中所示的黑板非常相似:

图 8.9 – 完成的黑板
黑板已完成,我们现在可以开始为我们的 AI 智能体创建行为树。
创建行为树
与黑板一样,行为树作为项目资产创建。让我们首先执行以下步骤:
-
在 内容抽屉 中打开 AI 文件夹。
-
右键单击 内容抽屉 并选择 人工智能 | 行为树 。
-
将新创建的资产命名为 BT_RoamerDummy 并双击它以打开它。
一旦打开,你应该看到一个与图 8.10非常相似的图:

图 8.10 – 行为树创建
注意,根节点已经包含在图中。正如前一章所述,图中只能有一个这样的节点。此外,在详细信息面板中,你应该看到BB_Dummy黑板资产已经被分配给黑板资产。如果你没有看到分配,只需点击黑板属性下拉菜单并选择BB_Dummy资产;它应该是你项目中唯一的那种资产。
在本节中,我们逐步介绍了创建一个将利用行为树实现 AI 逻辑的角色。我们还成功创建了黑板资产和行为树资产,它们是我们角色 AI 行为的基础。由于实现我们角色所有 AI 逻辑的过程相当复杂,我们将在下一节中处理它。敬请期待如何有效地为我们的角色实现 AI 行为的详细说明。
实现行为树任务和服务
在创建行为树之前,对预期的实现有一个清晰的理解至关重要。AI 代理需要管理以下行为:
-
在关卡中四处游荡寻找目标位置
-
默认移动速度是跑步,但如果电池电量低,移动速度应切换为步行
-
一旦电池耗尽,它应该停止移动
-
一旦电池完全充电,它应该返回到移动模式
值得注意的是,电池耗尽和充电逻辑已经在BaseDummyCharacter类中实现,所以我们不需要担心它——我们只需要监听角色发出的事件并相应地行事。
正如我在前一章中提到的,虚幻引擎 AI 系统为我们的 AI 提供了一套全面的任务、服务和装饰器。然而,值得注意的是,这些内置组件可能无法满足你游戏中所有的特定需求。毕竟,作为开发者,我们享受着构建新事物以适应我们独特需求并增加游戏开发整体乐趣的创造性过程!
因此,在将节点添加到我们的图中之前,我们需要实现一系列任务和服务,这将使我们的虚拟木偶生活变得更简单。特别是,我们将创建以下内容:
-
一个将找到随机可达位置的任务
-
一个根据电池电量控制代理速度的服务
-
一个将监听电池状态变化的服务
备注
行为树节点既可以用 C++实现,也可以用蓝图实现;为了这本书的目的,我将坚持使用 C++选项,但在第九章,“扩展行为树”中,我会给你一些有用的提示,告诉你如何与蓝图类一起工作。
那么,让我们开始为我们的虚拟木偶实现新功能!
实现 FindRandomLocation 任务
我们将首先实现第一个节点,它将使 AI 代理能够在具有特定标签的水平中搜索一个随机演员。这个功能将允许代理有一个特定的点去导航,从而提高其到达目的地的精确度。
要开始实现这个节点,从 Unreal Engine 编辑器中,创建一个新的 C++类,扩展BTTaskNode,并将其命名为BTTask_FindRandomLocation。
打开BTTask_FindRandomLocation.h头文件并添加以下声明:
public:
UPROPERTY(EditAnywhere, Category="Blackboard")
FBlackboardKeySelector BlackboardKey;
UPROPERTY(EditAnywhere, Category="Dummy Task")
FName TargetTag;
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
BlackboardKey属性将用于声明找到随机位置后应该使用哪个键来存储,而TargetTag属性将用于在水平中查找所有可用的演员,以便随机选择。最后,当任务节点需要执行时,将调用ExecuteTask()函数,并将包含所有随机目标位置的逻辑。这个函数需要返回任务是否成功或失败。
现在,打开BTTask_FindRandomLocation.cpp文件,作为第一步,在文件本身顶部添加所需的#include声明:
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
接下来,添加ExecuteTask()函数的实现:
EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
const auto BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == nullptr) { return EBTNodeResult::Failed; }
TArray<AActor*> TargetList;
UGameplayStatics::GetAllActorsWithTag(GetWorld(), TargetTag, TargetList);
if(TargetList.Num() == 0) { return EBTNodeResult::Failed; }
const auto RandomTarget = TargetList[FMath::RandRange (0, TargetList.Num() - 1)];
BlackboardComp->SetValueAsVector(BlackboardKey.SelectedKeyName, RandomTarget->GetActorLocation());
return EBTNodeResult::Succeeded;
}
代码从OwnerComp获取Blackboard组件并检查其有效性。然后,它检索具有特定标签的演员列表,并从该列表中随机选择一个元素。然后,它使用SetValueAsVector()方法将选定的目标位置更新到 Blackboard 中。注意使用EBTNodeResult::Succeeded和EBTNodeResult::Failed来返回所有这些操作的结果;这是为了向行为树指示任务是否成功的一个要求。
现在我们已经完成了这个任务节点,我们可以继续下一步,这涉及到创建一个服务。
实现 SpeedControl 服务
现在,我们已经准备好创建我们的第一个自定义服务,该服务将根据电池充电量监控角色的速度。如您从上一章所记得,服务通常以固定间隔运行,这正是我们将为速度控制服务类所做的一一我们将检查电池状态并根据需要更改角色速度。
要开始实现这个类,从 Unreal Engine 编辑器中,创建一个新的 C++类,扩展BTService,并将其命名为BTService_SpeedControl。
打开BTTask_SpeedControl.h头文件,并在public部分添加以下声明:
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
我们将要重写的 TickNode() 函数将在此服务附加到的每个节点计时器间隔上执行。为了调用此函数,bNotifyTick 需要设置为 true;这个值默认已经设置,但如果你需要禁用它——我们将在下一个服务中实现这一点——了解这一点是好的。
我们已经准备好实现服务,所以打开 BT_Service_SpeedControl.cpp 文件,并在顶部添加以下 #include 声明:
#include "BaseDummyAIController.h"
#include "BaseDummyCharacter.h"
然后,添加 TickNode() 的实现:
void UBTService_SpeedControl::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
const auto AIController = Cast<ABaseDummyAIController>(OwnerComp. GetAIOwner());
if (!AIController) return;
const auto ControlledCharacter = Cast<ABaseDummyCharacter> (AIController->GetPawn());
if (!ControlledCharacter) return;
switch (ControlledCharacter->GetBatteryStatus())
{
case EBatteryStatus::EBS_Empty:
break;
case EBatteryStatus::EBS_Low:
ControlledCharacter->SetWalkSpeed();
break;
case EBatteryStatus::EBS_Medium:
case EBatteryStatus::EBS_Full:
ControlledCharacter->SetRunSpeed();
break;
}
}
这个函数相当直接;它所做的只是根据角色的电池状态更新受控角色的速度。
在这个阶段,你可能想知道为什么我们每次计时都检索 AI 控制器和角色引用,而不是将它们存储起来。从计算能力方面来看,这可能看起来效率不高,但重要的是要记住,行为树是一个共享资源。这意味着行为树的单个实例(及其节点)将为所有使用它的 AI 代理执行。因此,存储类引用不会带来任何优势,并可能导致不可预测的行为。
如果确实需要存储一个引用,你需要创建一个节点实例,这正是我们将要在即将到来的服务中做的。
实现 BatteryCheck 服务
我们即将创建的第二个服务将比之前的一个更具挑战性。我们需要持续监控电池状态的变化;最直接的方法是使用节点计时器来不断检查角色的电池状态,类似于 UBTService_SpeedControl 类的操作。然而,正如我们在本章早期所学的,虚拟角色会派发电池状态事件。那么,为什么不利用这个特性并加以利用呢?
让我们从实现这个服务开始;从虚幻引擎编辑器中,创建一个新的 C++ 类,扩展 BTService 并将其命名为 BTService_BetteryCheck。
一旦创建了文件,打开 BTService_BatteryCheck.h 文件,并在 #include 部分之后,添加以下前置声明:
class ABaseDummyCharacter;
enum class EBatteryStatus : uint8;
接着,添加 public 部分 和 构造函数声明:
public:
UBTService_BatteryCheck();
在此之后,声明 protected 部分,以及所有必要的属性:
protected:
UPROPERTY()
UBlackboardComponent* BlackboardComponent = nullptr;
UPROPERTY()
ABaseDummyCharacter* ControlledCharacter = nullptr;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Blackboard")
FBlackboardKeySelector IsLowOnBatteryKey;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Blackboard")
FBlackboardKeySelector IsBatteryDepletedKey;
如您所见,我们正在做一些与之前服务略有不同的事情;我们声明了对 Blackboard 组件和角色的引用。在这种情况下,我们将与节点实例一起工作,因此每个 AI 代理都将拥有由该服务装饰的独立节点实例。我们还声明了两个 Blackboard 键来分配适当的值到 Blackboard。
然后,添加以下函数:
virtual void OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void OnCeaseRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
UFUNCTION()
void OnBatteryStatusChange(EBatteryStatus NewBatteryStatus);
当装饰节点变得活跃时,将调用OnBecomeRelevant()函数,而OnCeaseRelevant()函数则不再活跃。我们将使用这两个函数通过OnBatteryStatusChange()函数注册电池状态事件并相应地做出反应。
你现在可以打开BTService_BatteryCheck.cpp文件开始实现函数。作为第一步,在文件顶部添加所需的#include声明:
#include "BaseDummyAIController.h"
#include "BaseDummyCharacter.h"
#include "BatteryStatus.h"
#include "BehaviorTree/BlackboardComponent.h"
紧接着,添加构造函数实现:
UBTService_BatteryCheck::UBTService_BatteryCheck()
{
bCreateNodeInstance = true;
bNotifyBecomeRelevant = true;
bNotifyCeaseRelevant = true;
bNotifyTick = false;
}
虽然看起来我们只是在设置一些标志,但实际上我们正在对这个服务的功能进行重大更改。首先,我们创建一个节点实例;每个 AI 代理都将拥有这个服务的独立实例。接下来,我们禁用服务计时器,因为我们不需要它,然后激活相关性行为。
接下来,让我们实现OnBatteryStatusChange()方法:
void UBTService_BatteryCheck::OnBatteryStatusChange(const EBatteryStatus NewBatteryStatus)
{
switch (NewBatteryStatus)
{
case EBatteryStatus::EBS_Empty:
BlackboardComponent->SetValueAsBool(IsBatteryDepletedKey. SelectedKeyName, true);
break;
case EBatteryStatus::EBS_Low:
BlackboardComponent->SetValueAsBool(IsLowOnBatteryKey. SelectedKeyName, true);
BlackboardComponent->SetValueAsBool(IsBatteryDepletedKey. SelectedKeyName, false);
break;
case EBatteryStatus::EBS_Medium:
break;
case EBatteryStatus::EBS_Full:
BlackboardComponent->SetValueAsBool(IsLowOnBatteryKey. SelectedKeyName, false);
break;
}
}
这里没有太多花哨的东西;我们只是根据新的电池状态设置 Blackboard 键。之后,我们实现剩下的两个函数:
void UBTService_BatteryCheck::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::OnBecomeRelevant(OwnerComp, NodeMemory);
BlackboardComponent = OwnerComp.GetBlackboardComponent();
const ABaseDummyAIController* AIController = Cast<ABaseDummyAIController>(OwnerComp.GetAIOwner());
if (!AIController) return;
APawn* ControlledPawn = AIController->GetPawn();
if (!ControlledPawn) return;
ControlledCharacter = Cast<ABaseDummyCharacter>(ControlledPawn);
if (!ControlledCharacter) return;
ControlledCharacter->OnBatteryStatusChanged.AddDynamic (this, &UBTService_BatteryCheck::OnBatteryStatusChange);
}
void UBTService_BatteryCheck::OnCeaseRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::OnCeaseRelevant(OwnerComp, NodeMemory);
ControlledCharacter->OnBatteryStatusChanged.RemoveDynamic (this, &UBTService_BatteryCheck::OnBatteryStatusChange);
}
这两个函数注册和注销OnBatteryStatusChanged委托;此外,OnBecomeRelevantFunction()保存对 Blackboard 组件和 AI 控制器的引用——我们可以这样做,因为我们使用实例节点来处理这个服务。
在这个广泛的章节中,你获得了在 C++中创建自定义任务和服务的知识。通常,Unreal Engine 提供的预构建类可能不足以创建引人入胜的 AI 行为。因此,开发你自己的独特节点变得至关重要。在接下来的章节中,我们将创建这样的新类来构建一个完全运行的 AI 代理。
在代理上设置行为树
要深入了解 AI 代理行为树,第一步是编译整个项目。一旦过程完成,你的自定义任务和服务将作为选项出现在行为树中。
注意
如果你对 Unreal Engine 的编译过程不熟悉,我的建议是先看看附录 A,在 Unreal Engine 中理解 C++,然后再回到这一章。
一旦编译阶段完成,我们就可以开始编辑行为树了。
编辑行为树
打开我们之前创建的BT_RoamerDummy资产,并定位图中唯一的元素——根节点;你会看到它在底部有一个较暗的区域。

图 8.11 – 根节点
从这个区域点击并拖动将使所有可以连接到根节点的节点变得可用。
注意
在接下来的内容中,每当我提到添加节点的任务时,我都会要求你执行上述操作。图 8.12就是一个这样的例子。

图 8.12 – 添加节点
要开始,请执行以下步骤:
-
将一个复合 | 序列节点添加到根节点,并在详细信息面板中将其重命名为根序列。
-
从根序列节点,添加一个任务 | FindRandomLocation节点。
-
在选中新创建的节点后,在详细信息面板中,将黑板键下拉值设置为TargetLocation。
-
从根序列节点,在FindRandomLocation节点的右侧添加一个复合 | 选择器节点。
您的图表现在应该类似于图 8.13所示:

图 8.13 – 初始行为树
我们现在需要装饰选择器节点以赋予它额外的能力。为此,请按照以下步骤操作:
-
右键点击选择器节点并选择添加装饰器 | 条件循环。
-
再次右键点击并选择添加服务 | 电池检查。
-
点击条件循环装饰器,在详细信息面板中,将黑板键属性下拉设置为TargetLocation。键查询属性应保留为已设置。
-
点击电池检查服务,在详细信息面板中执行以下操作:
-
将电池电量低键下拉值设置为IsLowOnBattery。
-
将电池耗尽键下拉值设置为IsBatteryDepleted。
选择器节点现在应该看起来类似于图 8.14:
-

图 8.14 – 装饰后的选择器节点
注意,BatteryCheck服务应该显示一个永不触发注释;这是我们实现 C++类时设置的。
我们到目前为止所做的是 AI 代理行为的主要循环的基本内容;我们首先为 AI 代理找到一个目标位置,然后执行一个等待电池状态通知的选择器节点。条件循环装饰器将一直重复子节点(尚未添加),直到TargetLocation键被设置。
现在,我们将关注选择器节点,并执行以下操作:
-
添加一个复合 | 序列子节点,并将其重命名为漫游序列。
-
添加一个任务 | 等待子节点,并在其详细信息面板中执行以下操作:
-
将等待时间值设置为8.0
-
将随机偏差值设置为2.0
-
将节点名称设置为充电电池
我们需要为漫游序列节点添加额外的功能,所以我们执行以下操作:
-
-
右键点击漫游序列节点,选择添加装饰器 | 黑板
-
在选择装饰器后,在详细信息面板中执行以下操作:
-
将通知观察者下拉值设置为值更改时开启
-
将观察者中止下拉值设置为自身
-
将键查询下拉值设置为未设置
-
将黑板键下拉值设置为IsBatteryDepleted
图表的这部分现在应该看起来像图 8.15:
-

图 8.15 – 漫游循环
这部分图表将在两个阶段之间不断循环 – 一个漫游序列和一个 等待 节点。AI 代理将保持在漫游序列中,直到电池耗尽。之后,它将静止 6 到 10 秒(以便电池充电),然后返回漫游。
我们需要执行的最后一个步骤是实现漫游节点。为此,从 Roam Sequence 节点,执行以下操作:
-
添加一个 Tasks | Move To 节点,并在 详细信息 面板中,将 黑板键 下拉值设置为 TargetLocation。
-
添加一个 Tasks | FindRandomLocation 节点,并在 详细信息 面板中,将 目标标签 值设置为 TargetPoint。
-
添加一个 Tasks | Wait 节点,保留其属性为默认值。
-
我们需要向 Move To 节点添加额外的功能,因此右键单击它,选择 添加服务 | 速度控制。这部分图表应类似于 图 8 .16:

图 8.16 – 漫游序列
我们在这里做的事情相当简单;我们试图到达目标点,一旦到达,就寻找另一个点,然后通过等待一个随机的时间间隔来获得一些应得的休息。在移动过程中,我们不断检查电池状态,相应地改变 AI 代理的速度。
好消息!行为树图已经完成。现在,我们可以将其附加到我们的虚拟木偶上,并观察其行为。然而,在我们继续之前,我们需要创建一些合适的 Blueprint 来确保一切顺利运行。
创建 AI 代理 Blueprint
当使用 Unreal Engine 时,从 C++ 类创建 Blueprint 被认为是良好的实践。这种方法提供了诸如灵活性和可扩展性等优势,有助于提高开发效率。这就是为什么我们将从之前创建的类中创建一些 Blueprint 的原因。
创建控制器 Blueprint
让我们从将 BaseDummyAIController 类扩展到 Blueprint 开始。为此,在您的 内容浏览器 中创建一个新的文件夹,命名为 Blueprints,然后按照以下步骤操作:
-
创建一个新的从 BaseDummyAIController 派生的 Blueprint 类,并将其命名为 AIRoamerDummyController。
-
打开它,在 类默认值 面板中,查找 Dummy AI Controller 类别,并将 行为树 属性设置为 BT_RoamerDummy。

图 8.17 – 分配行为树
这就是设置漫游虚拟角色 AI 控制器的全部内容;我们现在将创建专门的角色。
创建角色 Blueprint
要创建一个角色 Blueprint,请按照以下步骤操作:
-
在您的内容浏览器的蓝图文件夹中创建一个新的蓝图类,从BaseDummyCharacter派生,并将其命名为BP_RoamerDummyCharacter。
-
打开它,在类默认值面板中,查找位于Pawn类别的AI 控制器类属性。从下拉菜单中选择AIRoamerDummyController。
恭喜你创建了自己的漫游代理!现在,我们只需添加一个最后的细节,使其更加完美 – 一个电池指示器。
添加外观
为了提供对 AI 代理状态的视觉反馈,我们将创建一个组件,通过灯光显示电池充电水平。这个视觉指示器将根据当前的充电水平调整灯光的强度。充电越高,灯光越亮。这将使用户能够轻松地一眼判断代理的电池状态,增强整体用户体验,并确保他们了解代理的电力水平。
让我们首先创建一个新的 C++类,从StaticMeshComponent类继承,命名为BatteryInjdicatorComponent。一旦创建了类,打开BatteryIndicatorComponent.h文件,并将UCLASS()行替换为以下内容:
UCLASS(BlueprintType, Blueprintable, ClassGroup="Unreal Agility Arena", meta=(BlueprintSpawnableComponent))
这将使组件对蓝图可用。然后,在GENERATED_BODY()代码行之后,添加以下代码:
public:
UBatteryIndicatorComponent();
protected:
UPROPERTY()
UMaterialInstanceDynamic* DynamicMaterialInstance;
virtual void BeginPlay() override;
UFUNCTION()
void OnBatteryStatusChange(EBatteryStatus NewBatteryStatus);
这里需要解释的只有DynamicMaterialInstance属性,它将被用来改变材质强度属性,使其更亮或更暗,以及OnBatteryStatusChange()函数,它将被用来处理电池状态改变事件。
现在,要开始实现组件,打开BatteryIndicatorComponent.cpp文件,并在其顶部添加以下声明:
#include "BaseDummyCharacter.h"
构造函数只需设置静态网格资产,因此添加以下代码:
UBatteryIndicatorComponent::UBatteryIndicatorComponent()
{
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(
TEXT("/Game/_GENERATED/MarcoSecchi/SM_HeadLight.SM_ Headlight"));
if (StaticMeshAsset.Succeeded())
{
UStaticMeshComponent::SetStaticMesh(StaticMeshAsset.Object);
}
}
接下来,通过添加以下代码块实现BeginPlay()函数:
void UBatteryIndicatorComponent::BeginPlay()
{
Super::BeginPlay();
ABaseDummyCharacter* Owner = Cast<ABaseDummyCharacter>(GetOwner());
if(Owner == nullptr) return;
AttachToComponent(Owner->GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, "helmet");
DynamicMaterialInstance = this->CreateDynamicMaterialInstance (1, GetMaterial(1));
Owner->OnBatteryStatusChanged.AddDynamic (this, &UBatteryIndicatorComponent::OnBatteryStatusChange);
}
在这个函数中,我们检查此组件的所有者是否是BaseDummyCharacter类的实例。接下来,我们将此组件附加到所有者的名为helmet的网格组件插座上 – 这是我已经在虚拟骨骼网格中为你提供的插座,如图 8 .18 所示:

图 8.18 – 头盔插座
之后,我们为这个组件创建一个动态材质实例 – 这将允许我们在运行时修改材质属性。最后,我们向拥有者的OnBatteryStatusChanged事件添加一个事件处理器,每当拥有者的电池状态改变时,都会调用此组件的OnBatteryStatusChange函数。
函数完成后,我们只需将事件处理器添加到我们的代码中:
void UBatteryIndicatorComponent::OnBatteryStatusChange(EBatteryStatus NewBatteryStatus)
{
const auto BatteryValue = StaticCast<float>(NewBatteryStatus);
const auto Intensity = (BatteryValue - 1.f) * 25.f;
DynamicMaterialInstance->SetScalarParameterValue(FName ("Intensity"), Intensity);
}
在这里,我们将NewBatteryStatus枚举转换为float值,计算光强度,然后在动态材质实例中设置一个标量参数,Intensity。
我们终于可以编译项目,使这个组件对角色可用。一旦编译过程完成,打开BP_RoamerDummy蓝图并执行以下操作:
-
在组件面板中,点击+ 添加按钮。
-
选择UnrealAgilityArena | Battery Indicator将此组件添加到角色中。

图 8.19 – 添加组件
您的虚拟角色已经设置完毕,并急切地等待在合适的健身房级别进行测试。
在健身房测试一个代理
我们现在准备好为我们的 AI 代理创建一个健身房,并观察它的行为。如您在第四章中已知的,设置导航网格,如何正确设置带有导航网格的健身房,我不会深入创建细节。相反,我将为您提供一些关于关卡创建的通用信息。以下是您应该做的事情:
-
创建您选择的关卡,从项目模板中提供的关卡实例和打包关卡演员开始
-
添加一个NavMeshBoundsVolume演员,使其覆盖所有可通行区域
-
添加一些障碍物使事情更有趣
-
添加一个或多个BP_RoamerDummyCharacter实例
-
添加一些作为目标点的NS_Target Niagara 演员
值得注意的是,您的 AI 代理将寻找带有TargetPoint标签的目标点。如果您不熟悉标签系统,以下是如何为您的演员添加标签的方法:
-
对于每个NS_Target Niagara 系统,在详细信息面板中搜索标签属性。
-
点击+按钮添加一个新的标签。
-
在将创建的索引[0]字段中,插入TargetPoint,如图图 8.20所示。

图 8.20 – 演员标签
一旦您的关卡完成,您就可以开始测试它;我的在图 8.21中显示:

图 8.21 – 完成的关卡
在开始模拟时,您应该看到以下事情发生:
-
根据起始电池电量(这是随机的),您的 AI 代理将开始奔跑、行走或静止不动
-
它们将尝试到达一个目标点,一旦到达,它们将休息大约一秒钟,然后寻找另一个目标点
-
如果电池电量低,它们将开始步行而不是跑步
-
如果电池电量耗尽,它们将停止,开始充电,然后再次开始运行
-
当 AI 代理电量充足时,前灯应该更亮,当电池电量低时关闭
这部分内容到此结束,您已经学习了如何构建一个完全工作的行为树,包括创建您定制的任务和服务。
摘要
在这个相当长的章节中,你被介绍了在虚幻引擎中创建行为树的基础知识。正如你所学的,创建一个完全工作的 AI 代理是现成功能、自定义类和一点独创性的混合体。
我们刚刚开始一段迷人的旅程,这个旅程将在接下来的章节中展开,从下一章开始,我们将深入探讨任务、服务和装饰器的复杂运作。准备好对你的心爱的小木偶进行一次重大的改造吧!
第九章:扩展行为树
理解行为树细微差别对于游戏开发者至关重要,因为它们使他们能够牢固掌握创建响应性和更具吸引力的 AI 角色。这就是为什么在本章中,我们将深入探讨 Unreal Engine 行为树系统的内部运作和最佳实践;我们将详细解释其功能,特别是如何为我们的 AI 代理创建更复杂的自定义任务、服务和装饰器。此外,我们还将回顾调试工具,看看如何在运行时分析行为树。
在本章中,我们将涵盖以下主题:
-
展示编写行为树的最佳实践
-
理解装饰器
-
理解服务
-
理解任务
-
调试行为树
技术要求
要跟随本章介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。
此外,如果你希望从本书的配套仓库开始编写代码,你可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。
要下载最后一章末尾的文件,请点击Unreal Agility Arena – 第八章 - 结束链接。
展示编写行为树的最佳实践
看来我们的小小说有了新的一章:
在他们测试 AI 木偶能力的过程中,马克斯博士和维克托利亚教授决定给他们配备飞镖枪。这个想法是创造一个有趣且引人入胜的场景,让木偶能够展示他们新获得的射击技巧。
在秘密实验室里,气氛充满了兴奋,因为木偶现在装备了非致命武器,准备接受挑战。凭借他们先进的 AI 编程和传感器系统,木偶能够分析环境,计算弹道,并以惊人的准确性瞄准目标。
当为你的 AI 代理设计行为树时,掌握最佳实践至关重要。此外,了解视频游戏行业中 AI 领域的最新进展和研究将提供有价值的见解,并指导你在设计更好的行为树时的决策过程。
列出最佳实践
在本小节中,我将为你提供一些宝贵的建议来优化和增强你 AI 的参与度;其中一些来自知名模式,而另一些则源于我个人的经验——大多数情况下是通过试错过程获得的。
使用适当的命名约定
在行为树中为新创建的任务、装饰器或服务正确命名是一种良好的做法。使用表明资产类型的命名约定,无论是任务、装饰器还是服务。作为一个经验法则,你应该使用以下前缀:
-
BTTask_ 用于任务
-
BTDecorator_ 用于装饰器
-
BTService_ 用于服务
这不仅可以帮助你清楚地了解你创建的资产类型,你还会得到一个有益的副作用;在行为树图中,系统将识别类类型,并将前缀移除,仅显示节点名称。使用不正确的命名约定会导致在尝试选择节点图中的节点时出现不规律的行为。
例如,在第八章,“设置行为树”中,你可能已经注意到我们给电池检查服务命名为BTService_BatteryCheck,但在行为树图中,它仅显示为电池检查。
给你的节点一个有意义的名称
行为树任务、装饰器和服务都有一个名为NodeName的属性,它将用于在图中显示节点名称;如果你想给你的节点一个不同于类名的名称,请使用它。
避免直接更改节点属性
而不是直接更改行为树中节点的属性,你应该利用黑板的力量并更改其键。或者,你也可以调用角色内的函数,对键进行必要的修改。这将有助于保持更干净、更有组织的结构。
例如,我们之前创建的虚拟木偶通过角色和一些专门的黑板键处理电池逻辑。
考虑性能优化
Unreal Engine 中的行为树系统在本质上进行了优化,因为它避免在每一帧评估整个树,而是依赖于成功和失败通知来确定下一个要执行的节点。然而,重要的是要谨慎行事,不要过度依赖某些功能,假设它们会自动按预期工作。
例如,在我们之前创建的电池检查服务中,我们禁用了 tick 间隔,利用了代理的力量。
使用模块化设计
尝试将复杂的行为分解成更小、可重用的模块,如任务、服务和装饰器。这种模块化方法将使维护和更新行为树变得更加容易。
例如,为了创建更复杂的行为,尝试检查第八章,“设置行为树”中实现的小任务Roam Sequence。
不要假设你的角色将是一个 AI 代理
在开发 AI 角色时,你可能倾向于直接在Character类中添加 AI 逻辑。然而,通常建议避免这种做法。AI Controller类已被专门设计来服务于特定目的,提供了一种更结构化和模块化的方法来监督角色的 AI 行为。通过利用AI Controller类,你可以有效地将 AI 逻辑与角色本身分离,从而提高可维护性并能够独立更新 AI 行为。
例如,在运行时能够在 AI 控制器和玩家控制器之间切换的能力可以提供几个优势,例如允许玩家控制 AI 角色。
作为一条经验法则,建议以允许玩家和 AI 可以相互切换控制的方式创建你的Pawn和Character类。
在本章的后面部分,我们将考虑到这一点创建一个新的 AI 代理。
经常调试和测试
定期测试和调试你的行为树以确保其按预期工作。使用由虚幻引擎提供的调试工具来识别和解决任何问题。
到本章结束时,我将向你展示如何正确使用调试工具与你的行为树。
现在你已经了解了一些关于如何有效地创建 AI 代理的信息,是时候回到我们的项目并开始制作一个新的角色了!让我们开始吧!
实现枪手角色逻辑
作为起点,我们将创建一个新的 AI 角色,并添加一些额外功能;特别是,它将具有一些射击目标的能力——通过非致命的 Nerf 枪。如前所述,当你在角色上工作时,优先考虑简单性和模块化是很重要的。在这种情况下,将射击逻辑集成到基础角色中是没有意义的。相反,利用虚幻引擎组件提供的功能将更为有效。这样做可以将射击功能保持为独立和模块化,从而在项目中提供更好的组织和灵活性。最终结果,AI 代理将能够通过使用 AI 行为射击目标。
创建 BaseWeaponComponent 类
要为我们的虚拟角色创建武器组件,我们首先将扩展StaticMeshComponent类;这将提供一个良好的起点——网格——我们只需要添加附加逻辑和射击逻辑。
要开始创建此组件,从虚幻引擎中创建一个新的 C++类,从StaticMeshComponent扩展,并将其命名为BaseWeaponComponent。
一旦创建了类,打开BaseWeaponComponent.h文件,并用以下代码行替换UCLASS()宏:
UCLASS(BlueprintType, Blueprintable, ClassGroup="UnrealAgilityArena",
meta=(BlueprintSpawnableComponent))
这将使组件对蓝图可用,并且你可以直接将其附加到蓝图类。
现在,在类内部,就在GENERATED_BODY()宏之后,添加以下声明:
public:
UBaseWeaponComponent();
UFUNCTION(BlueprintCallable)
virtual void Shoot();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bullet")
TSubclassOf<AActor> BulletClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bullet")
FVector MuzzleOffset = FVector(150, 30.f, 0.f);
protected:
virtual void BeginPlay() override;
在这里,我们声明了构造函数,一个名为Shoot()的函数,它将生成子弹;然后,我们声明了生成的子弹的BulletClass属性和MuzzleOffset以精确放置子弹生成点。最后,在protected部分,我们需要BeginPlay()声明来添加一些初始化。
现在,我们已经准备好实现组件,所以打开BaseWeaponComponent.cpp文件,并在它的最顶部添加此行代码:
#include "BaseDummyCharacter.h"
紧接着,添加类构造函数:
UBaseWeaponComponent::UBaseWeaponComponent()
{
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(
TEXT("/Game/KayKit/PrototypeBits/Models/Gun_Pistol.Gun_ Pistol"));
if (StaticMeshAsset.Succeeded())
{
UStaticMeshComponent::SetStaticMesh(StaticMeshAsset.Object);
}
}
此函数相当简单直接,因为我们只是为组件声明了一个默认网格——一个 Nerf 手枪手枪;你将在以后使用蓝图扩展此类时可以自由更改它;
之后,添加BeginPlay()实现:
void UBaseWeaponComponent::BeginPlay()
{
Super::BeginPlay();
const auto Character = Cast<ABaseDummyCharacter>(GetOwner());
if(Character == nullptr) return;
AttachToComponent(Character->GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, "hand_right");
}
此函数尝试将组件的所有者转换为Character对象,如果转换成功,则将组件附加到hand_right插座上的角色网格,以便枪保持在角色的右手;我已经在Dummy骨骼网格中为你提供了这样的插座,如图图 9 .1 所示:

图 9.1 – 手柄插座
作为最后一步,现在是时候实现Shoot()函数逻辑了,所以添加以下代码行:
void UBaseWeaponComponent::Shoot()
{
if (BulletClass == nullptr) return;
auto const World = GetWorld();
if (World == nullptr) return;
const FRotator SpawnRotation = GetOwner()->GetActorRotation();
const FVector SpawnLocation = GetOwner()->GetActorLocation() + SpawnRotation.RotateVector(MuzzleOffset);
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDont SpawnIfColliding;
World->SpawnActor<AActor>(BulletClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
}
此函数检查BulletClass是否有效,获取World的引用——代表地图引用的最高级对象——然后根据所有者的位置计算生成位置和旋转,最后使用BulletClass在计算的位置和旋转处生成一个 actor。
组件现在已准备就绪;下一步将是创建合适的子弹以在正确的时间射击。
创建 BaseBullet 类
在创建了生成子弹的武器组件之后,下一步的逻辑步骤是创建一个可生成的子弹。这将非常直接;我们将创建一个带有网格的对象,它会向前移动,对它所击中的任何东西造成伤害。让我们先创建一个新的 C++类,扩展Actor并命名为BaseGunBullet;在类创建完成后,打开BaseGunBullet.h头文件,并在#include部分之后添加以下前置声明:
class USphereComponent;
class UProjectileMovementComponent;
class UStaticMeshComponent;
之后,更改UCLASS()宏,使这个类成为创建蓝图的可接受基类:
UCLASS(Blueprintable)
现在,就在GENERATED_BODY()宏之后,添加所需组件声明:
UPROPERTY(VisibleDefaultsOnly, Category="Projectile")
USphereComponent* CollisionComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Projectile", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* MeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement", meta=(AllowPrivateAccess="true"))
UProjectileMovementComponent* ProjectileMovementComponent;
下一步是声明public函数——即构造函数和组件的获取器——所以添加以下代码行:
public:
ABaseGunBullet();
USphereComponent* GetCollision() const { return CollisionComponent; }
UProjectileMovementComponent* GetProjectileMovement() const { return ProjectileMovementComponent; }
UStaticMeshComponent* GetMesh() const { return MeshComponent; }
在protected部分,移除BeginPlay()声明,因为它将不再需要。相反,我们需要子弹碰撞事件的OnHit()处理程序:
protected:
virtual void BeginPlay() override;
UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
现在头文件已经完成,是时候开始实现类了,所以打开BaseGunBullet.cpp。作为第一步,在文件顶部添加所需的#include声明:
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/DamageEvents.h"
接下来,移除BeginPlay()实现,正如我之前所说的,它将不再需要。之后,添加构造函数实现:
ABaseGunBullet::ABaseGunBullet()
{
PrimaryActorTick.bCanEverTick = false;
InitialLifeSpan = 10.0f;
CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
CollisionComponent->InitSphereRadius(20.0f);
CollisionComponent->BodyInstance. SetCollisionProfileName("BlockAll");
CollisionComponent->OnComponentHit.AddDynamic(this, &ABaseGunBullet::OnHit);
CollisionComponent->SetWalkableSlopeOverride (FWalkableSlopeOverride(WalkableSlope_Unwalkable, 0.f));
CollisionComponent->CanCharacterStepUpOn = ECB_No;
RootComponent = CollisionComponent;
ProjectileMovementComponent =
CreateDefaultSubobject<UProjectileMovementComponent>(TEXT ("Projectile"));
ProjectileMovementComponent->UpdatedComponent = CollisionComponent;
ProjectileMovementComponent->InitialSpeed = 1800.f;
ProjectileMovementComponent->MaxSpeed = 1800.f;
ProjectileMovementComponent->bRotationFollowsVelocity = true;
ProjectileMovementComponent->ProjectileGravityScale = 0.f;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
MeshComponent->SetupAttachment(RootComponent);
MeshComponent->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
MeshComponent->SetRelativeScale3D(FVector(2.f, 2.f, 2.f));
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(
TEXT("/Game/KayKit/PrototypeBits/Models/Bullet.Bullet"));
if (StaticMeshAsset.Succeeded())
{
MeshComponent->SetStaticMesh(StaticMeshAsset.Object);
}
}
上述大部分代码之前已经讨论过或已解释清楚,尽管还有一些重要的事情需要说明。CollisionComponent的碰撞配置文件名称已被设置为BlockAll,以便获得适当的碰撞;此外,我们将OnComponentHit委托绑定到OnHit()方法,以便对任何子弹碰撞做出反应。
现在,我们可以添加最终的方法实现,它将处理子弹击中任何对象的情况:
void ABaseGunBullet::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
if (OtherActor != nullptr && OtherActor != this)
{
const auto DamageEvt = FDamageEvent();
OtherActor->TakeDamage(1.f, DamageEvt, nullptr, nullptr);
}
Destroy();
}
如您所见,我们只是调用被击中的Actor对象的TakeDamage()方法,然后销毁子弹。在这个游戏中,不需要担心伤害参数!它们不是本书的重点,所以您可以自由添加自己的伤害逻辑并坚持使用。请随意根据您的喜好定制游戏!
现在我们已经完成了子弹类的最终版本,是时候为它创建一个合适的靶子了。
实现目标类
我们现在需要创建一个基类 actor,我们将用它来实现射击 AI 代理的目标。所以,让我们首先创建一个新的 C++类,从Actor扩展,并命名为BaseTarget。一旦创建了类,就打开BaseTarget.h头文件。
作为第一步,在#include部分之后添加以下前置声明:
class UStaticMeshComponent;
接下来,移除BeginPlay()和Tick()函数,因为它们将不再需要,并在ABaseTarget()构造函数之后添加以下声明:
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Projectile", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* MeshComponent;
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
除了将用于显示目标网格的StaticMeshComponent属性外,我们还添加了用于处理子弹击中的TakeDamage()声明。
现在,打开BaseTarget.cpp文件,在移除BeginPlay()和Tick()函数实现后,用以下代码替换构造函数:
ABaseTarget::ABaseTarget()
{
PrimaryActorTick.bCanEverTick = false;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
MeshComponent->SetupAttachment(RootComponent);
MeshComponent->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
RootComponent = MeshComponent;
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(
TEXT("/Game/KayKit/PrototypeBits/Models/target_stand_B_target_ stand_B.target_stand_B_target_stand_B"));
if (StaticMeshAsset.Succeeded())
{
MeshComponent->SetStaticMesh(StaticMeshAsset.Object);
}
}
到现在为止,您应该已经熟悉了之前的代码;在禁用此 actor 的 tick 功能后,我们继续添加并初始化一个StaticMesh组件,它将在我们的项目中用于显示目标。
现在,将TakeDamage()实现添加到您的文件中:
float ABaseTarget::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator,
AActor* DamageCauser)
{
Tags[0] = "Untagged";
return DamageAmount;
}
如您所见,这里的方法相当简单;我们只是使用了未标记关键字,这将使分配给父对象的标签失效。这实际上使得它对我们将在本章后面构建的行为树任务不可见。我们不需要担心伤害逻辑;一旦目标被标记,它就会失效。
现在所有基类都已创建,我们准备实现所需的蓝图,包括一个全新的枪手角色。
创建蓝图
我们将继续创建将有助于生成新 AI 枪手代理的蓝图。具体来说,我们将专注于以下组件:
-
可生成的子弹
-
目标
-
枪手角色本身
让我们从创建子弹蓝图开始。
实现子弹蓝图
创建枪弹蓝图的过程相当直接。只需遵循以下步骤:
-
在虚幻引擎内容抽屉中打开蓝图文件夹。
-
右键点击并选择蓝图类。
-
在所有类部分,选择BaseGunBullet。
-
将新资产命名为BP_GunBullet。图 9.2显示了最终的蓝图类:

图 9.2 – 枪弹蓝图
实现目标蓝图
目标蓝图创建几乎与子弹蓝图相同;我们只需向对象添加一个标签。执行以下步骤:
-
在虚幻引擎内容抽屉中打开蓝图文件夹。
-
右键点击并选择蓝图类。
-
在所有类部分,选择BaseTarget。
-
将新资产命名为BP_Target。
-
在细节面板中,查找Actor | 高级类别中的Tags属性,并点击+按钮创建一个新的标签。
-
将标签命名为ShootingTarget,如图图 9.3所示:

图 9.3 – 目标标签
图 9.4显示了最终的蓝图类:

图 9.4 – 目标蓝图
实现枪手角色蓝图
现在是创建枪手角色蓝图的时候了。您已经熟悉这个过程,但以下是需要遵循的步骤:
-
在虚幻引擎内容抽屉中打开蓝图文件夹。
-
右键点击并选择蓝图类。
-
在所有类部分,选择BaseDummyCharacter。
-
将新资产命名为BP_GunnerDummy。
我们将在本章后面添加 AI 控制器,但我们需要在蓝图类中更改一个值,以便我们的角色能够正确工作。因此,打开此蓝图,在细节面板中,定位到Pawn类别中的Use Controller Rotation Yaw属性;这将允许在稍后使用 AI 逻辑时正确旋转角色。勾选该值,如图图 9.5所示:

图 9.5 – 使用控制器偏航旋转已勾选
此值将允许我们在射击目标时通过任务旋转角色。
现在,是时候为角色添加合适的武器了。为此,请按照以下步骤操作:
-
在打开的蓝图角色类中,定位到组件面板,并点击+ 添加按钮。
-
选择UnrealAgilityArena | 基础武器以将此组件添加到蓝图类中。
-
选择这个新添加的组件,在细节面板的子弹类别中,找到子弹类属性;从下拉菜单中选择BP_GunBullet。
现在我们已经将角色调整得完美无缺,是时候让它在我们指挥下展示其射击能力了!
使武器射击
为了使射击阶段正常工作,我们将使用动画通知——一个可以与动画序列同步的事件——这样我们就可以在动画时间轴的特定点调用Shoot()函数。
注意
你可能好奇为什么我们要实现这个特定的系统,而不是直接从代码的任何部分调用Shoot()函数。好吧,是这样的:射击动画有一个持续时间,子弹应该生成的时刻就在动画的某个地方。这就是动画通知发挥作用的地方。通过使用动画通知,我们可以在动画中指定子弹应该生成的确切时刻。
通过扩展AnimNotify类创建动画通知,所以首先创建一个新的 C++类,扩展AnimNotify,并将其命名为AnimNotify_Shoot。一旦创建了文件,打开AnimNotify_Shoot.h头文件,作为第一步,将UCLASS()宏声明更改为以下内容:
UCLASS(const, hidecategories=Object, collapsecategories, Config = Game, meta=(DisplayName="Shoot"))
不深入具体细节,只需说这些初始化设置对于类正确运行是必要的。
之后,将以下public声明添加到类中:
public:
UAnimNotify_Shoot();
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
UAnimNotify_Shoot()声明是构造函数,相当直观,而Notify()声明将在动画通知被触发时调用。
现在,打开AnimNotify_Shoot.cpp文件,并在其顶部添加所需的#include声明:
#include "BaseDummyCharacter.h"
#include "BaseWeaponComponent.h"
然后,添加构造函数实现:
UAnimNotify_Shoot::UAnimNotify_Shoot():Super()
{
#if WITH_EDITORONLY_DATA
NotifyColor = FColor(222, 142, 142, 255);
#endif
}
虽然这不是强制性的,但这个功能允许你在虚幻引擎编辑器中自定义通知标签的颜色。这非常方便,不是吗?
另一方面,Notify()函数对于游戏玩法相关的理由具有重要意义,因此添加以下代码行:
void UAnimNotify_Shoot::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
if(MeshComp == nullptr) return;
const auto Character = Cast<ABaseDummyCharacter> (MeshComp->GetOwner());
if(Character == nullptr) return;
const auto WeaponComponent = Character-> GetComponentByClass<UBaseWeaponComponent>();
if(WeaponComponent == nullptr) return;
WeaponComponent->Shoot();
}
这个函数会查找BaseWeaponComponent实例(如果有的话),并调用Shoot()函数。
在将此动画通知添加到射击动画之前,你需要编译你的项目。一旦新的类可用,查找位于Content/KayKit/PrototypeBits/Character/Animations文件夹中的AM_1H_Shoot蒙太奇。
注意
在虚幻引擎中,montage指的是一种专门资产,允许你为角色或对象创建复杂的动画。蒙太奇通常用于定义相关动画序列。由于蒙太奇不是本书的重点,我已经为你提供了所需的蒙太奇。
一旦你通过双击打开资产,你将注意到在资产时间轴中有一个ShootNotify_C标签;这是一个空占位符,我为你提供,以便让你知道通知应该放置的位置。

图 9.6 – 动画蒙太奇
右键单击该标签,选择Replace with Notify | Shoot以添加AnimNotify_Shoot实例。

图 9.7 – Shoot notify
现在,无论你的 AI 代理何时播放这个蒙太奇,它都会从蒙太奇本身收到通知,调用Shoot()函数。在本章的后面部分,我们将为我们的 AI 代理行为树创建一个专用任务,以便播放蒙太奇,但如果你想测试一下,你可以简单地使用BP_GunnerDummyCharacter的事件图中的Play Montage节点,如图图 9 .8 所示:

图 9.8 – Montage test
确保在测试完成后删除此节点,以防止你的 AI 代理表现出看似混乱的行为。
在本节中,我提供了一些关于通过最佳实践和我的个人经验来增强 AI 代理和行为树的见解。在此之后,我们为能够射击的更高级 AI 代理奠定了基础。接下来的章节将专注于开发自定义修饰器,为我们的项目增加一层复杂性。我们的事业即将迎来令人兴奋的发展!
理解修饰器
修饰器提供了一种向行为树的一部分执行添加额外功能或条件的方法。正如你从之前的章节中已经知道的那样,修饰器附加到组合节点或任务节点上,并确定树中的分支(甚至单个节点)是否可以执行。通过将修饰器与组合节点结合使用,你可以创建具有优先行为的行为树,允许处理复杂场景的强大逻辑。在第八章,“设置行为树”中,我们使用了一些内置修饰器,但在本节中,我将为你提供更多关于创建自己的自定义修饰器的详细信息。
解释 BTAuxiliaryNode 类
修饰器和服务都继承自BTAuxiliaryNode类,这将允许你实现以下功能:
-
OnBecomeRelevant():当辅助节点(装饰器或服务附加到的节点)变为活动时将被调用
-
OnCeaseRelevant():当辅助节点变为不活动时将执行
-
TickNode():这将在每个辅助节点计时器执行时执行
在第八章,“设置行为树”中,我向你介绍了一些这些函数,所以了解它们的来源是很好的。
创建 C++修饰器
装饰器从BTDecorator类扩展,在 C++中,其主要可实现的函数如下:
-
OnNodeActivation():当底层节点被激活时调用此函数
-
OnNodeDeactivation():当底层节点被停用时调用此函数
-
OnNodeProcessed():当底层节点被停用或未能激活时调用此函数
-
CalculateRawConditionalValue():计算装饰器条件值,不考虑逆条件
此外,您还可以使用IsInversed()函数来检查装饰器是否会处理逆条件值。
创建蓝图装饰器
每次使用蓝图可视化脚本创建装饰器时,您应该从BTDecorator_BlueprintBase类扩展,它包含一些额外的代码逻辑和事件,以便更好地管理它。您可以通过通常的方式创建装饰器 – 从内容抽屉 – 或者您可以从行为树图中选择新装饰器按钮,如图 9.9* 所示:

图 9.9 – 创建装饰器
当您使用由蓝图生成的装饰器工作时,您将可用的主要事件如下:
-
Receive Execution Start AI:当底层节点被激活时调用此函数
-
Receive Execution Finish AI:当底层节点完成执行其逻辑时调用此函数
-
Receive Tick AI:在每次 tick 时调用此函数

图 9.10 – 装饰器节点
通过记住这一点,您将能够为您的 AI 代理实现自己的蓝图装饰器。
现在,我们将实现我们自己的装饰器,一个将检查演员上的标签的装饰器。
实现检查演员标签的装饰器
现在是创建我们第一个装饰器的最佳时机。如您所回忆的那样,在实现BaseTarget类时,我们确保每当目标被击中时,其标签被设置为未定义值。通过实现一个检查演员实例标签的装饰器,我们可以确定演员本身是否是一个可行的目标。
因此,让我们先创建一个新的从BTDecorator扩展的 C++类,并将其命名为BTDecorator_CheckTagOnActor。一旦创建了类,打开BTDecorator_CheckTagOnActor.h文件并添加以下声明:
protected:
UBTDecorator_CheckTagOnActor();
UPROPERTY(EditAnywhere, Category=TagCheck)
FBlackboardKeySelector ActorToCheck;
UPROPERTY(EditAnywhere, Category=TagCheck)
FName TagName;
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
virtual void InitializeFromAsset(UBehaviorTree& Asset) override;
如您所见,我们将使用一个黑板键值 – ActorToCheck – 来检查其引用的值是否具有等于TagName的标签。这个检查将由CalculateRawConditionValue()函数处理。此外,我们还需要初始化任何与资产相关的数据,这通常在继承自BTNode超类的InitializeFromAsset()函数中完成。
现在,打开BTDecorator_CheckTagOnActor.cpp文件以开始实现函数。让我们先添加所需的# include文件:
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
接下来,让我们实现构造函数:
UBTDecorator_CheckTagOnActor::UBTDecorator_CheckTagOnActor()
{
NodeName = "Tag Condition";
ActorToCheck.AddObjectFilter(this, GET_MEMBER_NAME_ CHECKED(UBTDecorator_CheckTagOnActor, ActorToCheck), AActor::StaticClass());
ActorToCheck.SelectedKeyName = FBlackboard::KeySelf;
}
我们在这里所做的事情,即在命名节点之后,具有重大意义。我们正在过滤键值,仅允许 Actor 类。这一步骤确保只有与演员相关的有效黑板键将被接受,保持输入的完整性和适当性。
CalculateRawConditionValue() 函数将会相当直接:
bool UBTDecorator_CheckTagOnActor::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) const
{
const UBlackboardComponent* BlackboardComp = OwnerComp. GetBlackboardComponent();
if (BlackboardComp == nullptr) return false;
const AActor* Actor = Cast<AActor>(BlackboardComp- >GetValue<UBlackboardKeyType_Object>(ActorToCheck. SelectedKeyName));
return Actor != nullptr && Actor->ActorHasTag(TagName);
}
如您所见,我们检索了黑板组件,并获取了 ActorToCheck 键,以检查是否存在有效的 Actor 实例,以及它是否被标记为目标。
现在,实现最后一个必需的函数:
void UBTDecorator_CheckTagOnActor::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
if (const UBlackboardData* BBAsset = GetBlackboardAsset(); ensure(BBAsset))
{
ActorToCheck.ResolveSelectedKey(*BBAsset);
}
}
此函数检索 BlackboardData 资产,并从该资产中解析出 ActorToCheck 的选定键。
在本节中,您已经获得了有关装饰器的更高级信息,包括在 C++ 或蓝图中实现时的具体考虑。此外,您已成功创建了一个自定义装饰器,该装饰器将被我们即将推出的枪炮 AI 代理使用。这个自定义装饰器将在创建 AI 枪炮代理的行为和决策能力中发挥关键作用,进一步提高其性能和有效性。
在下一节中,我将向您展示有关如何实现服务的详细信息。
理解服务
由于您已经熟悉了前几章中的服务,我现在将为您提供更多相关信息,以进一步丰富您对该主题的理解。让我们探讨这些细节,以增强您在行为树中关于服务的专业知识。
创建 C++ 服务
服务扩展自 BTService 类,其主要可实现函数是 OnSearchStart(),它在行为树搜索进入底层分支时执行。您可以使用此功能在需要时创建某种初始化。
此外,重要的是要记住,服务扩展了 BTAuxiliaryNode 类,从而继承其所有功能。服务继承的一个特别关键的功能是 TickNode() 函数,它在服务的实现中起着至关重要的作用,因为它控制着行为树中服务节点的执行和定期更新。
创建蓝图服务
当使用蓝图可视化脚本创建服务时,建议从 BTService_BlueprintBase 类扩展,因为它提供了额外的代码逻辑和事件,有助于更好地管理服务本身。类似于装饰器,创建服务有两种方式:传统方法,涉及使用 内容抽屉,或者直接从行为树图中选择 新建服务 按钮,如图 图 9 .11 所示:

图 9.11 – 服务创建
当您使用蓝图生成的服务时,您将可用的主要事件如下:
-
接收激活 AI:当服务激活时被调用
-
接收去激活 AI:当服务变为非活动状态时被调用
-
接收搜索开始 AI:当行为树搜索进入底层分支时被调用
-
接收节拍 AI:在每次节拍时被调用

图 9.12 – 服务节点
考虑到这一点,您将能够实现自己的蓝图服务以用于行为树。
利用这些新知识,让我们实现一个新的服务,它将允许我们处理弹药状态。
实现设置弹药服务
我们现在准备好开始实现自己的服务;您已经在 第八章 设置行为树 中创建了一些服务,因此您应该已经熟悉一些展示的步骤。
在这种情况下,我们需要一个服务,它将允许我们告诉黑板何时武器已发射——因此需要重新装填——或者何时准备射击。像往常一样,让我们先创建一个新的从 BT_Service 继承的 C++ 类,并将其命名为 BTService_SetAmmo。一旦创建完成,打开 BTService_SetAmmo.h 文件,并添加以下声明:
public:
UBTService_SetAmmo();
protected:
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Blackboard")
FBlackboardKeySelector NeedsReload;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Blackboard")
bool bKeyValue = false;
virtual void OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
您应该已经熟悉这里的大多数代码;让我们只说我们将使用一个 NeedsReload 黑板键作为 bool 值来查看武器弹药是否耗尽。现在,打开 BTService_SetAmmo.cpp 文件,并在顶部添加以下 #include 声明:
#include "BehaviorTree/BlackboardComponent.h"
构造函数将非常直接,因为我们希望服务节拍被禁用,并且我们只想在它变得相关时执行它:
UBTService_SetAmmo::UBTService_SetAmmo()
{
NodeName = "SetAmmo";
bCreateNodeInstance = true;
bNotifyBecomeRelevant = true;
bNotifyTick = false;
}
OnBecomeRelevant() 函数将为我们设置黑板键值:
void UBTService_SetAmmo::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
const auto BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == nullptr) return;
BlackboardComp->SetValueAsBool(NeedsReload.SelectedKeyName, bKeyValue);
}
在本节中,您已经获得了有关服务的额外信息,包括在 C++ 或蓝图实现时的具体考虑。此外,您已成功创建了一个自定义服务,该服务将用于处理您的 AI 代理的枪械弹药。
在下一节中,我将向您提供有关如何实现任务的详细信息,因为我们将为即将到来的枪手代理创建更多任务。
理解任务
在本节中,我将提供额外信息以增强您对任务的理解。让我们一起探讨这些细节,以进一步巩固您对行为树中任务的理解。
创建 C++ 任务
任务从 BTTask 类扩展,其主要可实现的函数如下:
-
ExecuteTask():这将启动任务执行并返回任务结果
-
AbortTask():这将让您处理应该停止任务的事件
这通常就足够创建一个简单但功能齐全的任务。
创建蓝图任务
当使用蓝图视觉脚本创建任务时,你将扩展BTTask_BlueprintBase类,因为它提供了额外的代码逻辑以方便其实现。正如你可能已经猜到的,有两种创建任务的方式:从内容抽屉的常规创建,以及从行为树图直接选择新任务按钮,如图图 9.13所示:

图 9.13 – 任务创建
当使用蓝图生成的任务工作时,你将可用的主要事件如下:
-
接收执行 AI:当任务执行时调用
-
接收中止 AI:当任务被中止时调用
-
接收每帧 AI:在每一帧调用

图 9.14 – 任务节点
记住这一点,你将具备为你的行为树实现自己的蓝图任务的能力,这正是我们在接下来的步骤中将要做的。
实现 PlayMontage 任务
如你所知,我们 AI 代理的射击命令将由动画蒙太奇中的动画通知控制。不幸的是,没有现成的从行为树执行蒙太奇的任务;存在一个PlayAnimation任务,但它不会很好地处理蒙太奇,所以它不能满足我们的需求。幸运的是,由于我们对任务有现有的深入了解,实现代码逻辑将相对简单。此外,拥有一个可以播放蒙太奇的任务,在处理其他动画序列时将非常有用,例如装弹或当所有目标都被成功击中时的庆祝。因此,首先创建一个新的 C++类,扩展BTTask,并将其命名为BTTask_PlayMontage。在BTTask_PlayMontage.h文件中,添加以下自解释声明:
public:
UPROPERTY(EditAnywhere, Category="Dummy Task")
UAnimMontage* AnimMontage;
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
在BTTask_PlayMontage.cpp文件中,添加以下实现:
EBTNodeResult::Type UBTTask_PlayMontage::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
if(AnimMontage == nullptr) return EBTNodeResult::Failed;
const auto Controller = OwnerComp.GetAIOwner();
if(Controller == nullptr) return EBTNodeResult::Failed;
const auto Character = Cast<ACharacter> (Controller->GetCharacter());
if(Character == nullptr) return EBTNodeResult::Failed;
Character->PlayAnimMontage(AnimMontage, 1.f, FName("Default"));
return EBTNodeResult::Succeeded;
}
此函数简单地执行角色上的PlayAnimMontage()函数,并返回成功的结果。如果找不到任何所需的引用,则返回失败的结果。
在这个任务准备好之后,我们可以实现第二个任务,这是本章我们将需要的最后一个任务。
实现 FindAvailableTarget 任务
此任务将只有一个目标,即通过检查所有具有预定义标记的演员来找到一个可用的目标。这里没有什么花哨的,但我们将会需要它,因此创建一个新的 C++类,从BBTask继承,并将其命名为BTTask_FindAvailableTarget。在BTTask_FindAvailableTarget.h头文件中,添加以下声明:
public:
UBTTask_FindAvailableTarget();
UPROPERTY(EditAnywhere, Category="Blackboard")
FBlackboardKeySelector TargetActor;
UPROPERTY(EditAnywhere, Category="Dummy Task")
FName TargetTag;
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
这里不需要添加解释,让我们打开BTTask_FindAvailableTarget.cpp文件并添加所需的# include声明:
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
构造函数将仅过滤TargetValue键的类型条目:
UBTTask_FindAvailableTarget::UBTTask_FindAvailableTarget()
{
NodeName = "Find Available Target";
TargetActor.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTTask_ FindAvailableTarget, TargetActor), AActor::StaticClass());
TargetActor.SelectedKeyName = FBlackboard::KeySelf;
}
ExecuteTask()函数将遍历层级以找到所有正确标记的Actor实例,并从列表中返回一个随机元素。只需添加以下代码:
EBTNodeResult::Type UBTTask_FindAvailableTarget::ExecuteTask (UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
const auto BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == nullptr) { return EBTNodeResult::Failed; }
TArray<AActor*> TargetList;
UGameplayStatics::GetAllActorsWithTag(GetWorld(), TargetTag, TargetList);
if(TargetList.Num() == 0) { return EBTNodeResult::Failed; }
const auto RandomTarget = TargetList[FMath::RandRange(0, TargetList.Num() - 1)];
BlackboardComp->SetValueAsObject(TargetActor.SelectedKeyName, RandomTarget);
return EBTNodeResult::Succeeded;
}
如您所见,如果至少找到一个Actor实例,将返回成功的结果。
在本节中,我们简要地查看了一些任务的关键特性,甚至在我们的工具库中添加了更多。现在,我们似乎已经为与枪手 AI 角色一起踏上旅程做好了充分的准备。在讨论这个话题的同时,这是一个很好的机会来解释调试行为树的正确技术。那么,让我们深入探讨并开始吧!
调试行为树
使用虚幻引擎调试行为树对于确保你的 AI 驱动游戏平稳高效运行至关重要。通过仔细检查和分析行为树的执行情况,你可以识别并解决游戏过程中可能出现的任何问题或错误。你已经对如何在虚幻引擎中启用调试工具有所了解。在本节中,我们将深入探讨专为行为树设计的调试功能;在开始使用调试工具之前,我们需要创建一个适当且适度复杂的行为树。
创建黑板
行为树的黑板将会很简单;我们需要几个键来保持对目标的引用,以及一个标志来检查武器是否需要重新装填。因此,让我们先做以下操作:
-
打开内容抽屉并在内容/AI文件夹中创建一个黑板资产。
-
将资产命名为BB_GunnerDummy并打开它。
-
创建一个新的bool类型的键,并将其命名为NeedsReload。
-
创建一个新的Object类型的键,并将其命名为TargetActor。
你可能记得,在创建BTTask_FindAvailableTarget类时,我们决定过滤这个键,使其只接受Actor类型而不是通用的Object类型;这意味着你需要将这个键的基类设置为Actor类型。为此,请按照以下步骤操作:
-
选择TargetActor键,在黑板详情面板中打开键类型选项以显示基 类属性。
-
从基类下拉菜单中选择Actor,如图图 9 .15 所示:

图 9.15 – 目标演员键
创建行为树
我们将要实现的行为树将具有以下逻辑:
-
如果 AI 角色有一个有效的目标,它将射击该目标然后重新装填武器
-
如果没有设置目标,它将尝试在当前层级中找到一个目标并设置正确的键
-
如果在层级中没有可用的目标,这意味着所有目标都已命中,AI 角色将快乐地欢呼
首先做以下操作:
-
在内容/AI文件夹中创建一个新的行为树资产,并将其命名为BT_GunnerDummy。
-
在详细信息面板中,将黑板资产属性设置为BB_GunnerDummy。
-
将一个选择器节点连接到根节点,并将其命名为根选择器,如图图 9.16所示.16:

图 9.16 – 根选择器节点
如您所记得,选择器节点将按顺序执行子树,直到其中一个成功;这正是我们需要做的,以便创建我们的枪手 AI 逻辑。
添加射击逻辑
射击逻辑将被细分为两个阶段 – 射击和装弹 – 因此我们将使用另一个选择器节点。让我们做以下操作:
-
从根选择器中添加另一个选择器节点,并将其命名为射击选择器。
-
右键点击它,添加一个检查目标上的标签装饰器,并将其命名为演员是否为目标?。
-
选择此装饰器,在详细信息面板中执行以下操作:
-
将要检查的演员属性设置为TargetActor
-
将标签名称属性设置为ShootingTarget
-
基本上,这个选择器只有在黑板的TargetActor键中有一个有效的目标时才会执行;如果没有,根选择器将尝试执行下一个可用的子树。我们现在需要创建实际的射击逻辑,所以开始执行以下步骤:
-
在射击选择器中添加一个序列节点,并将其命名为射击序列。
-
右键点击它,添加一个黑板装饰器,并将其命名为是否有弹药?。
-
选择装饰器,在详细信息面板中执行以下操作:
-
将通知观察者属性设置为On Value Change
-
将键查询属性设置为Is Not Set
-
将黑板键属性设置为NeedsReload
这部分树将在NeedsReload键设置为true时执行;否则,它将尝试执行下一个子树。这部分树图应该看起来像图 9.17中描述的那样:
-

图 9.17 – 开始射击序列
让我们在射击序列中添加一些节点:
-
添加一个旋转面向 BBEntry任务,并将其命名为朝向目标旋转。
-
选择此节点,并在详细信息面板中设置黑板键属性为TargetActor。
-
从射击序列中添加一个播放蒙太奇任务,并将其命名为射击蒙太奇。确保这个任务位于朝向目标旋转任务的右侧。在详细信息面板中,将动画蒙太奇属性设置为AM_1H_Shoot。
-
右键点击此任务节点,添加一个设置弹药服务,并将其命名为耗尽弹药。
-
选择此服务并执行以下操作:
-
将需要重新加载属性设置为NeedsReload
-
检查键****值属性
-
-
从射击序列中添加一个等待节点,并确保这个任务位于射击蒙太奇任务的右侧。选择等待节点并执行以下操作:
-
将等待时间属性设置为2.0
-
将随机偏差属性设置为0.5
-
这部分行为树可以在图 9.18中看到:

图 9.18 – 完成的射击序列
行为树的一个优点是,当你给你的节点命名描述性名称时,你可以迅速地一眼看出正在发生什么。通过以准确反映其目的或功能的方式命名你的节点,你创建了一个清晰直观的 AI 视觉表示。
我们现在可以开始创建枪的重新装填序列。让我们先按照以下步骤操作:
-
从射击选择器,在射击序列右侧添加一个新的序列节点,并将其命名为重新装填序列。
-
从重新装填序列,添加一个播放剪辑任务并将其命名为重新装填剪辑。在详细信息面板中,将动画剪辑属性设置为AM_1H_Reload。
-
右键单击此任务节点,添加一个设置弹药服务,并将其命名为补充弹药。
-
选择此服务并执行以下操作:
-
将需要重新装填属性设置为NeedsReload
-
保持键值属性未勾选
-
-
从重新装填序列,添加一个等待节点,并确保此任务位于重新装填剪辑任务右侧。选择等待节点并执行以下操作:
-
将等待时间属性设置为3.0。
-
将随机偏差属性设置为0.5。这部分行为树可以在图 9.19中看到:
-

图 9.19 – 重新装填序列
每当我们进入树的这部分时,将需要重新装填键设置为false,然后我们稍等片刻再继续执行。完成这部分行为树后,我们可以实现目标搜索部分。
查找可用目标
每当没有可射击的目标时,根选择器将执行下一个子树;在这种情况下,我们将寻找一个新的可行目标。为此,请按照以下步骤操作:
-
从根选择器,在射击选择器节点右侧添加一个FindAvailableTarget任务。
-
选择任务并执行以下操作:
-
将目标演员属性设置为TargetActor
-
将目标标签属性设置为ShootingTarget
-
图 9.20显示了这部分行为树:

图 9.20 – 查找目标任务
现在是时候添加行为树逻辑的第三和最后一部分了。
完成 AI 逻辑
代码的最后部分将是使 AI 角色在所有目标被击中时欢呼。为此,请按照以下步骤操作:
-
从根选择器,在查找可用目标任务右侧添加一个序列节点,并将其命名为欢呼序列。
-
从欢呼序列,添加一个播放剪辑任务并将其命名为欢呼剪辑。在详细信息面板中,将动画剪辑属性设置为AM_Cheer。
-
从欢呼序列中添加一个等待节点,并确保这个任务位于欢呼蒙太奇任务的右侧。选择等待节点并执行以下操作:
-
将等待时间属性设置为3.0
-
将随机偏差属性设置为0.5
-
图表的这部分应该看起来像图 9 .21:

图 9.21 – 欢呼序列
现在行为树终于完成了,我们可以通过创建一个专门的 AI 控制器来继续前进。
创建 AI 控制器
AI 控制器将会非常简单;你只需要做以下几步:
-
打开内容抽屉,在内容/蓝图文件夹中,添加一个新的蓝图类,扩展BaseDummyAIController,并将其命名为AIGunnerDummyController。
-
打开它,在详细信息面板中,找到行为树属性并将其值设置为BT_GunnerDummy。
-
打开BP_GunnerDummyCharacter,在详细信息面板中,将AI 控制器类属性设置为AIDummyGunnerController。
现在我们已经准备好了控制器并且角色已经设置好,是时候测试和调试其行为。
在健身房上调试行为树
要开始调试新创建的行为树,让我们先创建一个新的关卡。让我们按照以下步骤进行:
-
创建一个你选择的关卡,从我在项目模板中提供的关卡实例和打包关卡演员开始。
-
添加一个BP_GunnerDummyCharacter实例。
-
添加一个或多个BP_Target实例,以便你的 AI 角色能够看到它们。我的健身房关卡如图图 9 .22所示:

图 9.22 – 健身房关卡
一旦测试了关卡,预期的行为是角色向每个目标射击,每次射击后重新装填,并在所有目标成功命中后欢呼。
添加断点
要测试你的行为树,你可以打开它并开始关卡模拟;你将看到树的活动部分,节点以黄色突出显示,如图图 9 .23所示:

图 9.23 – 测试树
有时,图表的一部分会执行得非常快,你可能看不到树的一部分是否已经执行。为了更好地理解发生了什么,你可以在节点上右键单击并选择添加断点,如图图 9 .24所示:

图 9.24 – 添加断点
注意
在虚幻引擎的行为树中,断点是一个调试功能,允许您在特定的节点处暂停行为树的执行。当执行达到断点时,行为树执行将暂时停止,给您提供检查 AI 角色状态和分析行为树流程的机会。执行可以在任何时候恢复,以继续行为树的执行。
当行为树执行时,它将在断点处暂停,提供对该时刻发生情况的清晰视图。通过在特定的断点暂停执行,您可以获得关于 AI 行为内部运作的宝贵见解,并识别任何需要解决的问题或意外行为。图 9 .25显示了断点位于查找可用目标节点上,表明在检查是否为 目标装饰器时,之前的子树失败了:

图 9.25 – 激活断点
使用调试工具
如您从第六章,“优化导航系统”中回忆的那样,虚幻引擎为 AI 系统提供了一系列调试工具。行为树也不例外,一旦您启用这些工具,您就可以通过按键盘数字键盘上的数字 2 来分析情况。此功能允许您深入了解 AI 角色的行为并实时评估行为树的执行情况。在图 9 .26中,我们可以观察到行为树显示在屏幕上的特定情况:

图 9.26 – 调试工具
这种视觉表示提供了对行为树结构和流程的清晰视图,使我们能够分析和理解其组织结构。
摘要
在这一全面章节中,我为您提供了有关创建更有效和高效行为树的信息。我们首先讨论了一些最佳实践,然后深入探讨了装饰器、服务和任务的关键特性。此外,我们探讨了针对特定要求的自定义节点的实现,最终创建了一个完全功能化的 AI 代理。为了验证我们的工作并利用虚幻引擎强大的调试工具,我们还开发了一个健身房环境进行彻底测试。
准备进入下一章,因为接下来将会有更多的激动人心的内容!在即将到来的这一章中,我将揭晓虚幻感知系统,你的 AI 代理将提升他们的感官,比以往任何时候都要更加警觉!
第十章:使用感知系统改进代理
AI 感知系统是虚幻引擎游戏框架中的一个强大工具,因为它允许 AI 控制的演员感知并对其环境中的各种刺激做出反应。它为 AI 代理提供了一种通过不同的感官(如视觉、听觉或触觉)感知其他演员(如玩家或敌人)存在的方法。通过正确配置和使用感知系统,开发者可以创建能够对其周围事件做出适当反应的 AI 代理。更重要的是,这个系统允许开发者根据他们游戏的具体需求实现和配置定制的感官。这种灵活性使开发者能够创建独特且引人入胜的 AI 体验。
在本章中,我们将介绍虚幻引擎感知系统的主要组件,从一些理论开始,然后应用这些新获得的知识到实际案例中。
在本章中,我们将涵盖以下主题:
-
介绍感知系统
-
为代理添加感知功能
-
调试感知
-
创建感知刺激
技术要求
要跟随本章中介绍的主题,您应该已经完成了前面的章节,并理解了它们的内容。
此外,如果您希望从本书的配套仓库开始编写代码,您可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。
要下载最后一章末尾的文件,请点击Unreal Agility Arena – 第九章 - 结束链接。
介绍感知系统
马库斯博士和维克托利亚教授似乎在他们的故事中开启了一个新的篇章:
马库斯博士和维克托利亚教授知道,如果允许像他们的 AI 木偶这样的复杂合成生物不受限制地漫游,可能会造成灾难。因此,他们开始不知疲倦地开发一个复杂的隐藏摄像头网络,可以随时监控他们的创造物的移动和行动。有了这个警惕的监控系统,他们希望对他们进行严格的监督,保持完全的控制,确保他们 有争议的研究 的安全。
在虚幻引擎中创建智能和反应灵敏的 AI 代理时,AI 感知系统是一个关键组件;这个强大的系统允许 AI 控制器——以及随之而来的 AI 代理——感知并对其虚拟环境中的不同刺激做出反应。
AI 感知系统的核心是感官和刺激。一个感官——如视觉或听觉——代表 AI 代理感知其环境的方式,并配置为检测特定类型的刺激,这些刺激是来自游戏世界中其他演员的感知数据源。
例如,视觉感官预先配置为检测任何可见的演员,而伤害感官在关联的 AI 控制器的演员受到外部伤害时触发。
注意
作为开发者,您可以通过扩展AISense类(如果您使用 C++)或AISense_Blueprint类(如果您使用蓝图)来创建针对您游戏特定需求定制的感官。
AI 感知系统组件
AI 感知系统由以下主要类组成:
-
AIPerceptionSystem:这是核心管理器,负责跟踪所有 AI 刺激源。
-
AIPerceptionComponent:这代表 AI 代理的头脑,负责处理感知刺激。它需要附加到 AI 控制器才能正常工作。
-
AIPerceptionStimuliSourceComponent:此组件添加到可以生成刺激的演员上,并负责向监听元素广播感知数据。
-
AIPerceptionSenseConfig:这定义了特定感官的属性、可以感知的演员以及感知随时间或距离衰减的方式。
当具有AIPerceptionStimuliSourceComponent的演员生成刺激时,附近的AIPerceptionComponents通过其配置的感官检测它。然后,AI 控制器处理这些感知数据以触发所需的行为。
一旦您将AIPerceptionComponent添加到 AI 控制器中,您需要添加一个或多个AIPerceptionSenseConfig元素,以便为您的 AI 代理提供专用的感官。图 10.1展示了基于触觉的感知示例:

图 10.1 – 触觉配置
从之前的屏幕截图,您可能已经注意到一个主导感官属性;此属性允许您指定一个在确定感知演员位置时优先于其他感官的特定感官。
让我们探索可用的感官配置,正如之前提到的,这些配置定义了每个特定感官的属性:
AIPerceptionSenseConfig 类型
Unreal Engine 提供了一系列预定义的AIPerceptionSenseConfig类,这些类很可能满足您的特定需求。让我们看看可用的选项:
-
AIDamage:如果您的 AI 代理需要响应诸如任何伤害、点伤害或径向伤害等伤害事件,请使用此配置
-
AIHearing:如果您需要检测周围环境中产生的声音,请使用此配置
-
AIPrediction:当您需要预测未来几秒钟的目标演员位置时,请使用此配置
-
AISight:当您希望您的 AI 代理在关卡中看到事物时,请使用此配置
-
AITeam:如果您想通知 AI 代理附近有盟友,请使用此配置
-
AITouch:当 AI 代理触摸其他演员或反过来,当其他事物触摸 AI 代理时,请使用此配置
刺激源
AIPerceptionStimuliSourceComponent类允许一个角色将自己注册为一个或多个感官的刺激源。例如,您可以将一个角色注册为视觉刺激源。这种注册允许 AI 代理在游戏关卡中视觉上感知到该角色。
一个刺激源可以为一个感官注册或注销,使其可检测或不可检测,由感知系统检测。
注意
由于Pawn和Character类在 Unreal Engine 中作为刺激源具有默认行为,因此它们对AISight感知是固有的可见的。这种设计选择通过消除为每个角色或单位手动配置可见性的需要,简化了 AI 行为开发。然而,如果您想让 AI 忽略特定的角色,您需要采取额外的步骤来相应地配置它们。
在本节中,我们介绍了感知系统及其主要元素。在下一节中,我们将致力于开发一个功能齐全的 AI 代理,它将让您在游戏中感知到其他角色。
为代理添加感知
在本节中,我们将创建一个新的 AI 代理,它将使用感知系统。我们将创建一个安全摄像头,它将探测附近的周边区域,寻找我们创建的模拟炮手可能的目标第九章 ,扩展行为树。将其视为一种在黑暗环境中的红外摄像头。一旦摄像头发现目标,它将标记它,以便炮手能够在环境中定位它。
我们将首先创建一个Actor类,它将被用作摄像头模型。
创建 BaseSecurityCam 类
尽管我们将在 AI 控制器中实现感知系统,但一个在关卡中显示的漂亮模型将有助于您环境的视觉效果,所以让我们首先创建一个新的 C++类,从Pawn类扩展,命名为BaseSecurityCam。一旦创建了类,打开BaseSecurityCam.h文件,在# include声明之后添加以下前置声明:
class UAIPerceptionComponent;
然后,通过将UCLASS()宏更改为以下内容,使该类成为Blueprintable:
UCLASS(Blueprintable)
之后,移除BeginPlay()和Tick()声明,因为我们不会使用它们。
作为最后一步,在GENERATED_BODY()宏之后添加以下组件声明,用于显示模型:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Security Cam", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* SupportMeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Security Cam", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* CamMeshComponent;
您现在可以打开BaseSecurityCam.cpp文件来实现这个类;作为第一步,移除BeginPlay()和Tick()函数。然后,找到构造函数并更改以下代码行:
PrimaryActorTick.bCanEverTick = true;
更改为以下内容:
PrimaryActorTick.bCanEverTick = false;
现在,在构造函数和上述代码行之后,添加以下代码块:
SupportMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Support Mesh"));
RootComponent = SupportMeshComponent;
static ConstructorHelpers::FObjectFinder<UStaticMesh> SupportStaticMeshAsset(
TEXT("/Game/_GENERATED/MarcoSecchi/SM_SecurityCam_Base.SM_SecurityCam_Base"));
if (SupportStaticMeshAsset.Succeeded())
{
SupportMeshComponent->SetStaticMesh(SupportStaticMeshAsset.Object);
}
CamMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Cam Mesh"));
CamMeshComponent->SetRelativeLocation(FVector(61.f, 0.f, -13.f));
CamMeshComponent->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder<UStaticMesh> CamStaticMeshAsset(
TEXT("/Game/_GENERATED/MarcoSecchi/SM_SecurityCam.SM_SecurityCam"));
if (CamStaticMeshAsset.Succeeded())
{
CamMeshComponent->SetStaticMesh(CamStaticMeshAsset.Object);
}
您已经从本书的前几章中了解了所有这些,所以我想没有必要再次解释。随着安全摄像头模型的创建,我们现在可以实现相应的 AI 控制器,以及其感知能力。
创建 BaseSecurityCamAIController 类
要添加一个合适的摄像头控制器,让我们创建一个扩展 AIController 的 C++ 类,并将其命名为 BaseSecurityCamAIController。一旦创建了类,打开 BaseSecurityCamAIController.h 文件,并在 # include 声明之后添加以下前置声明:
struct FAIStimulus;
struct FActorPerceptionUpdateInfo;
class UBehaviorTree;
然后,通过将 UCLASS() 宏更改为以下内容来使类 Blueprintable:
UCLASS(Blueprintable)
之后,在现有的构造函数声明之后添加以下代码块:
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dummy AI Controller")
TObjectPtr<UBehaviorTree> BehaviorTree;
virtual void OnPossess(APawn* InPawn) override;
UFUNCTION()
void OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus);
您已经熟悉了行为树属性和来自 第八章 的 OnPosses() 函数,设置行为树;此外,OnTargetPerceptionUpdate() 函数将用作从感知系统获取信息时的事件处理程序。
您现在可以打开 BaseSecurityCamAIController.cpp 并将以下 #include 声明添加到文件顶部:
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
现在,定位构造函数,并在其中添加以下代码块:
const auto SenseConfig_Sight = CreateDefaultSubobject<UAISenseConfig_ Sight>("SenseConfig_Sight");
SenseConfig_Sight->SightRadius = 1600.f;
SenseConfig_Sight->LoseSightRadius = 3000.f;
SenseConfig_Sight->PeripheralVisionAngleDegrees = 45.0f;
SenseConfig_Sight->DetectionByAffiliation.bDetectEnemies = true;
SenseConfig_Sight->DetectionByAffiliation.bDetectNeutrals = true;
SenseConfig_Sight->DetectionByAffiliation.bDetectFriendlies = true;
正如您所看到的,我们正在为视觉感知创建感知配置,以及一些其属性,例如 SightRadius 和 LoseSightRadius,这将确定感知系统可以检测到某物的距离和检测将丢失的距离。尽管这两个属性可能看起来很冗余,但请记住,一旦检测到目标,就很难失去对其的感知,除非这两个属性具有相同的值。PeripheralVisionAngleDegrees 将处理用于检查演员是否在视线中的锥形。最后,DetectionByAffiliation 属性用于处理检测到的演员是敌人、朋友还是中立;在这种情况下,我们想要检查所有这些,以便检测视线中的任何物体。
现在,是时候添加实际感知组件了,所以请在之前的代码之后添加以下代码段:
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("Perception"));
PerceptionComponent->ConfigureSense(*SenseConfig_Sight);
PerceptionComponent->SetDominantSense(SenseConfig_Sight- >GetSenseImplementation());
PerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(this, &ABaseSecurityCamAIController::OnTargetPerceptionUpdate);
正如您所看到的,我们创建了 AIPerceptionComponent 实例,然后我们分配了之前创建的视野配置。最后,我们注册到 OnTargetPerceptionUpdated 代理,该代理将通知组件感知系统检测到的任何变化。
现在,是时候实现 OnPosses() 函数了,这是我们已知如何处理的:
void ABaseSecurityCamAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (ensureMsgf(BehaviorTree, TEXT("Behavior Tree is nullptr! Please assign BehaviorTree in your AI Controller.")))
{
RunBehaviorTree(BehaviorTree);
}
}
最后一步是实现事件处理程序。为此,请添加以下代码块:
void ABaseSecurityCamAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus)
{
if (Actor->Tags.Num() > 0) return;
const auto SightID = UAISense::GetSenseID<UAISense_Sight>();
if (Stimulus.Type == SightID && Stimulus.WasSuccessfullySensed())
{
Actor->Tags.Init({}, 1);
Actor->Tags[0] = "ShootingTarget";
}
}
此函数首先检查目标是否已被标记;在这种情况下,这意味着它已经被发现。然后它检索视觉感官的 ID,调用GetSenseID(),并检查刺激类型是否等于视觉感官 ID 以及刺激是否被成功感知。如果两个条件都为真,它初始化Tags数组,第一个元素设置为ShootingTarget的值,以便使其成为我们可用的模拟枪手的有效目标。
安全摄像头现在已准备就绪;我们只需要一个合适的环境来测试它。
在本节中,我们向您展示了如何正确创建 AI 代理,利用感知系统。在下一节中,我们将测试这个代理并学习如何在运行时正确调试感知信息。
调试感知
现在是时候测试我们的感知逻辑并学习如何在运行时正确调试感知系统了。为了做到这一点,我们需要对基础模拟角色进行一些小的改进。如前所述,Pawn和Character类已经注册了视觉刺激,因此我们不需要实现此逻辑。然而,我们需要处理伤害,因为我们将在BP_RoamerDummyCharacter和BP_GunnerDummyCharacter上做一些实验。看起来激动人心且愉快的时光就在眼前!
提升漫游行为树
改进我们的 AI 代理的第一步将是添加一些逻辑来处理模拟漫游行为树的伤害。特别是,我们希望当 AI 代理被 Nerf 枪弹击中时,它会坐下。我们将首先向专用黑板添加一个新键。
改进黑板
黑板需要一个新标志来有效地监控并记录被击中的角色。因此,打开BB_Dummy资产并执行以下操作:
-
点击新建键按钮,然后从下拉菜单中选择布尔值。
-
将新创建的键命名为IsHit。
如您所知,这将向行为树公开一个新键;此外,该键还将向 AI 控制器公开,正如我们稍后将看到的。
改进行为树
行为树需要管理被击中的 AI 代理;在我们的案例中,我们希望播放一个角色坐下的蒙太奇,因为它已经被从游戏中淘汰。所以,让我们先打开BT_RoamerDummy资产并执行以下操作:
-
右键单击根序列节点并添加一个黑板装饰器,命名为是否被击中?。
-
在选择装饰器后,执行以下操作:
-
将通知观察者属性设置为值更改时
-
将观察者中止属性设置为自身
-
将键查询属性设置为未设置
-
将黑板键属性设置为IsHit
-
-
将根序列重命名为游戏序列并将其从ROOT节点断开连接。
-
将一个选择器节点添加到根节点,并将其命名为根选择器。
-
将根选择器节点连接到游戏****序列节点。
-
将根选择器节点连接到一个播放动作组任务,并将新创建的节点命名为坐动作组。此节点应位于游戏****序列节点的右侧。
-
在选择坐动作组节点时,将动画动作组属性设置为AM_Sit。修改后的行为树部分如图10.2所示:

图 10.2 – 修改后的行为树
如您所见,行为树将继续按之前的方式工作,除非 AI 代理受到攻击;在这种情况下,角色将坐下并停止徘徊。
现在是时候改进 AI 控制器,以便管理即将到来的伤害。
增强BaseDummyAIController
任何木偶角色的 AI 控制器都需要处理任何伤害。为了简化,我们将在 AI 控制器内部而不是在角色内部处理所有这些;我们需要与黑板通信,而从控制器本身进行操作要简单得多和直接。
让我们从打开BaseDummyAIController.h文件并添加以下声明开始,用于处理伤害:
UFUNCTION()
void OnPawnDamaged(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser);
现在,打开BaseDummyAIController.cpp文件,在OnPossess()函数中,添加以下代码行:
GetPawn()->OnTakeAnyDamage.AddDynamic(this, &ABaseDummyAIController::OnPawnDamaged);
接下来,添加以下实现:
void ABaseDummyAIController::OnPawnDamaged(AActor* DamagedActor, float Damage, const UDamageType* DamageType,
AController* InstigatedBy, AActor* DamageCauser)
{
const auto BlackboardComp = GetBlackboardComponent();
BlackboardComp->SetValueAsBool("IsHit", true);
if (DamagedActor->Tags.Num() > 0)
{
DamagedActor->Tags[0] = "Untagged";
}
}
当与 AI 控制器关联的 pawn 受到伤害时,将调用此函数;该函数检索 AI 控制器的黑板组件,并将IsHit键设置为true。然后,它将演员的第一个标签设置为未标记,这样它就不再是枪手木偶的有效目标。
在这个 AI 控制器设置完成后,现在是时候为安全摄像头创建一个蓝图了。
创建安全摄像头蓝图
现在,回到 Unreal Engine 编辑器,编译完成后,从BaseSecurityCamAIController类创建一个蓝图,命名为AISecurityCamController。您不需要添加行为树,因为所有逻辑都在控制器内部处理。
现在,从BaseSecurityCam类创建一个蓝图类,并将其命名为BP_SecurityCam。一旦创建完成,打开它,在详细信息面板中找到AI 控制器类属性,并将其值设置为AISecurityCamController。

图 10.3 – 安全摄像头蓝图
我们已经收集了所有必要的元素来让我们的新健身房焕发生机,我们现在准备采取下一步并开始调试感知系统的过程。
创建健身房
我们现在将创建一个关卡来测试和调试一切。我们希望实现以下行为:
-
一个或多个 AI 代理将在关卡中移动
-
安全摄像头会尝试识别 AI 代理并将它们标记为可攻击的目标
-
枪手将等待 AI 代理被标记,以便射击它们
因此,让我们首先创建健身房:
-
创建一个您选择的级别,从项目模板中提供的 Level Instances 和 Packed Level Actors 开始。
-
添加一个 NavMeshBoundsVolume 角色以使其覆盖所有可通行区域。
-
添加一些障碍物以使事情更有趣。
-
向级别中添加一个 BP_GunnerDummyCharacter 实例。
-
添加一个或多个 BP_RoamerDummyCharacter 实例。
-
添加一些 NS_Target Niagara 角色作为路径查找系统的目标点;只需记住将它们标记为 TargetPoint。确保路径将 AI 代理带到枪手的视线中。
-
向墙壁添加一个或多个 BP_SecurityCam 实例。最终结果应类似于 图 10 .4:

图 10.4 – 健身房
考虑到场景类型,枪手角色将向安全摄像头定位的目标射击,我决定通过添加模拟红外场景的后处理体积来使健身房更具吸引力,如图 图 10 .5 所示:

图 10.5 – 后处理体积的健身房
这显然不是强制性的,您可以自由设置您想要的后期处理环境。
现在健身房已经完成,是时候开始测试它并学习如何调试感知系统了。
启用感知调试
如果您开始模拟,您应该看到以下事情发生:
-
当漫游者四处游荡时,枪手由于没有意识到他们的存在,会欢呼
-
一旦某个漫游者进入摄像头的视线,枪手就会开始瞄准它
-
每当漫游者被击中时,它就会坐下并停止游荡
您可以调整安全摄像头的参数,使其对级别中发生的事情更加或更少关注。
在这一点上,您可能会好奇我们是如何确定代理是否在摄像头的视线中的。好吧,一旦启用调试工具,观察这一点实际上非常简单!
因此,让我们首先启用调试工具,如 第六章 中所述,优化导航网格。
注意
一旦开始模拟,您可能会想知道为什么安全摄像头顶部有一个小红图标,而所有虚拟木偶都有一个绿色图标,如图 图 10 .6 所示。当启用 AI 调试工具时,如果 pawn 设置并运行了某些 AI 逻辑,则显示绿色图标;否则,图标将是红色。在我们的情况下,安全摄像头被一个专门的 AI 控制器控制,但没有行为树,所以图标将是红色。

图 10.6 – AI 图标
现在,当模拟进行时,选择一个安全摄像头,并通过按下数字键盘上的 4 和 5 键分别启用 感知 和 感知系统 工具。你应该立即看到安全摄像头视感的可视化,如图 图 10.7 所示:

图 10.7 – 感知调试工具
显示将显示有关 AI 代理感知的一些重要信息,例如活动感官及其数据。此外,你还将看到代理感官的视觉表示。特别是,视感将显示以下内容:
-
一个代表代理视距范围的绿色圆形区域。
-
一个代表代理视距最大范围的粉红色圆形区域。一旦被发现的代理超出这个范围,视线接触将丢失。
-
一个代表代理周围视野的绿色角度。
一旦 AI 代理进入绿色圆圈,它将被感知系统检测到,你应该看到一条从安全摄像头开始到检测到的棋子结束的绿色线,如图 图 10.8 所示:

图 10.8 – 检测到的棋子
一个绿色的线框球体将显示检测点,当检测到的 AI 代理移动时,该点将更新。
一旦 AI 代理离开视线,检测点将停止跟随它,你应该看到球体旁边的 年龄 标签,更新其值;这是自检测丢失以来经过的时间。图 10.9 显示了这种情况:

图 10.9 – 检测丢失
每个感官都有其显示信息的方式,因此我的建议是开始尝试每个感官,以了解如何调试和从调试工具中获取信息。
在本节中,我们创建了一个新的 gym 并测试了感知系统的工作方式;更重要的是,我向您展示了如何启用调试工具并更好地理解在运行时级别中发生的事情。在下一节中,我们将探讨感知刺激,以便使我们的级别更加精致和引人入胜。
创建感知刺激
在上一节中,我们使用了开箱即用的棋子功能,使其对视感可见。现在,我们将分析默认不可感知的演员;这意味着我们需要将 AIPerceptionStimuliSourceComponent 添加到演员中。更重要的是,我们将学习如何注册或注销这些刺激,以便使演员对感知系统可见或不可见。
创建目标演员
在本小节中,我们将创建一个新的角色,该角色将作为虚拟枪手木偶的目标,但有一个转折——这个角色将在关卡中产生一些干扰,并且不会被安全摄像头看到。一旦你知道如何从感知系统中注册和注销角色,实现这种功能相当简单。我们基本上创建了一个干扰设备,它将干扰——也就是说,它将对枪手视觉感知不可见。
为了保持简单,我将创建一个蓝图类;当处理刺激时,通常更方便直接从蓝图配置设置,而不是使用 C++类。通过利用蓝图,我们可以轻松调整和微调刺激的各个方面,使整个过程更加灵活和易于访问。这种方法允许更快地进行迭代和修改,最终导致更顺畅和更高效的流程。
要创建我们的干扰器,打开蓝图文件夹,创建一个从Actor扩展的新蓝图类,并将其命名为BP_Scrambler。一旦蓝图打开,按照以下步骤操作:
-
在组件面板中,添加一个静态网格组件。
-
在详细信息面板中,将静态网格属性设置为SM_RoboGun_BaseRemote,将缩放属性设置为(3.0, 3.0, 3.0)。
-
添加一个AIPerceptionStimuliSource组件。
-
在详细信息面板中,找到注册为感官源属性,通过点击+按钮添加新元素,并将值设置为AISense_Sight。
-
保持自动注册为源属性未选中

图 10.10 – 刺激源
现在,打开此蓝图的事件图,并执行以下操作:
-
从Event Begin Play节点的输出执行引脚,添加一个Delay节点。
-
从Delay节点的完成引脚,添加一个注册感官节点;这将自动将AIPerception Stimuli Source引用添加到目标输入引脚。
-
从Delay节点的持续时间引脚,添加一个随机浮点数在范围内节点,将其最小值和最大值分别设置为4.0和6.0。
-
从感官类输入引脚的下拉菜单中选择AISense_Sight。最终的图表应该看起来像图 10.11中描述的那样:

图 10.11 – 事件图
此图表将在随机间隔后简单地记录该角色的视觉感知,使该角色本身在关卡中的感知系统中可见。
注意
如果你需要注销刺激源,相应的蓝图节点是从感官注销。
让我们在一个全新的健身房中测试这个功能。
测试健身房
要创建测试关卡,按照以下步骤操作:
-
创建一个你选择的关卡,从我在项目模板中提供的 Level Instances 和 Packed Level Actors 开始。
-
添加一些障碍物以使事情更有趣。
-
在关卡中添加 BP_GunnerDummyCharacter。
-
添加一个 BP_Scrambler 实例,以便枪手木偶可以对其射击。
-
在墙上添加一个 BP_SecurityCam 实例,使其位于混乱角力场的视线范围内。最终结果应类似于 图 10.12:

图 10.12 – 混乱角力场
通过启动模拟,您可以看到混乱角力场在随机间隔后才会被安全摄像头发现,在此之前它将不被注意。之后,混乱角力场将被标记为可行的目标,枪手将对其射击。尝试启用调试工具以检查感知系统发生了什么。
在本节的最后部分,我介绍了如何使任何演员可被感知系统检测到。通过遵循提供的步骤和指南,您可以无缝地将感知系统集成到您的项目中,使您的演员能够在虚拟环境中被准确识别和交互。
摘要
在本章中,您学习了 Unreal Engine 感知系统的基本知识。首先,我们展示了您可以为您的 AI 代理添加感官的主要元素;之后,您构建了一个具有视觉感官的 pawn,它可以检测到关卡周围的移动角色。然后,您学习了如何在运行时调试活跃的感官。最后,您向一个演员添加了一个刺激源,以便使其可被感知系统本身检测到。所有这些都为使用 Unreal Engine AI 框架创建沉浸式和动态体验打开了无限可能。
在下一章中,我将揭示一种从环境中收集数据的新方法;准备好吧,因为这个前沿特性仍处于实验阶段。但不用担心,我的朋友,因为这是一种值得获取的知识!
第十一章:理解环境查询系统
在虚幻引擎中的环境查询系统(EQS)是 AI 框架中的一个强大功能,它允许开发者通过让 AI 代理查询环境并基于返回的结果做出明智的决策来收集关于虚拟环境的数据。在本章中,你将学习如何正确设置环境查询以及如何将其集成到 AI 代理的行为树中。
通过掌握 EQS,你将获得创建能够根据其周围环境做出明智决策的智能 AI 系统的能力。无论是寻找最佳观察点、定位关键资源还是为最佳游戏策略进行规划,EQS 打开了一个充满可能性的世界。虽然仍然是一个实验性功能,但了解 EQS 将让你能够在虚幻引擎中创建智能且动态的 AI 系统。
在本章中,我们将涵盖以下主题:
-
介绍环境查询系统
-
设置环境查询
-
在行为树中处理环境查询
-
显示 EQS 信息
技术要求
要跟随本章介绍的主题,你应该已经完成了前面的章节并理解了它们的内容。
此外,如果你希望从本书的配套仓库中的代码开始,你可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。
要下载最后一章末尾的文件,请点击Unreal Agility Arena – 第十章 - 结束链接。
介绍环境查询系统
嗯,看起来马克斯博士在他的秘密实验室中取得了一些进展:
在隐藏的实验室深处,马克斯博士和维克托利亚教授不知疲倦地致力于他们最新的努力;他们决心通过赋予他们分析和探索环境的新颖方式的能力来革新他们的 AI 木偶。
他们的心中充满了兴奋,马克斯博士和维克托利亚教授精心设计了复杂的算法,他们将他们的创造物注入了无止境的知识渴望,为他们配备了实验性的方法来观察和与世界周围的事物互动。随着 AI 木偶的苏醒,他们的眼睛闪烁着新的智慧火花,每一个都在探索其周围的环境并分析 环境。
Unreal Engine 的 EQS 是一个强大的工具,允许开发者定义复杂的查询来收集有关游戏世界的信息。EQS 使开发者能够创建能够动态适应环境条件变化的 AI 行为。通过使用 EQS,NPC 或其他游戏实体可以根据其周围环境做出智能决策。凭借其灵活性和易用性,EQS 是 Unreal Engine 中创建沉浸式和交互式游戏体验的有价值功能。
使用 EQS,你可以通过一组 测试 来查询收集到的数据,这些测试将生成与查询性质最接近的元素。
一个 EQS 查询 可以在行为树内部触发——或者,通过脚本——根据测试结果来指导决策。这些查询主要由 生成器(确定要测试和加权的地点或演员的元素)和 上下文(为测试或生成器提供参考框架的元素)组成。EQS 查询使 AI 角色能够定位执行攻击玩家、检索健康或弹药拾取、寻找最近的掩护点等任务的最佳位置,以及其他选项。
让我们首先详细检查所有这些元素。
解释生成器
生成器创建将要进行测试和加权的地点或演员——称为 项;结果将返回到属于查询的行为树。以下是一些开箱即用的生成器:
-
类演员:这将找到给定类的所有演员,并将它们作为测试项返回
-
组合:这将允许你创建一个生成器数组并用于测试
-
当前位置:这将允许你获取指定上下文的位置并使用它来验证测试
-
Points:生成器可用于在预定义位置周围创建基于形状的轨迹——圆形、圆锥形、甜甜圈、网格和路径网格。
此外,你可以通过扩展 EnvQueryGenerator 类(如果使用 C++ 开发)或 EnvQueryGenerator_BlueprintBase(如果使用蓝图开发)来实现自己的生成器。
注意
在 C++ 中创建的生成器通常比在蓝图(Blueprints)中开发的生成器运行得更快。
解释上下文
一个上下文为不同的测试和生成器提供了一个参考点,它可以从 查询者——当前由 AI 控制器执行的决策树所拥有的棋子——到涉及某一类型所有演员的更复杂场景。例如,Points: Circle 这样的生成器可以使用一个上下文,该上下文将提供多个位置或演员。
可用的 上下文 类如下:
-
EnvQueryContext_Item:这代表一个位置——作为一个向量——或一个演员
-
EnvQueryContext_Querier:这代表执行行为树的查询者
如您所猜测的,如果您在 C++ 中开发,可以通过扩展 EnvQueryContext 类来实现自己的上下文;如果您使用 Blueprints 开发,则扩展 EnvQueryContext_BlueprintBase 。
解释测试
一个测试确定 环境查询(即对环境的实际请求)使用的标准,以在给定上下文中从生成器中选择最佳项目。
一些现成的可用测试如下:
-
距离:这将返回项目位置和另一个位置之间的距离
-
重叠:盒子:这可以用来检查一个项目是否在测试本身定义的边界内
-
PathExists:从查询器:这可以用来检查是否存在通往上下文的路,并返回一些有用的信息,例如路径有多长
您可以通过扩展 EnvQueryTest 类在 C++ 和 Blueprints 中实现自己的测试。
现在您已经对主要的 EQS 元素有了基本的了解,是时候深入了解并开始使用它实现一个完全工作且有效的 AI 代理了。
设置环境查询
在本节中,您将学习如何将查询添加到行为树中;特别是,我们将调整虚拟枪手 AI 脑部,以便让它能够通过环境查询射击目标。
注意
如前所述,EQS 仍然是一个实验性功能,因此如果您想使用它开发游戏,应谨慎行事。在撰写本书时,EQS 默认通过使用 环境查询 编辑器 插件启用。
创建健身房
作为第一步,我们将创建一个合适的健身房,所以请先按照以下步骤操作:
-
从项目模板中提供的 Level Instances 和 Packed Level Actors 开始,创建您选择的关卡。
-
添加一个 BP_GunnerDummyCharacter 实例;只需记住检查 使用控制器偏航,这样它就可以在需要时旋转并指向目标。
-
添加一个或多个 BP_Target 实例,以便您的 AI 角色能够看到它们。
-
添加一些障碍物,以阻挡 AI 代理的视线。最终的健身房应类似于 图 11 .1 中描述的:

图 11.1 – 完成的健身房
现在是时候为 AI 代理设置 AI 控制器了。
创建 AI 控制器
第二步是创建一个专门的行为树和 AI 控制器,所以请按照以下步骤操作:
-
在 AI 文件夹中,创建一个新的行为树并将其命名为 BT_EQSGunnerDummy 。
-
在 Blueprints 文件夹中,创建一个新的 Blueprint 类,从 BaseDummyAIController 扩展,命名为 AIEQSGunnerDummyController,并打开它。
-
在 详情 面板中,查找 虚拟 AI 控制器 类别,并将 行为树 属性设置为 BT_EQSGunnerDummy 。
-
在关卡中选择枪手虚拟角色,并将其AI 控制器类属性设置为AIEQSGunnerDummyController。
现在 AI 控制器及其控制器角色已经正确设置,是时候设置一个我们将在行为树中使用的环境查询了。
创建环境查询
我们现在将创建一个环境查询,它将在关卡中寻找一个可行的目标。这将与我们在第九章中实现的FindAvailableTarget任务非常相似,有一些不同之处;我们将搜索特定类别的实例,并且目标需要与枪手视线对齐。此外,我们不会编写任何代码。因此,让我们先执行以下步骤:
-
在AI文件夹中,右键单击并选择人工智能 | 环境查询以创建此类资产。
-
将新创建的资产命名为EQS_FindTarget并打开它。
您将看到一个名为查询图(与行为树类似)的图形,它将允许您实现自己的查询。
-
从ROOT节点点击并拖动,添加一个演员类生成节点。
-
选择新创建的节点,在详细信息面板中执行以下操作:
-
将搜索演员类属性设置为BP_Target
-
将搜索半径属性设置为3000.0
-
-
右键单击ActorsOfClass节点,选择添加测试 | 追踪;将在节点内添加一个测试。
-
选择测试,在详细信息面板中执行以下操作:
-
将测试目的属性设置为仅过滤
-
将项目高度偏移属性设置为50.0
-
将上下文高度偏移属性设置为50.0
-
-
取消选中布尔 匹配属性。
环境查询的完整图形如图11 .2 所示:

图 11.2 – 完成环境查询
我们在这里所做显然需要一些解释;我们添加了一个将寻找所有特定类别的演员 – BP_Target – 并且这些演员靠近查询者(执行此环境查询的 AI 代理)。只有那些与查询视线对齐的项目将通过追踪测试,因此它们将是唯一被考虑用于选择可行目标的项目。测试目的已设置为仅过滤,因为我们只需要有一个项目列表,对它们在搜索中的重要性 – 或分数 – 不感兴趣。
现在环境查询已经定义,我们可以开始实现行为树。
在行为树中处理环境查询
现在我们已经准备好实现行为树,正如你将看到的,它与BT_GunnerDummy资产有一些相似之处;唯一的区别是我们获取可能目标的方式。因此,我们将使用与BT_GunnerDummy相同的 Blackboard。所以,无需多言,打开BT_EQSGunnerDummy,在Details面板中,将Blackboard Asset属性设置为BB_GunnerDummy。
现在,专注于行为树图并执行以下步骤:
-
在ROOT节点上添加一个Selector节点并将其命名为Root Selector。
-
在Root Selector节点上添加一个Sequence节点并将其命名为Shoot Sequence。
-
在Shoot Sequence节点上添加一个Blackboard装饰器并将其命名为Has Ammo?。在Details面板中,执行以下步骤:
-
将Notify Observers属性设置为Value Change
-
将Key Query属性设置为Is Not Set
-
将Blackboard Key属性设置为NeedsReload
到目前为止的图应该看起来像图 11.3中显示的那样:
-

图 11.3 – 开始图
现在,让我们将环境查询连接到图中。
-
将Run EQSQuery任务添加到Shoot Sequence,并将其命名为Find Visible Target
-
在选择任务后,执行以下步骤:
-
将Query Template属性设置为EQS_FindTarget。
-
将Run Mode属性设置为从最佳 25%中随机选择一个
-
将Blackboard Key属性设置为TargetActor。
我们在这里执行环境查询,一旦返回结果项,我们就随机选择一个并将其分配给 Blackboard 的TargetActor键,以便它对行为树可用。现在,让我们继续我们的 AI 逻辑。
-
-
在Find Visible Target节点右侧的Shoot Sequence中添加RotateToFaceBBEntry并将其命名为Rotate Towards Target。
-
选择新创建的节点并将Blackboard Key属性设置为TargetActor
你应该已经熟悉这个图的部分了;我们只是在将 AI 代理旋转到目标方向,以便准备射击。
让我们继续处理图。
-
在Shoot Sequence节点右侧的Rotate Towards Target节点右侧添加一个Play Montage节点,并将其命名为Shoot Montage。
-
将Anim Montage属性设置为AM_1H_Shoot。
-
在节点上添加一个Set Ammo服务并将其命名为Deplete Ammo。在Details面板中,执行以下步骤:
-
将Needs Reload属性设置为NeedsReload
-
检查Key Value属性
这将启动射击动画蒙太奇,随后将生成一个弹体。
-
-
在Shoot Montage节点右侧的Shoot Sequence中添加一个Wait任务,将Wait Time属性设置为2.0,将Random Deviation属性设置为0.5。这部分图应该看起来像图 11.4中描述的那样:

图 11.4 – 射击序列
我们现在需要创建装填序列。
-
在射击序列右侧的根选择器中添加一个序列节点,并将其命名为装填序列。
-
在装填序列节点中添加一个播放蒙太奇节点,并将其命名为装填蒙太奇。
-
将动画蒙太奇属性设置为AM_1H_Reload。
-
在节点中添加一个设置弹药服务,并将其命名为补充弹药。在详细信息面板中,执行以下步骤:
-
将需要重新装填属性设置为NeedsReload
-
不勾选键值属性
-
-
在装填序列中,在装填蒙太奇节点右侧添加一个等待任务,将等待时间属性设置为2.0,将随机偏差属性设置为0.5。此部分图表如图11 .5 所示:

图 11.5 – 装填序列
行为树现在已经完成,准备使用。
在本节中,我们展示了如何在行为树中集成环境查询;在下一节中,我们将测试其功能,我将向您展示如何使用调试工具与它一起使用。
显示 EQS 信息
要测试您的健身房,只需启动模拟;你应该注意到以下行为:
-
人工智能代理将射击目标并重新装填武器
-
人工智能代理将避免射击视线外的目标
这种行为应该无限期地进行;这是完全可以接受的,因为枪手正在通过搜索BP_Target类来寻找目标。为了提高健身房,您可能希望实现一些额外的逻辑,例如为被击中的目标实现销毁系统或为每个被击中的目标实现得分计数器。
要在屏幕上显示正在发生的事情,您需要启用调试工具,如第六章中所述,优化导航系统。一旦启用调试工具,您只需按下您的数字键盘上的3键来显示 EQS 信息,然后选择人工智能代理;在我们的例子中,您应该看到一组类似于图11 .6 中所示的信息:

图 11.6 – EQS 调试
显然,每个环境查询都将有其自己的信息。在这个例子中,您会注意到每个目标都会显示一些数据和红色线框,选定的目标将被标记为Winner,如图11 .7 所示:

图 11.7 – 选定的目标
将不会与查询者视线对齐的目标将标记为Trace(0)以显示跟踪失败,如图11 .8 所示:

图 11.8 – 目标不在视线内
如果您想获取查询中每个项目的更详细信息,如图 11.9所示,您可能需要从数字键盘上按除号键(/*):

图 11.9 – 详细信息
我强烈建议您创建自己的查询并在关卡中进行测试。使用调试工具可以在您项目开发过程中节省大量时间。充分利用这些工具以简化工作流程并提高效率。
摘要
在本章中,我们看到了一种新的方法——尽管是实验性的——让您的 AI 代理探测关卡并收集信息,以便做出有意义的决策。
从一点理论开始,我们创建了一个环境查询,开发了一个利用环境查询完全可操作的行为树,并在健身房环境中进行了测试。此外,我们还探讨了使用调试工具来分析关卡内活动的方法。
这本书的第三部分结束。在接下来的章节中,我们将深入探讨 AI 框架中的新功能,从了解实现 AI 代理的替代方法:分层状态机开始。
第四部分:探索高级主题
在本书的第四部分,您将了解 Unreal Engine 生态系统中的某些前沿和实验性 AI 功能。此外,您还将获得如何在您的项目中集成这些功能的知识。
本部分包括以下章节:
-
第十二章 ,使用分层状态机与状态树
-
第十三章 ,使用质量实现面向数据的计算
-
第十四章 ,使用智能对象实现交互元素
第十二章:使用状态树中的层次状态机
虚幻引擎提供了一个强大的框架,通过称为状态树的层次状态机来创建复杂的 AI 行为。通过定义各种状态和转换,开发者可以设计复杂的 AI 逻辑,使其能够适应动态环境。状态树提供了一种结构化的方法来管理 AI 行为,允许高效决策并与其他虚幻引擎中的系统无缝集成。更重要的是,使用状态树,您可以构建聪明的 AI 代理,它们能够对环境刺激做出反应,并以自然和逼真的方式与游戏世界互动。本章的目的是向您介绍虚幻引擎中的状态树框架及其基本概念。
在本章中,我们将简要介绍虚幻引擎中可用的状态树系统,并了解如何在项目中实现状态树。
在本章中,我们将涵盖以下主题:
-
介绍状态树
-
创建和管理状态树
-
使用高级状态树功能
技术要求
要跟进本章中介绍的主题,您应该已完成前面的章节并理解其内容。
您将使用本书配套仓库中可用的起始内容,该仓库位于github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5。通过此链接,找到本章的相应部分,并下载以下.zip文件:Unreal Agility Arena – 起始内容。
虽然不是强制性的,但您可以使用到目前为止创建的代码或通过点击Unreal Agility Arena – 第十一章 - 结束链接下载与上一章结尾对应的文件。
介绍状态树
很显然,迟早有人会揭露马克斯博士的秘密实验:
在他们的秘密实验室中,马克斯博士和维克托利亚教授继续进行他们的开创性实验。然而,他们非凡发明的消息开始像野火一样迅速传播;摄影师和好奇的人开始涌向该地区,渴望揭开实验室墙壁隐藏的秘密。
马克斯博士和维克托利亚教授意识到他们需要采取极端措施来保护他们宝贵的研究。凭借他们的专业知识,这对天才组合开始用高级算法和行为模式增强他们钟爱的 AI 木偶。他们编程木偶以检测和响应未经授权的入侵,试图确保实验室 和研究的 安全。
状态树是虚幻引擎中层次状态机的版本,它将行为树的选择器与状态机合并,使用户能够构建高效且井然有序的逻辑。
注意
层次状态机是一种在软件开发中用于模拟具有多个状态和转换的复杂系统的设计模式。它通过引入层次嵌套状态的概念扩展了传统有限状态机的概念。在层次状态机中,状态可以被组织成一个层次结构,其中高级状态封装并控制低级状态;这种嵌套允许对系统行为进行更模块化和组织化的表示。每个状态都可以有自己的子状态集,这些子状态还可以进一步拥有自己的子状态,形成一个层次树状结构。这种模式的主要优势是它提供了一种在多个状态之间重用行为的方法。而不是在不同的状态中重复类似的逻辑,可以在高级状态中定义共同的行为,并由其子状态继承。这促进了代码的重用性,减少了冗余,并简化了整体设计。
状态树是按层次结构组织的,状态选择过程通常从根开始。然而,状态选择可以从树中的任何节点启动。
当选择状态时,系统评估状态本身的进入条件集;如果条件满足,选择将进展到子状态。如果没有子状态存在,这意味着已经到达了叶节点,当前状态将被激活。
激活一个状态将触发从根到叶状态的所有状态,其中每个状态都包含任务和转换。
选择状态后,所选状态及其所有父状态变为活动状态,从根到叶状态执行所有活动状态的任务。状态中的所有任务都是并发执行的,第一个完成任务将触发可能导致选择新状态的转换。
转换可以指向树中的任何状态,并且它们由必须满足以使转换继续的一系列触发条件触发。图 12.1展示了状态树的典型示例(示例取自 Epic Games Marketplace 中免费提供的城市样本项目):

图 12.1 – 状态树示例
总结来说,状态树的主要元素如下:
-
根状态:当状态树开始执行其逻辑时首先选择的状态
-
选择器状态:具有子状态的状态
-
状态进入条件:列出决定状态是否可以被选择的条件
-
任务:列出当状态激活时将执行的一系列操作
-
转换:将触发状态选择过程的条件
注意
如果您感到好奇,状态树和行为树都是人工智能中使用的决策架构,但它们有不同的用途。状态树围绕离散状态和转换构建,关注实体的当前状态及其对事件的响应方式。这使得它们适用于需要明确、清晰状态的场景。相比之下,行为树旨在处理更复杂和流动的决策,允许模块化和分层任务执行。它们通过将简单动作组合成复杂序列,实现任务之间的平滑过渡,并能处理更复杂的行为。
现在您已经对主要状态树术语有了基本的了解,我们将展示如何扩展您自己的状态树。
扩展状态树
可以创建状态树以在 AI 控制器上执行或直接从演员执行。有两个不同的组件可用于处理状态树:
-
StateTreeComponent:这可以附加到任何演员上并由演员本身执行
-
StateTreeAIComponent:这可以附加到任何 AI 控制器上并由 AI 控制器本身执行
此外,如您可能已经猜到的,状态树系统是考虑到可扩展性而创建的,这意味着您可以创建自己的任务、评估器和条件。虽然您可以创建自己的 C++结构,但状态树是考虑到蓝图创建而实现的。特别是,可用的主要类如下:
-
StateTreeTaskBlueprintBase:用于实现您自己的任务
-
StateTreeEvaluatorBlueprintBase:用于实现您自己的评估器
-
StateTreeConditionBlueprintBase:用于实现您自己的条件
在扩展状态树时,建议使用蓝图而不是 C++来实现您自己的节点逻辑,这样可以提高灵活性和易用性。
理解状态树流程
状态树中的状态选择从树根开始,通过评估每个进入条件沿着树向下进行。评估过程遵循以下步骤:
-
如果进入条件不满足,选择将跳转到下一个兄弟状态
-
如果进入条件满足且状态是叶子节点,则将其选为新的状态
-
如果进入条件满足但状态不是叶子节点,则对第一个子状态执行过程
应该注意的是,状态选择是动态运行的,由转换触发。在第一次 tick 时,有一个隐式转换到根状态,然后确定要执行的初始状态。随后,一旦选择了这个状态,转换指定了触发选择逻辑的条件,确定何时以及在哪里执行。
一旦选择了状态,所有其任务都会执行,并且会持续执行,直到转换触发新的状态选择过程。
数据绑定
在游戏编程中,数据绑定指的是连接游戏不同部分之间数据的过程——例如用户界面和游戏逻辑——它涉及创建一个链接,允许数据在游戏的各个元素之间同步和更新。这有助于保持游戏元素的一致性和与最新信息的更新。
状态树使用数据绑定在树内传输数据以及为执行建立条件或配置任务。数据绑定允许以指定方式访问传递到状态树或节点之间的数据。
状态树节点具有以下元素以实现数据绑定:
-
参数:这些可以在树的执行过程中引用。
-
上下文数据:这表示状态树中可用的预定义数据。
-
评估器:这些是可以在运行时执行且可以公开数据的独立类,这些数据无法通过参数和上下文数据提供。
-
全局任务:这些在根状态之前执行,可以在你需要永久数据时在状态选择中使用。
值得注意的是,状态树中的节点可以在自己之间共享数据,并且可以以三种先前提到的方式绑定数据:
-
状态进入条件
-
转换条件
-
任务
在本节中,我们介绍了状态树及其关键特性。在下一节中,我们将通过构建自己的状态树来深入实践,以便在健身房环境中有效地使用它们。
创建和管理状态树
从本节开始,我们将基于几个 AI 代理使用状态树而不是行为树来创建一个新的健身房。这将帮助我们理解这种新开发模式背后的基本原理。
为了帮助我们理解这些原理,我们将执行以下操作:
-
在级别中创建/放置一个演员,该演员将使用专用状态树定期发出噪音
-
创建/放置一个虚拟角色,该角色将由另一个状态树管理,并将执行以下操作:
-
在其起始位置保持空闲
-
当感知到噪音时,到达噪音位置
-
在调查位置后不久返回其起始位置
-
虽然这个逻辑相当简单,但它可以用作一个起始点,用于构建一个调查级别寻找入侵者并对周围任何可疑噪音做出反应的守护者 AI 代理。
由于状态树功能默认未启用,首先需要做的事情就是进入 插件 窗口并启用它。
启用状态树插件
要开始使用状态树,您需要启用一些专用插件。为此,请按照以下步骤操作:
-
通过从主菜单中选择 编辑 | 插件 来打开 插件 窗口。
-
搜索 GameplayStateTree 和 StateTree 插件并启用它们。
-
重新启动虚幻引擎编辑器以激活它们。

图 12.2 – 插件窗口
如果您计划使用 C++中的状态树(我们确实如此),您需要将模块添加到 Unreal Engine 构建文件中。
在我们的情况下,我们需要在构建中包含GameplayStateTreeModule依赖项,以便该模块可以供您的 C++实现使用。为此,打开您的 IDE,并在您的项目中定位到UnrealAgilityArena.build.cs文件;它应该位于UnrealAgilityArena/Source文件夹中。
注意
Unreal Engine 的.build.cs文件负责定义项目的构建方式,包括定义模块依赖项的选项。
查找以下代码行:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
更新如下:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayStateTreeModule" });
因此,一旦您声明了这个模块,您将拥有在 C++中处理状态树所需的所有内容。
注意
在撰写本书时,似乎在 C++中声明StateTreeAIComponent类时存在一些问题,而StateTreeComponent类则运行良好。基本上,StateTreeAIComponent类似乎在模块中不可用,使用此类不会编译您的项目。为了克服这个问题,我们需要在需要时从 Blueprint 中添加StateTreeAIComponent类。
一旦插件被激活,我们就可以开始使用状态树实现我们的第一个 AI 代理:噪声发射器。
实现噪声发射器演员
现在,我们将创建一个演员,其唯一目的是通过感知系统定期发出噪声。这项任务很简单,可以以任何您认为合适的方式实现。然而,为了这个演示,我们将使用状态树来掌握这个系统的基本原理。
创建噪声发射器类
我们将首先创建噪声发射器的基类;我们需要声明所有视觉元素,最重要的是所需的感知系统组件和状态树组件。此外,我们将包含一个生成噪声的函数,而不关心将管理它的逻辑;这项责任将委托给状态树。
让我们从创建一个新的 C++类开始,该类扩展Actor并命名为BaseNoiseEmitter。一旦创建了类,打开BaseNoiseEmitter.h文件,并在#include声明之后添加以下前置声明:
class UAIPerceptionStimuliSourceComponent;
class UStateTreeComponent;
之后,将类更改为 Blueprint 可接受的基类,通过将UCLASS()宏更改为以下内容:
UCLASS(Blueprintable)
查找Tick()声明并将其删除,因为我们不会使用它。
接下来,在GENERATED_BODY()宏之后添加所需的组件:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Dummy Target", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* BaseMeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Dummy Target", meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* DummyMeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Dummy Target", meta=(AllowPrivateAccess="true"))
UAIPerceptionStimuliSourceComponent* PerceptionStimuliSourceComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Dummy Target", meta=(AllowPrivateAccess="true"))
UStateTreeComponent* StateTreeComponent;
如前述代码所示,我们将使用一些静态网格,以及所需的状态树和感知系统组件。
现在,就在构造函数声明之后,添加以下声明:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Noise Generation")
float MaxNoiseRange = 3000.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Noise Generation")
float NoiseRangeRandomDeviation = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Noise Generation")
FName NoiseTag = "EmitterNoise";
UFUNCTION(BlueprintCallable)
void EmitNoise();
如你所见,我们正在公开一些属性以自定义级别中的发射器实例,并声明一个EmitNoise()函数,我们将使用它来在需要时激活噪音发射。最后,NoiseTag属性将用于标记噪音,并被监听智能体识别。
现在是时候打开BaseNoiseEmitter.cpp文件并实现方法了。作为第一步,移除Tick()函数,并在构造函数中修改以下代码行:
PrimaryActorTick.bCanEverTick = true;
将其修改为以下内容:
PrimaryActorTick.bCanEverTick = false;
之后,添加所需的#include声明,所以将以下代码块添加到文件顶部:
#include "Components/StateTreeComponent.h"
#include "Perception/AIPerceptionStimuliSourceComponent.h"
#include "Perception/AISense_Hearing.h"
现在,让我们在构造函数中初始化静态网格组件,添加以下代码:
BaseMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
RootComponent = BaseMeshComponent;
static ConstructorHelpers::FObjectFinder<UStaticMesh> BaseStaticMeshAsset(
TEXT("/Game/KayKit/SpaceBase/landingpad_large.landingpad_large"));
if (BaseStaticMeshAsset.Succeeded())
{
BaseMeshComponent->SetStaticMesh(BaseStaticMeshAsset.Object);
}
DummyMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT
("DummyMesh"));
DummyMeshComponent->SetupAttachment(RootComponent);
DummyMeshComponent->SetRelativeRotation(FRotator
(0.f, -90.f, 0.f));
DummyMeshComponent->SetRelativeLocation(FVector
(0.f, 0.f, 80.f));
static ConstructorHelpers::FObjectFinder<UStaticMesh> DummyStaticMeshAsset(TEXT("/Game/KayKit/PrototypeBits/Models/Dummy_ Base.Dummy_Base"));
if (DummyStaticMeshAsset.Succeeded())
{
DummyMeshComponent->SetStaticMesh(DummyStaticMeshAsset.Object);
}
你应该已经熟悉所有这些,所以我们可以继续并声明感知系统的刺激源和状态树,添加以下代码:
PerceptionStimuliSourceComponent = CreateDefaultSubobject <UAIPerceptionStimuliSourceComponent>(TEXT("PerceptionStimuli Source"));
PerceptionStimuliSourceComponent->RegisterForSense (UAISense_Hearing::StaticClass());
StateTreeComponent = CreateDefaultSubobject<UStateTreeComponent>(TEXT
("StateTree"));
如你所见,我们只是在创建所需组件;此外,我们正在将听觉感知注册为感知系统的刺激源。
现在,在BeginPlay()函数中,添加以下代码行:
PerceptionStimuliSourceComponent->RegisterWithPerceptionSystem();
StateTreeComponent->StartLogic();
在这里,我们正在注册感知系统,并开始状态树的逻辑。这意味着游戏开始后,状态树将开始执行。
最后要做的就是实现EmitNoise()函数,所以添加以下这段代码:
void ABaseNoiseEmitter::EmitNoise()
{
const auto NoiseRange = MaxNoiseRange +
FMath::RandRange(-1.f * NoiseRangeRandomDeviation,
NoiseRangeRandomDeviation);
UAISense_Hearing::ReportNoiseEvent(GetWorld(),
GetActorLocation(), 1.f, this, NoiseRange, NoiseTag);
}
在第十章 ,通过感知系统改进智能体,你已经学习了如何处理视觉感知。在听觉方面,情况略有不同;虽然可见性是持续发生的,但只有当你发出噪音时才会被听到。这就是为什么我们随机化一个噪音范围——基于之前声明的属性——我们使用ReportNoiseEvent()函数来发出实际的噪音。
这个类已经准备好了,我们现在可以专注于实际状态树的创建,从自定义任务开始:告诉演员发出噪音的东西。
创建发出噪音的任务
我们将要创建的状态树任务只需要告诉BaseNoiseEmitter实例执行EmitNoise()函数。这个任务将作为一个蓝图类创建,所以,在虚幻引擎编辑器的内容浏览器中,导航到AI文件夹,执行以下操作:
-
创建一个新的蓝图类,从StateTreeTaskBlueprintBase扩展,命名为STT_EmitNoise。双击它以打开它。
-
在我的蓝图面板中,悬停在函数部分,并点击出现的覆盖下拉菜单。
-
选择进入状态选项,如图图 12 .3 所示:

图 12.3 – 创建进入状态函数
将在事件图中添加一个事件进入状态节点;当状态树中的新状态被进入且任务是活动状态的一部分时,将执行此事件。
对于这个状态,我们需要一个指向拥有演员的引用;如前所述,状态树使用数据绑定进行通信。因此,我们将利用这一特性来创建引用。为此,请按照以下步骤操作:
-
创建一个新的变量,它是BaseNoiseEmitter类型的对象引用,并将其命名为Actor。
-
选择变量,在详细信息面板中,找到类别属性,并在输入字段中键入上下文,如图图 12 .4 所示:

图 12.4 – 上下文类别
虽然变量的创建是显而易见的,但将类别名称设置为上下文的值需要一些解释;每次向上下文类别添加属性时,该属性本身将通过数据绑定暴露给将要执行的任务的状态树。当你需要从执行状态树获取信息或反之亦然时,这非常有用。
在有了这个新参考后,执行以下步骤:
-
从变量部分,将Actor变量拖入事件图,并添加一个获取 Actor节点。
-
从Actor节点的输出引脚,连接一个发出 噪声节点。
-
将Event Enter State节点的输出执行引脚与发出 噪声节点的输入执行引脚连接。
-
从Emit Noise节点的输出执行引脚,连接一个完成 任务节点。
-
选择Finish Task节点的成功复选框。最终的图形应该看起来像图 12 .5 中所示的那样:

图 12.5 – 发出噪声图
这里唯一值得提的是,在发出噪声后,Finish Task节点将返回一个成功值。现在这项任务已经完成,我们终于可以开始处理状态树了。
创建噪声发射状态树
如前所述,我们将从演员执行状态树;这意味着我们需要能够与StateTreeComponent类一起使用的东西。为了做到这一点,我们需要创建一个遵循由StateTreeComponentSchema类规定的规则的资产,该类确保可以访问执行状态树的演员。要创建此类资产,请按照以下步骤操作:
-
在内容浏览器中,打开AI文件夹,右键单击它,选择人工智能 | 状态树。
-
从为状态树选择模式弹出窗口,选择StateTreeComponentSchema。

图 12.6 – 状态树创建
-
将新创建的资产命名为ST_NoiseEmitter,双击它以打开它。
一旦打开资产,找到位于编辑器左侧的StateTree标签,并注意有一个Context Actor Class属性,如图图 12 .7 所示:

图 12.7 – 状态树上下文演员
这是所有者演员的引用;目前,它设置为通用演员,但我们需要更具体一些,因此点击下拉菜单并选择BaseNoiseEmitter的对象引用。从现在起,树中的每个节点都将有权访问此引用。
现在,让我们开始实现状态树逻辑:
-
点击添加状态按钮三次以创建三个状态。
-
选择每个状态,并在详细信息面板中分别命名为随机延迟、调试信息和发出噪声。您的状态树应类似于图 12 .8 中所示:

图 12.8 – 初始状态
如您所见,我们已经创建了状态树的基础结构,包含三个主要状态,这些状态将等待随机时间,显示调试信息,并最终发出噪声。
我们现在需要实现每个状态及其自己的任务和转换;让我们从第一个开始。选择随机延迟任务并执行以下操作:
-
在详细信息面板中,定位到任务部分,并点击+按钮添加一个新任务。
-
从新创建的任务中,点击下拉菜单并选择延迟任务。
-
通过点击任务名称旁边的微小箭头图标来扩展任务。将持续时间属性设置为10.0并将随机偏差属性设置为3.0。
-
定位到转换部分,并点击+按钮创建一个新的转换。
-
您应该看到一个标记为在状态转换完成后转到状态根的项目。点击微小的箭头图标以展开它,并从转换到属性的下拉菜单中选择下一个状态。此状态应类似于图 12 .9 中所示:

图 12.9 – 随机延迟状态
我们现在可以开始处理第二个状态——即调试信息——它将显示游戏中的消息。这显然不是使我们的状态树工作所必需的,但它有助于学习事物是如何工作的。选择此状态并执行以下操作:
-
在详细信息面板中,定位到任务部分,并点击+按钮添加一个新任务。
-
从新创建的任务中,点击下拉菜单并选择调试文本任务。
-
通过点击任务名称旁边的微小箭头图标来扩展任务。将文本属性值设置为发出噪声!并将文本颜色属性设置为您的选择颜色——在我的情况下,我选择了明亮的黄色。
-
创建另一个任务;点击下拉菜单并选择延迟任务。
-
通过点击任务名称旁边的微小箭头图标来扩展任务,并将持续时间属性设置为0.5。
-
定位到转换部分,并点击+按钮创建一个新的转换。
-
你应该看到一个标记为 On State Transition Completed Go to State Root 的项;点击小箭头图标展开它,并从 Transition To 属性的下拉菜单中选择 Next State 。这个状态应该看起来像 图 12.10 :

图 12.10 – 调试信息状态
现在选择最后一个状态——即 Emit Noise ——并执行以下操作:
-
添加一个新任务,类型为 STT Emit Noise 。
-
通过点击小箭头图标展开任务;你应该注意到一个名为 Actor 并标记为 CONTEXT 的属性。在其最右边,你应该看到一个下拉菜单箭头。点击它打开它并选择 Actor 。

图 12.11 – 绑定
最后这个动作在状态树的 Actor 属性和我们创建 STT_EmitNoise 任务时添加的 Actor 属性之间建立了绑定。这个最后的属性已经被公开,因为在任务蓝图;我们将它的类别设置为 Context 。
最后这个状态将看起来像 图 12.12 中所示的那样:

图 12.12 – 发出噪声状态
请注意,我们没有为这个任务添加转换。默认行为是指向 Root 节点,我们想要创建一个无限循环,所以我们简单地保留了默认行为。
注意
如果你对数据绑定不熟悉,一开始事情可能看起来有点奇怪,但不要担心。随着你逐渐习惯,事情将会变得相当简单且易于理解。
状态树已完成,应该看起来像 图 12.13 :

图 12.13 – 完成的状态树
如你所见,了解每个状态及其状态树流程如何进展是非常容易的。现在是时候将所有东西整合起来,让噪声发射器运行起来!
创建噪声发射器蓝图
从 BaseNoiseEmitter 类创建蓝图相当直接,所以我们按照以下步骤进行:
-
创建一个新的蓝图类,从 BaseNoiseEmitter 继承,并命名为 BP_NoiseEmitter 。
-
打开它,并在 AI 部分找到 State Tree 属性。从下拉菜单中,将其值设置为 ST_NoiseEmitter 。

图 12.14 – 完成的 BP_NoiseEmitter
这就是你实现噪声发射器所需做的所有事情。重要的是要注意,我们所创建的从技术上讲不是一个 AI 代理。这是状态树的美丽之处;一旦你掌握了这个概念,你将能够将你的逻辑应用到许多不同类型的用例中。
现在噪声发射器已经准备好了,是时候在一个关卡中测试它了。
测试噪声发射器
要测试噪声发射器,您只需创建一个新的健身房并添加一些 BP_NoiseEmitter 实例。为此,请按照以下步骤操作:
-
创建一个新的关卡,从我在项目模板中提供的 Level Instances 和 Packed Level Actors 开始。
-
将一个或多个 BP_NoiseEmitter 实例添加到关卡中。
-
播放关卡。
注意
请注意,如果我们点击 Simulate 按钮,Debug Text Task 消息将不会显示。要显示游戏中的消息,您需要使用常规的 Play 按钮。
当关卡播放时,您将看到每个 BP_NoiseEmitter 实例在随机时间显示调试消息,如图 图 12 .15 所示:

图 12.15 – 测试健身房
在本节中,我们看到了如何在演员内部实现状态树。从创建一个调用拥有者演员中方法的自定义任务开始,然后我们创建了我们的第一个无限循环的状态树,为感知系统发出噪声信号。
在以下部分,我们将创建一个守卫木偶,它将监听噪声事件并相应地做出反应。我们将使用状态树来完成这项工作。
使用高级状态树功能
在本节中,我们将再次扩展 BaseDummyCharacter 类以创建一个能够监听噪声信号并移动到噪声发生位置的 AI 代理。一旦检查了位置,AI 代理将返回到其原始位置。我们将首先创建一个 AI 控制器,它将通过感知系统获得听觉能力,并通过状态树处理其 AI 逻辑。我们实际上正在开发一个守卫来保护关卡免受入侵者。像往常一样,让我们首先创建我们自己的基础 C++ 类。
创建 C++ AI 控制器
人工智能控制器类需要实现听觉感知并执行状态树逻辑。如前所述,在撰写本书时,似乎存在一个 Unreal Engine 的错误,阻止我们在 C++ 中声明 StateTreeAIComponent 类,因此,暂时我们将只实现听觉感知,并从蓝图添加状态树组件。让我们创建一个新的 C++ 类,称为 BaseGuardAIController。然后,打开 BaseGuardAIController.h 文件,在 #include 声明之后添加以下前置声明:
struct FAIStimulus;
然后,添加一个 public 部分,包含以下函数声明:
public:
ABaseGuardAIController();
UFUNCTION(BlueprintCallable,
BlueprintImplementableEvent)
void OnTargetPerceptionUpdate(AActor* Actor,
FAIStimulus Stimulus);
我们已经熟悉了这些函数声明,但请注意,OnTargetPerceptionUpdate() 函数添加了 BlueprintImplementableEvent 指示符;这将使我们能够从扩展蓝图而不是直接从此类中实现此函数。这意味着我们将实现此函数的责任留给了蓝图。现在,让我们打开 BaseGuardAIController.cpp 文件来实现函数。您应该添加的所需 #include 声明如下:
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Hearing.h"
然后,添加构造函数实现:
ABaseGuardAIController::ABaseGuardAIController()
{
const auto SenseConfig_Hearing =
CreateDefaultSubobject<UAISenseConfig_Hearing>
("SenseConfig_Hearing");
SenseConfig_Hearing->
DetectionByAffiliation.bDetectEnemies = true;
SenseConfig_Hearing->
DetectionByAffiliation.bDetectNeutrals = true;
SenseConfig_Hearing->
DetectionByAffiliation.bDetectFriendlies = true;
SenseConfig_Hearing->HearingRange = 2500.f;
SenseConfig_Hearing->SetStartsEnabled(true);
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT
("Perception"));
PerceptionComponent->
ConfigureSense(*SenseConfig_Hearing);
PerceptionComponent->
SetDominantSense(SenseConfig_Hearing->
GetSenseImplementation());
PerceptionComponent->
OnTargetPerceptionUpdated.AddDynamic(this,
&ABaseGuardAIController::OnTargetPerceptionUpdate);
}
我们已经知道如何配置 AIPerceptionComponent,请参阅 第十章 ,通过感知系统改进代理,因此我不会在这里提供额外细节。只需注意代码的最后一行,其中我们注册了将管理听觉刺激的委托。
您现在可以编译项目,使此类对蓝图系统可用,因为在接下来的几个步骤中,我们将创建 AI 控制器蓝图。
实现 AI 控制器蓝图
现在基本 AI 控制器已准备就绪,我们可以开始实现一个将管理状态树的蓝图版本。要开始,在 Blueprints 文件夹中,创建一个从 BaseGuardAIController 扩展的新蓝图类,命名为 AIGuardController,并打开它。然后,执行以下步骤:
-
创建一个名为 NoiseTag 的新变量,并设置为 Name 类型;将其设置为 Instance Editable。编译此蓝图后,将此变量的默认值设置为 EmitterNoise。
-
创建另一个名为 NoiseLocation 的 Vector 类型的变量;将其设置为 Instance Editable。
-
创建一个名为 StartLocation 的 Vector 类型的第三个变量;将其设置为 Instance Editable。
现在,让我们按照后续步骤处理状态树。
-
添加一个 StateTreeAI 类型的组件。
-
将组件拖入事件图以添加对该组件本身的引用。
-
从 State Tree AI 节点的输出插针连接一个 Start Logic 节点。
-
将 Event Begin Play 节点的输出执行插针连接到 Start Logic 节点的输入执行插针。事件图应类似于 图 12 .16:

图 12.16 – Event Begin Play
此部分图将启动状态树的执行。现在,让我们存储 AI 角色的起始位置。为此,请按照以下步骤操作。
-
从 Variables 部分将 StartLocation 变量拖入事件图,并将其设置为设置节点。
-
将 Start Logic 节点的输出执行插针连接到 Set Start Location 节点的输入执行插针。
-
添加一个 Get Actor Location 节点,并将其 Return Value 插针连接到 Set Start Location 节点的 Start Location 插针。
-
添加一个 Get Controller Pawn 节点,并将其 Return Value 插针连接到 Get Actor Location 节点的 Target 插针。此部分图示在 图 12 .17 中:

图 12.17 – 存储起始位置
Event BeginPlay 代码逻辑已完成,因此我们可以开始实现之前声明的 OnTargetPerceptionUpdated() 函数。为此,请按照以下步骤操作:
-
在事件图中右键单击并添加一个 Event On Target Perception Update 节点。
-
从Stimulus输出引脚点击并拖动,添加一个Break AIStimulus节点。
-
从变量面板,拖动NoiseTag变量的 getter 节点。
-
从Noise Tag节点的输出引脚,添加一个Equal (==)节点。
-
将Break AIStimulus节点的Tag输出引脚连接到Equal (==)节点的第二个输入引脚。
-
将Event On Target Perception Update节点的输出执行引脚连接到一个Branch节点。
-
将Equal (==)节点的输出引脚连接到Break节点的Condition引脚。此部分图示见图 12 .18:

图 12.18 – 检查刺激标签
我们现在需要存储噪声的位置,所以请按照以下步骤操作:
-
从变量部分,拖动NoiseLocation引用以创建一个Set节点。
-
将节点的输入执行引脚连接到Branch节点的True执行引脚。
-
将Set Noise Location节点的Noise Location引脚连接到Break AI Stimulus节点的Stimulus Location引脚。此部分图示见图 12 .19:

图 12.19 – 存储噪声位置
我们最后需要做的是通知状态树已经听到了噪声,并且 AI 代理需要相应地做出反应。为此,我们将使用Gameplay Tags。
注意
在虚幻引擎中,游戏玩法标签系统用于标记和分类游戏元素。游戏玩法标签是轻量级的标识符,可以轻松地附加到游戏实体(如演员或组件)上,以灵活和高效的方式组织和分类它们。学习如何使用游戏玩法标签超出了本书的范围;我们将只学习最基本的内容,以便正确地与状态树通信。
让我们继续进行事件图的实现,按照以下步骤操作:
-
在图中添加一个Make StateTreeEvent节点。
-
在图中添加一个Send State Tree Event节点。
-
将StateTreeAI组件的引用拖入图中。
-
将Set Noise Location节点的输出执行引脚连接到Send State Tree Event节点的输入执行引脚。
-
将State Tree AI节点连接到Send State Tree Event节点的Target引脚。
-
将Make State Tree Event节点的输出引脚连接到Send State Tree Event节点的输入Event引脚。
-
在Make StateTreeEvent节点的Origin输入字段中,键入AI Controller。此部分图应类似于图 12 .20:

图 12.20 – 发送状态树事件
这段代码负责通过发送事件与状态树通信;该事件需要被标记以便状态树本身能够识别。为此,我们需要创建一个游戏标签。您可以通过以下步骤完成此操作:
-
在 Make StateTreeEvent 节点中,点击 标签 输入引脚旁边的下拉菜单。目前,它应标记为 None。
-
您将获得一个可用标签列表;点击 管理游戏标签… 选项以打开 游戏标签 管理器 窗口。

图 12.21 – 管理游戏标签…选项
-
一旦 游戏标签管理器 窗口打开,点击 + 按钮。在 名称 输入字段中,输入 UnrealAgilityArena.StateTree.HeardNoise,并在 来源 字段中选择 DefaultGameplayTags.ini。
-
点击 添加新标签 按钮以确认创建新的游戏标签。

图 12.22 – 创建游戏标签
- 在 Make StateTreeEvent 节点中,点击 标签 下拉菜单并选择 HeardNoise 复选框以选择该游戏标签。

图 12.23 – 选择游戏标签
我们几乎完成了 AIGuardController 蓝图的制作;唯一剩下的事情就是包含状态树引用,但我们必须先创建它!
实现状态树
现在,我们将实现状态树。这次,我们将从 AIGuardController 蓝图中执行它。这就是为什么我们需要一个常规状态树的子类 – 那就是状态树 AI – 它将有一个对拥有 AI 控制器的引用。
主要状态如下:
-
空闲:AI 代理将停留在其起始位置
-
警报:AI 代理已收到噪音通知,它将去检查位置
-
返回起始位置:AI 代理将返回其起始位置
因此,让我们先打开 AI 文件夹并执行以下步骤:
-
右键点击并选择 人工智能 | 状态树。
-
从 选择状态树架构 弹出窗口中,选择 StateTreeAIComponentSchema 并将新创建的资产命名为 STAI_Guard 。双击它以打开它。
-
在 状态树 左侧面板中,将 AI 控制器类 属性设置为 AIGuardController,将 上下文演员类 属性设置为 BP_Guard。
正如我们之前提到的,上述步骤将状态树绑定到拥有 AI 控制器和演员;这样您将有权访问它们的属性。
我们现在将实现基本状态。为此,执行以下步骤:
-
创建三个状态,分别命名为 Idle、Warned、Resume。
-
选择 警告 和 恢复 状态,并在 详细信息 面板中,将 类型 属性设置为 组。
我们将警告和恢复状态标记为组,因为它们不会包含任务,而是将任务委托给子状态。它们基本上充当状态容器。
作为额外选项,状态树面板有一个主题部分,它允许您定义可以应用于详细信息面板中每个状态及其子状态的州颜色。在我的情况下,我选择了图 12 .24中显示的颜色:

图 12.24 – 基础状态
现在我们将分别实现每个状态。
实现空闲状态
空闲状态将会非常简单;我们将让 AI 代理在一个无限循环中等待,直到我们收到噪声通知。要实现此状态,选择它并执行以下步骤:
-
添加一个具有持续时间属性设置为10.0的延迟任务。
-
添加一个具有以下设置的过渡:
-
触发属性设置为在状态完成时
-
过渡到属性设置为空闲
-
-
添加另一个具有以下设置的过渡:
-
触发属性设置为在事件发生时
-
事件标签属性设置为UnrealAgilityArena.StateTree.HeardNoise
-
过渡到属性设置为警报
-
优先级属性设置为高
-

图 12.25 – 空闲状态
如您所见,我们将在这个状态内部持续循环,直到从 AI 控制器收到通知,表示已经听到噪声。在这种情况下,我们将过渡到警报状态。
实现警报状态
一旦进入警报状态,AI 代理将尝试移动到噪声位置。一旦到达那个位置,它将等待一段时间后再改变状态;为此我们需要两个子状态。
因此,要创建第一个子状态,请执行以下步骤:
-
创建一个新的状态并命名为移动到 噪声位置。
-
添加一个动作 | 移动到类型的任务,并按照以下步骤进行操作:
-
通过点击下拉箭头并选择AIController将AIController属性(标记为上下文)绑定到拥有者 AI 控制器。
-
定位到目标属性,点击下拉箭头,选择AIController | 噪声位置以将此属性绑定到 AI 控制器拥有者的NoiseLocation属性。
-
-
添加一个具有以下设置的过渡:
-
触发属性设置为在状态成功时
-
过渡到属性设置为下一个状态
-
-
添加另一个具有以下设置的过渡:
-
触发属性设置为在状态失败时
-
过渡到属性设置为恢复
-

图 12.26 – 移动到噪声位置状态
- 状态树的这一部分将 AI 代理移动到拥有 AI 控制器内噪声位置属性中设置的位置。一旦成功,将执行下一个状态。如果位置无法到达,它将回到原始位置。
现在通过以下步骤创建警报状态的第二个子状态:
-
创建一个新的状态,并将其命名为检查 噪声位置。
-
添加一个延迟任务类型的任务,并按照以下步骤操作:
-
将持续时间属性设置为3.0
-
将随机偏差属性设置为1.0
-
-
添加一个具有以下设置的转换:
-
将触发属性设置为在状态完成时
-
将转换到属性设置为恢复
-

图 12.27 – 检查噪声位置状态
状态树的这一部分将使 AI 代理稍作等待,然后返回到其原始位置。如果 AI 代理没有发现任何可疑之处,它将返回到其警戒位置。
实现恢复状态
恢复状态需要将 AI 代理带回到其原始位置;此外,在任何时候,如果通知了新的噪声,此状态应被中断。因此,要创建第一个子状态,请按照以下步骤操作:
-
创建一个新的状态,并将其命名为移动到 起始位置。
-
添加一个动作 | 移动到类型的任务,并按照以下步骤操作:
-
通过点击下拉箭头并选择AI 控制器,将标记为上下文的AI 控制器属性绑定到拥有 AI 控制器。
-
定位到目标属性,点击下拉箭头,选择AI 控制器 | 起始位置以将此属性绑定到 AI 控制器所有者的起始位置属性。
-
-
添加一个具有以下设置的转换:
-
将触发属性设置为在状态完成时
-
将转换到属性设置为下一个状态
-

图 12.28 – 移动到起始位置状态
此状态与移动到噪声位置类似;唯一的区别是目标属性,在这种情况下,是 AI 代理的原始位置。
现在通过以下步骤创建恢复状态的第二个子状态:
-
创建一个新的状态,并将其命名为等待。
-
添加一个延迟任务类型的任务,并按照以下步骤操作:
-
将持续时间属性设置为2.0
-
将随机偏差属性设置为1.0
-
-
添加一个具有以下设置的转换:
-
将触发属性设置为在状态完成时
-
将转换到属性设置为空闲
-

图 12.29 – 等待状态
作为最后一步,如果听到新的噪声,我们需要中断Resume状态,因此选择Resume状态并执行以下步骤:
-
添加以下设置的过渡:
-
将触发属性设置为On Event
-
将事件标签属性设置为UnrealAgilityArena.StateTree.HeardNoise
-
将过渡到属性设置为Alert
-
将优先级属性设置为高
-

图 12.30 – Resume 状态
状态树相当完整,应该看起来像图 12 .31 中所示的那样:

图 12.31 – 完成的状态树
现在状态树已准备就绪,我们需要将其添加到 AI 控制器中。
将状态树分配给 AI 控制器
将新创建的状态树分配给 AI 控制器相当直接。只需打开AIGuardController蓝图,在详细信息面板中找到AI部分。将状态树属性设置为STAI_Guard。
完成此操作后,我们可以创建 AI 代理蓝图。
创建守卫蓝图
现在我们将创建我们的守卫蓝图并将 AI 逻辑分配给它。为此,打开内容抽屉。在蓝图文件夹中,创建一个从Base Dummy Character扩展的新蓝图,并将其命名为BP_Guard。
打开新创建的蓝图,在详细信息面板中,将AI 控制器类属性设置为AIGuardController。

图 12.32 – 守卫蓝图
守卫 AI 代理现在已准备就绪;我们只需要在健身房中测试它。
在健身房中测试
当蓝图类准备就绪时,是时候测试它了。你只需要将其添加到之前创建的健身房,并玩这个关卡。每当噪声发生器演员发出噪声时,你应该看到BP_Guard实例试图到达噪声位置,然后过一会儿回到其原始位置。显然,所有这些都会在 AI 代理在引起噪声的位置范围内时工作。你可以显然利用你对 AI 调试工具的理解来获得关于 AI 代理听觉能力和范围的宝贵见解。

图 12.33 – 测试健身房
在这个相当长的部分中,你获得了更多关于如何实现自己的状态树的高级信息。从一个具有一些听觉能力的 AI 控制器开始,我们学习了如何控制状态树并在状态树和 AI 控制器之间绑定重要数据。最后,我们将 AI 控制器和状态树添加到 AI 代理中,并在健身房中测试其行为。
我们创建的 AI 代理为完整的 AI 守卫代理奠定了基础。目前,它仅仅检查任何可疑的声音并调查其来源。我强烈建议添加你自己的逻辑来引入更多动作,例如在检测到敌人或试图攻击声音来源时发出警报。
摘要
在虚幻引擎中,状态树对于 AI 框架至关重要,因为它们有助于高效地管理和组织 AI 代理的决策过程。它们根据你的设计和开发模式提供了一个整洁的替代方案,即行为树。
在本章中,我们学习了状态树的基础知识,这是在虚幻引擎中实现的一个分层状态机框架。从其主要概念开始,例如状态是如何处理的,我们被引入了涉及的主要元素——包括任务、转换和条件。之后,我们创建了我们的自己的演员,利用了状态树的优势。
在下一章中,我们将处理一个完全不同的主题:如何在你的关卡内管理大量对象,将它们作为一个集体组来处理或进行模拟。
第十三章:使用 Mass 实现面向数据的计算
Mass 框架是 Unreal Engine 中相对较新的系统,允许开发者高效地管理和操作游戏环境中的大量对象。它提供了处理这些对象、优化性能以及实现 AI 和游戏机制行为的工具和功能。Mass 框架正成为创建需要大量 NPC 的游戏关卡并保持项目性能水平的必备工具。利用 Mass 框架对于游戏开发者来说至关重要,他们需要在保持最佳性能的同时,创建沉浸式和引人入胜的游戏体验。
在本章中,我们将介绍 Mass 的基础知识,并展示如何创建自己的 Mass 系统。特别是,我们将涵盖以下主题:
-
介绍 Mass 框架
-
设置 Mass
-
创建蓝图
技术要求
要跟随本章介绍的主题,您应该已经完成了前面的章节,并理解了它们的内容。
此外,如果您希望从本书的配套仓库开始编写代码,您可以下载项目仓库中提供的 .zip 项目文件:github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5
要下载最后一章末尾的文件,请点击 Unreal Agility Arena – 第十二章 - 末尾 链接。
介绍 Mass 框架
Mass 框架是一个面向数据系统的框架,旨在管理大量实体上的高性能计算。正如您已经知道的,在 Unreal Engine 中,传统的做法是使用演员和组件来创建关卡对象。这种方法在演员内部组合逻辑方面提供了极大的灵活性,但随着项目的扩大,它通常会导致数据不一致,从而引发性能问题。例如,考虑一个大型在线多人游戏,其中不同的 AI 代理可以根据复杂的逻辑执行各种动作。最初,这种灵活性允许开发者轻松实现诸如角色交互和物品交易等功能,同时受到的最小限制。然而,随着更多角色和交互的添加,数据更新在网络中的不一致性可能会出现。
与此同时,Mass 采用面向数据的设计框架,提供了一种替代的数据存储方法,将数据与处理逻辑分离。这允许轻松管理关卡中的大量 – 或甚至巨大的 – 实体数量。
注意
在本章中,我经常会使用术语 细节级别(LOD)。如果您不熟悉这个术语,它是在游戏开发中一个关键概念,指的是根据它们与摄像机的距离来管理 3D 模型复杂性的技术。LOD 的主要目标是优化渲染性能,同时保持视觉保真度。当玩家靠近静态网格时,引擎使用该网格的高细节版本以确保其看起来清晰和详细。然而,当玩家远离时,引擎可以切换到更少细节的网格版本,这些版本需要更少的资源来渲染。
在接下来的小节中,我将为您快速介绍构成质量框架的主要定义和元素。
质量框架插件
质量框架依赖于四个主要插件:
-
MassEntity:使质量框架工作所需的核心插件
-
MassGameplay:此插件管理世界内的交互、移动、对象可视化、LOD 等情况
-
MassAI:此插件管理状态树、世界导航和规避等功能
-
MassCrowd:此插件专门用于处理人群,并拥有自己的专用可视化和导航系统
注意
在撰写本书时,MassEntity 处于测试版,被认为是生产就绪的。另一方面,MassGameplay、MassAI 和 MassCrowd 仍然是实验性的;这意味着您应该小心处理它们,因为随着时间的推移,事情可能会发生变化。
理解质量元素
MassEntity 中的主要数据结构是 片段——用于计算的数据的原子单元,例如可以表示变换、速度或 LOD 索引。片段可以组织成集合,其中特定的集合实例与一个 ID 相关联,形成一个 实体。
创建实体类似于面向对象编程中的类实例化。然而,实体是通过片段组合构建的,而不是严格定义一个类及其功能。这些组合组件在运行时是可修改的。
注意
片段和实体是仅包含数据的元素,不包含任何逻辑。
具有相同组成的实体集合称为 架构;每个架构都包含以特定方式排列的各种类型的片段。例如,一个架构可能具有具有变换和速度的片段组合,这表明与该架构相关联的所有实体共享相同的片段结构。
架构内的实体被分组到内存 块 中,优化了从内存中检索属于相应架构的实体的片段,以增强性能。
ChunkFragment 是连接到块而不是实体的片段,用于存储在处理中使用的特定于块的数据。ChunkFragments 是实体组成的一个组成部分。
标签是一个不包含任何数据的简单片段;标签包含在实体的组成中。
处理器是无状态的类,为片段提供处理逻辑;通过使用实体查询,它们指定它们操作所需的片段类型。处理器可以添加或删除实体的片段或标签,从而有效地改变组成。
提供特定功能的片段和处理器被称为特性;可以将多个特性的实例合并到实体中。每个特性的实例负责集成和设置片段,以确保实体显示与该特性相关的行为。典型的特性包括状态树管理、规避或调试可视化。
由于MassGameplay可能是 Mass 插件中最重要的一项,我们将重点关注它。让我们深入了解其主要功能。
MassGameplay
如前所述,组成 Mass 框架的插件之一是MassGameplay插件,它直接源自MassEntity插件。MassGameplay插件包含一系列强大的子系统,如下所示:
-
质量表示:此子系统负责管理实体的不同视觉方面。
-
质量生成器:此子系统管理实体的生成过程。
-
质量 LOD:此子系统管理每个质量实体的 LOD。
-
质量状态树:此子系统将允许您在MassEntity中集成状态树。
-
质量信号:此子系统以类似事件分派的方式管理实体之间的消息。
-
质量移动:此子系统管理 Mass 代理的移动。
-
质量智能对象:此系统负责将智能对象(我将在下一章中介绍的功能)集成到MassEntity中。
-
质量复制:此子系统负责在多人游戏中通过网络复制实体;在撰写本书时,它仍处于实验阶段。
随着所有这些子系统的可用性,Mass 已成为一个极其强大的工具,可以有效地管理大量实体。
在本节中,我们介绍了 Mass 框架及其主要元素。在下一节中,我们将使用 Mass 创建我们的第一个关卡,利用其一些主要功能。
设置 Mass
故事还在继续……
马库斯博士和维多利亚教授在深入研究他们关于人工智能的开创性研究时,发现自己面临着一个新的挑战;实验室现在充满了大量的人工智能木偶,每个木偶都设计用来模仿人类的行为和反应。然而,管理和同步这些木偶证明是一项艰巨的任务。
为了克服这个障碍,马克斯博士和维多利亚教授首先开发了一套复杂的算法,这些算法将作为管理木偶的核心系统。这个系统将使他们能够同步和协调 AI 木偶的运动和动作,使它们无缝协作。
在本节中,我们将展示使用 Mass 所需的插件以及如何启用它们。此外,我们还将创建一个新的关卡以初步了解生成系统。
启用插件
为了使 Mass 完全功能化,您需要启用以下插件:
-
MassEntity
-
MassGameplay
-
MassAI
-
MassCrowd
-
ZoneGraph
-
StateTree
ZoneGraph和StateTree不是 Mass 框架的一部分,但需要包含在内,因为代码中存在一些依赖关系。
您已经熟悉插件窗口,所以打开它并启用上述插件。您将收到有关您将使用的实验性功能的警告,然后您将被提示重新加载 Unreal Engine 编辑器以注册插件。
一旦此过程完成,您就可以开始使用 Mass 了。
创建 MassEntityConfigAsset
在 Mass 中,配置实体是通过 Unreal Engine 数据资产实现的。
如果您不熟悉数据资产,它是一种以结构化格式存储数据的资产类型。它通常用于存储各种类型的数据,例如配置设置、游戏参数、本地化文本等。
在 Mass 中,您将使用MassEntityConfigAsset数据类型,该类型将存储特性和片段信息。作为一个起点,我们将创建一个配置资产,它将使我们能够在关卡中调试对象生成位置的信息。这将使我们能够可视化我们正在开发的系统的当前状态,并在运行时进行分析。
要开始,在内容抽屉中,创建一个名为DataAssets的新文件夹并打开它。然后,执行以下步骤:
-
右键点击内容抽屉并选择杂项 | 数据资产;为新创建的资产命名为ME_DebugVisualizationConfig。
-
在配置部分,您应该看到一个特性数组;点击+按钮在数组中创建一个新项。
-
从数组项下拉菜单中选择调试可视化。
-
通过点击箭头展开数组项。
-
展开调试形状属性。
-
在网格属性中,搜索Dummy_Base并选择它,如图图 13 .1 所示:

图 13.1 – ME_DebugVisualizationConfig
我们在这里所做的是相当简单的。我们创建了一个MassEntity配置文件,其中包含一个负责在关卡中对象生成位置显示调试网格的单个特性。
注意
如果你需要检查一切是否正常,你可以点击位于数据资产窗口中的验证实体配置按钮。你应该会收到一条消息,表明此资产没有错误。
我们现在将进行对象生成实现。
创建一个生成 EQS
为了利用 Mass Spawner 子系统,我们需要一个 EQS,它将返回一组位置,告诉子系统在哪里生成对象。如果你需要关于 EQS 的复习,我的建议是查看第十一章,理解环境查询系统,然后在你准备好时回到这一章。我们现在将创建一个环境查询,它将在网格上生成一些点。为了做到这一点,打开AI文件夹并按照以下步骤操作:
-
右键单击并选择人工智能 | 环境查询。将新创建的资产命名为EQS_SpawnEntitiesOnGrid。双击资产以打开它。
-
从根节点开始,点击并拖动以从生成器类别创建一个点:网格节点。
-
点击节点,在详细信息面板中,执行以下操作:
-
将GridHalfSize属性设置为1500.0
-
在投影数据部分,将轨迹模式属性设置为几何 通过通道
-

图 13.2 – 环境查询
我们刚刚实现的场景查询将创建一个 3,000 x 3,000 大小的网格——因为我们已将GridHalfSize属性设置为1500.0单位——并且将使用通过通道的几何轨迹方法来设置每个项目的位置。
注意
如果你不知道轨迹是什么以及虚幻引擎如何使用它们,我的建议是浏览官方文档,通过访问此页面来查看:dev.epicgames.com/documentation/en-us/unreal-engine/traces-in-unreal-engine---overview。
我们现在准备好在关卡中使用 Mass Spawner 子系统。
创建健身房
为了测试生成功能,我们将创建一个简单的健身房。要开始,创建一个你选择的关卡,从我在项目模板中提供的 Level Instances 和 Packed Level Actors 开始。如果你愿意,可以添加一些障碍物;我的关卡如图 13 .3 所示:

图 13.3 – 健身房
现在,按照以下步骤操作:
-
在编辑器工具栏上,点击快速添加到项目按钮,搜索Mass Spawner。选择它以在关卡中添加其实例。
-
将MassSpawner对象放置在关卡中心。
-
现在你已经将MassSpawner对象放置在关卡中,请选择它。在详细信息面板中,执行以下操作:
-
将计数属性设置为20
-
点击实体类型属性上的+按钮,向数组中添加一个项目
-
展开标记为Index[0]的项目,并将实体配置属性设置为ME_DebugVisualizationConfig。
-

图 13.4 – 条目类型
我们刚刚设置了MassSpawner属性,使其生成 20 个实体,这些实体将使用我们之前创建的数据资产作为配置资产。现在我们需要告诉MassSpawner属性在哪里生成它们。为此,请按照以下步骤操作:
-
在详细信息面板中,向生成数据生成器数组添加一个元素,并展开Index[0]元素。
-
将生成器实例属性设置为EQS SpawnPoints 生成器。
-
展开生成器实例及其查询子项。
-
将查询模板设置为EQS_SpawnEntitiesOnGrid。

图 13.5 – 生成点生成器
在前面的步骤中,我们已经将生成器逻辑设置为我们之前创建的实体查询。现在是时候测试健身房了。
测试健身房
要测试健身房,你只需要模拟或玩游戏;你将看到 20 个调试模型在一个网格上生成,如图图 13 .6 所示:

图 13.6 – 健身房
请注意,模型将被正确地放置在对象上;这是因为我们在环境查询中使用了按通道几何跟踪模式。
通过启用调试工具,你将获得有关级别内部发生情况的一些有见地的信息,以及大量选项,如图图 13 .7 所示:

图 13.7 – 调试工具
我强烈建议你探索各种调试工具选项,因为它们可以极大地提高你识别和解决游戏问题的效率。例如,图 13 .8 显示了通过使用Shift + A键组合启用的架构信息:

图 13.8 – 架构信息
在本节中,我们瞥见了使用 Mass 可以创建的内容;特别是,我们看到了如何通过MassSpawner子系统创建出生区域。在下一节中,我们将通过创建一组蓝图而不是一些调试模型来更详细地介绍。
生成蓝图
在本节中,我们将通过MassSpawner子系统进一步深入,因为我们将生成一组蓝图实例而不是简单地显示调试网格;这将使我们从简单的调试健身房过渡到真实案例环境。此外,我们将学习如何处理生成实体的 LOD。
让我们想象一下,我们想要创建一个音乐会,观众会自动生成,并且由 Mass 整体管理。我们将使用 Mass 框架创建这样的场景。
创建观众蓝图
作为第一步,我们将创建一个蓝图,它将作为一场想象中的音乐会的观众;实体将欢呼、坐下或简单地保持静止。此外,我们还将使用质量 LOD 管理系统。我们不会只创建一个蓝图,而是创建两个:一个将在靠近相机时由管理系统激活,另一个将在远离相机时激活。这将使我们能够在靠近相机时管理和显示更复杂的实体,而在远离相机时退回到较简单的实体。为了演示,我们将使用一个将保持静止且不会动画化的蓝图——当远离相机时可视化的那个,以及一个将显示随机动画的蓝图。
让我们从第一个蓝图开始,按照以下步骤操作:
-
创建一个新的蓝图类,从BaseDummyCharacter继承,命名为BP_AudienceLow。
-
打开蓝图,在动画模式属性中,选择使用****动画资产。
-
取消选择Looping和Playing复选框。
-
将初始位置属性设置为0.4。
一旦在级别中可视化,这个角色将只是站立并展示欢呼的姿态。
第二个蓝图将稍微复杂一些。要实现它,请按照以下步骤操作:
-
创建一个新的蓝图类,从BaseDummyCharacter继承,命名为BP_AudienceHigh。
-
创建一个新的Anim Montage Object Reference类型变量,命名为MontageList,并将其设置为数组。
-
创建两个浮点型变量,分别命名为MinInterval和MaxInterval。
-
编译蓝图以暴露默认值属性,并将MinInterval值设置为3.0,MaxInterval设置为6.0。
-
在MontageList 默认值中添加三个项目,分别设置为AM_Cheer、AM_Interact和AM_Sit。
-
在事件图中,将Event Begin Play执行引脚连接到一个Delay节点。
-
将Delay节点的Duration引脚连接到一个Random Float in Range节点。
-
从变量部分,拖动一个MinInterval获取节点并将其连接到Random Float in Range节点的Min引脚。
-
从变量部分,拖动一个MaxInterval获取节点并将其连接到Random Float in Range节点的Max引脚。这部分图表在图 13.8中描述:

图 13.9 – 延迟逻辑
这部分图表并没有什么特别之处,只是给代码逻辑添加了一个随机延迟。让我们继续编写代码,按照以下步骤操作:
-
将Delay节点的输出执行引脚连接到一个Play Montage节点。
-
从组件面板,将一个网格引用拖动到事件图中,并将其连接到Play Montage节点的In Skeletal Mesh Component引脚。
-
在事件图中拖动一个MontageList获取器,并将其引脚连接到Random Array Item节点。
-
将Random节点的Out Item引脚连接到Play Montage节点的Montage to Play引脚。此部分图表显示在图 13 .10中:

图 13.10 – 播放蒙太奇逻辑
在这里我们正在做一件相当直接的事情;我们从数组中获取一个随机的动画蒙太奇,然后播放它。
最后要做的事情是创建一个无限循环,在延迟后持续播放动画蒙太奇。为此,请按照以下步骤操作:
-
将Play Montage节点的On Completed输出执行引脚连接到Delay节点的输入执行引脚。
-
添加几个重定向节点,以便使图表更清晰。代码显示在图 13 .11中:

图 13.11 – 循环
我们的角色现在已准备就绪,一旦在级别中实例化,将不断播放随机动画。我们现在将创建放置在健身房中的MassSpawner的MassEntityConfigAsset。
创建一个 MassEntityConfigAsset
我们将要创建的MassEntityConfigAsset将比之前的更复杂。在这种情况下,我们需要在级别中生成角色,并且我们需要根据与摄像机的距离处理实例化内容。
要开始,打开DataAssets文件夹,创建一个新的Mass Entity Config Asset类型的Data Asset,并将其命名为ME_AudienceConfig。然后,打开它并执行以下步骤:
-
在特性部分,通过点击+按钮三次添加三个数组元素。
-
从每个项目的下拉菜单中,分别选择Mass Stationary Distance Visualization Trait、Assorted Fragments和LODCollector。

图 13.12 – 观众配置数据资产
我们现在将检查它们中的每一个,以获取它们如何工作的信息。
配置大众站距可视化特性
此特性负责在世界上可视化实体,并且需要正确设置,以便正确显示演员在级别中。因此,请按照以下步骤操作:
-
展开特性项以显示所有设置。
-
将High Res Template Actor属性设置为BP_AudienceHigh。
-
将Low Res Template Actor属性设置为BP_AudienceLow。
-
展开参数部分,将High和Medium属性设置为High Res Spawned Actor,将Low属性设置为Low Res Spawned Actor。
-
检查Keep Actor Extra Frame属性。

图 13.13 – 大众站距可视化设置
如您在此处所见,我们根据之前创建的蓝图定义了一个高分辨率和低分辨率模板。然后我们使用这些定义来设置实体的 LOD 表示。检查保留演员额外帧标志将有助于在不同 LOD 之间切换时的渲染。
配置各种片段
各种片段特性使您能够定义一个可能由其他特性所需的片段数组。为了实现这一点,打开各种片段特性部分,然后是片段部分,并执行以下操作:
-
通过点击+按钮两次添加两个片段到数组中。
-
将列表的第一项设置为变换片段,第二项设置为Mass 演员片段。

图 13.14 – 各种片段设置
变换 片段负责存储实体世界变换,而Mass 演员片段将保留一个指向将被可视化特性使用的演员的指针。
配置 LODCollector
实体配置需要LODCollector特性来促进 LOD 级别之间的调整。LODCollector处理器通过考虑实体与观众的距离及其与摄像机视锥体的关系来评估每个实体的适当 LOD。
这不需要任何配置,所以您可以保持原样。
在数据资产正确配置后,我们可以继续设置我们的系统。
启用自动处理器注册
在创建和测试健身房之前,我们还需要再走一步。在撰写本书时,Mass 的当前版本需要一些属性在 Mass 设置中启用自动与处理阶段注册标志;这是由于 Mass 仍然处于测试版状态,尚未是最终版本。不设置此标志将导致 Mass 实体在游戏中不可见。这是由于与即将解决的MassCrowd插件存在一些冲突。
要解决这个问题,从主菜单中选择编辑 | 项目设置并打开引擎 | Mass部分。之后,执行以下步骤:
-
展开模块 设置部分。
-
展开Mass 实体部分。
-
展开处理器 CDOs部分。
-
在处理器数组中搜索MassLODCollectorProcessor、MassRepresentationProcessor和MassVisualizationLODProcessor。展开处理器并检查每个的自动与处理阶段注册标志。

图 13.15 – 与处理器阶段自动注册设置
启用这些设置后,我们可以继续创建测试健身房。
创建健身房
由于我们将与许多演员一起工作,我认为带我们心爱的木偶到户外呼吸新鲜空气会很有趣!这就是为什么,我们不会使用通常的封闭健身房,而会设置一个露天环境。让我们从以下步骤开始:
-
通过使用打开 世界模板创建一个新的关卡。
-
在关卡中添加一个MassSpawner演员并选择它。
-
在设置面板中,将计数属性设置为50。
-
在实体类型数组中添加一个新项目并展开它。
-
将实体配置属性设置为ME_AudienceConfig。
-
在生成数据生成器数组中添加一个新项目并展开它。
-
将生成器实例属性设置为EQS SpawnPoints Generator并展开其查询部分。
-
展开EQSRequest部分,并将查询模板属性设置为EQS_SpawnEntitiesOnGrid。

图 13.16 – 大量生成器详细信息面板
模拟关卡,您应该得到...一些奇怪的东西!

图 13.17 – 错误位置
看起来所有角色都位于地板一半以下。这是因为角色的胶囊组件被计算为中心在局部坐标(0.0, 0.0, 0.0)。修复这个问题很简单,您只需要调整环境查询的单个属性。
让我们从复制EQS_SpawnEntitiesOnGrid并将其命名为EQS_SpawnEntitiesOnGrid_ZOffset开始。然后,执行以下步骤:
-
打开新创建的资产并选择SimpleGrid节点。
-
在详细信息面板中,展开投影数据部分,并将投影后垂直偏移设置为120.0。
-
在您的关卡中,选择MassSpawner演员并将查询模板更改为EQS_SpawnEntitiesOnGrid_ZOffset。
如果您现在测试关卡,您的角色应该被正确定位,如图图 13 .18 所示:

图 13.18 – 正确位置
此外,您应该注意到靠近摄像机的角色将会被动画化,而那些在远处的角色将保持在欢呼的位置,如图图 13 .19 所示:

图 13.19 – 非动画角色
这是因为我们在质量静止距离可视化特性中定义的配置设置导致的。我强烈建议您重新打开ME_AudienceConfig资产,并调整LODDistance值——高、中、低和关闭——以查看您的实体行为。
在本节中,我向您展示了构成质量框架的一些更高级的特性。我们创建了一个新的配置数据资产,其任务是管理生成的实体的 LOD(细节层次)。然后我们创建了一个专门的 Mass Spawner,它通过利用环境查询在级别中创建大量演员。
摘要
在本章中,我们向您介绍了实验性但功能强大的 Mass 框架。从基础知识开始,我们介绍了构成整个系统的插件。之后,我们创建了一些利用 Mass 的工作示例:一个简单的调试场景用于检查 Mass Spawner,以及一个更复杂的示例,以深入了解如何将特性组合起来处理大量实体。
在需要模拟人群、物理交互和动态实体管理的场景中,使用此框架特别有益。因此,如果您的游戏需要这些类型的功能,您将极大地从使用 Mass 中受益。
在下一章——也是最后一章中,我们将向您展示另一个功能,它将提供一种处理和管理 AI 角色和玩家各种活动和交互的方法。准备好进行一次引人入胜的探索,因为事情即将变得更加有趣!
第十四章:使用智能对象实现可交互元素
在虚幻引擎中,智能对象代表一个高级系统,旨在帮助开发者创建游戏中的交互式和上下文感知元素。智能对象旨在通过允许角色(无论是玩家角色还是 AI 代理)通过预留系统以更有意义的方式与环境交互来增强游戏玩法。学习如何使用智能对象将使你,作为开发者,能够创建动态和交互式环境,从而增强游戏玩法并改善 AI 行为,以提供更沉浸式的玩家体验。
在本章中,你将学习智能对象交互的基础知识。我们将参观一个简单的例子,展示如何使 AI 代理与智能对象交互。
在本章中,我们将涵盖以下主题:
-
介绍智能对象
-
创建智能对象定义数据资产
-
实现智能对象逻辑
-
与智能对象交互
技术要求
要跟随本章介绍的主题,你应该已经完成了第三部分,使用决策制作的所有章节,并理解了它们的内容。特别是,我们将使用直到第十一章 ,理解环境 查询系统中实现的部分代码。
此外,如果你希望从本书的配套仓库开始编写代码,你可以下载项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Artificial-Intelligence-in-Unreal-Engine-5
要下载最后一章末尾的文件,请点击Unreal Agility Arena – 第十一章 - 末尾链接。
介绍智能对象
智能对象是在级别内放置的元素,AI 代理和玩家都可以与之交互。这些对象不包含任何执行逻辑,但持有所有必要的交互信息;此外,它们可以通过不同的方法在运行时查询,例如环境查询。
智能对象代表一个级别内的一组活动,可以通过预留系统访问;如果一个智能对象槽位被 AI 代理占用,则其他代理将无法使用它,直到它被释放。
介绍智能对象框架的主要元素
与所有虚幻引擎插件一样,智能对象被组织成一系列元素,每个元素负责特定任务。
智能对象子系统负责监控级别内所有可用的智能对象,并在启用智能对象插件时自动在世界上实例化。智能对象会自动注册到子系统中,以便于访问和跟踪。
智能对象定义是一个数据资产,它持有在多个智能对象运行时实例之间共享的不变数据。它包含过滤信息,例如用户所需的标签、活动标签、对象激活标签以及用于与智能对象交互的默认行为定义集。此外,智能对象定义还包含一个或多个槽位,玩家或 AI 代理可以声明使用该特定智能对象。每个槽位都可以相对于其父对象定位,允许你为同一演员定义不同位置的不同槽位。
要将一个演员指定为智能对象,你将使用SmartObjectComponent;这个组件将引用一个智能对象定义资产。
智能对象定义可以包含一个或多个活动标签来描述对象。它们还可以具有一个标签查询,它由一系列期望的标签组成。这个标签查询作为一个表达式来评估请求访问智能对象的用户是否有权与之交互。例如,一个智能对象定义可能需要一个充电插头标签,只有具有该确切标签的 AI 代理才能使用。
现在你已经对智能对象框架有了快速的了解,你就可以开始你的项目,并开始创建你自己的智能对象了。
从下一节开始,我们将向您介绍在虚幻引擎中使用智能对象的基础知识;我们不会涵盖框架的所有方面,因为它可以用多种方式使用——独立使用,或与行为树、状态树以及甚至与 Mass 一起使用。然而,到本章结束时,你应该对你可以用它实现什么有一个清晰的理解。
是时候卷起袖子,深入代码了!
创建智能对象定义数据资产
马克斯博士靠在椅背上,满意地微笑着,环顾着杂乱的实验室。工具散落在各处,机器的微弱嗡嗡声充满了空气。在房间另一边,维克托利亚教授弯腰在一具他们的 AI 假人木偶上,她的眉头紧锁,全神贯注。
随着他们沉浸在工作中,升级木偶的想法逐渐成形。目标是明确的:创建不仅能够执行任务,而且在出现问题时能够自我修复的木偶。这一创新承诺将革命性*。
马克斯博士重新集中了注意力,手指在键盘上飞舞,输入着将赋予他们愿景生命的代码行。每一次按键都让他们更接近突破,当他们在讨论项目的潜力时,维克托利亚教授的眼睛闪烁着兴奋的光芒。
在本节以及随后的章节中,你将创建一个健身房,其中 AI 代理在需要时将使用智能对象;我们将使用在第九章中实现的BP_GunnerDummyCharacter蓝图,通过更改其 AI 逻辑。枪手角色将随机行走,四处射击,当枪卡住时,将尝试在关卡中找到一个工作台来修复枪支。
要开始使用智能对象框架,我们首先需要做的是启用插件。
启用插件
要启用智能对象框架,请打开插件窗口,查找SmartObjects和GameplayBehaviorSmartObjects,并启用两者。当第二个插件仍然标记为实验性时,你会收到一个警告;继续并重新启动虚幻引擎编辑器。
一旦插件启用,你就可以创建你的第一个智能对象。
创建工作台定义资产
现在,我们将创建智能对象定义,正如之前提到的,它不会包含任何代码逻辑;它仅用于定义将使你即将创建的演员成为智能对象的数据。为此,打开AI文件夹并执行以下步骤:
-
右键单击并选择人工智能|智能对象定义。
-
将新创建的资产命名为SOD_Workbench并双击打开它。
-
在详细信息面板中,找到默认行为定义数组属性并单击+按钮添加一个元素。
-
将Index[0]元素设置为游戏行为智能对象****行为定义。

图 14.1 – 智能对象行为定义
如前所述,智能对象定义包含系统将使用的过滤数据。由于我们不会实现任何复杂的功能,我们不需要自定义定义,我们只需使用基本定义。
现在是添加插槽的时候了,所以请执行以下操作:
-
在详细信息面板中,找到插槽部分并单击+按钮添加一个新的插槽。展开新创建的插槽。
-
在名称属性中输入工作区域。
-
将形状属性设置为矩形。
-
将大小属性设置为120.0。
-
将偏移属性设置为(50.0, 0.0,0.0)。

图 14.2 – 插槽定义
此插槽将是 AI 代理可以声称的位置,以便在枪卡住后修复它,并且应该看起来像图 14 .3 中的那样:

图 14.3 – 插槽
请记住,你可以添加你认为合适的任意数量的插槽。例如,更大的工作台可能有更多的工作区域,甚至可能是不同的区域——例如,一个用于修复枪支,另一个用于充电电池。
在本节中,我们创建了我们将要使用的智能对象定义,以创建智能对象演员。正如您所看到的,这里没有涉及任何逻辑,因为它将由我们在下一节中创建的蓝图来处理。
实现智能对象逻辑
我们现在准备好创建包含智能对象定义的蓝图,使其成为一个完全功能的智能对象。为此,请按照以下步骤操作:
-
打开蓝图文件夹并创建一个新的从Actor扩展的蓝图。命名为BP_Workbench。双击新创建的资产以打开它。
-
在组件面板中,添加一个静态网格组件并选择它。
-
在详细信息面板中,将旋转属性设置为(0.0, 0.0, -90.0),将静态网格属性设置为Workbench_Decorated_Workbench_Decorated。
-
在组件面板中,添加一个智能对象组件并选择它。
-
在详细信息面板中,定位智能对象类别并将智能对象定义属性设置为SOD_Workbench。现在视口应该看起来像图 14.4中所示:

图 14.4 – 视口
现在,打开事件图面板并执行以下步骤:
-
添加一个OnSmartObjectEvent节点。
-
将事件数据引脚连接到断点 SmartObjectEventData节点。
-
将Break SmartObjectEventData节点的原因引脚连接到根据 ESmartObjectChangeReason 切换节点,并点击展开按钮以显示所有切换情况。到目前为止,图应该看起来像图 14.5:

图 14.5 – 开始图
我们在这里所做的是相当简单的;每次我们从智能对象获取事件时,我们都会检查是什么导致了这个事件。这将帮助我们响应来自人工智能代理的任何交互。让我们通过以下步骤继续代码逻辑。
-
将OnSmartObject Event节点的交互者引脚连接到获取 AIController节点。
-
将获取 AI 控制器节点的返回值连接到获取 黑板节点。

图 14.6 – 检索黑板
-
将获取黑板节点的返回值引脚连接到设置值作为 布尔值节点。
-
将Switch on ESmartObjectChangeReason节点的On Released输出执行引脚连接到设置值作为 布尔值节点的输入执行引脚。
从设置值作为布尔值节点的键名引脚处点击并拖动。释放鼠标,然后从弹出菜单中选择提升为变量以创建一个新变量。在变量面板中,将新创建的变量命名为NeedsReloadKeyName,这样图中的节点将显示需要重新加载键名标签。此部分图示在图 14.7:

图 14.7 – 设置黑板键
-
编译蓝图并将变量默认值属性设置为WeaponJammed。
在这个图的最后部分,我们设置了与智能对象交互的 AI 代理的黑板键值。我们是在On Released事件中这样做,即当智能对象已被声明、交互并随后释放以再次可声明时。这意味着一旦 AI 代理完成与智能对象的交互,我们将修复卡壳的枪械。
黑板和随之而来的WeaponJammed键尚未实现。在下一节中,我们将着手处理黑板本身及其相关的行为树,以便实现 AI 代理。
与智能对象交互
在本节中,我们将通过创建一个将充分利用之前创建的工作台蓝图的人工智能代理来最终确定智能对象系统。如前所述,AI 代理将在随机移动和射击;偶尔,枪械会卡壳,因此枪手需要前往工作台进行修理。AI 行为将由行为树处理,它将相当直接,但将帮助我们了解如何与智能对象交互。
让我们先创建一个有用的任务,我们将在行为树中使用它。
创建抛硬币任务
我们现在将创建一个模拟枪械卡壳的任务。这个任务将是一种加权硬币抛掷,并返回一个bool值——即正面或反面的结果。抛掷的权重将帮助我们定义枪械卡壳的可能性有多大。您应该已经熟悉行为树任务,但为了快速回顾,您可以查看第八章 ,设置行为树。
要实现此任务,首先创建一个新的 C++类,扩展BTTaskNode,并将其命名为BTTask_TossCoin。然后,打开BTTask_TossCoin.h文件并添加以下代码块:
public:
UBTTask_TossCoin();
UPROPERTY(EditAnywhere, Category="Blackboard")
FBlackboardKeySelector BlackboardKey;
UPROPERTY(EditAnywhere, Category="Task")
float TrueProbability = 0.5f;
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) override;
这里值得提及的只有TrueProbability属性,它将允许我们加权结果——在我们的案例中,是枪械卡壳的概率。现在,打开BTTask_TossCoin.cpp文件,并在其顶部添加以下声明:
#include "BehaviorTree/BlackboardComponent.h"
构造函数将非常简单,因为它只会给节点一个有意义的名称。将以下代码添加到您的类实现中:
UBTTask_TossCoin::UBTTask_TossCoin()
{
NodeName = "Toss Coin";
}
所有的代码逻辑都将放置在ExecuteTask()函数内部。让我们添加以下代码块:
EBTNodeResult::Type UBTTask_TossCoin::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
const auto BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == nullptr)
{ return EBTNodeResult::Failed; }
const auto RandomNumber = FMath::RandRange(0.0f, 1.0f);
BlackboardComp->
SetValueAsBool(BlackboardKey.SelectedKeyName,
RandomNumber < TrueProbability);
return EBTNodeResult::Succeeded;
}
如您所见,一旦我们检索到黑板组件,我们将随机生成一个bool结果,并在黑板本身中设置一个键值。
完成这个类后,我们现在可以专注于在行为树内部需要的一些环境查询。
创建环境查询
要实现枪手行为树,我们需要一些环境查询:一个用于生成枪手到达的随机位置,另一个用于查找工作台智能对象。让我们从第一个开始。
创建 FindShootLocation 环境查询
此查询将负责在Nav Mesh级别生成一组随机位置;基本上,我们将创建一个点阵,然后从这些点中选择一个作为射击点。为此,打开AI文件夹,通过Artificial Intelligence | Environment Query创建一个环境查询。将其命名为EQS_FindShootLocation。打开它并执行以下步骤:
-
在图中,将ROOT节点连接到一个Points: Grid节点。
-
选择新创建的节点,并将GridHalfSize属性设置为2500.0。
-
确认Projection Data | Trace Node设置为Navigation。

图 14.8 – FindShootLocation 查询
如您所见,我们正在使用导航网格来寻找位置;这将确保我们的代理能够到达所选点。
创建 FindWorkbenchLocation 环境查询
第二个查询将在预定义区域内搜索智能对象。让我们先创建一个环境查询(Artificial Intelligence | Environment Query),并将其命名为EQS_FindWorkbench。打开它并执行以下步骤:
-
在图中,将ROOT节点连接到一个SmartObjects节点。
-
通过点击+按钮在Behavior Definition Classes数组属性中添加一个项。
-
将Index[0]项设置为GameplayBehaviorSmartObjectBehaviorDefinition。
-
将Query Box Extent设置为(5000.0, 5000.0, 500.0)。

图 14.9 – FindWorkbench 查询
此查询将寻找任何设置为GameplayBehaviorSmartObjectBehaviorDefinition的行为定义的智能对象;这是默认定义,也是我们在SOD_Workbench资产中使用的定义。需要注意的是,为了简化,我们在这里保持了非常基础的级别。我强烈建议您尝试通过使用标签过滤器或扩展您自己的工作台或其他智能对象的行为定义来实现一个过滤系统。
当环境查询完成后,我们可以开始实现行为树,从黑板开始。
创建黑板
我们的人工智能代理的黑板需要存储两个位置:一个用于射击目标,另一个用于工作台。此外,应该有一个标志来指示枪是否卡住。让我们先创建一个黑板,并将其命名为BB_Tinkerer,以清楚地反映我们人工智能代理的能力。然后,添加以下键:
-
一个名为WorkbenchLocation的Vector
-
一个名为ShootLocation的Vector
-
一个名为WeaponJammed的Bool
当黑板完成设置后,我们就可以创建行为树了。
创建行为树
行为树将有两个主要子分支来处理枪支卡住和不卡住的情况。让我们先创建一个新的行为树,命名为 BT_Tinkerer 。打开它,并将 Blackboard Asset 属性设置为 BB_Tinkerer 。然后,执行以下操作:
-
将 ROOT 节点连接到一个 Selector 节点;命名为 Root Selector 。
-
在 Root Selector 节点添加两个 Sequence 节点。左边的命名为 Shoot Sequence ,右边的命名为 Fix Sequence 。
-
在 Shoot Sequence 节点添加一个 Blackboard Decorator 并选择它。
-
在 Details 面板中执行以下操作:
-
将 Notify Observer 属性设置为 On Value Change
-
将 Key Query 属性设置为 Is Not Set
-
将 Blackboard Key 设置为 WeaponJammed
到目前为止,图表应该看起来像 图 14.10 中所示的那样:
-

图 14.10 – 序列
现在,让我们专注于图表中的 Shoot Sequence 部分。首先执行以下步骤:
-
添加一个 Move To 任务,命名为 Move to Shoot Location ,并将 Blackboard Key 属性设置为 ShootLocation 。
-
在 Move to Shoot Location 任务右侧添加一个 PlayMontage 任务,命名为 Play Shoot Montage ,并将 Anim Montage 属性设置为 AM_1H_Shoot 。
-
在 Play Shoot Montage 任务右侧添加一个 Wait 任务。将 Wait Time 属性设置为 3.0 和 Random Deviation 设置为 0.5 。
-
在 Wait 任务右侧添加一个 TossCoin 任务,命名为 Randomize Jam ,将 Blackboard Key 属性设置为 WeaponJammed ,并将 True Probability 属性设置为 0.35 。
图表的 Shoot Sequence 部分应该看起来像 图 14.11 中描述的那样:

图 14.11 – 射击序列
图表的这部分将移动 AI 代理到选定的位置,开始射击序列,等待一段时间,并检查枪是否卡住。然而,我们需要添加一个服务使其完全功能化。让我们先选择 Move to Shoot Location 并添加一个 Run EQSQuery 节点。选择查询服务,并在 Details 面板中执行以下操作:
-
命名为 Find Shoot Location 。
-
将 Query Template 属性设置为 EQS_FindShootLocation 。
-
将 Run Mode 设置为 从最佳 25% 中随机选择一个项目 。
-
将 Blackboard Key 属性设置为 ShootLocation 。图 14.12 显示了图表中最终的 Shoot Sequence 部分:

图 14.12 – 最终的射击序列
此服务将执行环境查询,从生成的位置中选择一个随机项目,并将其分配给 ShootLocation 属性。
在这部分行为树完成之后,我们可以专注于图表的 Fix Sequence 部分。按照以下步骤操作:
-
添加一个移动到任务,命名为移动到工作台位置,并将黑板键属性设置为WorkbenchLocation。
-
在移动到工作台位置任务右侧添加一个播放剪辑任务,命名为播放装弹剪辑,并将动画剪辑属性设置为AM_1H_Reload。
-
在播放射击剪辑任务右侧添加一个等待任务。将等待时间属性设置为3.0,并将随机偏差设置为0.5。
-
在等待任务右侧添加一个查找并使用游戏行为智能对象任务,命名为使用工作台,将查询模板属性设置为EQS_FindWorkbench,并将运行模式属性设置为单次最佳项。修复序列应类似于图 14 .13 中所示:

图 14.13 – 修复序列
你已经熟悉了大多数图形,但最后一个任务需要一些解释,因为它是最重要的。它将找到一个合适的智能对象,声明它并使用它。然后它将释放资源。尽管你无法完全控制智能对象的每个阶段,因为它们一个接一个地执行,但在实现像我们创建的简单行为时非常方便。
我们最后需要添加一个服务来在关卡中查找工作台。为此,选择移动到工作台位置任务并添加一个运行 EQS 查询节点。选择查询服务,并在详细信息面板中执行以下操作:
-
命名为查找****工作台位置。
-
将查询模板属性设置为EQS_FindWorkbench。
-
将运行模式属性设置为单次****最佳项。
-
将黑板键属性设置为工作台位置。图 14 .14 显示了图形的最终修复序列部分:

图 14.14 – 最终修复序列
行为树现在已完成;我们只需将其集成到 AI 代理中并观察其行为。
创建角色蓝图
为了最终完成我们的 AI 代理,我们需要创建 AI 控制器和角色蓝图。幸运的是,我们已经实现了必要的类,我们只需要扩展它们。
让我们从以下步骤开始 AI 控制器:
-
在蓝图文件夹中,创建一个新的蓝图类,从BaseDummyAIController扩展。
-
命名为AITikererDummyController并打开它。
-
将行为树属性设置为BT_Tinkerer。
现在是创建角色的时候了,请按照以下步骤操作:
-
在蓝图文件夹中,右键单击BP_GunnerDummyCharacter并选择创建子****蓝图类。
-
将新创建的资产命名为BP_TinkererDummyCharacter并打开它。
-
在详细信息面板中,将AI 控制器类属性设置为AITinkererDummyController。
我们现在可以开始在健身房测试了。
在健身房测试智能对象
到现在为止,你应该已经熟悉了创建和测试健身房的过程,所以只需创建一个新的等级,并在其中添加一个NavMeshBoundsVolume演员,以便让你的 AI 代理能够通过寻路系统移动。然后,将一个BP_Tinkerer实例和一个BP_Workbench实例添加到等级中,并开始模拟。
你应该会观察到 AI 代理四处移动和射击。偶尔,武器会卡住,促使代理寻找工作台进行修理,然后再返回射击活动。

图 14.15 – 最终修复序列
在本节中,我们创建了一个入门级但功能齐全的健身房,有效地使用了智能对象。我强烈鼓励你通过调整卡住概率或添加更多工作台和枪手来尝试一些事情,以观察这些变化如何影响整体行为。
摘要
在本章的最后,我们学习了智能对象,这是一个高级框架,旨在帮助开发者构建游戏中的交互式和上下文感知元素。智能对象的目标是通过预订系统使玩家角色和 AI 代理能够更深入地与环境互动,从而丰富游戏体验,正如我们在本章所学。
作为一名开发者,通过使用智能对象,你可以创建更加沉浸式和互动的游戏体验,因为环境对象将使游戏世界中的对象能够实现复杂的行为和上下文相关的交互。利用这项技术,你的游戏将越来越吸引玩家,并使他们更加投入。
后记
在他们的秘密实验室里,马克斯博士和维克托利亚教授看着他们的 AI 木偶准备进行一场史诗般的战斗,手持五彩缤纷的 飞镖枪 。
马克斯笑着,一个大胆的木偶从实验室桌子后面冲出来,发射泡沫飞镖。“我们创造了世界上第一个 AI 飞镖战斗联赛!”他 自豪地宣称 *。
维克托利亚笑着,对他们的创造物的热情感到高兴。“谁知道他们会如此热情地拥抱战斗 呢?”
随着显示器发出的光芒照亮整个实验室,马克斯建议,“接下来是什么?一场 锦标赛?”
“绝对!”维克托利亚回答,她的 兴奋具有传染性 *。
在那个隐藏的创新天堂中,他们意识到他们已经创造了一些非凡的东西——一个充满创造力和友好竞争的奇妙世界,充满了 无限的可能性 *。
因此,这本书到此结束;我希望你和我写作时一样喜欢它!
这只是您进入虚幻引擎中令人兴奋的人工智能开发世界的起点!你可能正在问自己,“下一步是什么?”我完全理解,面对我展示的所有插件、框架和技术,可能会感到有些不知所措。为了帮助您,我已经为您准备了一个有趣的新任务!在项目模板中,您将找到一个名为 CaptureTheFlag 的关卡。请随意深入其中,创建您自己的以木偶为主角的 Capture the Flag 游戏。尝试将您迄今为止所学的一切结合起来,开发您自己的 AI 代理。
以您能聚集的所有热情投身其中,不要害怕实验和玩耍。记住,最好的体验往往来自于放松和享受乐趣,所以请尽情发挥,让它成为你自己的。
玩得开心!
附录——理解虚幻引擎中的 C++
本附录旨在为您提供额外的见解、资源和实用信息,以增强您对虚幻引擎框架中 C++ 编程的理解。这将帮助您在阅读本书时作为复习或参考,以防在跟随展示的代码时需要帮助。
本附录作为一本宝贵参考书,补充了本书的主要内容,为您提供在虚幻引擎中成功导航 C++ 世界所需的技术和信息。无论您是经验丰富的开发者,希望提高自己的技能,还是对虚幻引擎经验较少的熟练 C++ 程序员,我希望这个附录能丰富您的学习体验!
我们将涵盖以下主题:
-
介绍基本概念
-
解释高级功能
-
探索核心机制
技术要求
要跟随本章介绍的主题,您应该对编程有一定的了解,特别是对 C++ 语言的一些基本理解。此外,您还需要对虚幻引擎有良好的理解。
注意
本章将为您提供一个轻松的介绍,涉及在虚幻引擎中使用 C++ 的主要主题。如果您想更全面地探索使用虚幻引擎进行 C++ 编程,我建议您阅读 Zhenyu George Li 的书籍,书名为《Unreal Engine 5 Game Development with C++ Scripting》,由 Packt 出版。
介绍基本概念
如果你和我一样热爱游戏开发和编程,你可能会同意在虚幻引擎中编写 C++ 代码既愉快又出人意料地容易。Epic Games 在整合简化 C++ 使用的功能方面做得非常出色,这些功能几乎适用于每一位程序员。
虽然在虚幻引擎中确实可以编写标准的 C++ 代码,但利用引擎最广泛使用的功能——例如内置的垃圾回收器和反射系统——将有助于你在游戏中实现更好的性能。
在本节中,我将介绍 Unreal Engine C++特性的基本原理。
理解 C++类
令人惊讶的是,Unreal Engine C++类本质上是一个标准的 C++类!如果你已经对 C++中的面向对象编程有扎实的理解,你会发现这个环境非常熟悉。创建新的 C++类的过程首先是通过确定你想要表示的对象类型,例如演员或组件。在定义类型后,你在头文件(使用.h扩展名)中声明变量和方法,并在源文件(使用.cpp扩展名)中实现逻辑。
源文件的工作方式与任何常规的 C++文件相同,但头文件允许你为将可由继承自你的类的 Blueprint 访问的变量和函数指定附加信息。这也简化了运行时内存管理,我将在稍后解释。
让我们先介绍 Unreal Engine 框架使用的基类型。
基本类型
在 UE 中,开发过程中你将主要从以下三种类类型中进行派生:
-
UObject:这是 Unreal Engine 的基类,提供了如网络支持和属性及方法反射的核心功能
-
AActor:这是一个UObject类型,可以通过编辑器或运行时添加到游戏关卡中
-
UActorComponent:这是定义可以附加到演员或同一演员的另一个组件的组件的基本类
此外,UE 还提供了以下实体:
-
UStruct:用于创建简单的数据结构
-
UEnum:用于表示元素枚举
Unreal Engine 前缀
Unreal Engine 类名以特定的字母开头;这些前缀用于指示类类型。主要使用的前缀如下:
-
U:用于从UObject派生的泛型对象,例如组件。一个很好的例子是UStaticMeshComponent类。
-
A:用于从演员(即AActor类)派生的对象,并且可以添加到关卡中。
-
F:用于泛型类和结构,例如FColor结构。
-
T:用于模板,例如TArray或TMap。
-
I:用于接口,例如IGameplayTaskOwnerInterface。
-
E:用于枚举,例如EActorBeginPlayState。
注意,这些前缀是强制性的;如果你尝试命名一个从AActor派生的类而没有A前缀,你将在编译时遇到错误。此规则仅适用于 C++类;Blueprint 可以没有这些前缀命名。一旦你进入编辑器,Unreal Engine 将隐藏 C++前缀。
属性
如您可能已经知道的,在编程语言中,属性指的是在类内部声明的变量。向 C++类添加属性需要一些额外的注意——具体来说,我们需要考虑我们编写的代码是否应该对从我们的类继承的蓝图可见或隐藏。
声明属性
属性声明使用标准的 C++变量声明语法,在UPROPERTY()宏之前,该宏指定了各种属性——例如在蓝图中的可见性——以及任何相关的元数据。以下是一个示例代码:
UPROPERTY(VisibleAnywhere, Category="Damage")
float Damage;
在前面的示例中,将Damage变量设置为VisibleAnywhere将使该属性在蓝图上可见,但不能修改。此外,它将在Details面板的Damage类别中进行逻辑分组。
属性指定符
如您已经注意到的,UPROPERTY()可以包含一个参数列表,称为属性指定符,它将为属性添加额外的功能。其中一些列在这里:
-
VisibleAnywhere:属性在Details面板中显示,但不能修改。
-
EditAnywhere:属性可以在Details面板中修改,无论是蓝图还是放置在关卡中的实例。
-
EditDefaultsOnly:属性可以在蓝图的Details面板中修改,但不能在放置在关卡中的实例中进行修改。
-
EditInstanceOnly:属性可以在放置在关卡中的实例的Details面板中修改,但不能在蓝图中进行修改。
-
BlueprintReadOnly:属性可以在蓝图中被读取,但不能被分配。
-
BlueprintReadWrite:属性可以在蓝图中被读取和分配。
函数
函数与常规 C++函数的工作方式相同。此外,就像属性一样,您可以用宏——在这种情况下,UFUNCTION()——对其进行装饰,该宏可以包含适当的指定符。以下是一个示例代码:
UFUNCTION(BlueprintCallable)
void Heal(float Amount);
在这种情况下,函数可以从子蓝图中调用,因为它已经用BlueprintCallable指定符进行了装饰。
要获取属性和函数指定符的完整列表,请访问官方文档:dev.epicgames.com/documentation/en-us/unreal-engine/metadata-specifiers-in-unreal-engine。
C++头文件预览
Unreal Engine 内置了一个令人难以置信的检查工具,称为C++ Header Preview,它允许您像 C++编写的蓝图类一样检查您的蓝图类。要激活此工具,只需导航到主菜单并选择Tools | C++ Header Preview,然后选择您希望查看的类。例如,图 A.1显示了本书项目中BP_Scrambler的头文件:

图 A.1 – C++头文件预览工具
如果你已经有一些蓝图知识,并希望温和地接触 C++编程,这是一个令人惊叹的工具。
在本节中,我向你介绍了虚幻引擎中 C++的一些最基本的功能;在下一节中,我将介绍一些你可能已经在 C++中熟悉但虚幻引擎中处理方式略有不同的更高级功能。
解释高级功能
在本节中,我们将探讨虚幻引擎如何处理 C++的一些常见功能,如类型转换和代表。
类型转换
在 C++(以及其他编程语言)中,类型转换是将变量从一种数据类型转换为另一种数据类型的过程。它允许你将对象视为不同类型,这在各种情况下可能很有用,例如在处理继承或与 API 接口时。要在虚幻引擎中进行类型转换,你使用Cast
APlayerCharacter* PlayerCharacter =
Cast<APlayerCharacter>(Actor);
如你所见,我们正在尝试将Actor指针转换为APlayerCharacter类型。
在虚幻引擎中,Cast
在虚幻引擎中进行类型转换应谨慎对待,有多个原因。首先,它可能会引入性能开销;如果频繁使用,这可能会减慢你的游戏。此外,过多的类型转换可能会使代码可读性和理解性复杂化。它可能会掩盖类之间的关系,使得开发者更难掌握代码库的结构。减少类依赖关系的一个有效方法是通过使用接口。
接口
在许多编程语言中,接口提供了一种为多个类定义函数的方法,而无需任何特定实现。例如,你的玩家角色可能会以不同的方式激活不同的物品。通过定义一个声明Activate()方法的接口,实现该接口的每个物品都将定义它自己的个人逻辑。
注意
在虚幻引擎中,接口与传统编程接口不同,因为你不需要为声明的函数提供实现。
在虚幻引擎中,接口需要UINTERFACE()宏声明和两个类声明(在同一文件中),具有两个不同的前缀。以下是一个示例,一个具有Activate()函数的Activatable接口,其代码大致如下:
UINTERFACE(MinimalAPI)
class UActivatable : public UInterface
{
GENERATED_BODY()
};
class IActivatable
{
GENERATED_BODY()
void Activate();
public:
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void Activate();
};
在这个例子中,UActivatable类声明是一个包含与接口相关的所有反射信息的UObject类。作为一个UObject类,它具有你通常期望的所有功能,例如具有名称、序列化能力和反射支持。
相反,IActivatable类声明是编译器实际用于将虚拟函数注入你的类的原生类。
代表
在 C++ 中,代理 是一种类型,允许你引用一个函数,从而间接调用该函数。代理通常用于事件处理和回调机制,允许程序的不同部分以解耦的方式通信。在虚幻引擎中,代理专门设计用于与引擎的事件系统一起使用。它们允许你将函数绑定到事件,以便当事件发生时,绑定的函数会自动调用。
在虚幻引擎中,存在几种类型的代理:
-
单播:这些代理一次只允许将一个函数绑定到代理。它们适用于你想要确保只有一个事件处理器响应事件的场景。
-
多播:这些代理可以绑定多个函数,允许多个事件处理器响应同一事件。它们适用于需要多个组件或类监听同一事件并相应响应的场景。
-
动态:这些代理是一种可以序列化并与虚幻引擎的反射系统兼容的代理类型。它们允许你在运行时绑定和解绑函数,并且可以轻松地暴露给蓝图,使它们非常灵活。
-
动态多播:这些代理结合了动态和多播代理的特性。
如果你在一个混合蓝图/C++项目中工作,那么你很可能会主要使用动态多播代理;这将使你能够将代理暴露给蓝图并将多个函数绑定到它们。只需记住,尽管它们非常灵活且强大,但由于运行时绑定,使用它们可能会影响性能。
声明代理
每次声明代理时,你将使用以 DECLARE_ 前缀开始的宏。例如,要声明一个具有单个参数的动态多播代理,你将使用以下语法:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(OnDamageTakenSignature, float, Amount);
如你所见,代理声明定义了代理名称、参数类型和参数名称。
注意
要查看可用的代理声明的完整列表,请参阅官方文档:dev.epicgames.com/documentation/en-us/unreal-engine/delegates-and-lamba-functions-in-unreal-engine 。
创建代理类型的变量
要从代理类型创建变量,你将使用以下语法:
UPROPERTY(BlueprintAssignable)
OnDamageTakenSignature OnDamageTaken;
注意使用了 BlueprintAssignable 指示符,这将使此属性对蓝图可访问。
订阅代理
订阅代理的方式因是否为多播以及是否为动态或非动态而异。在我们的例子中,代理是动态且多播的,因此我们将使用以下语法:
OnDamageTaken.AddDynamic(this, &ClassName::HandleDamage);
HandleDamage() 函数将与以下代码类似:
void HandleDamage(float Amount)
{ /** Function implementation **/ }
调用代理
调用一个委托相当简单,您将使用Broadcast()函数来调用多播委托,否则使用Execute()函数。从我们的示例中调用委托将类似于以下代码:
OnDamageTaken.Broadcast(30.f);
这里,30.f是监听对象所承受的伤害。
在本节中,我介绍了一些区分 Unreal Engine C++和标准 C++编程的关键特性。在下一节中,我们将深入探讨 Unreal Engine 的一些最重要的内部特性。
探索核心机制
在本节中,我们将深入了解更复杂的功能,例如内存管理和反射,并解释 Unreal Engine 是如何处理它们的。
垃圾回收
如您可能已经知道,垃圾回收(GC)是一种自动管理内存的方式。在 GC 管理的系统中,一旦一个对象不再被使用,它将被自动从内存中移除以释放空间。这允许您创建一个新的对象并使用它,当您完成使用后,您只需简单地继续即可。这个系统由垃圾回收器管理,它不断监控哪些对象仍在使用中。当一个对象不再需要时,垃圾回收器会自动释放相关的内存。
虽然许多现代编程语言(如 C#和 Python)都使用 GC,但像 C 和 C++这样的底层语言默认不包含垃圾回收器。因此,程序员必须手动跟踪内存使用情况,并在不再需要时释放它。这个过程可能会出错,并且对开发者来说更具挑战性。为了解决这个问题,Unreal Engine 实现了自己的 GC 系统。
实际上,Unreal Engine 是如何使用 GC 的?
当从UObject类派生的对象被实例化时,它将被注册到 Unreal Engine 的 GC 系统中。该系统会自动在预定义的时间间隔内运行——大约 30 到 60 秒——以识别和移除任何不再使用的对象。
GC 系统保留了一组定义为无限期保持活跃的根对象。此外,它使用反射——这是 C++所缺乏的,但 Unreal Engine 具有原生支持的功能——来检查对象的属性。这允许 GC 系统跟踪其他对象及其属性的引用。
在遍历其他对象时如果发现了一个对象,并且其中之一是根集合的一部分,那么该对象被认为是可到达的并且保持活跃状态。一旦检查完所有对象,如果无法通过引用到达根集中的任何对象,那么该对象被认为是不可到达的,并标记为垃圾回收。
当一个对象被垃圾回收时,它占用的内存被释放并返回给系统;任何引用此对象的指针将被设置为null。
注意
应该注意的是,如果你来自纯 C++背景,手动内存管理——你应该习惯使用的方法——在 Unreal Engine 中仍然是一个选项,但不能用于任何从UObject派生的类。
在 Unreal Engine 中使用 GC
如果你函数内部有一个指针,你不需要担心 GC,因为函数内的指针表现得像标准的 C/C++指针,并且不需要任何修改。
相反,如果你需要跨帧持久化对象的指针,你将需要添加一些小的附加代码;该指针需要作为你的类中的一个成员变量存储,并且你必须在它之前添加UPROPERTY()宏。这就是你需要做的,以便让后续的引用被 GC 系统考虑。
注意
UPROPERTY()宏只能在从UObject派生的类中使用;否则,你将不得不手动处理内存。
反射系统
反射这个术语指的是程序在运行时检查其自身结构的能力;这个特性非常宝贵,并且是 Unreal Engine 的核心技术之一,支持各种系统,如编辑器中的Detail面板、序列化、GC 以及蓝图和 C++之间的通信。
由于 C++没有对反射的原生支持,Epic Games 创建了自己的系统来收集、检查和修改与 C++类、结构体等相关数据,这些数据在引擎内部。
注意
反射系统还赋予了所有编辑器面板的能力,使得 Unreal Engine 的 UI 高度可定制。
为了让系统使用反射,你需要对任何你想暴露给系统的类型或属性进行注释。这个注释将使用宏,如UCLASS()、UFUNCTION()或UPROPERTY()。最后,为了启用这些注释,你需要添加#include "AClassName.generated.h"声明。这个声明是在你从 Unreal Engine 编辑器创建类时自动生成的,所以你不需要担心它。
例如,考虑以下从你在本书项目中创建的BaseSecurityCam.h文件中的代码块:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "BaseSecurityCam.generated.h"
class UAIPerceptionComponent;
UCLASS(Blueprintable)
class UNREALAGILITYARENA_API ABaseSecurityCam :
public APawn
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere,
BlueprintReadOnly,
Category="Security Cam",
meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* SupportMeshComponent;
UPROPERTY(VisibleAnywhere,
BlueprintReadOnly,
Category="Security Cam",
meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* CamMeshComponent;
public:
ABaseSecurityCam();
};
你可能已经注意到了#include "BaseSecurityCam.generated.h"声明以及UPROPERTY()宏在组件声明中的使用。
以下列表概述了反射系统内可用的基本标记元素:
-
UCLASS():为从UObject派生的类生成反射数据
-
USTRUCT():为结构体生成反射数据
-
GENERATED_BODY():将被替换为类类型所需的所有必要样板代码
-
UPROPERTY():通知引擎关联的成员变量将具有额外的功能,例如蓝图可访问性
-
UFUNCTION():允许我们在扩展蓝图类中调用装饰过的函数或从蓝图本身覆盖该函数
反射系统也被垃圾回收器使用,所以你不需要担心内存管理,正如 GC 子节中解释的那样。
摘要
在本附录中,我概述了 C++在 Unreal Engine 中的应用,突出了其独特的特性和功能。我们探讨了 C++与引擎架构的集成,Unreal Engine C++与标准 C++之间的一些差异,以及在内置引擎中使用 C++的好处。我们还讨论了诸如委托和内存管理等关键概念,强调了它们最重要的特性和特性。此外,我们介绍了 C++头文件预览工具;如果你对 Unreal Engine 的 C++经验不多,但又想从蓝图过渡到 C++,这样的工具是必不可少的。


浙公网安备 33010602011771号