第三章 Combining Steering Behavior

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/asd77882566/article/details/77104790

3.4 结合转向行为(Combining Steering Behavior)

独立的转向行为可以实现一个很成熟的移动效果,很多游戏中转向行为仅仅是由朝向给定位置移动的寻找(Seek)行为组成。

更高等级的决策工具负责决定角色朝哪里移动。这通常是一个寻路算法,在达到最终目标之前产生路径的中间目标。

然而一个移动的角色常常需要超过一个的转向行为,它需要抵达目标,避免和其它角色的碰撞,安全的移动并且避免撞上墙壁。墙和障碍物避免和其他转向行为一起起作用时通常实现上会很困难。另外,一些复杂的操作,例如群集和编队,只能够由多个转向行为同时起作用才能完成。

接下来介绍的是一些复杂的组合操作:从简单的混合转向行为的输出到明确支持碰撞避免的复杂管线结构设计(from simple blending of steering outputs to complicated pipeline architectures designed explicitly to support collision avoidance)。

混合和仲裁(Blending And Arbitration)

混合将各个转向行为的结果通过权重或者优先级结合获得最终输出,仲裁选中一个或者多个转向行为来完全控制角色。混合的权重或优先级可能会随着时间或者角色内部状态改变。

有些转向系统也需要结合混合和仲裁两种方法。

权重混合

最简单的组合各个操作行为的输出的方式是通过权重对输出相加。

作为一个参数化的系统,各个权重值的选择会是一个有创造力的猜测或者试错的过程。已经有一些研究来尝试来通过神经网络等算法生成这些转向操作权重值,但是并没有很好的结果。手动试验似乎仍然是一个最明智的方法。

实现代码如下:

class BlendingSteering:
    struct BlendingAndWeight:
        Behavior
        weight

    # 当前使用的所有BlendingAndWeight实例
    behaviors

    maxAcceleration
    maxRotation

    def getSteering():
        steering = new Steering()

        for behavior in behaviors:
            steering += behavior.weight * behavior.behavior.getSteering()

        # 应该视具体情况吧,或者使用min()会比较符合
        steering.linear = max(steering.linear, maxAcceleration)
        steering.angular = max(steering.angular, maxRotation)

        return steering

集群和蜂拥(Flocking And Swarming)

集群算法基于混合三种简单的转向行为:远离太靠近的角色(分离),和集群保持一样的朝向和速度(排列和速度匹配),朝群体中心靠近(聚合)。聚合转向行为通过计算群体重心作为目标来使用抵达(Arrive)行为移动。

对于简单的群集行为,使用相等的权重可能就会有效果。不过,相对来说分离行为比聚合行为更重要,接着是排列,后两个重要性也可能会有交换。

flockingbehaviors

在大部分的集群行为实现中是不考虑很远处的对象。其中的每一个转向行为都会有一组邻居需要考虑。分离仅仅躲避靠近的对象,聚合和排列也是基于附近的对象的位置,朝向和速度来计算输出。其他对象是否是邻居是通过检测是否在角色某个半径的范围内判定,或者是是否在前方的扇形范围内,如果所示:

neighborofboid

问题

混合转向行为在真实的游戏中会有几个很重大的问题,使用这种行为的角色在房间内部或者城市里的环境中可能会发生卡住的情况,并且很难调试。

这些问题中的其中一部分可以通过之后介绍的仲裁转向系统来解决。

稳定平衡/僵持(Stable Equilibria)

混合行为在两个转向行为做完全冲突(相反)的事情时会产生问题,导致角色什么都不做,被限制在这个平衡中。如下图角色尝试抵达目标同时又要躲避敌人:

anunstableequilibrium

稳定平衡有一个吸引盆地(a basin of attraction):角色在这个区域中将掉入僵持状态中。如果这个盆地很大,角色受限制的机会就会很大,如下图所示中走廊里吸引的盆地会有一个不限距离的大小:

aStableEquilbibrium

吸引的盆地不仅被定义成一系列的位置,也可能是角色特定的速度或者特定的旋转角度,由于这种原因会很难设想和调试。

约束的环境(Constrined Environments)

单独的或者混合的转向行为在又很少约束的环境中工作的很好,在3d空间中移动就有很少的限制。然而大多数游戏是在2d世界中。室内环境,跑道和编队行为极大地增加了角色移动的约束。

