三.移动

3.1 基本移动算法

静态的(Statics)

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

struct Static:position            # a 2D vectororientation         # a single point value

动力学(Kenematics)

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

struct Kinematic:position            # a 2 or 3D vectororientation     # a single floating point valuevelocity            # another 2 or 3D vectorrotation            # a single floating point value

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

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

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

struct Kinematic:# other Member data before...def update(steering, time):# Update the posiiton and orientationposition += velocity * time + 0.5 * steering.linear * time * timeorientation += rotation * time + 0.5 * steering.angular * time * time# and the velocity and rotationvelocity += steering.linear * time# 原代码:orientation += steering.angular * timerotation += steering.angular * time

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

struct Kinematic:# other Member data before...def update(steering, time):# Update the posiiton and orientationposition += velocity * timeorientation += rotation * time# and the velocity and rotationvelocity += steering.linear * time# 原代码:orientation += steering.angular * timerotation += 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:velocityrotation        # 应为朝向依据速度,所以该信息无用class KinematicSeek:# 存储当前控制的对象和目标对象charactertarget# 存储当前对象移动的最大速度maxSpeeddef getSteering():steering = new KinematicSteeringOutput()# 计算移动速度steering.velocity = target.position - character.positionsteering.velocity.normalize()steering.velocity *= maxSpeed# 我觉得也可以利用上rotation, steering.rotation = getNewOrientation(character.orientation, steering.velocity)character.orientation = getNewOrientation(character.orientation, steering.velocity)steering.rotation = 0return steering

逃离(Flee)

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

steering.velocity = character.position - target.position

抵达(Arriving)

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

class KinematicArrive:charactertargetmaxSpeed# 进入目标该半径之内不再移动radius# 如果radius为0时,从减速到停止共花费时间timeToTargettimeToTarget = 0.25def getSteering():steering = new KinematicSteeringOutput()steering.velocity = target.position - character.position# 抵达目标半径内,停止移动if steering.velocity.length() < radius:return None# 控制速度steering.velocity /= timeToTargetif steering.velocity.length() > maxSpeed:steering.velocity.normalize()steering.velocity *= maxSpeedcharacter.orientation = getNewOrientation(character.orientation, steering.velocity)steering.rotation = 0return steering

巡逻(Wander)

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

class KinematicWander:charactermaxSpeed# 旋转偏移随机在[-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() * maxRotationreturn steering

3.3 转向行为(Steering Behaviors)

寻找(Seek)和逃离(Flee)

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

struct Kinematic:# 其他成员变量def update(steering, maxSpeed, time):position += velocity * timeorientation += rotation * timevelocity += steering.linear * timeorientation += steering.angular * timeif velocity.length() > maxSpeed:velocity.normalize()velocity *= maxSpeed

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

class Seek:charactertarget# 最大加速度maxAccelerationdef getSteering():steering = new SteeringOutput()steering.linear = target.position - character.positionsteering.linear.normalize();# 一直施加最大加速度steering.linear *= maxAcceleration;steering.angular = 0return steering

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

steering.linear = character.position - target.position

Seek和Flee行为路径

抵达(Arrive)

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

抵达行为实现代码如下:

class Arrive:charactertargetmaxAccelerationmaxSpeed# 进入目标该范围算是抵达目标targetRadius# 开始减速的范围半径slowRadius# 减速时长timeToTargetdef getSteering():steering = new SteeringOutput()direction = target.position - character.positiondistance = direction.length()# 好像有问题,进入这个范围会匀速出去,并不会停if distance < targetRadiusreturn Noneif distance > slowRadius:targetSpeed = maxSpeedelse:targetSpeed = maxSpeed * distance / slowRadiustargetVelocity = directiontargetVelocity.normalize()targetVelocity *= targetSpeedsteering.linear = targetVelocity - character.velocitysteering.linear /= timeToTargetif steering.linear.length() > maxAcceleration:steering.linear.normalize()steering.linear *= maxAccelerationsteering.angular = 0return steering

排列(Align)

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

class Align:charactertargetmaxAngularAccelerationmaaxRotation# 和目标朝向保持一致的范围targetRadius# 开始减速的半径slowRadiustimeToTarget = 0.1def 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 = maxRotationelse:targetRotation = maxRotation * rotationSize / slowRadius# -1或1signDir = rotation / rotationSizetargetRotation *= signDir# 原代码好像有问题, targetRotation是一个差值steering.angular = targetRotation - character.rotationsteering.angular /= timeToTargetangularAcceleration = abs(steering.angular)if angularAcceleration > maxAngularAcceleration:steering.angular /= angularAccelerationsteering.angular *= maxAngularAccelerationsteering.linear = 0return steering

速度匹配(Velocity Matching)

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

实现代码如下:

class VelocityMatch:charactertargetmaxAccelerationtimeToTargetdef getSteering():steering = new SteeringOutput()steering.linear = target.velocity - character.velocitysteering.linear /= timeToTargetif steering.linear.length() > maxAcceleration:steering.linear.normalize()steering.linear *= maxAccelerationsteering.angular = 0return steering

委托行为(Delegated Behaviors)

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

追逐和躲避(Pursue And Evade)

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

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

实现代码如下:

# 继承自Seek
class Pursue(Seek):# 最大预测时间maxPrediction# 我们要追逐的目标target# 其他一些继承自父类的信息def getSteering():direction = target.position - character.positiondistance = direction.length()speed = character.velocity.length()# 根据当前速度计算预测时间if speed <= distance / maxPrediction:prediction = maxPredictionelse:prediction = distance / speed# 原代码:Seek.target = explicitTargetSeek.target = targetSeek.target.position += target.velocity * predictionreturn Seek.getSteering()

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

面对(Face)

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

实现代码如下:

class Face(Align):target# 其他继承自父类的数据def getSteering():direction = target.position - character.positionif direction.length == 0:return target# 原代码 Align.target = explicitTargetAlign.target = targetAlign.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:returntarget.orientation = atan2(-character.velocity.x, character.velocity.z)return Align.getSteering()

游荡(Wander)

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

实现代码:

class Wander(Face):# 目标偏移圆的中心偏移点和半径wanderOffsetwanderRadius# 最大随机旋转偏移值wanderRate# 当前游荡对象的朝向wanderOrientationdef getSteering():wanderOrientation += randomBinomial() * wanderRatetargetOrientation = wanderOrientation + character.orientationtarget = character.position + wanderOffset * character.orientation.asVector()target += wanderRadius * targetOrientation.asVector()# Face.target = targetsteering = 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)。

如图所示:

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

实现代码如下所示:

class FollowPath(Seek):path# 路径中产生目标的距离,如果是反方向时值为负数pathOffset# 路径中当前位置currentParam# 预测未来时间角色的位置predictTime = 0.1def 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:charactertargetsthresholddecayCoefficientmaxAccelerationdef getSteering():steering = new SteeringOutput()for target in targets:direction = target.position - character.positiondistance = direction.length()if distance < threshold:strength = min(decayCoefficient/ (distance * distance), maxAcceleration)direction.normalize()steering.linear += strength * directionreturn steering

碰撞避免(Collision Avoidance)

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

tclosest=−dpdv|dv|2

t_{closest} = -\frac{d_pd_v}{|d_v|^2}

其中

dp=pt−pc

d_p = p_t - p_c

dv=vt−vc

d_v = v_t - v_c

ptp_t为当前目标位置,pcp_c为当前角色位置,vtv_t为当前目标速度,vcv_c为当前角色速度。
如果tclosestt_{closest} 为负数时,说明当前已经过了最近点。

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

pc‘=pc+vc∗tclosest

p_c`= p_c + v_c * t_{closest}

pt‘=pt+vt∗tclosest

p_t`= p_t + v_t * t_{closest}

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

实现代码如下:

class CollisionAvoidance:charactertargetsmaxAcceleration# 角色的碰撞半径(假设都一样)radiusdef getSteering():shortestTime = infinity# 存储当前已经碰撞的目标curMinDistance = infinitycurMinTarget = None# 存储即将碰撞最近目标信息firstTarget = NonefirstMinSeparationfirstDistancefirstRelativePosfirstRelativeVelfor target in targets:# 计算达到最近点的时间relativePos = target.position - character.positionrelativeVel = target.velocity - character.velocityrelativeSpeed = relativeVel.length()timeToCollision =- (relativePos * relativeVel) / (relativeSpeed * relativeSpeed)# 判断是否会发生碰撞distance = relativePos.length()# 原代码 minSeparation = distance - relativeSpeed * shortestTimefutureMinPos = relativePos - relativeVel * timeToCollisionminSeparation = futureMinPos.length()if minSeparation > 2 * radius:continue# 判断是否是最先达到最近位置的目标if timeToCollision > 0 and timeToCollision < shortestTime:shortestTime = timeToCollisionfirstTarget = targetfirstMinSeparation = minSeparationfirstDistance = distancefirstRelativePos = relativePosfirstRelativeVel = relativeVelelse if distance <= 2 * radius and distance < curMinDistance:curMinDistance = distancecurMinTarget = targetendif 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.positionelse if firstMinSeparation > 0:relativePos = firstRelativePos + firstRelativeVel * shortestTimerelativePos.normalize()steering.linear = relativePos * maxAccelerationreturn steering;

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

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

效果图如下示:

实现代码如下:

class ObstacleAvoidance(Seek):# 碰撞探测器,检测与障碍的碰撞collisionDetector# 选择躲避点距离碰撞点的距离avoidDistance# 角色朝前方射出射线的距离lookaheaddef getSteering():rayVector = character.velocityrayVector.normalize()rayVector *= lookaheadcollision = collisionDetector.getCollision(character.position, rayVector)if not collision:return None# Seek.target = targettarget = collision.position + collision.normal * avoidDistancereturn Seek.getSteering()class CollisionDetector:def getCollision(position, moveAmount)struct Collision:positionnormal

碰撞检测的问题

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

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

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

拐角困境(The Corner Trap)

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

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

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

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

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

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

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

介绍过的转向行为总结

实现代码:

基于以上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

Steering Behaviors相关推荐

  1. “AS3.0高级动画编程”学习:第二章转向行为(上)

    因为这一章的内容基本上都是涉及向量的,先来一个2D向量类:Vector2D.as (再次强烈建议不熟悉向量运算的童鞋,先回去恶补一下高等数学-07章空间解释几何与向量代数.pdf) package { ...

  2. Free C/C++ Libraries(免费的C/C++库)

    推荐一些免费的开源C/C++程序库.该内容来源于:http://www.programmerworld.net/resources/c_library.htm. 1.Boost - Provides ...

  3. c++ 工具库 (zz)

    下面是收集的一些开发工具包,主要是C/C++方面的,涉及图形.图像.游戏.人工智能等各个方面,感觉是一个比较全的资源.供参考! 原文的出处:http://www.codemonsters.de/hom ...

  4. 【C/C++开发】c++ 工具库 (zz)

    下面是收集的一些开发工具包,主要是C/C++方面的,涉及图形.图像.游戏.人工智能等各个方面,感觉是一个比较全的资源.供参考!  原文的出处:http://www.codemonsters.de/ho ...

  5. 游戏人工智能编程案例精粹(修订版) (Mat Buckland 著)

    https://www.jblearning.com/catalog/productdetails/9781556220784 第1章 数学和物理学初探 (已看) 第2章 状态驱动智能体设计 (已看) ...

  6. Bookmarks_2012_06_13

    Bookmarks 书签栏 VeryCD 邮件 - 入职申请SVN - zengfeng@verycd.com2345网址导航-我的个性化主页-中国最好的网址站我的工作台 - 心动游戏项目管理 手册A ...

  7. 【笔记】寻路技术整合

    pathfinding,先用unity navmesh烘培,再用lockstepengine里的工具导出    然后test find  #游戏地图的划分 Grid (方格) Navigation M ...

  8. 谈谈游戏AI的一些基础知识

    文章目录 前言 AI和游戏 AI在Gameplay中的模块 移动和寻路 移动 到达目的地的判断 寻路 世界表述 决策制定 行动列表(Action Lists) 状态机 行为树 GOAP AI的战术和策 ...

  9. Game AI resources

    原文地址:http://www.cnblogs.com/yaoyansi/articles/1837284.html from http://www.red3d.com/cwr/games/#ai A ...

最新文章

  1. 2020中国人工智能年度评选正在征集!开放4大类别7大奖项
  2. 流水账日记20150626
  3. 武汉大学2013年数学分析考研试题参考解答
  4. python-主成分分析-降维-PCA
  5. esd防护_电路级ESD防护方法
  6. Alpha冲刺阶段博客汇总
  7. 国产SSD市场机遇与挑战并存
  8. Linux的DNS高速缓存
  9. struts2异常处理流程_Struts2异常处理示例教程
  10. DHCP中继原理与配置
  11. 中国武术和泰拳的对抗史
  12. Android 四大组件之Activity
  13. SPSS——相关分析——偏相关(Partial)分析
  14. jenkins 下载插件失败 有效的处理办法(亲测)
  15. 2020第一本书《自私的基因》
  16. 高动态范围图像(HDR)处理
  17. 基于Arch GNU/Linux的简体中文live系统 archlive
  18. springboot 之 Starter
  19. git clone大仓库(>1G)时速度慢并出现RPC failed断开连接错误的真正解决方法
  20. [转载] C#开发实战1200例(第I卷)目录

热门文章

  1. Swin-Transformer-Object-Detection V2.11.0 训练visdrone数据(二)
  2. 详解生物地理学优化(BBO)算法(一)
  3. Cesium之地图清晰度解决方案
  4. go语言打包生成更小的体积
  5. Ubuntu10下载安装Android 2.2 froyo 源码
  6. linux debian vi,Debian 安装 vim
  7. 网络编程“惊群“问题
  8. css网页制作的基本步骤,以图例方式介绍CSS制作网页详细步骤
  9. 基于MATLAB GUI的魔方三维动态还原仿真程序
  10. 管桩的弹性模量计算公式_弹性模量法测定桩身应力分析