Steering Behaviors

本文详细介绍了游戏开发中的转向行为,包括基本移动算法、动力学移动算法、各种转向行为及其实现代码,如追寻、逃离、抵达、排列等,并探讨了碰撞避免、障碍物躲避等高级主题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

三.移动

3.1 基本移动算法

静态的(Statics)

存储移动信息的数据结构如下:

struct Static:
    position            # a 2D vector
    orientation         # a single point value

动力学(Kenematics)

存储移动信息的数据结构如下:

struct Kinematic:
    position            # a 2 or 3D vector
    orientation     # a single floating point value
    velocity            # another 2 or 3D vector
    rotation            # a single floating point value

Steering behaviors使用动力学的数据结构,返回加速度和角速度:

struct SteeringOutput:
    linear              # a 2 or 3D vector
    angular             # a single floating point value

如果游戏里边有物理层那么它负责来更新角色位置和方向,如果需要自己手动更新的话,可以使用如下更新算法:

struct Kinematic:
    # other Member data before...
    def update(steering, time):
        # Update the posiiton and orientation
        position += velocity * time + 0.5 * steering.linear * time * time
        orientation += rotation * time + 0.5 * steering.angular * time * time

        # and the velocity and rotation
        velocity += steering.linear * time
        # 原代码:orientation += steering.angular * time
        rotation += steering.angular * time

然而当游戏以很高频率运行时,每次更新位置和方向的加速度变化非常的小,可以使用一种更常用的比较粗糙的更新算法:

struct Kinematic:
    # other Member data before...
    def update(steering, time):
        # Update the posiiton and orientation
        position += velocity * time
        orientation += rotation * time

        # and the velocity and rotation
        velocity += steering.linear * time
        # 原代码:orientation += steering.angular * time
        rotation += steering.angular * time

在真实世界中我们通过施加力来驱动物体,而不能直接给予物体加速度。这也是一般游戏物理层给予物理对象的接口。

3.2 动力学移动算法(Kinematic Movement Algorithms)

动力学移动算法使用静态数据(位置和方向,没有速度)输出一个期望的速度,输出通常是根据当前到目标的方向,以全速移动或者静止。

动力学中的方向

很多游戏只是简单的以当前移动方向作为对象的方向(朝向),如果对象静止,则保持之前方向,获取方向实现如下:

def getNewOrientation(currentOriention, velocity):
    if velocity.length() > 0:
        # 源代码是 return atans(-static.x, static.z)
        return atans(-velocity.x, velocity.z)
    else:
        return currentOriention

寻找(Seek)

以最大速度匀速朝一个目标移动,最终在目标附近来回穿插或者静止不动(恰好到目标位置):
算法如下:

struct KinematicSteeringOutput:
    velocity
    rotation        # 应为朝向依据速度,所以该信息无用

class KinematicSeek:
    # 存储当前控制的对象和目标对象
    character
    target

    # 存储当前对象移动的最大速度
    maxSpeed

    def getSteering():
        steering = new KinematicSteeringOutput()

        # 计算移动速度
        steering.velocity = target.position - character.position
        steering.velocity.normalize()
        steering.velocity *= maxSpeed

        # 我觉得也可以利用上rotation, steering.rotation = getNewOrientation(character.orientation, steering.velocity)
        character.orientation = getNewOrientation(character.orientation, steering.velocity)

        steering.rotation = 0
        return steering

逃离(Flee)

逃离和寻找唯一区别是移动方向相反,差异代码如下:

steering.velocity = character.position - target.position

抵达(Arriving)

抵达目的是进入目标的一个范围之内,在范围之外和寻找一样朝目标移动,进入一定距离后逐渐减速(由timeToTarget和maxSpeed决定何时减速),进入目标的半径范围内静止。实现代码如下:

class KinematicArrive:
    character
    target

    maxSpeed
    # 进入目标该半径之内不再移动
    radius
    # 如果radius为0时,从减速到停止共花费时间timeToTarget
    timeToTarget = 0.25

    def getSteering():
        steering = new KinematicSteeringOutput()

        steering.velocity = target.position - character.position
        # 抵达目标半径内,停止移动
        if steering.velocity.length() < radius:
            return None
        # 控制速度
        steering.velocity /= timeToTarget
        if steering.velocity.length() > maxSpeed:
            steering.velocity.normalize()
            steering.velocity *= maxSpeed

        character.orientation = getNewOrientation(character.orientation, steering.velocity)

        steering.rotation = 0
        return steering