如下图展示了一个具有追逐转向行为的角色的路径,如果只有追逐行为的话就会撞到墙上,但是增加了墙壁躲避则使角色远离了正确的追逐敌人的路径:

cannotavoidobstacleandchase

这种问题常见于角色尝试在一个尖锐的角移动转过狭窄的门,如下图所示,障碍躲避行为使角色远离门,错过他想执行的路径。

missingnarrowdoorway

短视(Nearsightedness)

转向行为表现的很局部(locally).他们仅仅根据当前这一瞬间的环境做决定。作为人类,我们产生我们行为的结果并且会考虑这个结果是否值得。基础转向行为不能够这么做,所以它们经常做出错误的行为来实现目标。

下图展示了一个角色使用一个标准的障碍躲避技术避免墙壁碰撞,角色的移动将跑到错误的一边并处于角落里,他将永远不会追到敌人,并且也永远意识不到这一点。

longdistancefailureinsteeringbehavior

转向行为无法解决这些问题,任何不做预测的行为都将被它们眼界内的问题所挫败。唯一的解决办法是将寻路加入转向系统。下边部分将介绍这个整合。寻路将在下章介绍。

优先级(Priorities)

有很多的转向行为在一些条件下会需要一个加速度,不像探索和躲避,总是提供一个加速度,碰撞避免,分离和抵达在一些情况下不会建议一个加速度。

但是当这些行为建议一个加速度的时候,无视他会很不明智,例如碰撞避免行为,应该马上关注到来防止撞到其他角色。

当转向行为和其他行为混合,他们的加速度请求将被其他行为的请求所冲淡。一个探寻行为,总是提出一个最大的加速度,如果和碰撞避免行为混合,那么碰撞避免对角色移动的影响永远超过不了50%,这可能就会造成问题。

根据不同行为的重要程度,设定不同的优先级,来决定角色使用哪一个行为移动。以下是实现代码:

Class PrioritySteering:
    # 根据优先级由高到低排列的一系列转向行为实例
    groups
    # 行为提供的加速度或角速度的长度超过这个值,这个行为就会被使用
    epsilon

    def getSteering():
        for group in groups:

            # 这个group也可能是一组行为,在这一组行为中使用混合方式获得相加的结果
            steering = group.getSteering()

            if steering.linear.length() > epsilon or
                abs(steering.angular) > epsilon:
                return steering

        # 如果没有超过这个值的行为,我们选择优先级最低的行为(return the small acceleration from the final group)
        return steering

平衡回退(Equilibria Fallback)

基于优先级方式的一个显著地特点是可以对抗平衡僵持情况。如果一组行为处于平衡当中,他们总的加速度接近为0,在这种情况下算法将给一些最低优先级(例如巡逻)行为,打破这种平衡回到一个之前的行为。如下图所示:

prioritysteeringavoidunstableequilibrium

弱点

使用最低优先级行为的方式也许能够慢慢离开这种平衡状态的区域,但是他不能够避免非常大的稳定平衡,可能会在回到先前状态后,又重新进入这个区域,进入稳定平衡状态。

可变优先级

上边的算法给不同行为给予了一个固定的优先级。然而在一些情况下,我们需要更多的控制。例如当碰撞躲避并不是迫在眉睫时,需要处于一个较低的优先级。我们可以指定每一个行为返回一个动态的优先级,我们根据这些优先级来重新排序来决定使用哪一个行为的结果。

不考虑提供的方法偶尔会卡住角色的情况,这种方法只有较小的优点。另外每个行为产生动态优先级并重新排序也要花费时间。虽然这是一个明显的扩展,不过如果你要朝这个方向完善,也许升级成一个完整的合作仲裁系统会更好。

合作仲裁(Cooperative Arbitration)

到目前为止我们关注以一种各自独立的方式组合转向行为,每一个转向行为只知道他自己并且总是返回同样的答案,我们把其中一个或者多个结果混合起来。这种方式的优点是每一个独立的转向行为都很简单并且容易替换。它们可以被单独测试。

但是正如我们看到的,这个方法也有一些重要的弱点。

目前结合转向行为的算法实现有一个朝向算法复杂度增加的趋势。一个核心的特点是趋向于在不同的行为之间组合。

例如,一个角色使用追击行为在追逐目标,同时还要躲避墙体碰撞。下图展示了可能的解决方法。碰撞的发生迫在眉睫需要马上躲避:

imminentcollisionduringpursuit

躲避行为产生一个躲避的加速度来远离墙壁。应为碰撞马上就要发生,所以它具有优先权,角色在加速离开。

角色总的运动效果如上图,他很明显的减慢了速度因为躲避行为仅仅提供了一个切向的加速度。

这种情况可以通过混合追击和障碍躲避行为(虽然,简单的混合可能增加其他移动问题例如僵持)来缓解。甚至这种情况仍然由于追击的加速度效果被冲淡变现的很明显(的虚假)。

为了获取一个可信的行为,我们想要障碍躲避行为能够关注追逐行为想要实现什么。下图展示了同一种情况的一个版本。障碍躲避行为对环境敏感(Context Sensitive),他理解追逐行为想要去哪,返回一个都考虑到的加速度:

context-sensitivewallavoidance

明显的,关注环境的方式增加了转向行为的复杂度。我们不能够简单的构造独立的块来只做她们自己的事情。

很多合作仲裁技术的实现是基于第五章的决策制定,它有效果。我们可以有效地制定去哪里和如何去的决策。决策树,状态机,和黑板结构在已经被使用来操控转向行为。比如黑板结构很适合转向行为的协作,每一个行为在做自己决定时都能够(从黑板中)读取其他行为想要做什么。

至今在游戏中并没有一个清晰的方式变成一个标准。组合转向行为仍是一个困扰很多开发者的技术区。然而纵然缺乏共识,还是值得探究一个深入的例子,所以我们介绍一个转向管线算法,一个不适用第五章决策制定方式的专用方法。

转向管线(Steering Pipeline)

这种转向管线方式是由Marcin Chady首创的,处于一个简单的混合或者优先级转向行为和一个完全实现的移动规划方式(第四章介绍)之间。这是一个在很多有问题的情况下表现的很好的合作仲裁方式,包括窄的通道和使用寻路整合转向。到目前为止这个技术只有很少开发者使用。

在继续看之前请记得这只是其中的一种仲裁例子,并不是说这是唯一的实现方式。

下图展示了转向管线的大致结构:

steeringpipeline

在这个管线中有四个阶段:目标者(targeters)解决移动的目标是哪里,分解者(decomposers)将主要目标分成一系列子目标,约束者(constraints)限制角色实现目标的方式,执行者(actuator)限制角色物理移动的能力。

在最后阶段(In all but the final stage),有一个或者多个组件。管线中的每一个组件都有不同的工作,所有都是转向行为,他们合作的方式取决于所处的阶段。

目标者(Targeters)

目标者为角色产生一个最高级的目标。可能有好几个目的:一个位置目的,朝向目的,速度目的,转速目的,我们称这里的每一个元素为目标的一个频道(例如位置频道,速度频道)。所有的目的可以包含任一个或者多个频道。未指定的目的则不需关心。

每一个频道都可以被不同的行为产生(追逐行为可能产生位置频道,朝向目标产生一个转向频道),或者一个目标者产生多个频道。当多个目标者被使用时,每一个频道只能产生一个目标值。不需要考虑避免后边的目标者重写频道的情况。

最好的情况(to the greastest extent possible),转向系统尝试填满所有频道,纵然一部分目标可能很难同时被实现。我们在执行阶段在考虑这种情况。

选择一个单独的目标来进行转向行为初看时会觉得很怪异。逃离行为或者躲避行为有躲避的目标,而不是探索(not to seek)。管线强制你根据角色的目的来考虑,如果目的是逃离,那么目标者需要选择跑到哪。目的可能会随着追逐敌人一直改变,但是一直会有一个目的。其他的“逃离”行为,像躲避障碍物不会成为一个转向管线的目的,他是角色在移动过程中在约束阶段发现的一个约束。

分解者(Decomposers)

分解者将总目标分解成更容易实现的子目标。

目标者在游戏过程中产生一个目的。一个分解者会检查这个目的,看它是否能够被直接实现,并且计划一个完整的路线(例如使用一个寻路算法),他返回这个计划的第一步作为一个子目标。这是最常见使用的分解者:将寻路无缝的加入转向管道中。

