原文:
zh.annas-archive.org/md5/36713AD44963422C9E116C94116EA8B8
译者:飞龙
第十八章:为构建敌人实现游戏 AI
如果没有玩家需要利用角色的能力来应对不同的情景,那么游戏还有什么意义呢?每个游戏都对玩家施加不同类型的障碍,而我们游戏中的主要障碍就是敌人。创建具有挑战性和可信度的敌人可能会很复杂,它们需要像真实角色一样行为,并且足够聪明,不容易被杀死,但也不至于太容易。我们将使用基本但足够好的 AI 技术来实现这一点。
在本章中,我们将研究以下 AI 概念:
-
使用传感器收集信息
-
使用 FSM 做出决策
-
执行 FSM 动作
使用传感器收集信息
AI 首先通过获取周围的信息,然后分析这些数据来确定行动,最后执行所选择的行动,正如你所看到的,没有信息我们什么也做不了,所以让我们从这部分开始。我们的 AI 可以使用多种信息源,比如关于自身的数据(生命和子弹)或者游戏状态(胜利条件或剩余敌人),这些都可以通过我们迄今为止看到的代码轻松找到,但一个重要的信息源也是 AI 的感知。根据我们游戏的需求,我们可能需要不同的感知,比如视觉和听觉,但在我们的情况下,视觉就足够了,所以让我们学习如何编写它。
在本节中,我们将研究以下传感器概念:
-
创建三过滤器传感器
-
使用 Gizmos 进行调试
让我们开始看看如何使用三过滤器方法创建传感器。
创建三过滤器传感器
编写感知的常见方法是通过三过滤器方法来丢弃视线之外的敌人。第一个过滤器是距离过滤器,它将丢弃太远无法看到的敌人,然后是角度检查,它将检查我们视野内的敌人,最后是射线检查,它将丢弃被障碍物遮挡的敌人,比如墙壁。在开始之前,我想给出一个建议:我们将在这里使用向量数学,深入讨论这些主题超出了本书的范围。如果你不理解某些内容,可以随意复制并粘贴屏幕截图中的代码,并在网上查找相关概念。让我们按照以下方式编写传感器:
- 创建一个名为
0,0,0``0,0,0)
和1,1,1
的空GameObject
,这样它就会与敌人对齐。虽然我们当然可以直接将所有 AI 脚本放在敌人身上,但我们之所以这样做只是为了分离和组织:
图 18.1 - AI 脚本容器
-
创建一个名为
Sight
的脚本,并将其添加到 AI 子对象中。 -
创建两个
float
类型的字段,分别命名为distance
和angle
,另外创建两个LayerMask
类型的字段,分别命名为obstaclesLayers
和ObjectsLayers
。distance
将用作视觉距离,angle
将确定视野锥的幅度,ObstacleLayers
将被我们的障碍物检查使用,以确定哪些对象被视为障碍物,ObjectsLayers
将用于确定我们希望视线检测到的对象类型。我们只希望视线看到敌人;我们对墙壁或道具等对象不感兴趣。LayerMask
是一种属性类型,允许我们在代码中选择一个或多个层,因此我们将通过层来过滤对象。稍后你将看到我们如何使用它:
图 18.2 - 用于参数化我们视线检查的字段
- 在
Update
中,调用Physics.OverlapSphere
,如下一个截图所示。此函数在由第一个参数指定的位置创建一个虚拟球体,并使用第二个参数(distance
属性)中指定的半径来检测第三个参数(ObjectsLayers
)中指定的层中的对象。它将返回一个包含在球体内找到的所有对象碰撞器的数组,这些函数使用物理学来进行检查,因此对象必须至少有一个碰撞器。这是我们将使用的方法,以获取视野距离内的所有敌人,并且我们将在接下来的步骤中进一步对它们进行过滤。
重要说明
完成第一个检查的另一种方法是只检查到玩家的距离,或者如果寻找其他类型的对象,则检查到包含它们列表的管理器,但我们选择的方式更加灵活,可以用于任何类型的对象。
另外,您可能希望检查Physics.OverlapSphereNonAlloc
版本的此函数,它执行相同的操作,但通过不分配数组来返回结果,因此性能更高。
- 遍历函数返回的对象数组:
图 18.3 - 获取特定距离处的所有对象
- 要检测对象是否落在视野锥内,我们需要计算我们的观察方向和对象本身方向之间的角度。如果这两个方向之间的角度小于我们的锥角,我们认为对象落在我们的视野内。我们可以开始检测朝向对象的方向,这是通过归一化对象位置与我们位置之间的差异来计算的,就像下面的截图中所示的那样。您可能会注意到我们使用
bounds.center
而不是transform.position
;这样,我们检查对象的中心方向而不是其枢轴。请记住,玩家的枢轴在地面上,射线检查可能会在玩家之前与其发生碰撞:
图 18.4 - 从我们的位置计算朝向碰撞器的方向
- 我们可以使用
Vector3.Angle
函数来计算两个方向之间的角度。在我们的情况下,我们可以计算朝向敌人的方向和我们的前向量之间的角度:
图 18.5 - 计算两个方向之间的角度
重要信息
如果您愿意,您可以使用Vector3.Dot
,它将执行点积。Vector3.Angle
实际上使用了这个函数,但是为了将点积的结果转换为角度,它需要使用三角函数,这可能会导致昂贵的计算。无论如何,我们的方法更简单快速,只要您没有大量传感器(50+,取决于目标设备),这在我们的情况下不会发生。
-
现在检查计算出的角度是否小于
angle
字段中指定的角度。请注意,如果我们设置为 90 度,实际上将是 180 度,因为如果Vector3.Angle
函数返回,例如,30,它可以是 30 度向左或向右。如果我们的角度为 90 度,它可以是左侧或右侧的 90 度,因此它将检测到 180 度弧中的对象。 -
使用
Physics.Line
函数在我们的位置和碰撞体位置之间创建一条虚拟线,以检测在第三个参数中指定的层(obstacles
层)中的对象,并返回一个boolean
,指示该射线是否击中了某物体。这个想法是使用这条线来检测我们和检测到的碰撞体之间是否有障碍物,如果没有障碍物,这意味着我们对该对象有直线视线。再次提醒,这个函数依赖于障碍物对象有碰撞体,而在我们的情况下,我们有(墙壁、地板等):
图 18.6 - 使用线性投射检查传感器和目标对象之间的障碍物
- 如果对象通过了三个检查,这意味着这是我们当前看到的对象,所以我们可以将它保存在一个名为
detectedObject
的Collider
类型字段中,以便其他 AI 脚本稍后使用这些信息。考虑使用break
来停止for
循环,以防止浪费资源检查其他对象,并在for
之前将detectedObject
设置为null
,以清除上一帧的结果,所以在这一帧中,如果我们没有检测到任何东西,它将保持空值,这样我们就可以注意到传感器中没有东西:
图 18.7 - 完整的传感器脚本
重要信息
在我们的情况下,我们只是使用传感器来寻找玩家,这是传感器负责寻找的唯一对象,但如果你想使传感器更高级,你可以保持一个检测到的对象列表,将通过三个测试的每个对象放入其中,而不仅仅是第一个对象。
- 在编辑器中,根据需要配置传感器。在这种情况下,我们将
ObjectsLayer
设置为Player
,这样我们的传感器将专注于具有该层的对象,并将obstaclesLayer
设置为Default
,这是我们用于墙壁和地板的层:
图 18.8 - 传感器设置
- 为了测试这一点,只需在玩家面前放置一个移动速度为 0 的敌人,选择其 AI 子对象,然后播放游戏,看看属性在检查器中是如何设置的。还可以尝试在两者之间放置障碍物,并检查属性是否显示为“None”(
null
)。如果没有得到预期的结果,请仔细检查你的脚本、它的配置,以及玩家是否有Player
层,障碍物是否有Default
层。此外,你可能需要稍微提高 AI 对象,以防止射线从地面下方开始并击中地面:
图 18.9 - 传感器捕捉玩家
即使我们的传感器工作了,有时检查它是否工作或配置正确需要一些我们可以使用Gizmos
创建的视觉辅助工具。
使用 Gizmos 进行调试
当我们创建我们的 AI 时,我们将开始检测到一些边缘情况的错误,通常与错误配置有关。你可能认为玩家在敌人的视线范围内,但也许你没有注意到视线被物体遮挡,特别是当敌人不断移动时。调试这些情况的一个好方法是通过仅在编辑器中可见的视觉辅助工具,称为Gizmos
,它允许你可视化不可见的数据,比如视线距离或执行线性投射以检测障碍物。
让我们开始看如何通过绘制代表视线距离的球体来创建Gizmos
,方法如下:
-
在
Sight
脚本中,创建一个名为OnDrawGizmos
的事件函数。这个事件只在编辑器中执行(不在构建中执行),是 Unity 要求我们绘制Gizmos
的地方。 -
使用
Gizmos.DrawWireSphere
函数,将我们的位置作为第一个参数,距离作为第二个参数,以在我们的位置绘制一个半径为我们距离的球体。您可以检查随着更改距离字段而 Gizmo 大小的变化:
图 18.10 - 球体 Gizmo
- 可选地,您可以更改 Gizmo 的颜色,设置
Gizmos.color
然后调用绘图函数:
图 18.11 - Gizmos 绘图代码
重要信息
现在你不断地绘制Gizmos
,如果你有很多敌人,它们可能会用太多的Gizmos
污染场景视图。在这种情况下,可以尝试使用OnDrawGizmosSelected
事件函数,它只在对象被选中时绘制Gizmos
。
- 我们可以使用
Gizmos.DrawRay
来绘制代表锥体的线,它接收要绘制的线的起点和线的方向,可以乘以某个值来指定线的长度,如下面的屏幕截图所示:
图 18.12 - 绘制旋转线
- 在屏幕截图中,我们使用
Quaternion.Euler
根据我们想要旋转的角度生成一个四元数。如果将这个四元数乘以一个方向,我们将得到旋转后的方向。我们正在取我们的前向矢量,并根据角度字段旋转它,以生成我们的锥体视觉线。此外,我们将这个方向乘以视距,以绘制线条,使其能够看到我们的视线有多远;您将看到线条如何与球体的末端匹配:
图 18.13 - 视觉角线
我们还可以绘制线条投射,检查障碍物,但是由于这些取决于游戏的当前情况,例如通过前两个检查的对象及其位置,因此我们可以使用Debug.DrawLine
,它可以在Update
方法中执行。这个版本的DrawLine
设计为仅在运行时使用。我们在编辑器中看到的Gizmos
也是在编辑器中执行的。让我们尝试以下方式:
- 首先,让我们调试
LineCast
未检测到任何障碍物的情况,因此我们需要在我们的传感器和对象之间绘制一条线。我们可以在调用LineCast
的if
语句中调用Debug.DrawLine
,如下面的屏幕截图所示:
图 18.14 - 在 Update 中绘制一条线
- 在下一个屏幕截图中,您可以看到
DrawLine
的效果:
图 18.15 - 指向检测到的对象的线
- 当视线被对象遮挡时,我们还希望以红色绘制一条线。在这种情况下,我们需要知道 Line Cast 的命中位置,因此我们可以使用函数的一个重载,它提供了一个
out
参数,可以提供有关线碰撞的更多信息,例如命中的位置、法线和碰撞的对象,如下面的屏幕截图所示:
图 18.16 - 获取有关 Linecast 的信息
重要信息
请注意,Linecast
并不总是与最近的障碍物发生碰撞,而是与它在线上检测到的第一个对象发生碰撞,这可能会按顺序变化。如果您需要检测最近的障碍物,请查找该函数的Physics.Raycast
版本。
- 我们可以使用这些信息在
else
子句中绘制从我们的位置到命中点的线,当线与某物发生碰撞时:
图 18.17 - 在我们遇到障碍物时绘制一条线
- 在下一个屏幕截图中,您可以看到结果:
图 18.18 - 当障碍物遮挡视线时的线
现在我们的传感器已经完成,让我们使用它们提供的信息来使用**有限状态机(FSM)**做出决策。
使用 FSM 做出决策
我们在过去使用 Animator 时探讨了 FSM 的概念。我们了解到 FSM 是一组状态的集合,每个状态代表对象可以执行的动作,以及一组决定状态切换方式的转换。这个概念不仅在动画中使用,而且在许多编程场景中都有应用,其中一个常见的应用是在 AI 中。我们可以用 AI 代码替换状态中的动画,就得到了 AI FSM。
在本节中,我们将研究以下 AI FSM 概念:
-
创建 FSM
-
创建转换
让我们开始创建我们的 FSM 骨架。
创建 FSM
创建我们自己的 FSM,我们需要回顾一些基本概念。记住,FSM 可以为它可以执行的每个可能动作都有一个状态,而且一次只能执行一个动作。在 AI 方面,我们可以巡逻,攻击,逃跑等。还要记住,状态之间存在转换,确定改变一个状态到另一个状态需要满足的条件,就 AI 而言,这可以是用户靠近敌人开始攻击或生命值低开始逃跑。在下一个截图中,你可以找到一个门的两种可能状态的简单提醒示例:
图 18.19 - FSM 示例
有几种方法可以为 AI 实现 FSM;你甚至可以使用 Animator,或者从 Asset Store 下载一些 FSM 系统。在我们的情况下,我们将采取尽可能简单的方法,一个带有一组If
语句的单个脚本,这可能很基础,但仍然是理解概念的良好开始。让我们通过以下方式实现它:
-
在 Enemy 的 AI 子对象中创建一个名为
EnemyFSM
的脚本。 -
创建名为
EnemyState
的enum
,其中包含GoToBase
,AttackBase
,ChasePlayer
和AttackPlayer
值。我们将在我们的 AI 中拥有这些状态。 -
创建一个名为
currentState
的EnemyState
类型字段,它将保存我们的 Enemy 的当前状态:
图 18.20 - EnemyFSM 状态定义
-
创建三个以我们定义的状态命名的函数。
-
根据当前状态在
Update
中调用这些函数:
图 18.21 - 基于 If 的 FSM
重要信息
是的,你完全可以在这里使用 switch,但我更喜欢常规的if
语法。
- 在编辑器中测试如何改变
currentState
字段将改变哪个状态是活动的,看到在控制台中打印的消息:
图 18.22 - 状态测试
如你所见,这是一个非常简单但完全功能的方法,所以让我们继续使用这个 FSM,创建它的转换。
创建转换
如果你记得在 Animator Controller 中创建的转换,那些基本上是一组条件,如果转换所属的状态处于活动状态,则检查这些条件。在我们的 FSM 方法中,这简单地转换为在状态内检测条件的 If 语句。让我们按照以下方式创建我们提出的状态之间的转换:
-
在我们的 FSM 脚本中添加一个名为
sightSensor
的Sight
类型字段,并将 AIGameObject
拖到该字段中,将其连接到那里的Sight
组件。由于 FSM 组件与Sight
位于同一对象中,我们也可以使用GetComponent
,但在高级 AI 中,你可能有不同的传感器检测不同的对象,所以我更喜欢为这种情况准备我的脚本,但选择你最喜欢的方法。 -
在
GoToBase
函数中,检查Sight
组件检测到的对象是否不为null
,这意味着我们的视线内有东西。如果我们的 AI 正在前往基地,但在路上检测到一个对象,我们必须切换到Chase
状态以追击玩家,所以我们改变状态,如下面的屏幕截图所示:
图 18.23 - 创建转换
- 此外,我们必须在靠近必须受损的对象时切换到
AttackBase
。我们可以创建一个Transform
类型的字段,称为baseTransform
,并将基地生命对象拖放到那里,以便我们可以检查距离。记得添加一个名为baseAttackDistance
的float
字段,以使该距离可配置:
图 18.24 - 前往基地转换
- 在
ChasePlayer
的情况下,我们需要检查玩家是否不在视线内,以切换回GoToBase
状态,或者我们是否足够接近玩家以开始攻击它。我们将需要另一个distance
字段,用于确定攻击玩家的距离,我们可能希望为这两个目标设置不同的攻击距离。考虑在转换中进行早期返回,以防止在没有对象时尝试访问传感器检测到的对象的位置时出现null
引用异常:
图 18.25 - 追击玩家转换
- 对于
AttackPlayer
,我们需要检查Player
是否不在视线内,以返回到GoToBase
,或者它是否足够远,以返回追击它。您可以注意到我们将PlayerAttackDistance
乘以1.1
,使停止攻击的距离比开始攻击的距离大一点;这将防止在玩家接近该距离时快速在攻击和追击之间切换。您可以使其可配置,而不是硬编码1.1
:
图 18.26 - 攻击玩家转换
-
在我们的情况下,
AttackBase
不会有任何转换。一旦敌人靠近基地足够攻击它,即使玩家开始向它射击,它也会保持这样。一旦到达那里,它的唯一目标就是摧毁基地。 -
记得你可以使用
Gizmos
来绘制距离:
图 18.27 - FSM 小工具
- 在点击播放之前,测试选择 AI 对象的脚本,然后移动玩家,检查状态在检查器中的变化。您还可以保留每个状态中的原始打印消息,以在控制台中查看它们的变化。记得设置攻击距离和对象的引用。在屏幕截图中,您可以看到我们使用的设置:
图 18.28 - 敌人 FSM 设置
现在我们将遇到的一个小问题是,生成的敌人将没有必要的引用,无法进行基地变换的距离计算。如果您尝试将场景中的敌人的更改应用到预制件(None
),您将注意到这一点。请记住,预制件不能包含对场景中对象的引用,这使得我们的工作变得复杂。一个替代方法是创建BaseManager
,一个保存对伤害位置的引用的单例,这样我们的EnemyFSM
就可以访问它。另一个方法可能是利用GameObject.Find
等函数来找到我们的对象。
在这种情况下,我们将尝试后者。即使它可能比 Manager 版本的性能要差一些,我还是想向你展示如何使用它来扩展你的 Unity 工具集。在这种情况下,只需在Awake
中将baseTransform
字段设置为GameObject.Find
的返回值,使用BaseDamagePoint
作为第一个参数,它将查找一个叫这个名字的对象,就像下面的截图一样。同时,可以自由地从baseTransform
字段中删除 private 关键字;现在通过代码设置了它,将其显示在编辑器中除了用于调试之外没有太大意义。你会看到,现在我们生成的敌人将改变状态:
图 18.29 - 按名称在场景中搜索对象
现在我们的 FSM 状态已经编码并且过渡正常,让它们做点什么吧。
执行 FSM 动作
现在我们需要做最后一步 - 让 FSM 做一些有趣的事情。在这里,我们可以做很多事情,比如射击基地或玩家,并将敌人移向其目标(玩家或基地)。我们将使用 Unity 路径规划系统NavMesh
来处理移动,这是一个允许我们的 AI 计算和穿越两点之间的路径并避开障碍物的工具,需要一些准备工作才能正常工作。
在本节中,我们将讨论以下 FSM 动作概念:
-
计算我们场景的路径规划
-
使用路径规划
-
添加最后的细节
让我们开始为路径规划准备我们的场景。
计算我们场景的路径规划
路径规划算法依赖于场景的简化版本。在实时中分析复杂场景的完整几何形状几乎是不可能的。表示从场景中提取的路径规划信息的方法有很多,比如图形和NavMesh
几何。Unity 使用后者 - 一个简化的网格,类似于跨越 Unity 确定为可行走区域的所有区域的 3D 模型。在下一个截图中,你可以找到一个在场景中生成的NavMesh
的示例,即浅蓝色的几何体:
图 18.30 - 场景中可行走区域的 NavMesh
生成NavMesh
可能需要几秒到几分钟,这取决于场景的大小。这就是为什么 Unity 的路径规划系统在编辑器中计算一次,所以当我们分发我们的游戏时,用户将使用预先生成的NavMesh
。就像光照贴图一样,NavMesh
被烘焙到一个文件中以供以后使用。与光照贴图一样,主要的警告是NavMesh
对象在运行时不能改变。如果你销毁或移动地板砖,AI 仍然会走在那个区域。NavMesh
也没有注意到地板不在了,所以你不能以任何方式移动或修改这些对象。幸运的是,在我们的情况下,我们不会在运行时遭受场景的任何修改,但是请记住,有一些组件,比如NavMeshObsacle
,可以在这些情况下帮助我们。
要为我们的场景生成NavMesh
,请执行以下操作:
-
选择任何可行走的对象以及其上的障碍物,比如地板、墙壁和其他障碍物,并将它们标记为
Static
。你可能还记得Static
复选框也会影响光照贴图,所以如果你希望一个对象不参与光照贴图但对NavMesh
的生成有贡献,你可以点击静态检查左侧的箭头,并选择NavMesh
生成速度。在我们的情况下,使地形可通行会大大增加生成时间,我们永远不会在那个区域玩。 -
在窗口|AI|导航中打开
NavMesh
面板。 -
选择
NavMesh
:
图 18.31 - 生成 NavMesh
基本上你需要做的就是这些。当然,还有很多设置可以调整,比如NavMesh
,但是由于我们的场景简单明了,所以默认设置就足够了。
现在,让我们让我们的 AI 在 NavMesh 周围移动。
使用路径规划
为了制作一个使用 NavMesh 移动的 AI 对象,Unity 提供了 NavMeshAgent 组件,它将使我们的 AI 粘附在 NavMesh 上,防止对象离开它。它不仅会自动计算到指定目的地的路径,还会通过模拟人类移动方式的转向行为算法来沿着路径移动对象,在拐角处减速并使用插值进行转向,而不是瞬间转向。此外,该组件能够躲避场景中运行的其他 NavMeshAgent,防止所有敌人聚集在同一位置。
让我们通过以下方式使用这个强大的组件:
- 选择敌人 Prefab 并向其添加 NavMeshAgent 组件。将其添加到根对象,称为 Enemy,而不是 AI 子对象 - 我们希望整个对象移动。你会看到对象周围有一个圆柱体,表示对象在 NavMesh 中所占据的区域。请记住,这不是一个碰撞体,所以它不会用于物理碰撞:
图 18.32 - NavMeshAgent 组件
-
移除 ForwardMovement 组件;从现在开始,我们将使用 NavMeshAgent 来驱动我们敌人的移动。
-
在 EnemyFSM 脚本的 Awake 事件函数中,使用 GetComponentInParent 函数来缓存 NavMeshAgent 的引用。这将类似于 GetComponent - 它将在我们的 GameObject 中查找组件,但如果组件不存在,这个版本将尝试在所有父级中查找该组件。记得添加 using UnityEngine.AI 行来在这个脚本中使用 NavMeshAgent 类:
图 18.33 - 缓存父级组件引用
重要信息
你可以想象,还有一个 GetComponentInChildren,它首先在 GameObject 中搜索组件,然后在必要时在所有子对象中搜索。
- 在 GoToBase 状态函数中,调用 NavMeshAgent 引用的 SetDestination 函数,传递基本对象的位置作为目标:
图 18.34 - 设置我们的 AI 的目的地
- 保存脚本并在场景中测试一下,或者使用波次生成的敌人进行测试。你会看到敌人永远不会停止朝着目标位置前进,甚至在它们的有限状态机状态在靠近目标时发生变化时也会进入对象内部。这是因为我们从未告诉 NavMeshAgent 停止,我们可以通过将代理的 isStopped 字段设置为 true 来实现这一点。你可能想调整基本攻击距离,使敌人停下来的位置更近或更远:
图 18.35 - 停止代理移动
- 我们可以对 ChasePlayer 和 AttackPlayer 做同样的操作。在 ChasePlayer 中,我们可以将代理的目的地设置为玩家的位置,在 AttackPlayer 中,我们可以停止移动。在这种情况下,AttackPlayer 可以再次返回到 GoToBase 或 ChasePlayer,所以你需要在这些状态或在进行转换之前将 isStopped 代理字段设置为 false。我们将选择前者,因为这个版本将覆盖其他也会停止代理的状态而不需要额外的代码。我们将从 GoToBase 状态开始:
图 18.36 - 重新激活代理
- 然后,继续进行 Chase Player:
图 18.37 - 重新激活代理并追逐玩家
- 最后,继续进行攻击玩家:
图 18.38 - 停止移动
- 您可以调整
NavMeshAgent
的Acceleration
、Speed
和Angular Speed
属性来控制敌人的移动速度。还记得将更改应用到生成的敌人 Prefab 中。
现在我们的敌人有了移动,让我们完成 AI 的最后细节。
添加最后的细节
这里有两件事情还没有完成,敌人没有射击任何子弹,也没有动画。让我们开始通过以下方式修复射击:
-
在我们的
EnemyFSM
脚本中添加一个bulletPrefab
字段,类型为GameObject
,以及一个名为fireRate
的float
字段。 -
创建一个名为
Shoot
的函数,并在AttackBase
和AttackPlayer
中调用它:
图 18.39 - 射击函数调用
- 在
Shoot
函数中,放置与PlayerShooting
脚本中使用的类似代码,以特定的射击速率射击子弹,如下截图所示。记得在敌人 Prefab 中设置敌人层,以防止子弹伤害到敌人自身。您可能还希望稍微提高 AI 脚本以在另一个位置射击子弹,或者更好地,添加一个shootPoint
变换字段,并在敌人中创建一个空对象作为生成位置。如果这样做,考虑使空对象不旋转,以便敌人的旋转正确影响子弹的方向:
图 18.40 - 射击函数代码
重要信息
在PlayerShooting
和EnemyFSM
之间找到了一些重复的射击行为。您可以通过创建一个名为Weapon
的行为来修复这个问题,该行为具有一个名为Shoot
的函数,用于实例化子弹并考虑射击速率,并在两个组件内调用它以进行重复利用。
- 当代理停止时,不仅移动停止,而且旋转也停止。如果玩家在敌人受到攻击时移动,我们仍然需要敌人面对它以向其方向射击子弹。我们可以创建一个
LookTo
函数,该函数接收要查看的目标位置,并在AttackPlayer
和AttackBase
中调用它,传递要射击的目标:
图 18.41 - LookTo 函数调用
- 通过获取我们的父对象到目标位置的方向来完成
LookTo
函数,我们使用transform.parent
访问我们的父对象,因为记住,我们是子 AI 对象,移动的对象是我们的父对象。然后,我们将方向的Y
分量设置为0
,以防止方向指向上方或向下方 - 我们不希望我们的敌人垂直旋转。最后,我们将父对象的前向矢量设置为该方向,以便立即面向目标位置。如果您愿意,您可以用四元数插值替换它,以使旋转更加平滑,但现在让我们尽可能保持简单:
图 18.42 - 面向目标
最后,我们可以使用与玩家相同的 Animator Controller 为敌人添加动画,并使用其他脚本设置参数,具体步骤如下:
-
为敌人添加一个
Animator
组件,如果还没有的话,并设置与玩家相同的控制器;在我们的情况下,这也被称为Player
。 -
创建并添加一个脚本到 Enemy 根对象,名为
NavMeshAnimator
,它将获取NavMeshAgent
的当前速度并将其设置到 Animator 控制器中。这将类似于VelocityAnimator
脚本,并负责更新 Animator 控制器的velocity
参数以匹配对象的速度。我们没有在这里使用它,因为NavMeshAgent
不使用Rigidbody
来移动。它有自己的速度系统。实际上,如果我们愿意,我们可以将Rigidbody
设置为kinematic
,因为它移动但不受物理影响:
总结
通过这样,我们结束了本书的第二部分,关于 C#脚本。在接下来的短篇中,我们将完成游戏的最后细节,从优化开始。图 18.43 - 将 NavMeshAgent 连接到我们的 Animator 控制器
- 图 18.45 - 打开射击动画
通过这样,我们已经完成了所有的 AI 行为。当然,这个脚本足够大,值得在将来进行一些重构和拆分,一些动作,如停止和恢复动画和NavMeshAgent
可以以更好的方式完成。但是通过这样,我们已经原型化了我们的 AI,并且可以测试直到我们对它满意,然后我们可以改进这段代码。
图 18.44 - 访问父级的 Animator 引用
- ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.43_B14199.jpg)
](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.46_B14199.jpg)
](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.45_B14199.jpg)
在所有非射击状态(如GoToBase
和ChasePlayer
)中关闭boolean
:
在Shoot
函数中打开Shooting
animator
参数,以确保每次射击时该参数被设置为true
(选中):,设置 Unity。一旦进入新的空白项目中,就像我们之前安装其他软件包一样,从软件包管理器中安装 AR Foundation 软件包;也就是说,从窗口|软件包管理器。记得设置软件包管理器,以便显示所有软件包,而不仅仅是项目中的软件包(窗口左上角的软件包按钮需要设置为Unity Registry)。在撰写本书时,最新版本是 4.0.2。请记住,您可以使用查看其他版本按钮,该按钮出现在列表中软件包项下的软件包左侧的三角形上,以显示其他版本选项。如果您找到比我的更新版本,可以尝试使用该版本,但通常情况下,如果某些功能与我们想要的不同,请安装此特定版本:
图 22.2 - 安装 AR Foundation
在安装其他所需软件包之前,现在是讨论 AR Foundation 框架的一些核心思想的好时机。这个软件包本身什么也不做;它定义了移动设备提供的一系列 AR 功能,比如图像跟踪、云点和对象跟踪,但如何实现这些功能的实际实现包含在提供程序软件包中,比如 AR Kit 和 AR Core XR 插件。这样设计是因为,根据您想要使用的目标设备,这些功能的实现方式会发生变化。例如,在 iOS 中,Unity 使用 AR Kit 来实现这些功能,而在 Android 中,它使用 AR Core;它们是特定于平台的框架。
需要考虑的是,并非所有 iOS 或 Android 设备都支持 AR Foundation 应用程序。在互联网上搜索 AR Core 和 AR Kit 支持的设备时,您可能会找到受支持设备的更新列表。撰写本文时,以下链接提供了受支持设备列表:
-
iOS:
www.apple.com/lae/ios/augmented-reality/
(页面底部)
此外,没有 PC 提供程序包,因此迄今为止测试 AR Foundation 应用程序的唯一方法是直接在设备上进行测试,但测试工具即将发布。在我的情况下,我将为 iOS 创建一个应用程序,因此除了AR Foundation软件包外,我还需要安装ARKit XR插件。但是,如果您想为 Android 开发,请安装ARCore XR插件(如果您针对两个平台,请安装两者)。我将使用 ARKit 软件包的 4.0.2 版本,但在撰写本书时,ARCore 推荐的版本是 4.0.4 通常,AR Foundation和提供程序软件包的版本匹配,但应用与选择AR Foundation版本时相同的逻辑。在下面的屏幕截图中,您可以看到软件包管理器中的ARKit软件包:
图 22.3 - 安装特定于平台的 AR 提供程序包
](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_22.3_B14199.jpg)
图 22.3 - 安装特定于平台的 AR 提供程序包
现在我们有了所需的插件,我们需要为 AR 准备一个场景,如下所示:
-
在文件|新场景中创建一个新场景。
-
删除主相机;我们将使用另一个。
-
在游戏对象|XR菜单中,创建一个AR 会话对象。
-
在同一个菜单中,创建一个AR 会话起源对象,其中包含一个相机:
图 22.4 - 创建会话对象
- 您的层次结构应如下所示:
图 22.5 - 起始 ARSCcene
AR 会话对象将负责初始化 AR 框架,并处理 AR 系统的所有更新逻辑。AR 会话原点对象将允许框架相对于场景定位跟踪对象,如图像和点云。设备会通知跟踪对象相对于设备认为的“原点”的位置。这通常是您在应用程序开始检测对象时指向的房屋的第一个区域,因此 AR 会话原点对象将代表该区域。最后,您可以检查原点内的相机,其中包含一些额外的组件,其中最重要的是AR 姿势驱动器,它将使您的相机对象随着您的设备移动。由于设备的位置是相对于会话原点对象的点,因此相机需要在原点对象内部。
在 URP 项目(我们的情况)中的一个额外步骤是,您需要设置渲染管道,以便支持在应用程序中渲染相机图像。为此,请转到创建项目时生成的Settings
文件夹,查找Forward Renderer
文件,并选择它。在Renderer Features列表中,单击添加渲染器功能按钮,然后选择AR 背景渲染器功能。请注意,如果您使用的是早于 AR Foundation 和 Provider 插件 4.0.0 版本的版本,则此选项可能不可用。在以下截图中,您可以看到前向渲染器资产应该是什么样子的:
图 22.6 - 添加对 URP 的支持
就是这样!我们已经准备好开始探索 AR Foundation 组件,以便我们可以实现跟踪功能。
使用跟踪功能
对于我们的项目,我们将需要 AR 中最常见的两种跟踪功能(但不是唯一的):图像识别和平面检测。第一种是检测特定图像在现实世界中的位置,以便我们可以将数字对象放在其上,例如玩家。第二种,平面检测,是识别现实生活中的表面,如地板、桌子和墙壁,以便我们知道可以放置对象的位置,例如敌人的生成点。只有水平和垂直表面被识别(某些设备上只有垂直表面)。
我们需要做的第一件事是告诉我们的应用程序它需要检测哪些图像,如下所示:
- 向项目添加一个图像,您可以打印或在手机上显示。有一种在现实世界中显示图像的方式是必要的来测试这一点。在这种情况下,我将使用以下图像:
图 22.7 - 要跟踪的图像
重要提示
尽量获取包含尽可能多特征的图像。这意味着图像具有许多细节,如对比度、锐利的角落等。这些是我们的 AR 系统用来检测的;细节越多,识别就越好。在我们的情况下,我们使用的 Unity 标志实际上并没有太多细节,但有足够的对比度(只是黑白)和锐利的角落,以便系统识别它。如果您的设备在检测时出现问题,请尝试其他图像(经典的 QR 码可能会有所帮助)。
请注意,某些设备可能会对某些图像(例如本书中建议的图像)产生问题。如果在测试时出现问题,请尝试使用其他图像。您将在本章的后续部分在您的设备上测试这一点,所以请记住这一点。
- 通过单击Project Panel中的**+按钮并选择XR** | Reference Image Library来创建一个包含我们希望应用程序识别的所有图像的资产,创建一个参考图像库:
图 22.8 – 创建参考图像库
-
选择库资产并单击添加图像按钮以向库中添加新图像。
-
将纹理拖到纹理槽(标有None的槽)。
-
打开Specify Size并将Physical Size设置为图像在现实生活中的大小,以米为单位。在这里尽量准确;在某些设备上,如果这个值不正确,可能会导致图像无法被跟踪:
图 22.9 – 添加要识别的图像
既然我们已经指定了要检测的图像,让我们通过在真实世界的图像顶部放置一个立方体来测试这一点:
-
创建一个立方体的预制体并向其添加AR Tracked Image组件。
-
将AR Tracked Image Manager组件添加到AR Session Origin对象中。这将负责检测图像并在其位置创建对象。
-
将Image Library资产拖到组件的Serialized Library属性中,以指定要识别的图像。
-
将Cube预制体拖到组件的Tracked Image Prefab 属性中:
图 22.10 – 设置 Tracked Image Manager
就是这样!我们将看到一个立方体在现实世界中与图像相同的位置生成。请记住,您需要在设备上测试这一点,我们将在下一节中进行测试,所以现在让我们继续编写我们的测试应用程序:
图 22.11 – 放置在手机显示的图像顶部的立方体
我们还要准备我们的应用程序,以便它可以检测和显示相机识别的平面表面。只需将AR Plane Manager组件添加到AR Session Origin对象即可:
图 22.12 – 添加 AR Plane Manager 组件
当我们在房子上移动相机时,这个组件将检测表面平面。检测它们可能需要一段时间,所以重要的是要可视化检测到的区域,以确保它正常工作。我们可以通过组件引用手动获取有关平面的信息,但幸运的是,Unity 允许我们轻松可视化平面。让我们来看一下:
-
创建一个平面的预制体,首先在GameObject | 3D Object | Plane中创建平面。
-
添加一个Line Renderer。这将允许我们在检测到的区域边缘上画一条线。
-
将
0.01
,Color属性设置为黑色,并取消选中Use World Space:
图 22.13 – 设置 Line Renderer
- 记得为Line Renderer创建一个合适的着色器材质,并将其设置为渲染器的材质:
图 22.14 – 创建 Line Renderer 材质
- 另外,创建一个透明材质并在MeshRenderer平面中使用。我们希望能透过它看到真实表面,以便轻松地看到下面的真实表面:
图 22.15 – 用于检测平面的材质
-
向Plane预制体添加AR Plane和AR Plane Mesh Visualizer组件。
-
将预制体拖动到AR Plane Manager组件的Plane Prefab属性中的AR Session Origin对象:
图 22.16 – 设置平面可视化预制件
现在,我们有一种方法来看到平面,但看到它们并不是我们唯一能做的事情(有时,我们甚至不希望它们可见)。平面的真正力量在于将虚拟对象放置在现实表面上,点击特定平面区域,并获取其现实位置。我们可以使用 AR Plane Manager 或访问可视化平面的 AR Plane 组件来访问平面数据,但更简单的方法是使用AR Raycast Manager组件。
Unity 物理系统的Physics.Raycast
函数,您可能还记得,用于创建从一个位置开始并朝着指定方向的虚拟射线,以使它们击中表面并检测确切的击中点。由AR Raycast Manager提供的版本,与物理碰撞体不同,它与跟踪对象发生碰撞,主要是点云(我们不使用它们)和我们正在跟踪的“平面”。我们可以通过以下步骤测试这个功能:
-
将AR Raycast Manager组件添加到AR Session Origin对象中。
-
在AR Session Origin对象中创建一个名为
InstanceOnPlane
的自定义脚本。 -
在
ARRaycastManager
中。您需要在脚本顶部添加using UnityEngine.XR.ARFoundation;
行,以便在我们的脚本中可用。 -
创建一个
List<ARRaycastHit>
类型的私有字段并实例化它;Raycast 将检测我们的射线击中的每个平面,而不仅仅是第一个:
图 22.17 – 存储射线击中的列表
-
在
KeyCode.Mouse0
下按下。在 AR 应用中,鼠标是用设备的触摸屏模拟的(您还可以使用Input.touches
数组来支持多点触控)。 -
在
if
语句中,添加另一个条件来调用AR Raycast Manager的Raycast
函数,将鼠标的位置作为第一个参数,将击中列表作为第二个参数。 -
这将向玩家触摸屏幕的方向投射射线,并将击中的结果存储在我们提供的列表中。如果有东西被击中,它将返回
true
,否则返回false
:
图 22.18 – 发射 AR 射线
-
添加一个公共字段来指定要在我们触摸的位置实例化的预制件。您可以只创建一个球体预制件来测试这个;这里不需要为预制件添加任何特殊组件。
-
在列表中存储的第一个击中的Pose属性的Position和Rotation字段中实例化预制件。击中是按距离排序的,所以第一个击中是最近的。您的最终脚本应如下所示:
图 22.19 – 射线投射器组件
在本节中,我们学习了如何使用 AR Foundation 创建新的 AR 项目。我们讨论了如何安装和设置框架,以及如何检测现实图像的位置和表面,然后如何将对象放置在其上。
正如您可能已经注意到的,我们从未点击播放按钮来测试这个,遗憾的是,在撰写本书时,我们无法在编辑器中测试这个。相反,我们需要直接在设备上测试这个。因此,在下一节中,我们将学习如何为 Android 和 iOS 等移动设备构建。
为移动设备构建
Unity 是一个非常强大的工具,可以非常轻松地解决游戏开发中最常见的问题之一,其中之一是为多个目标平台构建游戏。现在,为这些设备构建我们的项目的 Unity 部分很容易,但是每个设备都有其与 Unity 无关的细微差别,用于安装开发构建。为了测试我们的 AR 应用程序,我们需要直接在设备上测试它。因此,让我们探索如何使我们的应用程序在 Android 和 iOS 上运行,这是最常见的移动平台。
在深入讨论这个话题之前,值得一提的是,以下程序随时间变化很大,因此您需要在互联网上找到最新的说明。Unity Learn 门户网站(learn.unity.com/tutorial/building-for-mobile
)可能是一个很好的选择,如果本书中的说明失败,请先尝试这里的步骤。
在本节中,我们将研究以下移动构建概念:
-
为 Android 构建
-
为 iOS 构建
让我们首先讨论如何构建我们的应用程序,以便在 Android 手机上运行。
为 Android 构建
与其他平台相比,创建 Android 构建相对容易,因此我们将从 Android 开始。请记住,您需要一台能够运行 AR Foundation 应用程序的 Android 设备,请参考本章第一节中提到的关于 Android 支持设备的链接。我们需要做的第一件事是检查我们是否已安装了 Unity 的 Android 支持并配置了我们的项目以使用该平台。要做到这一点,请按照以下步骤操作:
-
关闭 Unity 并打开Unity Hub。
-
进入Installs部分,找到您正在使用的 Unity 版本。
-
单击 Unity 版本右上角的三个点按钮,然后单击Add Modules:
图 22.20 – 向 Unity 版本添加模块
- 确保勾选Android Build Support以及单击左侧箭头时显示的子选项。如果没有,请勾选它们,然后单击右下角的Done按钮进行安装:
图 22.21 – 向 Unity 添加 Android 支持
-
打开我们在本章中创建的 AR 项目。
-
进入Build Settings(File | Build Settings)。
-
从列表中选择Android平台,然后单击窗口右下角的Switch Platform按钮:
图 22.22 – 切换到 Android 构建
要在 Android 上构建应用程序,我们需要满足一些要求,例如安装 Java SDK(而不是常规的 Java 运行时)和 Android SDK,但幸运的是,Unity 的新版本会处理这些。只是为了再次确认我们已安装所需的依赖项,请按照以下步骤操作:
-
进入Unity Preferences(Windows 上为Edit | Preferences,Mac 上为Unity | Preferences)。
-
单击External Tools。
-
检查 Android 部分上所有标有Installed with Unity的选项是否都已被选中。这意味着我们将使用 Unity 安装的所有依赖项:
图 22.23 – 使用已安装的依赖项
还有一些额外的与 Android AR Core 相关的设置需要检查,您可以在developers.google.com/ar/develop/unity-arf/quickstart-android
找到。如果您使用的是更新版本的 AR Core,这些设置可能会发生变化。您可以按照以下步骤应用它们:
-
进入Player Settings(Edit | Project Settings | Player)。
-
取消选中Multithreaded Rendering和Auto Graphics API。
-
从Graphics APIs列表中删除Vulkan。
-
将Minimum API Level设置为Android 7.0:
图 22.24 – AR Core 设置
现在,您可以像往常一样从文件 | 构建设置构建应用,使用构建按钮。这一次,输出将是一个单独的 APK 文件,您可以通过将文件复制到您的设备并打开它来安装。请记住,为了安装未从 Play 商店下载的 APK 文件,您需要设置您的设备允许安装未知应用。这个选项的位置可能会有很大不同,取决于您使用的 Android 版本和设备,但这个选项通常位于安全设置中。一些 Android 版本在安装 APK 时会提示您查看这些设置。
现在,我们可以每次想要创建构建时复制和安装生成的 APK 构建文件。但是,我们可以让 Unity 使用构建和运行按钮为我们完成这些工作。这个选项在构建应用程序后,会查找通过 USB 连接到您的计算机的第一个 Android 设备,并自动安装应用程序。为了使这个工作,我们需要准备好我们的设备和 PC,具体操作如下:
在您的设备上,在设置部分找到构建号,其位置可能会根据设备而变化。在我的设备上,它位于关于手机 | 软件信息部分:
图 22.25 – 查找构建号
-
轻点几次,直到设备显示您现在是一个程序员。这个过程会在设备中启用隐藏的开发者选项,您现在可以在设置中找到它。
-
打开开发者选项并打开USB 调试,这允许您的 PC 在您的设备上拥有特殊权限。在这种情况下,它允许您安装应用程序。
-
从您手机制造商的网站上安装 USB 驱动程序到您的计算机上。例如,如果您有一部三星设备,请搜索
三星 USB 驱动程序
。另外,如果您找不到,您可以搜索Android USB 驱动程序
来获取通用驱动程序,但如果您的设备制造商有自己的驱动程序,这可能不起作用。在 Mac 上,这一步通常是不必要的。 -
连接您的设备(如果已连接,请重新连接)。设备上将出现允许 USB 调试的选项。选择始终允许并点击确定:
图 22.26 – 允许 USB 调试
-
接受出现的允许数据提示。
-
如果这些选项不出现,请检查您的设备的USB 模式是否设置为调试而不是其他任何模式。
-
在 Unity 中,使用构建和运行按钮进行构建。
-
如果您在检测我们实例化播放器的图像时遇到问题,请记得尝试另一张图片(在我这里是 Unity 标志)。这可能会根据您的设备能力而有很大不同。
就是这样!现在您的应用程序已经在您的设备上运行了,让我们学习如何在 iOS 平台上做同样的事情。
为 iOS 构建
在 iOS 开发时,您需要花一些钱。您需要运行 Xcode,这是一款只能在 OS X 上运行的软件。因此,您需要一台可以运行它的设备,比如 MacBook,Mac mini 等。可能有办法在 PC 上运行 OS X,但您需要自己找出来并尝试。除了在 Mac 和 iOS 设备(iPhone,iPad,iPod 等)上花钱外,您还需要支付 99 美元/年的 Apple 开发者账户费用,即使您不打算在 App Store 上发布应用程序(可能有替代方案,但同样,您需要自己找到)。
因此,要创建 iOS 构建,您应该执行以下操作:
-
获取一台 Mac 电脑。
-
获取一个 iOS 设备。
-
创建一个 Apple 开发者账户(在撰写本书时,您可以在
developer.apple.com/
上创建一个)。 -
从 App Store 上安装 Xcode 到您的 Mac 上。
-
检查 Unity Hub 中是否安装了 iOS 构建支持。有关此步骤的更多信息,请参考在 Android 上构建部分:
图 22.27 - 启用 iOS 构建支持
- 在构建设置下切换到 iOS 平台,选择 iOS 并点击切换平台按钮:
图 22.28 - 切换到 iOS 构建
- 点击构建设置窗口中的构建按钮,然后等待。
您会注意到构建过程的结果是一个包含 Xcode 项目的文件夹。Unity 无法直接创建构建,因此它生成了一个项目,您可以使用我们之前提到的 Xcode 软件打开。在本书中使用的 Xcode 版本(11.4.1)创建构建的步骤如下:
- 双击生成的文件夹中的
.xcproject
文件:
图 22.29 - Xcode 项目文件
-
转到Xcode | 首选项。
-
在帐户选项卡中,点击窗口左下角的**+**按钮,并使用您注册为苹果开发者的苹果帐户登录:
图 22.30 - 帐户设置
- 连接您的设备,并从窗口左上角选择它,现在应该显示通用 iOS 设备:
图 22.31 - 选择设备
-
在左侧面板中,点击文件夹图标,然后点击Unity-iPhone设置以显示项目设置。
-
从目标列表中,选择Unity-iPhone,然后点击签名和功能选项卡。
-
在
个人团队
中:
图 22.32 - 选择团队
-
如果看到一个
com.XXXX.XXXX
),然后点击重试,直到问题解决。一旦找到一个有效的,设置在 Unity 中(播放器设置下的包标识符)以避免在每次构建中都需要更改它。 -
点击窗口左上角的播放按钮,等待构建完成。在这个过程中,您可能会被提示输入密码几次,请务必这样做。
-
构建完成后,请记得解锁设备。会有提示要求您这样做。请注意,除非您解锁手机,否则流程将无法继续。
-
完成后,您可能会看到一个错误,说应用无法启动,但已经安装了。如果尝试打开它,会提示您需要信任应用的开发者,您可以通过转到设备的设置来执行。
-
从那里,转到通用 | 设备管理,并选择列表中的第一个开发者。
-
点击蓝色的信任…按钮,然后信任。
-
尝试再次打开应用程序。
-
如果在实例化播放器的图像上遇到问题,请记得尝试另一张图像(在我的情况下是 Unity 标志)。这可能会有很大的变化,取决于您设备的能力。
在本节中,我们讨论了如何构建一个可以在 iOS 和 Android 上运行的 Unity 项目,从而使我们能够创建移动应用程序 - 特别是 AR 移动应用程序。与任何构建一样,我们可以遵循方法进行分析和调试,就像我们在查看 PC 构建时所看到的那样,但我们不打算在这里讨论。现在我们已经创建了我们的第一个测试项目,我们将通过向其添加一些机制将其转换为一个真正的游戏。
创建一个简单的 AR 游戏
正如我们之前讨论的,我们的想法是创建一个简单的游戏,我们可以在移动真实图像的同时移动我们的玩家,并通过点击放置一些敌人生成器,比如墙壁、地板、桌子等。我们的玩家将自动射击最近的敌人,敌人将直接射击玩家,所以我们唯一的任务就是移动玩家以避开子弹。我们将使用与本书的主要项目中使用的非常相似的脚本来实现这些游戏机制。
在本节中,我们将开发以下 AR 游戏功能:
-
生成玩家和敌人
-
编写玩家和敌人的行为
首先,我们将讨论如何使我们的玩家和敌人出现在应用程序中,特别是在现实世界的位置,然后我们将使它们移动并相互射击,以创建指定的游戏机制。让我们从生成开始。
生成玩家和敌人
让我们从玩家开始,因为这是最容易处理的:我们将创建一个带有我们希望玩家拥有的图形的预制体(在我的情况下,只是一个立方体),一个带有0.05
,0.05
,0.05
的Rigidbody
。由于原始立方体的大小为 1 米,这意味着我的玩家将是5x5x5厘米。您的玩家预制体应如下所示:
图 22.33 – 起始“玩家”预制体
敌人将需要更多的工作,如下所示:
-
创建一个名为
Spawner
的预制体,其中包含您希望生成器具有的图形(在我的情况下是一个圆柱体)和其真实大小。 -
添加一个自定义脚本,每隔几秒生成一个预制体,如下截图所示。
-
您将注意到使用
Physics.IgnoreCollision
来防止生成器与Spawner
对象发生碰撞,获取两个对象的碰撞体并将它们传递给函数。您也可以使用层碰撞矩阵来防止碰撞,就像我们在本书的主要项目中所做的那样,如果您愿意的话:
图 22.34 – 生成器脚本
-
创建一个带有所需图形(在我的情况下是一个胶囊体)和一个勾选了Is Kinematic复选框的
Rigidbody
组件的Enemy
预制体。这样,敌人将移动但不受物理影响。记得考虑敌人的真实大小。 -
将生成器的Prefab属性设置为在所需的时间频率生成我们的敌人:
图 22.35 – 配置生成器
- 在AR Session Origin对象中添加一个新的
SpawnerPlacer
自定义脚本,使用 AR 射线系统在玩家点击的地方实例化一个预制体,如下截图所示:
图 22.36 – 放置生成器
- 设置
SpawnerPlacer
的预制体,以便生成我们之前创建的生成器预制体。
这就是第一部分的全部内容。如果您现在测试游戏,您将能够点击应用程序中检测到的平面,并看到生成器开始创建敌人。您还可以查看目标图像,看到我们的立方体玩家出现。
现在我们在场景中有了这些对象,让我们让它们做一些更有趣的事情,从敌人开始。
编写玩家和敌人的行为
敌人必须朝着玩家移动以射击他们,因此它需要访问玩家的位置。由于敌人是实例化的,我们无法将玩家引用拖到预制体上。然而,玩家也已经被实例化,所以我们可以向玩家添加一个使用单例模式的PlayerManager
脚本(就像我们在管理器中所做的那样)。要做到这一点,请按照以下步骤进行:
- 创建一个类似于下图所示的
PlayerManager
脚本,并将其添加到玩家:
图 22.37 – 创建 PlayerManager 脚本
- 现在敌人已经有了对玩家的引用,让我们通过添加一个
LookAtPlayer
脚本使它们朝向玩家,如下所示:
图 22.38 – 创建 LookAtPlayer 脚本
- 此外,添加一个简单的
MoveForward
脚本,如下面截图中所示的脚本,使LookAtPlayer
脚本使敌人面向玩家,这个沿 z 轴移动的脚本就足够了:
图 22.39 – 创建 MoveForward 脚本
现在,我们将处理玩家的移动。记住,我们的玩家是通过移动图像来控制的,所以这里实际上是指旋转,因为玩家需要自动瞄准并射击最近的敌人。要做到这一点,请按照以下步骤进行:
-
创建一个
Enemy
脚本并将其添加到Enemy预制件中。 -
创建一个像下面截图中所示的
EnemyManager
脚本,并将其添加到场景中的一个空的EnemyManager
对象中:
图 22.40 – 创建 EnemyManager 脚本
- 在
Enemy
脚本中,确保在EnemyManager
中注册对象,就像我们之前在本书的主项目中使用WavesManager
一样:
图 22.41 – 创建 Enemy 脚本
- 创建一个像下面截图中所示的
LookAtNearestEnemy
脚本,并将其添加到Player预制件中,使其朝向最近的敌人:
图 22.42 – 瞄准最近的敌人
现在,我们的对象旋转和移动如预期般进行,唯一缺少的是射击和造成伤害:
- 创建一个像下面截图中所示的
Life
脚本,并将其添加到Life
中,而不需要每帧检查生命是否已经降至零。我们创建了一个Damage
函数来检查是否造成了伤害(执行了Damage
函数),但本书项目的另一个版本也可以工作:
图 22.43 – 创建 Life 组件
-
创建一个带有所需图形的
Bullet
预制件,带有Is Kinematic选中的Rigidbody
组件的碰撞体(一个运动学触发碰撞体),以及适当的真实尺寸。 -
将
MoveForward
脚本添加到Bullet预制件中使其移动。记得设置速度。 -
将
Spawner
脚本添加到Player和Enemy组件中,并将Bullet预制件设置为要生成的预制件,以及所需的生成频率。 -
向Bullet预制件添加一个像下面截图中所示的
Damager
脚本,使子弹对其触及的物体造成伤害。记得设置伤害:
图 22.44 – 创建 Damager 脚本 – 第一部分
- 向
Destroy
时间添加一个像下面截图中所示的AutoDestroy
脚本:
图 22.45 – 创建 Damager 脚本 – 第二部分
就是这样!正如你所看到的,我们基本上使用了几乎与主游戏中使用的相同的脚本来创建了一个新的游戏,主要是因为我们设计它们是通用的(而且游戏类型几乎相同)。当然,这个项目还有很大的改进空间,但我们已经有了一个很好的基础项目,可以在此基础上创建令人惊叹的 AR 应用程序。
总结
在本章中,我们介绍了 AR Foundation Unity 框架,探讨了如何设置它,以及如何实现几个跟踪功能,以便我们可以将虚拟对象放置在现实对象之上。我们还讨论了如何构建我们的项目,使其可以在 iOS 和 Android 平台上运行,这是我们在撰写时测试我们的 AR 应用程序的唯一方法。最后,我们创建了一个简单的 AR 游戏,基于我们在主项目中创建的游戏,但修改了它,使其适用于 AR 场景的使用。
有了这些新知识,您将能够开始作为 AR 应用程序开发人员的道路,通过检测真实对象的位置,创建可以用虚拟对象增强真实对象的应用程序。这可以应用于游戏、培训应用程序和模拟。您甚至可能能够找到新的使用领域,因此利用这项新技术及其新的可能性!