巡逻(Wander)

每次随机一个朝向偏移值,改变朝向,并朝这个方向全速移动,(如果每帧都要改变朝向的话,会一直在抖动,效果并不好..)
实现代码如下:

class KinematicWander:
    character

    maxSpeed
    # 旋转偏移随机在[-maxRotation,maxRotation]范围之内
    maxRotation

    # return [-1, 1]
    def randomBinomial():
        return random() - random()

    def getSteering():
        steering = new KinematicSteeringOutput()
        steering.velocity = maxSpeed * character.orientation.asVector()

        steering.rotation = randomBinomial() * maxRotation

        return steering

3.3 转向行为(Steering Behaviors)

寻找(Seek)和逃离(Flee)

转向行为将基于动力学行为增加加速图的支持,对象的位置信息更新代码如下:

struct Kinematic:
    # 其他成员变量

    def update(steering, maxSpeed, time):
        position += velocity * time
        orientation += rotation * time

        velocity += steering.linear * time
        orientation += steering.angular * time

        if velocity.length() > maxSpeed:
            velocity.normalize()
            velocity *= maxSpeed

转向行为寻找和动力学算法中表现得不同的是它将呈螺旋路径的方式靠近目标,而不是直线向目标移动。实现代码如下:

class Seek:
    character
    target
    # 最大加速度
    maxAcceleration

    def getSteering():
        steering = new SteeringOutput()

        steering.linear = target.position - character.position
        steering.linear.normalize();
        # 一直施加最大加速度
        steering.linear *= maxAcceleration;

        steering.angular = 0
        return steering

逃离实现与寻找唯一差别如下:

steering.linear = character.position - target.position

Seek和Flee行为路径

seekandfleePath

抵达(Arrive)

由于寻找行为一直以最大加速度移动所以它不会在目标处停止而是一直在目标附近徘徊,抵达行为与寻找表现差异如下图:

arriveandseekpath

抵达行为实现代码如下:

class Arrive:
    character
    target

    maxAcceleration
    maxSpeed
    # 进入目标该范围算是抵达目标
    targetRadius
    # 开始减速的范围半径
    slowRadius
    # 减速时长
    timeToTarget

    def getSteering():
        steering = new SteeringOutput()

        direction = target.position - character.position
        distance = direction.length()

        # 好像有问题,进入这个范围会匀速出去,并不会停
        if distance < targetRadius
            return None

        if distance > slowRadius:
            targetSpeed = maxSpeed
        else:
            targetSpeed = maxSpeed * distance / slowRadius

        targetVelocity = direction
        targetVelocity.normalize()
        targetVelocity *= targetSpeed

        steering.linear = targetVelocity - character.velocity
        steering.linear /= timeToTarget

        if steering.linear.length() > maxAcceleration:
            steering.linear.normalize()
            steering.linear *= maxAcceleration

        steering.angular = 0
        return steering

排列(Align)

排列使角色的转向保持与目标一致,它不关注角色或者目标的位置或速度,转向将不直接和动力学行为中的速度直接相关。
实现代码如下:

class Align:
    character
    target

    maxAngularAcceleration
    maaxRotation

    # 和目标朝向保持一致的范围
    targetRadius
    # 开始减速的半径
    slowRadius

    timeToTarget = 0.1

    def getSteering():
        steering = new SteeringOutput()

        rotation = target.orientation - character.orientation

        # 角度值转到(-pi, pi)之间
        rotation = mapToRange(rotation)
        # 源代码是:rotationSize = abs(rotationDirection)
        rotationSize = abs(rotation)

        # 已经达到目标
        if rotationSize < targetRadius:
            return None

        # 如果在减速半径之外,全速转动
        if rotationSize >  slowRadius:
            targetRotation = maxRotation
        else:
            targetRotation = maxRotation * rotationSize / slowRadius

        # -1或1
        signDir = rotation / rotationSize
        targetRotation *= signDir

        # 原代码好像有问题, targetRotation是一个差值
        steering.angular = targetRotation - character.rotation
        steering.angular /= timeToTarget

        angularAcceleration = abs(steering.angular)
        if angularAcceleration > maxAngularAcceleration:
            steering.angular /= angularAcceleration
            steering.angular *= maxAngularAcceleration

        steering.linear = 0
        return steering

速度匹配(Velocity Matching)