因为顺序被严格的执行,所以我们可以很有效的使用分层分解者。上层的分解者提供一些大体的分解,例如他们可以被实现做一个粗糙的寻路。返回的子目标对于角色来说仍然会很远,之后下层的分解者对这些子目标进行分解,他们不需要考虑大方向。这个方法和我们之后看到的分成寻路相似。实用转向管线,我们不需要分层寻路引擎,

约束者(Constraints)

约束者限制角色实现目标或者子目标的能力。他们查明当前子目标是否违反限制的规则,如果违反的话,他们提出一个避免的方式。约束者趋向于代表障碍:移动的障碍例如其他角色或者静态的障碍如墙壁。

约束者被用来和执行者联系起来,下面介绍。执行者根据角色当前的子目标计算出要移动的路径。每一个约束者被允许回顾这条路径并决定是否明智。如果这个路径违反了限制,那么约束者将提出新路径并查看这个是否能够工作等等,直到发现一个可用的路径。

需要知道的是约束者只提供子目标已确定的频道。下图展示了一个即将到来的碰撞。碰撞避免约束将产生一个可选择的子目标 ,让角色侧转开障碍。

collisionavoidanceconstraints

当然,解决一个限制可能违反其他限制,所以算法需要来回循环来发现一个每个限制这都高兴地妥协方案。这并不总是有可能,转向系统就需要放弃尝试避免一个死循环。转向管线包换一个特殊的转向行为,死锁,作为这种情况的控制行为。他可能被实现成一个简单的巡逻行为来使角色闲逛走出困境。对于一个完整的解决方法,可能会调用一个综合的移动规划算法。

这个转向管线倾向于提供一个可信的轻量的转向行为,这样它可以被用来模拟大量的角色。我们可以使用完整的规划系统替换当前的约束来满足算法,并且管线将能够解决任意的移动问题。但是,我们发现最好是保持简单,在主要的情况里,额外的复杂度都是不需要的,基础算法就能工作很好。

执行者(The Actuator)

不想管线的其他阶段,每一个角色只有一个执行者。执行者的目标是决定如何完成当前的子目标。执行者同时决定子目标的哪一个频道具有优先权哪一些需要被无视。

对于简单的角色,例如一个移动的哨兵或者漂浮的鬼魂,路径会很简单:朝向目标。执行者通常不需要考虑速度和旋转频道,之确保角色朝向目标。

如果一个角色更加关注速度,目标是以一个特定的速度抵达目标,我们可能会选择侧向绕目标然后进行加速,如下图:

takingarunup

更加受限制的角色,像是ai控制的车:车在固定不动的时候无法转向,他不能移动到别的方向而只能够往当前朝向移动 ,并且轮胎有一个受限制的最大转向速度。这个结果路径可能会更加复杂,他可能需要无视一些主要的频道,例如如果子目标想要我们在一个不同的朝向实现一个特定的速度,这时我们会知道这么目标是不可能的。因此我们可能提出一个朝向频道。

我们将假设执行者根据一个目标,返回一个角色将执行的一个路径的描述。最终,我们想要执行者执行转向。执行着最终的工作是返回力和扭矩(或者其他动力控制)来实现预测的路径。

转向管线实现代码如下:

class SteeringPipeline:
    # 管线的组件列表
    targeters
    decomposers
    contraints
    actuator

    # 算法发现一个未约束路径尝试的次数
    contraintSteps
    # 死锁时的转向行为
    deadlock
    # 角色当前动力学信息
    kinematic

    def getSteering():
        # 首先获取最高等级的目的
        goal
        for targeter in targeters:
            goal.updateChannels(targeter.getGoal(kinematic))

        # 分解目的
        for decomposer in decomposers:
            goal = decomposer.decompose(kinematic, goal)

        # 循环执行者和限制者
        validPath = false
        for i in 0.. constraintSteps:
            # 从执行着中获取路径
            path = actuator.getPath(kinematic, goal)

            # 检查是否违反约束规则
            for constraint in constraints:
                # 如果违反,返回一个建议
                if constraint.isViolated(path):
                    goal = constraint.suggest(path, kinematic, goal)

                    # 返回上一级重新获取路径
                    break continue

            # 到这里说明我们发现了一个路径
            return actuator.output(path, kinematic, goal)

        # 死锁
        return deadlock.getSteering()

class Targeter:
    def getGoal(kinematic)

class Decomposer:
    def decompose(kinematic, goal)

class Constraint:
    def willViolate(path)
    def suggest(path, kinematic, goal)

