游戏 AI - 12

转向行为III

Posted by Kang Cai on September 28, 2020

我们已经介绍了多个基本构建块行为:“寻找” 和 “逃离”,“到达”,和“对齐”。

接下来要介绍的所有行为都有相同的基本结构:它们计算一个目标,或者一个位置或方向(它们可以使用速度,但我们将不涉及这些),然后它们委托给其他行为之一来计算转向。目标计算可以基于许多输入。例如,“追赶” 根据另一个目标的运动为 “寻找” 计算目标;碰撞避免基于近似障碍的方法创造一个目标来逃离;而 “漫游” 则创造了它自己的目标,在它移动的过程中蜿蜒而行。

事实上,“寻找”、“对齐”和“速度匹配”是唯一的基本行为(通过类比,有一个旋转匹配行为,但我从未见过它的应用程序)。正如我们在前面的算法中看到的,“到达” 可以分为 “创建(速度)目标” 和应用 “速度匹配” 算法。下面的许多委托行为可以反过来作为另一个委托行为的基础,“到达” 算法可以作为 “追赶” 的基础,而 “追赶” 算法又可以作为其他算法的基础,等等。

在下面的代码中,我将使用多态风格的编程来捕获这些依赖关系。您也可以使用委托,让原语算法由新技术调用。这两种方法都有各自的问题。在我们的例子中,当一个行为扩展另一个行为时,通常是通过计算替代目标来实现的。使用继承意味着我们需要能够更改超类工作的目标。

如果我们使用委托方法,我们需要确保每个委托行为都有正确的角色数据、maxAcceleration和其他参数。这需要使用子类删除的复制和数据复制。

一、追逐 和 逃避

到目前为止,我们只根据位置移动。如果我们追逐一个正在移动的目标,那么不断地向它当前的位置移动是不够的,因为会出现我们到达它现在的位置时,它已经移动到别处的情况,这也是本节中“追逐”与前面文章的主要区别。事实上,追逐一个正在移动的目标,当目标离我们很近,并且我们在每一帧都要重新考虑它的位置时,这并不是什么大问题,我们最终会到达那里的;但是如果角色离目标很远,它就会朝着一个明显错误的方向出发,如图3.11所示。

我们需要预测它在未来某个时候的位置,并朝着那个方向努力,而不是瞄准它当前的位置。我们小时候玩捉迷藏的游戏,很自然为什么最难抓住的标签玩家是那些不断改变方向,破坏我们预测的人。

我们可以使用各种算法来进行预测,但大多数都是过犹不及的。针对被追逐角色的最佳预测和最佳策略已经进行了各种研究(例如,躲避来袭导弹是军事研究中一个活跃的话题)。克雷格·雷诺兹最初的方法简单得多:我们假设目标将继续以相同的速度移动,它目前已经。在短距离中这是一个合理的假设,即使是在较长的距离中,这也不会显得太愚蠢。

该算法计算出角色与目标之间的距离,并计算出以最大速度到达目标需要多长时间。它使用这个时间间隔作为它的预测前向。如果目标继续以当前速度移动,它会计算目标的位置。然后,这个新位置被用作标准寻求行为的目标。

如果人物移动缓慢,或者目标距离很远,预测时间可能会非常长。目标不太可能永远沿着同一条道路走下去,所以我们要为我们的目标设定一个限度。因此,该算法有一个最大时间参数。如果预测时间超过这个值,则使用最大时间。

图3.12所示为追求同一目标的寻求行为和追求行为。追求行为在追求中更有效。

1.1 伪代码

“追逐”(pursue)行为源于 “寻找”(seek),计算一个代理目标,然后委托 seek 执行转向计算:

# Work out the distance to target.
direction = target.position - character.position
distance = direction.length()

# Work out our current speed.
speed = character.velocity.length()

# Check if speed gives a reasonable prediction time.
if speed <= distance / maxPrediction:
    prediction = maxPrediction
# Otherwise calculate the prediction time.
else:
    prediction = distance / speed

# Put the target together.
Seek.target = explicitTarget
Seek.target.position += target.velocity * prediction

# 2. Delegate to seek.
return Seek.getSteering()

1.2 性能

时间和空间复杂度仍都是 O(1)。

1.3 规避

追逐的反面行为是规避,在代码中将继承于 Seek 改成 Flee,最终返回值也从 Seek.getSteering 变为 Flee.getSteering

二、面向

“面向” 的行为使一个角色看着它的目标,它委托 “对齐” 行为来执行旋转,但首先计算目标方向。

目标方向是由目标和角色的相对位置产生的。这和我们在 getOrientation 函数中用于运动的过程是一样的。

2.1 伪代码

实现十分简单:

三、朝你要去的地方看

到目前为止,我已经假设了一个角色面对的方向不一定是它的运动方向。然而,在很多情况下,我们希望角色朝着它正在移动的方向。在运动学运动算法中,我们直接对其进行设置。使用对齐行为,我们可以给字符角加速度,使其面向正确的方向。通过这种方式,角色的面会逐渐改变,这样看起来会更自然,尤其是在空中车辆,如直升机或气垫船或人类角色,可以横向移动(提供回避动画可用)。

这个过程类似于上面的面部行为。目标方向是通过字符的当前速度来计算的。如果没有速度,则目标方向设置为当前方向。在这种情况下,我们对任何方向都没有偏好。

3.1 伪代码

实现:

3.2 注意事项

在这种情况下,我们不需要另一个目标成员变量。没有总体目标;我们从头开始创建当前目标。所以,我们可以简单地使用 “对齐”。针对计算目标的目标(与我们对追求和其他派生算法所做的相同方法)。

3.3 性能

时间和空间复杂度仍都是 O(1)。

四、漫游

漫游行为控制一个角色漫无目的地移动。

当我们观察运动学漂移行为时,我们在每次运行时随机扰动漂移方向。这使得角色平稳地向前移动,但是角色的旋转是不稳定的,在移动的过程中出现了从一边到另一边的抽搐。

最简单的漫游方法是在一个随机的方向上移动。这是不可接受的,因此几乎从未使用过,因为它产生线性抖动。运动学版本增加了一层间接,但产生了旋转抖动。我们可以通过添加一个额外的层来平滑这个抖动,使字符的方向间接地依赖于随机数生成器。

我们可以把运动漫游看作是一种委托寻找行为。在字符周围有一个圆圈,目标被限制在上面。每次运行该行为时,我们都会随机地将目标在圆周上移动一点。然后角色寻找目标。图3.13演示了这个配置。

我们可以通过移动目标受到约束的圆来改善这一点。如果我们将它移到字符前面(其中front由它当前面对的方向决定)并将其缩小,我们将得到图3.14所示的情况。

字符在每一帧中尝试面对目标,使用面部行为来对齐目标。然后它添加了一个额外的步骤:在当前方向上应用完全加速度。

我们还可以通过让它寻找目标并执行一个看你要去哪里的行为来纠正它的方向来实现行为。

在这两种情况下,字符的方向在调用之间保持不变(从而平滑方向上的变化)。圆圈的边缘与角色的对应角度决定了它的旋转速度。如果目标在这些极端点之一,它将迅速转向。目标会在圆圈的边缘抽搐和抖动,但是角色的方向会平稳地改变。

这种徘徊行为会使角色转向(或转向)。从角色的角度来看,目标会花更多的时间去接近圆圈的边缘。

4.1 伪代码

4.2 性能

时间和空间复杂度仍都是 O(1)。