速度匹配目的是使角色速度与目标一致,可以用来模仿目标的运动,他自身很少使用,更常见于和其他行为组合使用,比如说它是群集行为的一个组成部分。

实现代码如下:

class VelocityMatch:
    character
    target

    maxAcceleration

    timeToTarget

    def getSteering():
        steering = new SteeringOutput()

        steering.linear = target.velocity - character.velocity
        steering.linear /= timeToTarget

        if steering.linear.length() > maxAcceleration:
            steering.linear.normalize()
            steering.linear *= maxAcceleration

        steering.angular = 0
        return steering

委托行为(Delegated Behaviors)

我们之前介绍的一些基本行为,可以衍生创建出更多其他行为。探寻,逃离,抵达和排列这些基础行为可以表现出更多行为,以下介绍的一些代理行为如追逐等就是这种情况,他们同样计算出目标的位置或者朝向,代理一个或者多个其他行为来产生转向输出。

追逐和躲避(Pursue And Evade)

到目前为止我们移动角色仅仅基于位置,如果我们在追逐一个移动的目标,那么只是朝向他当前位置将不再有效。在角色抵达目标当前位置时,目标已经离开了,如果他们距离足够近还好,如果它们之间距离足够远,我们就需要预测目标的未来位置,这就是追组行为。他使用探寻(Seek)这个基础行为,假设目标将以当前速度保持移动进行预测将来位置。

追逐和探寻表现差异如下图:

seekandpursue1

seekandpursue2

实现代码如下:

# 继承自Seek
class Pursue(Seek):
    # 最大预测时间
    maxPrediction

    # 我们要追逐的目标
    target
    # 其他一些继承自父类的信息

    def getSteering():
        direction = target.position - character.position
        distance = direction.length()

        speed = character.velocity.length()

        # 根据当前速度计算预测时间
        if speed <= distance / maxPrediction:
            prediction = maxPrediction
        else:
            prediction = distance / speed

        # 原代码:Seek.target = explicitTarget
        Seek.target = target
        Seek.target.position += target.velocity * prediction

        return Seek.getSteering()

躲避与追逐的唯一区别是躲避使用Flee行为。

面对(Face)

面对行为使角色看向目标,它代理排列行通过计算目标的朝向为来表现旋转。

实现代码如下:

class Face(Align):
    target

    # 其他继承自父类的数据

    def getSteering():
        direction = target.position - character.position

        if direction.length == 0:
            return target

        # 原代码 Align.target = explicitTarget
        Align.target = target
        Align.target.orientation = atan2(-direction.x, direction.z)

        return Align.getSteering()

朝你移动的方向看(Looking Where You’re Going)

以角色当前速度方向作为目标的朝向计算。

实现代码如下:

class LookWhereYouAreGoing(Align):

    def getSteering():
        if character.velocity.length() == 0:
            return

        target.orientation = atan2(-character.velocity.x, character.velocity.z)

        return Align.getSteering()

游荡(Wander)

该游荡行为为解决抽动问题,与原本行为区别:

kinematicwander

fullwander

实现代码:

class Wander(Face):
    # 目标偏移圆的中心偏移点和半径
    wanderOffset
    wanderRadius

    # 最大随机旋转偏移值
    wanderRate
    # 当前游荡对象的朝向
    wanderOrientation


    def getSteering():
        wanderOrientation += randomBinomial() * wanderRate

        targetOrientation = wanderOrientation + character.orientation

        target = character.position + wanderOffset * character.orientation.asVector()

        target += wanderRadius * targetOrientation.asVector()

        # Face.target = target
        steering = Face.getSteering()

        # 角色加速度保持满速
        steering.linear = maxAcceleration * character.orientation.asVector()

        return steering

路径跟随(Path Following)

路径跟随是一个以一整条路径作为目标的转向行为。一个拥有路径跟随行为的角色应该沿着路径的一个方向移动。他也是一个代理行为,根据当前角色的位置和路径来计算出目标的位置,然后使用探索(Seek)行为去移动。

目标位置的计算有两个阶段。第一,将角色位置转换到路径上的一个最近点;第二,在路径上选中一个目标而不是路径上的一个固定距离的映射点(a target is selected which is further along the path than the mapped point by a fixed distance)。

如图所示:

pathfollowingbehavior

predictivepathfollowingbehavior

同时可能出现越过一段路径,如下图:

ingoresomepath