class Actuator:
    def getPath(kinematic, goal)
    def output(path, kinematic, goal)

struct Goal:
    # 是否使用对应频道的flag
    hasPosition, hasOrientation, hasVelocity, hasRotation
    # 每一个频道的数据
    position, orientation, velocity, rotation

    def updateChannels(o):
        if o.hasPosition: position = o.position
        if o.hasOrientation: orientation = o.orientation
        if o.hasVelocity: velocity = o.velocity
        if o.hasRotation: rotation = 0.rotation

Paths

路径的格式不影响算法实现,他只是在转向组件中进行传递。

我们可以使用两种路径实现来驱动这个算法:一种是寻路风格的路径,由一系列的线段组成,给予点对点的移动信息,这对于旋转很快速的角色很适合,例如人类走路,点对点的路径生成的很快,他们也可以很快的检测是否违反约束,可以被执行者很快的转化成力;一种是产品版本,有一系列的策略组成,例如加速度等,他们适合大部分复杂的转向请求,包括赛车驱动,专项算法最终的测试,这种更难监测约束,因为他们涉及弧形路径。

再决定使用策略队列方式前值得考虑一下直线路径方式。

本书源码的实现方式:

class Pathkinematic
    goal

示例组件

一下是转向管线中各个组件的示例:

class ChaseTargeter(Targeter):
    # 被追逐的角色
    chasedCharacter
    # 预测时间
    lookahead

    def getSteering():
        goal = Goal()
        goal.position = chasedCharacter.position + chasedCharacter.velocity * lookahead
        goal.hasPosition = true

        return goal

class Decomposer:
    # 路径图
    graph
    heuristic

    def decompose(kinematic, goal):
        # 在路径图中量化我们当前位置和我们的目的
        start = graph.getNode(kinematic.position)
        end = graph.getNode(goal.position)

        if startNode == endNode: return goal

        path = pathfindAStar(graph, start, end, heuristatic)

        firstNode = path[0].to_node
        position = graph.getPosition(firstNode)

        goal.position = position
        return goal

class AvoidObstacleContraint(Constraint):
    # 障碍物的坐标和半径
    center, radius
    # 躲避时距离障碍物的间隔,是障碍物半径的倍数,值超过1
    margin

    # 如果违反约束,存储出问题的路径点索引
    problemIndex

    def willViolate(Path):
        for i in 0 .. len(path):
            segment = path[i]
            if distancePointToSegment(center, segment) < radius:
                problenIndex = i
                return true

            return false

    def suggest(path, kinematic, goal):
        # 找到线段中距离圆心最近的点
        closest = closestPointOnSegment(segment, center)

        # 判断是否穿过圆心
        if closest.length() == 0:
            dirn = segent.end - segment.start
            newDirn = dirn.anyVectorAtRightAngles()

            newPt = center + newDirn * radius * margin
        else:
            newPt = center + (closest - center).normalize() * radius * margin

        goal.position = newPt
        return goal

# 原书代码
class BasicActuator(Actuator):
    seek

    def createPathObject()
    def getPath(path, goal)
    def getSteering(SteeringOutput, path)

代码中障碍限制的建议实现原理制如下图示:

ObstacleAvoidanceContraint

结论

转向管线是一种组合仲裁的实现技术,不像决策树,黑板结构,它是专门为转向需求设计的。

另一方面,它不是最有效果的技术。他可能会在一些情况下运行很快,也可能在一些更加复杂的情况里变慢。如果你决定要是你的角色移动得更加智能,那么你将需要花费更多代价在执行速度上(事实上,为了保证你的目的,你将需要完全的运动计划(full motion planning),他的速度比转向管线更慢)。在很多游戏中,愚笨的转向并不是主要问题,使用一些更加简单的组合转向行为会更加容易,例如混合。


参考书籍和网址:

有一本和本书结构非常类似的unity3d版本的ai书籍,名字叫:《Unity 5.x Game AI Programming Cookbook》,只有不到300页。

一篇网络上关于本书第三章movement的英文笔记:https://web.cs.ship.edu/~djmoon/gaming/gaming-notes/ai-movement.pdf

本节内容unity3d(版本5.6.0f3)实现demo下载在此,表现效果并不是太理想。

猜你喜欢

转载自blog.csdn.net/asd77882566/article/details/77104790