实现代码如下所示:

class FollowPath(Seek):
    path
    # 路径中产生目标的距离,如果是反方向时值为负数
    pathOffset
    # 路径中当前位置
    currentParam
    # 预测未来时间角色的位置
    predictTime = 0.1

    def getSteering():
        # 计算预测位置
        futurePos = character.position + character.velocity * predictTime
        # 寻找路径中的当前位置
        currentParam = path.getParam(futurePos, currentPos)

        targetParam = currentParam + pathOffset
        # 获取目标位置
        target.position = path.getPosition(targetParam)

        return Seek.getSteering()

未实现Path类

class Path:
    def getParam(position, lastParam)
    def getPosition(param)

现在有一种更简单的实现方式,即使用一系列坐标点组成路径,使用Seek行为一个个抵达目标。

分离(Separation)

分离行为针对于拥有大致相同方向的一堆目标,使他们保持距离。但是它在目标相互移动穿插的情况无法作用,这时候需要之后介绍的碰撞避免行为。

有两种分离算法:
1. 线性分离

strength = maxAcceleration * (threshold - distance) / threshold
  1. 平方反比算法
strength = min(k / (distance * distance), maxAcceleration)

分离行为实现代码如下:

class Separation:
    character
    targets

    threshold

    decayCoefficient

    maxAcceleration

    def getSteering():
        steering = new SteeringOutput()

        for target in targets:
            direction = target.position - character.position
            distance = direction.length()
            if distance < threshold:
                strength = min(decayCoefficient/ (distance * distance), maxAcceleration)
                direction.normalize()
                steering.linear += strength * direction

        return steering

碰撞避免(Collision Avoidance)

判断两个目标达到最近距离的时间:

tclosest=dpdv|dv|2

其中

dp=ptpc

dv=vtvc

pt 为当前目标位置, pc 为当前角色位置, vt 为当前目标速度, vc 为当前角色速度。
如果 tclosest 为负数时,说明当前已经过了最近点。

达到最近距离时,角色和目标的坐标分别是:

pc=pc+vctclosest

pt=pt+vttclosest

躲避多个角色的实现并非合并平均他们的坐标和速度,算法需要找到最早会达到最近点的目标,然后规避该目标即可。

实现代码如下:

class CollisionAvoidance:
    character
    targets

    maxAcceleration
    # 角色的碰撞半径(假设都一样)
    radius

    def getSteering():
        shortestTime = infinity
        # 存储当前已经碰撞的目标
        curMinDistance = infinity
        curMinTarget = None

        # 存储即将碰撞最近目标信息
        firstTarget = None
        firstMinSeparation
        firstDistance
        firstRelativePos
        firstRelativeVel

        for target in targets:
            # 计算达到最近点的时间
            relativePos = target.position - character.position
            relativeVel = target.velocity - character.velocity
            relativeSpeed = relativeVel.length()
            timeToCollision =- (relativePos * relativeVel) / (relativeSpeed * relativeSpeed)

            # 判断是否会发生碰撞
            distance = relativePos.length()
            # 原代码 minSeparation = distance - relativeSpeed * shortestTime
            futureMinPos = relativePos - relativeVel * timeToCollision
            minSeparation = futureMinPos.length()
            if minSeparation > 2 * radius:
                continue

            # 判断是否是最先达到最近位置的目标
            if timeToCollision > 0 and timeToCollision < shortestTime:
                shortestTime = timeToCollision
                firstTarget = target
                firstMinSeparation = minSeparation
                firstDistance = distance
                firstRelativePos = relativePos
                firstRelativeVel = relativeVel
            else if distance <= 2 * radius and distance < curMinDistance:
                curMinDistance = distance
                curMinTarget = target
            end

        if not firstTarget and not curMinTarget:
            return None

        # 原代码 if firstMinSeparation <= 0 or distance <= 2 * radius:
        #               relativePos = firstTarget.position - character.position
        # distance 是哪一个?
        # 如果是当前最近的目标距离,那么firstTarget就是当前已经碰撞(因为他们距离小于半径2倍)的目标,并不是将来最先碰撞到的目标
        # 同时firstMinSeparation <= 0不能说明任何问题,只能说如果一直是当前速度下,角色和目标之前发生过碰撞,但是事实是角色和目标的当前速度可能只有在现在这一刻是这样。所以应该只需要考虑distance<=2*raidus就可以了
        # 如果已经发生碰撞
        if curMinDistance <= 2 * radius:
            relativePos = curMinTarget.position - character.position
        else if firstMinSeparation > 0:
            relativePos = firstRelativePos + firstRelativeVel * shortestTime

        relativePos.normalize()
        steering.linear = relativePos * maxAcceleration

        return steering;

障碍和墙躲避(Obstacle And Wall Avoidance)

碰撞躲避行为假设目标是球形的,它关注于躲避目标的中心位置。而障碍和墙躲避行为的目标形状更复杂,所以实现方式也不一样。通过从移动的角色前方发射一条或者多条有限长度的射线,如果碰撞到障碍,角色就开始进行躲避,根据碰撞信息获得一个移动的目标,进行探寻行为。

效果图如下示:

collisionrayavoidingawall

实现代码如下:

class ObstacleAvoidance(Seek):
    # 碰撞探测器,检测与障碍的碰撞
    collisionDetector
    # 选择躲避点距离碰撞点的距离
    avoidDistance
    # 角色朝前方射出射线的距离
    lookahead

    def getSteering():
        rayVector = character.velocity
        rayVector.normalize()
        rayVector *= lookahead

        collision = collisionDetector.getCollision(character.position, rayVector)

        if not collision:
            return None

        # Seek.target = target
        target = collision.position + collision.normal * avoidDistance

        return Seek.getSteering()

class CollisionDetector:
    def getCollision(position, moveAmount)

struct Collision:
    position
    normal
碰撞检测的问题

目前为止我们假设用一条射线检测碰撞,在使用上,这并不是一个好的解决办法。
下图显示了一条射线可能遇到的问题以及可以的一种解决方法:

grazingwallwithasiglerayorthree

因此一般情况下需要多条射线一起作用来躲避障碍,以下是可能的几种情况:

rayconfigforavoidance

这里并没有一种有力并且快速的规则来决定哪一种射线方式是更好地,每一种都有他们自己的特质。单独一条短射线并且带有短的触须通常是最好的初始尝试配置能够让角色很容易从紧密的通道中行走。单独一条射线配置被用在凹面环境中但是无法避免碰撞到凸面障碍物。平行射线配置在非常大的钝角环境中工作很好,但是很容易在角落中被困住,下边将有介绍。

拐角困境(The Corner Trap)

多条射线躲避墙壁算法会遭遇到尖锐的夹角障碍的问题,导致朝任意两边移动另一条射线都会碰撞到墙面,最终仍然撞向障碍物。如下图所示:

thecornertrapformultiplerays

扇形结构,如果有足够大的扇形夹角,可以减轻这个问题,通常这需要一些权衡。拥有一个大的夹角避免这种拐角困境或者小的夹角来通过小的通道。最差的情况,角色有一个180度角度的射线,角色将不能够很快速的对两边射线的碰撞检测进行反应从而导致仍然碰到墙上。

一些开发者提出了一些可接受的适应性扇形夹角,如果角色移动中没有检测到碰撞,那么夹角就变小,如果检测到了碰撞,那么扇形夹角将保持变大,减少出现拐角困境的机会。

其他一些开发者实现了一些特殊的专门解决拐角困境的代码,如果发生这种情况,那么只有其中一条射线的碰撞信息需要考虑,无视掉另外一条射线的碰撞检测信息。

除了以上两种方法,还有一种更加完整的方法,通过使用一个投影体积而不是使用射线来检测碰撞,如下图所示:

collisiondetectionwithprojectedvolumes

许多游戏引擎能够做这些事情(例如Unity3d的Physics类),为了模拟真实的物理。不像是ai,物理中使用的投影距离通常很小(Unlike AI, the projection distance required by physics are typically very small),然而,用在转向行为中计算将很慢。

到目前为止,最实用的解决方案是使用一个扇形夹角,中间一条射线两边有两条短触须

介绍过的转向行为总结

steeringfamilytree

实现代码:

基于以上Kinematic Behavior和Steering Behavior的伪代码,使用Unity3d(版本5.6.0f3)进行了实现,package包下载地址在此

关于ai介绍SteeringBehavior的相关博客:https://tutsplus.com/authors/fernando-bevilacqua;
http://natureofcode.com/book/chapter-6-autonomous-agents/ (The Nature of Code);

一份网络上关于ai-move的笔记:https://web.cs.ship.edu/~djmoon/gaming/gaming-notes/ai-movement.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值