Unity3D人工智能框架模型

  1. 游戏UI的框架模型

   尽管每种游戏需要的AI技术都有所不同,但绝大部分现代游戏中对UI的需求都可以分为三级:

  1. 运动层:导航和寻路是运动层AI的主要任务,它们决定了角色的移动路径。当然具体的移动行为还需要动画层的配合。运动层包含的算法能够把上层做出的决策转化为运动。
  2. 决策层:决策层的任务是决定角色在下一时间该做什么。一般情况下,每个角色都有许多不同的行为可以选择,列如攻击,隐藏,探索,巡逻等,因此,在每个时间步,决策系统都需要判断哪些行为是最合适的。当决策系统做出决策后,由运动层和动画系统来执行决策。
  3. 战略层:即使游戏中只有运动层和决策层,也可以实现很复杂的功能。事实上,大部分基于行为的三维游戏只用到了这两个层次,但是,如果需要团队协作,那么还需要某些战略AI。战略指的是一组角色的总体行为,这时AI算法并不是只控制单个角色,而是影响到多个角色的行为。小组中每个角色可以有它们自己的决策层和运动算法,但总体上,它们的决策层会受到团队战略的影响。

   在实际中,要构架出好的UI角色,只有运动层,决策层和战略层是不够的,还需要许多其他相关技术的支持。例如,运动层需要向“动画系统”或“物理仿真系统”发出请求,以便移动进一步转化为具体的运动。

   AI还需要感知游戏世界的信息,找出角色能够获知的信息,来做出合理的决策,可以称之为“感知系统”。它不仅仅包含每个角色可以看到和听到的内容,还包括游戏世界与AI的所有接口。

              实现AI角色的自主移动——操控行为

1.“操作行为”是指操作控制角色,让它们能以模拟真实的方式在游戏世界中移动。它的工方式是通过产生一定大小的方向和操作力,使角色以某种方式运动。

  1. 操控行为包括一组基本“行为”。对于单独的AI角色,基本面操控性为包括:

*使角色靠近或离开目标的“Seek”,“Flee”行为;

*当角色接近目标时使它减速的“Arrival”行为;

*当捕猎者追逐猎物的“Pursuit”行为;

*使角色逃离捕猎者的“Evade”行为;

*使角色在游戏世界中随机徘徊的“Wander”行为;

*使角色沿着某条预定路径移动的“Path Following”行为;

*使角色避开障碍物的“Obstacle Avoidance”行为等;

基本行为中的每一个行为,都产生相应的操控力,将这些操控力以一定的方式组合起来(实际上就相当于将这些基本“行为”进行了不同的组合),就能够得到更复杂的“行为”,从而实现更为高级的目标。

  1. 对于组成小队或群体的多个AI角色,包括基本的行为如下。

*与其他相邻角色保持一定距离的“Separation”行为;

*与其他相邻角色保持一致朝向的“Alignment”行为;

*靠近其他相邻角色的“Cohesion”行为。

                 操控行为术语中英文对照表

英语术语          中文术语               释义

Seek              靠近                   使角色靠近目标

Flee              离开                   使角色离开目标

Arrival            抵达                  当角色接近目标时使它减速

Pursuit           追逐                   使狩猎者追逐猎物

Evade            逃避                   使猎物逃离狩猎者

Wander          随机徘徊                使角色随机徘徊

Path Following    路径跟随                使角色沿着某条预定路径移动

Obstacle Avoidance 避开障碍               使角色避开障碍物

Group Behavior    组行为                 多角色成组的操控性为

Radar            雷达                   探测周围相邻角色位置

Separation        分离                   与群中邻居保持适当距离

Alignment        队列                    与群中邻居保持朝向一致

Cohesion         聚集                    成群聚集在一起

 

  1. Unity3D操控行为编程的主要基类

   编程中主要涉及到Vehicle类,AILocomotion类和Steering类,它们是是实现操控行为的基础。

  1. 将AI角色抽象成一个质点——Vehicle

  在AI构架模型中,操控AI角色的基类Vehicle把操作的对象抽象为一个质点,它包含位置(position),质量(mass),速度(velocity)等信息,而速度随着所施加力的变化而变化。由于速度意味着实际物理实体,施加在其上的力能达到的速度都是有限制的,因此还需要最大力(max_force)和最高速度(max_speed)两个信息,除此之外,还要包含一个朝向(orientation)的信息。

  综上,这个“交通工具”位置的计算方法是这样的:

  1. 确定每一帧当前的操控力(最大不超过max_force);
  2. 除以交通工具的质量mass,可以确定一个加速度;
  3. 将这个加速度和原来的速度相加,得到新的速度(最大不超过max_speed);
  4. 根据速度和这一帧流逝的时间,计算出位置的变化;
  5. 与原来的位置相加,得到“交通工具”的新位置。

在这个简单的“交通工具”模型中,来自操控行为部分的控制信号只是一个向量—steering_force。

在下面这个实现中,Vehicle是一个基类,其他所有可移动的游戏AI角色都由它派生而来。该实现封装了一些数据,用来描述被看做质点的“交通工具”,如车辆,马匹,飞机,潜水艇,动人,人类,怪物等。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class Vehicle : MonoBehaviour {

//这个AI角色包含的操控行为列表

private Steering[] steerings;

//设置这个AI角色能达到的最大速度

public float maxSpeed = 10;

//设置能施加到这个AI角色的力的最大值

public float maxForce = 100;

//最大速度的平方,通过预先算出并存储,节省资源

protected float sqrMaxSpeed;

//AI角色的质量

public float mass = 1;

//AI角色的速度

public Vector3 velocity;

//控制转向时的速度

public float damping = 0.9f;

//操控力的计算间隔时间,为了达到更高的帧率,操控力不需要每帧更新

public float computeInterval = 0.2f;

//是否在二位平面上,如果是,计算两个GameObject的距离时,忽略y值得不同;

public bool isPlanar = true;

//计算得到的操控力

private Vector3 steeringForce;

//AI角色的加速度

protected Vector3 acceleration;

//计时器

private float timer;

// Use this for initialization

protected void Start () {

steeringForce = new Vector3(0, 0, 0);

sqrMaxSpeed = maxSpeed * maxSpeed;

timer = 0;

//获得这个AI角色所包含的操控行为列表

steerings = GetComponents<Steering>();

}

// Update is called once per frame

void Update () {

timer += Time.deltaTime;

steeringForce = new Vector3(0, 0, 0);

//如果距离上次计算操控力的时间大于设定的时间间隔computeInterval

//再次计算操控力

if(timer>computeInterval)

{

//将操控行为列表中的所有操控行为对应的操控力进行带权重的求和

foreach(Steering s in steerings)

{

if (s.enabled)

steeringForce += s.Force() * s.weight;

}

//是操控力不大于maxForce

steeringForce = Vector3.ClampMagnitude(steeringForce, maxForce);

//力除以质量,求出加速度

acceleration = steeringForce / mass;

//重新从0开始计时

timer = 0;

}

}

}

  1. 控制AI角色移动——AILocomotion类

  AILocomotion类是Vehicle的派生类,它能真正的控制AI角色的移动,包括计算每次移动的距离,播放动画等,下面是一个示例实现:

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class AILocomotion : Vehicle {

//AI角色的角色控制器

private CharacterController controller;

//AI角色的Rigidbody

private Rigidbody theRigidbody;

//AI角色每次的移动距离

private Vector3 moveDistance;

// Use this for initialization

void Start () {

//获得角色控制器(如果有的话)

controller = GetComponent<CharacterController>();

//获得AI角色的Rigidbody

theRigidbody = GetComponent<Rigidbody>();

moveDistance = new Vector3(0, 0, 0);

//调用基类的Start()函数,进行所需的初始化

base.Start();

}

//物理相关操作在FixedUpadate()中更新

void FixedUpdate()

{

//计算速度

velocity += acceleration * Time.fixedDeltaTime;

//限值速度,要低于最大速度

if(velocity.sqrMagnitude>sqrMaxSpeed)

{

velocity = velocity.normalized * maxSpeed;

}

//计算AI角色的移动距离

moveDistance = velocity * Time.fixedDeltaTime;

//如果要求AI角色在平面上移动,那么将y置为0;

if(isPlanar)

{

velocity.y = 0;

moveDistance.y = 0;

}

//如果已经为AI角色添加了角色控制器,那么利用角色控制器使其移动

if (controller != null)

{

controller.SimpleMove(velocity);

}

//如果AI角色没有角色控制器,也没有rigidbody

//或AI角色拥有Rigidbody,但是要由动力学的方式控制它的移动

else if (theRigidbody == null || theRigidbody.isKinematic)

transform.position += moveDistance;

//用Rigidbody控制AI角色的运动

else

theRigidbody.MovePosition(theRigidbody.position + moveDistance);

//更新朝向,如果速度大于一个阈值(为了防止抖动)

if(velocity.sqrMagnitude>0.00001)

{

//通过当前朝向与速度方向的差值,计算新的朝向

Vector3 newForward = Vector3.Slerp(transform.forward, velocity, damping * Time.deltaTime);

//将y设置为0

if(isPlanar)

{

newForward.y = 0;

}

//将当前的方向设置为新的朝向

transform.forward = newForward;

}

//播放行走动画

GetComponent<Animation>().Play("run");

}

}

2.3各种操控行为的基类——Steering类

  Steering类是所有操控行为的基类,包含操作行为共有的变量和方法,操控AI角色的寻找,逃跑,追逐,逃避,徘徊,分离,队列,聚集等都可由此派生。这样,我们就可以在Unity3D的C#脚本中方便地使用上述派生类来编程

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class Steering : MonoBehaviour {

//表示每个操控力的权重

public float weight = 1;

// Use this for initialization

void Start () {

}

// Update is called once per frame

void Update () {

}

//计算操控力的方法,由派生类实现

public virtual Vector3 Force()

{

return new Vector3(0, 0, 0);

}

}

  1. 个体AI角色的操控行为

  3.1靠近

    操控行为中的靠近是指,指定一个目标位置,根据当前的运动速度向量,返回一个操控AI角色到达该目标位置的“操控力”,使AI角色能够自动向该位置移动。

要想让AI角色靠近目标,首先需要计算出AI角色在理想情况下到达目标的预期速度。该速度可以看作是AI角色的当前位置到目标位置的向量。操控向量是预期速度与AI角色当前速度的差,该向量大小随着当前位置变化而变化。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForSeek : Steering {

//需要寻找的目标物体

public GameObject target;

//预期速度

private Vector3 desiredVelocity;

//获得被操控AI角色,以便查询这个AI角色的最大速度等信息

private Vehicle m_vehicle;

//最大速度

private float maxSpeed;

//是否在二维平面上运动

private bool isPlanar;

// Use this for initialization

void Start () {

//获得被操控AI角色,并读取AI角色允许的最大速度,是否仅在平面上运动

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

isPlanar = m_vehicle.isPlanar;

}

//计算操控向量(操控力)

public override Vector3 Force()

{

//计算预期速度

desiredVelocity = (target.transform.position - transform.position).normalized * maxSpeed;

if (isPlanar)

desiredVelocity.y = 0;

//返回操控向量,即预期速度与当前速度的差

return (desiredVelocity - m_vehicle.velocity);

}

}

3.2离开

   离开与靠近行为刚好相反,它产生一个操控AI角色离开目标的力,而不是靠近目标的力。它们之间唯一的区别是DesiredVelocity具有相反的方向。

   接着,还可以进行进一步调整,只有当AI角色进入目标周围一定范围内时,才产生离开的力,这样可以模拟出AI角色的有限感知范围。

   using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForFlee : Steering {

public GameObject target;

//设置AI角色意识到危险并开始逃跑的范围

public float feraDistance = 20;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

}

public override Vector3 Force()

{

Vector3 tmpPos = new Vector3(transform.position.x, 0, transform.position.z);

Vector3 tmpTargetPos = new Vector3(target.transform.position.x, 0, target.transform.position.z);

//如果AI角色与目标距离大于逃跑距离,那么返回0向量

if (Vector3.Distance(tmpPos, tmpTargetPos) > feraDistance)

return new Vector3(0, 0, 0);

//如果AI角色与目标的距离小于逃跑距离,那么计算逃跑所需的向量

desiredVelocity = (transform.position - target.transform.position).normalized * maxSpeed;

return (desiredVelocity - m_vehicle.velocity);

}

}

  3.3抵达

     有时我们希望AI角色能够减速并停到目标位置,避免冲过目标,例如,车辆在接近十字路口是逐渐减速,然后停到路口处,这时就需要用到抵达行为。

     在角色距离目标较远时,抵达与靠近行为的状态是一样的,但是接近目标时,不再是全速向目标移动。而代之以使AI角色减速,刚好最终停到目标位置。何时开始减速是通过参数进行设置的,这个参数可以看成停止半径。当角色在停止半径之外是,以最大速度移动;当角色进入停止半径之内是,逐渐减少预期速度,直到减小为0。这个参数很关键,它决定了抵达行为的最终效果。

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForArrive : Steering {

public bool isPlanar = true;

public float arrivalDistance = 0.3f;

public float characterRadius = 1.2f;

//当与目标小于这个距离时,开始减速

public float slowDownDistance;

public GameObject target;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxspeed;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxspeed = m_vehicle.maxSpeed;

isPlanar = m_vehicle.isPlanar;

}

public override Vector3 Force()

{

//计算AI角色与目标之间的距离

Vector3 toTarget = target.transform.position - transform.position;

//预期速度

Vector3 desiredVelocity;

//返回的操作值

Vector3 returnForce;

if (isPlanar)

toTarget.y = 0;

float distance = toTarget.magnitude;

//如果与目标之间的距离大于所设置的减速半径

if(distance>slowDownDistance)

{

//预期速度是AI角色与目标点之间的距离

desiredVelocity = toTarget.normalized * maxspeed;

//返回预期速度与当前速度的差

returnForce = desiredVelocity - m_vehicle.velocity;

}

else

{

//计算预期速度,并返回预期速度与当前速度的差

desiredVelocity = toTarget - m_vehicle.velocity;

//返回预期速度与当前速度的差

returnForce = desiredVelocity - m_vehicle.velocity;

}

return returnForce;

}

private void OnDrawGizmos()

{

//在目标周围画白色线框球,显示出减速范围

Gizmos.DrawWireSphere(target.transform.position, slowDownDistance);

}

}

3.4 追逐

    追逐行为和靠近行为很像,只不过目标不再是静止不动,而是另外一个可移动的角色。最简单的追逐方式是直接向目标的当前位置靠近,不过这样看上去很不真实。举例来说,大家都知道,当动物追逐猎物的时候,绝不是直接向猎物的当前位置奔跑,而是预测猎物的未来位置,然后想着未来位置的方向追去,这样才能在最短的时间内追上猎物。在AI中,把这种操控行为称为“追逐”。

    怎样实现这种智能的追逐行为呢?我们可以使用一个简单的预测器,在每一帧重新计算它的值。假设采用一个线性预测器,又假设在预测间隔T时间内角色不会转向,角色经过时间T之后的未来位置可以用当前速度乘以T来确定,然后把得到的值加到角色的当前位置上,就可以得到预测位置来了。最后,再以预测位置作为目标,应用靠近行为就可以了。

    实现追逐行为的一个关键是如何确定预测间隔T。可以把它设为一个常数,也可以当追逐者距离目标较远时设为较大的值,而接近目标时设为较小的值。

    这里,设定预测时间和追逐者与逃避者之间的距离成正比,与二者的速度成反比。

    一些情况下,追逐可能提前结束。例如,如果逃避者在前面,几乎面对追逐者,那么追逐者应该直接向逃避者的当前位置移动。二者之间的关系可以通过计算逃避者朝向向量与AI角色朝向向量的点积得到,在下面代码中,逃避者朝向的反向和AI角色的朝向必须大约在20度范围之内,才可以被认为是面对着的。

    using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForPursuit : Steering {

public GameObject target;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

}

public override Vector3 Force()

{

Vector3 toTarget = target.transform.position - transform.position;

//计算追逐者的前向与逃避着前向之间的夹角;

float relativeDirection = Vector3.Dot(transform.forward, target.transform.forward);

//如果夹角大于0,且追逐者基本面对着逃避着,那么直接向逃避者当前位置移动

if(relativeDirection<-0.95f&&(Vector3.Dot(toTarget,transform.forward)>0))

{

//计算预期速度

desiredVelocity = (target.transform.position - transform.position).normalized * maxSpeed;

//返回操控向量

return (desiredVelocity - m_vehicle.velocity);

}

//计算预测时间,正比于追逐者与逃避者的距离,反比与追逐者和逃避者的速度和

float lookaheadTime = toTarget.magnitude / (maxSpeed + target.GetComponent<Vehicle>().velocity.magnitude);

//计算预期速度

desiredVelocity = (target.transform.position + target.GetComponent<Vehicle>().velocity * lookaheadTime - transform.position).normalized * maxSpeed;

//返回操控向量

return (desiredVelocity - m_vehicle.velocity);

}

}

3.5 逃避

   逃避行为是指使猎物逃避捕猎者,举例来说,鹿被狼追逐,鹿要不断变换逃跑方向,试图逃离狼预测的追逐方向。

   逃避行为与追逐行为不同的是它试图使AI角色逃离预测位置。实现追逐行为的一个关键是如何确定预测间隔T,可以把它设为一个常数,也可以当AI角色距离目标较远时,设为较大的值,而接近目标时,设为较小的值。

   using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForEvade : Steering {

public GameObject targrt;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

}

public override Vector3 Force()

{

Vector3 toTarget = targrt.transform.position - transform.position;

float lookaheadTime = toTarget.magnitude / (maxSpeed + targrt.GetComponent<Vehicle>().velocity.magnitude);

//计算预期速度

desiredVelocity = (transform.position - (targrt.transform.position + targrt.GetComponent<Vehicle>().velocity * lookaheadTime)).normalized * maxSpeed;

//返回控制向量

return (desiredVelocity - m_vehicle.velocity);

}

}

 3.6 随机徘徊

   许多时候,人们需要让游戏中的角色在游戏环境中随机移动(如巡逻的士兵,惬意吃草的牛羊等),就像这些角色是在等待某些事情发生,或者在寻找生命东西。当角色出现在玩家实现范围内时,人们通常希望这种随机移动看起来是真实的,如果玩家发现角色实际上是在沿着预先定义好的路径移动,就会有不真实的感觉,那么便会影响到它的游戏体验。

   随机徘徊操控行为就是让角色产生有真实感的随机移动那个。这会让玩家感觉到角色是有生命的,而且正在到处移动。

   利用操控行为来实现随机徘徊有很多种方法,最简单的方式是利用前面所提到的靠近行为(seek)。在游戏场景中随机的放置目标,让角色靠近目标,这样AI角色就会向目标移动,如果隔一定时间(如几秒)就改变目标的位置,这样角色就永远靠近目标而又不能到达目标(即使到达,目标也会再次移动)。这个方法很简答,粗略的看上去也很不错,但是最终结果可能不尽人意。角色有时会突然转头,因为目标移动到了它的后面,CraigReynolds提出的随机徘徊操控行为解决了这个问题。

   解决问题的工作原理同内燃机的气缸曲轴转动相似,在角色(气缸)通过连杆联结到曲轴上,目标被限定曲轴圆周上,移动目标(利用靠近行为)。为了看得更似随机徘徊,每帧给目标附加一个随机的位移,这样,目标便会沿着圆周不停的移动。将目标限制在这个圆周上,是为了对角色进行限制,使之不至于突然改变路线。这样,如果角色现在是在向右移动,下一时刻它仍然是在向右移动,只不过与上一时刻相比,有了一个小的角度差。利用不同的连杆长度(wander距离),角色到圆心的距离(wander半径),每帧随机偏移的大小,就可以产生各种不同的随机运动,如巡逻的士兵,惬意吃草的牛羊等。

   using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForWander : Steering {

//徘徊半径,即wander圆的半径

public float wanderRadius;

//徘徊距离,即wander圆凸出在AI角色前面的距离

public float wanderDistance;

//每秒加到目标的随机位移的最大值

public float wanderJitter;

public bool isPlanar;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

private Vector3 circleTarget;

private Vector3 wanderTarget;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

isPlanar = m_vehicle.isPlanar;

maxSpeed = m_vehicle.maxSpeed;

//选取圆圈上一点作为初始点

circleTarget = new Vector3(wanderRadius * 0.707f, 0, wanderRadius * 0.707f);

}

public override Vector3 Force()

{

//计算随机位移

Vector3 randomDisplacement = new Vector3((Random.value - 0.5f) * 2 * wanderJitter, (Random.value - 0.5f) * 2 * wanderJitter, (Random.value - 0.5f) * 2 * wanderJitter);

if (isPlanar)

randomDisplacement.y = 0;

//将随机位移加到初始点上,得到新的位置

circleTarget += randomDisplacement;

//由于新位置很可能不在圆周上,因此需要投影到圆周上

circleTarget = wanderRadius * circleTarget.normalized;

//之前计算出的值是相对于AI角色和AI角色的向前方向的,需要转化为世界坐标

wanderTarget = m_vehicle.velocity.normalized * wanderDistance + circleTarget + transform.position;

//计算预期速度,返回操控向量

desiredVelocity = (wanderTarget - transform.position).normalized * maxSpeed;

return (desiredVelocity - m_vehicle.velocity);

}

}

   

   3.7 路径跟随

       就像赛车在赛道上需要导航一样,路径跟随会产生一个操控力,使AI角色沿着预先设置的轨迹,构成路径的一系列路点移动。

       最简单的路径跟随方式是将当前路点设置为路点列表的第一个路点,用靠近行为产生操控力来靠近这个路点,直到非常接近这个点,然后寻找下一个路点,设置为当前路点,再次接近它。重复这样的过程直到到达路点列表的最后一个路点,再根据需要决定是要回到第一个路点,还是停止到最后一个路点上。

       这里假设路径是开放的,角色需要减速停止到最后一个路点上,一次需要用抵达行为和路径跟随行为。

       有时路径有起点和终点,有时路径是循环的,是一个永不结束的封闭路径。如果路径是封闭的,那么需要回到起点重新开始;如果是开放的,那么AI角色需要减速停到最后有个路点上。

       在实现路径跟随行为时,需要设置一个“路径半径”参数,即当AI角色距离当前路点多远时,可以认为它已经到达当前路点,从而继续向下一个路点前进。这个参数的设置会引起路径形状的变化。

       using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringFollowPath : Steering {

//由节点数组表示的路径

public GameObject[] waypoints = new GameObject[4];

//目标点

private Transform target;

//当前的路点

private int currentNode;

//与路点的距离小于这个值时,认为已经到达,可以向下一个路点触发

private float arriveDistance;

private float sqrArriveDistance;

//路点的数量

private int numberOfNodes;

//操控力

private Vector3 force;

private Vector3 desiredVeclocity;

private Vehicle m_vehicle;

private float maxSpeed;

private bool isPlanar;

public float slowDownDistance;

// Use this for initialization

void Start () {

//存储路点数组中的路点个数

numberOfNodes = waypoints.Length;

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

isPlanar = m_vehicle.isPlanar;

//设置当前路点数为第0个路点

currentNode = 0;

//设置当前路点为目标点

target = waypoints[currentNode].transform;

arriveDistance = 1.0f;

sqrArriveDistance = arriveDistance * arriveDistance;

}

public override Vector3 Force()

{

force = new Vector3(0, 0, 0);

Vector3 dis = target.position - transform.position;

if (isPlanar)

dis.y = 0;

//如果当前路点已经是路点数组中的最后一个

if(currentNode==numberOfNodes-1)

{

//如果与当前路点距离大于减速距离

if(dis.magnitude>slowDownDistance)

{

//求出预期速度

desiredVeclocity = dis.normalized * maxSpeed;

//计算操控向量

force = desiredVeclocity - m_vehicle.velocity;

}

else

{

//与当前路点距离小于减速距离,开始减速,计算操控向量

desiredVeclocity = dis - m_vehicle.velocity;

force = desiredVeclocity - m_vehicle.velocity;

}

}

else

{

//当前路点不是路点数组中的最后一个,即正走向中间路点

if(dis.sqrMagnitude<sqrArriveDistance)

{

currentNode++;

target = waypoints[currentNode].transform;

}

//计算预期速度和操控向量

desiredVeclocity = dis.normalized * maxSpeed;

force = desiredVeclocity - m_vehicle.velocity;

}

return force;

}

}

3.8 避开障碍

      避开障碍行为是指操控AI角色避开路上的障碍物,例如在动物奔跑时避免和树,墙碰撞。当AI角色的行进路线上发现比较近的障碍时,产生一个“排斥力”,使AI角色远离这个障碍物;当前方发现多个障碍物时,只产生躲避最近的障碍物的操控力,这样,AI角色就会一个接一个地躲避这些障碍物。

      在这个算法中,首先需要发现障碍物。每个AI角色唯一需要担心的障碍物就是挡在它行进路线前方的那些物体,其他远离的障碍物可以不必考虑。该算法分析如下:

  1. 速度向量代表了AI角色的行进方向,可以利用这个速度向量生成一个新的向量ahead,它的方向与速度向量一致,但长度有所不同。这个ahead向量代表了AI角色的视线范围,其计算方法为:

            ahead=position+normalize(velocity) * MAX_SEE_AHEAD

       ahead向量的长度(MAX_SEE_AHEAD)定义了AI角色能看到的距离。MAX_SEE_AHEAD的值越大,AI角色看到障碍的时间越早,因此,它开始躲避障碍的时间也越早。

  1. 每个障碍物都要用一个几何形状来表示,这里采用包围球来标识场景中的每个障碍。

一种可能的方法是检测从AI角色向前延伸的ahead向量与障碍物的包围是否相交。这种方法当然可以,但这里采用简化的方法,更容易理解,且能够达到相似的效果。

这里还需要一个向量ahead2,这个向量与ahead向量唯一的区别是:ahead2的长度是ahead的一半。计算方法如下;

      ahead=position+normalize(velocity) *MAX_SEE_AHEAD *0.5

  1. 接下来进行碰撞检测,来测试这两个向量是否存在障碍物的包围球内。方法很简单,只需要比较向量的终点和球心的距离d就可以了。如果距离小于或等于球的半径,那么就认为向量在包围球内,即AI角色将会和包围球发生碰撞。

如果ahead与ahead2中任一向量在包围球内,那么就说明障碍物挡在前方。

如果多个障碍张住了路,那么选择最近的那个障碍(即“威胁最大”的那个)进行计算。

  1. 接下来,计算能够将AI角色推离障碍物的操控力。可以利用球心和ahead这两个向量计算,方法如下:

            avoidance_force=ahead-obstacle_center

            Avoidance_force=normalize(avoidance_force) * MAX_AVOID_FORCE

       这里,MAX_AVOID_FORCE用于决定操控力的大小,值越大,将AI角色推离障碍物的力就越大。

       采用这种那个方法的缺点是,当AI角色接近障碍而操控力正使它远离的时候,即使AI角色正在旋转,也可能会检测到碰撞。一种改进方法是根据AI角色的当前速度调整ahead向量,计算方法如下:

            dynamic_length=length(velocity)/MAX_VELOCITY

            ahead=position+normalize(velocity) * dynamic_length

      这时,dynamic_length变量范围是0-1.当AI角色全速前进时,dynamic_length的值是1

          using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForCollisionAvoidance : Steering {

public bool isPlanar;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

private float maxForce;

//避免障碍所产生的操控力

public float avoidanceForce;

//能向前看的最大距离

public float MAX_SEE_AHEAD = 2.0f;

//场景中所有碰撞体组成的数组

private GameObject[] allColliders;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

maxForce = m_vehicle.maxForce;

isPlanar = m_vehicle.isPlanar;

//如果避免障碍所产生的操控力大于最大操控力,将它截断到最大操控力

if (avoidanceForce > maxForce)

avoidanceForce = maxForce;

//存储场景中所有的碰撞体,即Tag为obstacle的那些物体

allColliders = GameObject.FindGameObjectsWithTag("obstacle");

}

public override Vector3 Force()

{

RaycastHit hit;

Vector3 force = new Vector3(0, 0, 0);

Vector3 velocity = m_vehicle.velocity;

Vector3 normalizedVelocity = velocity.normalized;

//画出一条射线,需要考察与这条射线相交的碰撞体

Debug.DrawLine(transform.position, transform.position + normalizedVelocity * MAX_SEE_AHEAD * (velocity.magnitude / maxSpeed));

if(Physics.Raycast(transform.position,normalizedVelocity,out hit,MAX_SEE_AHEAD*velocity.magnitude/maxSpeed))

{

//如果这条射线与某个碰撞相交,表示可能与该碰撞体发生碰撞

Vector3 ahead = transform.position + normalizedVelocity * MAX_SEE_AHEAD * (velocity.magnitude / maxSpeed);

//计算避免碰撞所需的操控力

force = ahead - hit.collider.transform.position;

force *= avoidanceForce;

if (isPlanar)

force.y = 0;

//将这个碰撞体的颜色变为绿色,其他的都变为灰色

foreach(GameObject c in allColliders)

{

if (hit.collider.gameObject == c)

{

c.GetComponent<Renderer>().material.color = Color.black;

}

else

c.GetComponent<Renderer>().material.color = Color.white;

}

}

else

{

//如果向前看的有限范围内,没有发生碰撞的可能

//将所有碰撞体设为灰色

foreach(GameObject c in allColliders)

{

c.GetComponent<Renderer>().material.color = Color.white;

}

}

//返回操控力

return force;

}

}

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class ColliderColorChange : MonoBehaviour {

// Use this for initialization

void Start () {

}

// Update is called once per frame

void Update () {

}

private void OnTriggerEnter(Collider other)

{

//如果与其他碰撞体碰撞,那么碰撞体变为红色

if (other.gameObject.GetComponent<Vehicle>() != null)

this.GetComponent<Renderer>().material.color = Color.red;

}

private void OnTriggerExit(Collider other)

{

//碰撞体变成灰色

this.GetComponent<Renderer>().material.color = Color.gray;

}

}

  1. 群体的操控行为

   操控行为的强大之处就在于它对群体行为的模拟能力。

  1. 组行为

  正如大多数人工仿真一样,组行为是展示操控行为的一个很好的例子,它的复杂性来源于个体之间的交互,并遵循一些简单的规则。

  模拟群体行为需要下面几种操控行为:

     分离(Separation):避免个体在局部过于拥挤的操控力;

     队列(Alignment):朝向附近同伴的平均朝向的操控力;

     聚集(Cohesion):向附近同伴的平均位置移动的操控力;

4.2 检测附近的AI角色

  从上面的几种操控行为可以看出,每种操控行为都决定角色相对相邻的其他角色做出何种反应。为了实现组行为,首先需要检测位于AI角色“邻域”中的其他AI角色,这要用一个雷达脚本来实现。

  一个角色的邻域由一个距离和一个角度来定义,其他角色位于邻域内时,便认为是AI角色的邻居,否则将被忽略。这个区域可以被认为是AI角色的可视范围。有时为了简化,不考虑AI角色的可见范围,而只是用一个圆来定义邻域。

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class Randar : MonoBehaviour {

//碰撞体的数组

private Collider[] colliders;

//计时器

private float timer = 0;

//邻居列表

public List<GameObject> neighbors;

//无需每帧进行检测,该变量设置检测的时间间隔

private float checkInterval = 0.3f;

//设置领域半径

public float detectRadius = 10f;

//设置检测哪一层的游戏对象

public LayerMask layersChecked;

// Use this for initialization

void Start () {

//初始化邻居列表

neighbors = new List<GameObject>();

}

// Update is called once per frame

void Update () {

timer += Time.deltaTime;

//如果距离上次检测的时间大于所设置的检测时间间隔,那么再次检测

if(timer>checkInterval)

{

//清除邻居列表

neighbors.Clear();

//查找当前AI角色邻域内的所有碰撞体

colliders = Physics.OverlapSphere(transform.position, detectRadius, layersChecked);

//对于每个检测到的碰撞体,获取Vehicle组件,并且加入邻居列表中

for(int i=0;i<colliders.Length;i++)

{

if (colliders[i].GetComponent<Vehicle>())

neighbors.Add(colliders[i].gameObject);

}

//计时器归0

timer = 0;

}

}

}  

当然在,这只是一个简单的实现,使用时还可以进一步增加可视域的限制(只需要测试AI角色的朝向向量与潜在邻居的向量之间的点积,就可以实现这个功能),甚至可以动态调整AI角色的可视域,例如,在战争游戏中,士兵的可视域可能受到疲劳的影响,降低察觉环境的能力。

            

4.3 与群中邻居保持适当距离——分离

      分离行为的作用是当角色与周围的其他角色保持一定距离,这样可以避免多个角色相互挤到一起。当分离行为应用在许多AI角色(如鸟群中的鸟)上时,它们将会向四周散开,尽可能地拉开距离。

      实现时,为了计算分离行为所需的操控力,首先要搜索指定邻域内的其他邻居(通过前面的脚本实现),然后对每个邻居,计算AI角色到该邻居的向量r,将向量r归一化,得到排斥力的方向,由于排斥力的大小是与距离成反比的,因此还需要除以|r|,然后把来自所有邻居的排斥力相加,就得到了分离行为的总操控力。

       using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForSeparation : Steering {

//可接受的距离

public float comfortDistance = 1;

//当AI角色与邻居之间的距离过近时的惩罚因子

public float multiplierInsideComfortDistance = 2;

// Use this for initialization

void Start () {

}

public override Vector3 Force()

{

Vector3 steeringForce = new Vector3(0, 0, 0);

//遍历这个AI角色的邻居列表中的每个邻居

foreach(GameObject s in GetComponent<Randar>().neighbors)

{

//如果s不是当前AI角色

if((s!=null)&&s!=this.gameObject)

{

//计算当前AI角色与邻居s之间的距离

Vector3 toNeighbor = transform.position - s.transform.position;

float length = toNeighbor.magnitude;

//计算这个邻居所引起的操控力(可以认为是排除力,大小与距离成反比)

steeringForce += toNeighbor.normalized / length;

//如果两者之间距离小于可接受距离,排斥力再乘以一个额外因子

if(length<comfortDistance)

{

steeringForce *= multiplierInsideComfortDistance;

}

}

}

return steeringForce;

}

}

4.4 与群中邻居朝向一致——队列

  队列行为试图保持AI角色的运动朝向与邻居一致,这样就会得到向鸟群朝着一个方向飞行的效果。

  通过迭代所有邻居,可以求出AI角色朝向向量的平均值以及速度向量的平均值,得到想要的朝向,然后减去AI角色的当前朝向,就可以得到队列操控力。

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForAlignment : Steering {

// Use this for initialization

void Start () {

}

public override Vector3 Force()

{

//当前AI角色的邻居的平均朝向

Vector3 averageDirection = new Vector3(0, 0, 0);

//邻居的数量

int neighborCount = 0;

//遍历当前AI角色的所有邻居

foreach(GameObject s in GetComponent<Randar>().neighbors)

{

//如果s不是当前AI角色

if((s!=null)&&(s!=this.gameObject))

{

//将s的朝向向量加到averageDirection之中

averageDirection += s.transform.forward;

//邻居数量加1

neighborCount++;

}

}

//如果邻居数大于0

if(neighborCount>0)

{

//将累加得到的朝向向量除以邻居的个数,求出平均朝向向量

averageDirection /= (float)neighborCount;

//平均朝向向量减去当前朝向向量,得到操控向量

averageDirection -= transform.forward;

}

return averageDirection;

}

}

需要注意的是,由于每个迭代步,每个角色会根据其他邻居的朝向调整自身的朝向,并且由于惯性过冲,因此结果可能不太稳定。

队列行为对于集群并不是必须的,使用与否取决于具体应用。例如飞向目标的群鸟,沿着道路同向行驶的汽车就用到了队列的行为。

4.5 成群聚集在一起——聚集

  聚集行为产生一个使AI角色移向邻居质点的操控力。这个操控力使得多个AI角色聚集在一起。

  实现时,迭代所有邻居求出AI角色位置的平均值,然后利用靠近行为,将这个平均值作为目标位置。

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForCohesion : Steering {

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

}

public override Vector3 Force()

{

//操控向量

Vector3 steeringForce = new Vector3(0, 0, 0);

//AI角色的所有邻居的质点,即平均位置

Vector3 centerOfMass = new Vector3(0, 0, 0);

//AI角色的邻居数量

int neighborCount = 0;

//遍历邻居列表中的每个邻居

foreach(GameObject s in GetComponent<Randar>().neighbors)

{

//如果s不是当前AI角色

if((s!=null)&&(s!=this.gameObject))

{

//累加s的位置

centerOfMass += s.transform.position;

//邻居数量加1

neighborCount++;

}

}

//如果邻居数量大于0

if(neighborCount>0)

{

//将位置累加值除以邻居数量,得到平均值

centerOfMass /= (float)neighborCount;

//预期速度为邻居平均值与当前位置之差

desiredVelocity = (centerOfMass - transform.position).normalized * maxSpeed;

//预期速度减去当前速度,求出操控向量

steeringForce = desiredVelocity - m_vehicle.velocity;

}

return steeringForce;

}

}

这里,聚集的程度取决于Radar.cs脚本中DetectRadius参数的设置,如果这个值很大,那么每个角色都会考虑到更多的邻居,结果是所有的角色聚到一起;而如果这个值较小,那么每个角色都只会考虑附近小范围内的邻居,结果是聚集到多处;这个值越小,得到的聚集点越多。

  

  1. 个体与群体的操控行为组合

   组行为的基础是分离,队列和聚集,根据实际需要,可以与前面的个体操控行为相结合,例如避开障碍,随机徘徊,靠近与抵达等,从而产生更复杂的行为。

  1. 示例

  如果想要角色从道路A移动到B(路径跟随),  在路上要避开障碍,还要避开其他角色(分离),就要将这三种行为组合起来。

  如果要实现一个鹿群的行为,它们是群聚的(包括分离,聚集和队列),同时还在环境中随机徘徊,当然还要避开石头和树木(避开障碍)。另外,当它们遇到狼走近时就四处逃散(逃避),该怎么办呢?

  前面的一些基本的操控行为,可以实现追逐,逃避,路径跟随,聚集等行为。在实际应用中,为了得到理想的行为,常常将操控行为组合起来使用。当然,也可以直接把这些行为都加进去,但问题是,当AI角色在徘徊时,就不需要逃散,当逃散时,既不需要徘徊,也不需要聚集和队列,这时该怎么办呢。

  1. 解决方案

  在AI角色的Vehicle类中,有一个steering的实例,可以通过它来动态开启或关闭某种行为,从而激活或注销这个行为。那么,如何决定开启或关闭不同的行为的时间呢?这就需要更高的决策层做出决定,这个决策层可以利用状态机来实现。例如:

  如果与玩家的距离小于60米,AI角色的生命值大于80,那么追逐玩家

  如果与玩家的距离小于60米,AI角色的生命值小于80,那么逃避;

  否则,在场景中徘徊。

   那么,如何组合多个行为呢?最简单的方式是直接把各个行为所产生的操控力加到一起,这样得到的总操控力会反映出这些行为。

  在Vehicle脚本的Update函数中,可以看到下面的代码;

   foreach(Steering s in steerings)

{

if (s.enabled)

steeringForce += s.Force() * s.weight;

}

//是操控力不大于maxForce

steeringForce = Vector3.ClampMagnitude(steeringForce, maxForce);

这段代码的目的是计算所有已激活行为的合力,当实现组合行为时,就会把所有的行为产生的操控力都考虑在内。但是,由于AI角色A有最大操控力maxForce的限值,不能简单的把所有的操控力加起来,这时就需要以某种方式截断这个总和,确保得到的值不超过这个最大力的限值。实现截断可以有不同的方法,最简答的方法称为加权截断总和。

  加权截断总和会为每个操控力乘上一个权重,将他们加在一起,然后将结果截断到允许的最大操控力,也就是实现中所采用的方法。

  这种方式很简单,但当操控力相互冲突时,有时就会产生问题。例如,如果分离的操控力较大,而避开障碍的操控力较小,那么AI角色就可能撞到障碍物上。解决办法是增加避开障碍的权重。但如果把这个权重调的过大,当AI角色单独接近障碍物时,有可能出现奇怪的行为,好像被障碍物大力推开一样。

  其他截断方法还有带优先级的加权截断累计,待优先级的抖动等方法。

  1. 几种操控行为的编程解析

  6.1模拟鸟群运动

  1. 分析

 模拟鸟群飞行时,我们希望鸟群看上去既不像行军般单调一致,也不要像分子运动那样完全随机,这里的关键在于设定了每只鸟的简单移动方式。每只鸟都有这样的行为模式:

   *避免挤到鸟群中去——分离

   *向鸟群的平均移动方向移动——队列

   *向鸟群的中心位置移动——聚集

这些简单的逻辑可以有很好的效果,当鸟的数量非常多时,能够很好的模拟出鸟群在空中飞行的姿态。

  1. 场景布置

 向场景中添加一个空物体,然后为他加上生成鸟群的脚本PlaceObjects.cs.

 using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class PlaceObject : MonoBehaviour {

public GameObject objectToPlace;

public int Count;

//初始位置在一个半径为radius的球体内产生

public float radius;

public bool isPlanar;

private void Awake()

{

Vector3 position = new Vector3(0, 0, 0);

for(int i =0;i<Count;i++)

{

position = transform.position + Random.insideUnitSphere * radius;

if(isPlanar)

{

position.y = objectToPlace.transform.position.y;

}

//实例化预置体

Instantiate(objectToPlace, position, Quaternion.identity);

}

}

}

  

  

  6.2 多AI角色障碍赛

(1)目标

 前面介绍的是单个AI角色的障碍避免实现,现在是多个AI角色的障碍避免的例子。在这个例子中,AI角色会避开障碍物,还会相互避开.

(2)场景布置

 场景与前面单个AI角色的障碍避免很相似,在原来的那个AI角色的基础上,再加上脚本Radar和SteeringForSeparation.cs,然后做成预设体。

 创建一个空物体,用于生产多个AI角色,为它加上GenerateBost脚本.

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class GenerateBots : MonoBehaviour {

public GameObject botPrefab;

public int botCount;

public GameObject target;

//长方体“盒子”定义了随机生成AI的初始位置

public float minX = 75.0f;

public float maxX = 160.0f;

public float minZ = -650.0f;

public float maxZ = -600.0f;

public float Yvalue = 4.043f;

// Use this for initialization

void Start () {

Vector3 spawnPosition;

GameObject bot;

for(int i=0;i<botCount;i++)

{

//随机选择一个生成点,实例化预设体

spawnPosition = new Vector3(Random.Range(minX, maxX), Yvalue, Random.Range(minZ, maxZ));

bot = Instantiate(botPrefab, spawnPosition, Quaternion.identity) as GameObject;

bot.GetComponent<SteeringForArrive>().target = target;

}

}

// Update is called once per frame

void Update () {

}

}

  6.3 实现动物迁徙中的跟随领队行为

   (1)目标

     对于动物迁徙过程的模拟,可以通过“跟随领队”行为实现。

     跟随领队行为属于一种组合行为,实现此效果只要用靠近和追逐行为不就可以了吗?

     遗憾的是,这样的效果并不好。我们知道,在靠近行为中,AI角色会被推向目标,最终于目标点占据相同的位置,而追逐行为把AI角色推向另一个角色,目的在于抓住它而不是跟随他。

     在领队行为中,目标是接近领队,但稍微落后。当角色距离领队较远时,可能会较快的移动,但是当距离领队较近时,会降低速度。这可以通过下列行为的组合来实现:

  *抵达:向领队移动,在即将到达是减慢速度。

  *逃避:如果AI跟随者挡住了领队的路线,它需要迅速移开;

  *分离:避免多个跟随者过于拥挤,

    要实现这种行为,首先要找到正确的跟随点。AI跟随者要与领队保持一定的距离,就像士兵跟随队长后面一样,跟随点(behind)可以利用领队的速度来确定,因为它也表示了领队的行进方向。

     tv=leader.velocity*(-1);

     tv=normalize(tv) *LEADER_BEHIND_DIST;

     behind=leader.position+tv;

    LEADER_BEHIND_DIST的值越大,跟随点距离领队越远,这代表着跟随者与领队的距离越大。

    接下来,只要以behind点为目标,应用到达行为就可以了,返回的操控力也就是FollowLeader的返回力。

    然后,为了避免跟随者过于拥挤,需要加上分离行为。

    如果领队突然改变方向,那么跟随者可能会挡住领头者。因此我们需要跟随者在领队之后,而不会死挡在它前面,因此,在发生情况时,跟随者必须马上移开,这时就需要加入逃避行为。

    为了加测AI跟随者角色是否在头领的视线内,我们采用和碰撞避免行为中相似的方法。基于领队的当前速度和方向,找到它前方的一个点(ahead),如果领队的ahead点与AI跟随者的距离小于某个值,那么认为跟随者在领队的视线之内并且需要离开。

    ahead点的计算方法与behind点几乎是相同的,差别在于速度向量不再取负值

    tv=leader.velocity*(1);

     tv=normalize(tv) *LEADER_BEHIND_DIST;

     behind=leader.position+tv;

(2)实现

 将领队AI拖入场景中,然后为它加上Character Controller组件,另外添加三个脚本,分别是AILocomotion.cs,SteeringForWander.cs,DrawGizmos.cs。DrawGizmos脚本用于显示出领队行进路线前方的检测球。如果跟随者进入这个检测球,说明跟随者挡住了领队的线路,需要暂时为这个跟随者加上躲避行为,好让他为领队让路。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class DrawGizmos : MonoBehaviour {

public float evadeDistace;

//领队前方的一个点

private Vector3 center;

private Vehicle vehicleScript;

private float LEADER_BEHIND_DIST;

// Use this for initialization

void Start () {

vehicleScript = GetComponent<Vehicle>();

LEADER_BEHIND_DIST = 2.0f;

}

// Update is called once per frame

void Update () {

center = transform.position + vehicleScript.velocity.normalized * LEADER_BEHIND_DIST;

}

private void OnDrawGizmos()

{

//画出一个位于领队前方的线框球,如果其他角色进入这个范围内,就需要激发逃避行为

Gizmos.DrawWireSphere(center, evadeDistace);

}

}

接下来,在场景中添加一个空物体,用于生成过个跟随者,为它添加GenerateBotsForFollowLeader.cs

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class GenerateForFollowLeader : MonoBehaviour {

public GameObject botPrefab;

public GameObject leader;

public int botCount;

//长方体“盒子”定义随机生成的AI的初始位置

public float minX;

public float maxX;

public float minZ;

public float maxZ;

public float Yvalue;

// Use this for initialization

void Start () {

Vector3 spawnPosition;

GameObject bot;

for(int i=0;i<botCount;i++)

{

spawnPosition = new Vector3(Random.Range(minX, maxX), Yvalue, Random.Range(minZ, maxZ));

bot = Instantiate(botPrefab, spawnPosition, Quaternion.identity) as GameObject;

bot.GetComponent<SteeringForLeaderFollowing>().leader = leader;

bot.GetComponent<SteeringForEvade>().targrt = leader;

bot.GetComponent<SteeringForEvade>().enabled = false;

bot.GetComponent<EvadeController>().leader = leader;

}

}

}

为跟随者添加Character Controller组件。接下来,为它添加AILocomotion.cs,SteeringForArrive.cs,SteeringForSeparation.cs,SteeringForLeaderFollowing.cs,Randar.cs,SteeringForEvade,EvadeController

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

[RequireComponent(typeof(SteeringForArrive))]

public class SteeringForLeaderFollowing : Steering {

public Vector3 targrt;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

private bool isPlanar;

//领队游戏体

public GameObject leader;

//领队的控制脚本

private Vehicle leaderController;

private Vector3 leaderVelocity;

//跟随者落后领队的距离

private float LEADER_BEHIND_DIST = 2.0f;

private SteeringForArrive arriveScript;

private Vector3 randomOffset;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

isPlanar = m_vehicle.isPlanar;

leaderController = leader.GetComponent<Vehicle>();

//为抵达行为指定目标点

arriveScript = GetComponent<SteeringForArrive>();

arriveScript.target = new GameObject("arriveTarget");

arriveScript.target.transform.position = leader.transform.position;

}

public override Vector3 Force()

{

leaderVelocity = leaderController.velocity;

//计算目标点

targrt = leader.transform.position + LEADER_BEHIND_DIST * (-leaderVelocity).normalized;

arriveScript.target.transform.position = targrt;

return new Vector3(0, 0, 0);

}

}

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class EvadeController : MonoBehaviour {

public GameObject leader;

private Vehicle leaderLocomotion;

private Vehicle m_vehicle;

private bool isPlanar;

private Vector3 leaderAhead;

private float LEADER_BEHIND_DIST;

private Vector3 dist;

public float evadeDistance;

private float sqrEvaderDistance;

private SteeringForEvade evadeScript;

// Use this for initialization

void Start () {

leaderLocomotion = leader.GetComponent<Vehicle>();

m_vehicle = GetComponent<Vehicle>();

isPlanar = m_vehicle.isPlanar;

LEADER_BEHIND_DIST = 2.0f;

evadeScript = GetComponent<SteeringForEvade>();

sqrEvaderDistance = evadeDistance * evadeDistance;

}

// Update is called once per frame

void Update () {

//计算领队前方的一个点

leaderAhead = leader.transform.position + leaderLocomotion.velocity.normalized * LEADER_BEHIND_DIST;

//计算角色当前位置与领队前方某点的距离,如果小于某个值,就需要躲避

dist = transform.position - leaderAhead;

if (isPlanar)

dist.y = 0;

if(dist.sqrMagnitude<sqrEvaderDistance)

{

//如果小于躲避距离,激活躲避行为

evadeScript.enabled = true;

Debug.DrawLine(transform.position, leader.transform.position);

}

else

{

//躲避行为处于非激活状态

evadeScript.enabled = false;

}

}

}

    

6.4 排队通过狭窄通道

  (1)分析

这个场景可以模拟在室外的一队AI角色从宽广的地方来到一个狭窄的通道。例如,如果发现玩家在大厅内,它们就会通过唯一的门进入大厅,捕捉玩家。这时,我们希望它们能有序的进入,这就是排队行为所要实现的目标

实现排队行为,需要用到的两种操控行为

*靠近

*避开障碍

如果没有排队行为,那么AI角色会试图从彼此的头上过去,而排队行为会让这些AI排起队,有序的通过这个通道。

首先,角色需要确定前方是否有其他AI角色,然后根据这个信息来确定自己是继续前进还是停止等待。这里我们利用与碰撞避免行为中相同的方法来检测前方是否有其他AI角色。首先需要计算出角色前方的ahead点,如果这个点与其他某个AI角色的距离小于一个值MAX_QUEUE-RADIUS,那么表示这个AI角色的前方有其他AI角色,必须减速或停止等待。

ahead的计算方法是这样的:

ahead=normalize(velocity) *MAX_QUEUE_AHEAD

ahead=qa+position;

那么,如何让AI角色停止等待呢?需要知道的是,操控行为是基于变化的力的,因此系统是动态的。即使某个行为返回的力为0,甚至总和力为0,还是不会让AI角色的速度变为0。

那么如果让AI角色停止等待呢?一种方法是计算其他所有力的合力,然后像抵达行为中一样,消除这些力的影响,让他停下来。另一种方法是直接控制AI角色的速度向量,而忽略其他的力。这种方法便是此处采用的方法:直接将速度乘以一个比例因子,例如0.2,在此因子的作用下,AI角色的一定能够会迅速减慢,但当前方没有遮拦时,会慢慢恢复到原来的速度。

然后,再加上靠近和避开障碍行为。当然,根据具体情况,还可以加入分离行为等。

(2)场景设置

   新建一个场景,创建一个平面,用于AI角色行走,然后添加两面墙,这两面墙之间留有一个狭窄的出口,让多个AI角色和目标正好位于墙的不同侧。这样,这些AI角色就必须通过这个狭窄的出口,走向目标。

   墙是通过创建Cube来实现的,注意要为它加上obstacle标签,并且还要创建一个新的layer,编号为9,名称也可以是“obstacle”,墙体除了有obstacle标签外,还位于obstacle层。

   创建目标,这里用哪个一个小球体表示目标,注意要删去碰撞体组件。

   创建一个空物体,称为BotsGenerator,用于在指定位置的区域生成多个AI角色。为它添加脚本GenerateForQueue.cs。

   using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class GenerateBotsForQueue : MonoBehaviour {

public GameObject botPrefab;

public int botCount;

public GameObject target;

public float minX;

public float maxX;

public float minZ;

public float maxZ;

public float Yvalue;

// Use this for initialization

void Start () {

Vector3 spawnPosition;

GameObject bot;

//在一个长方形盒子定义的范围,随机生成多个角色

//为生成的角色指定目标

for(int i=0;i<botCount;i++)

{

spawnPosition = new Vector3(Random.Range(minX, maxX), Yvalue, Random.Range(minZ, maxZ));

bot = Instantiate(botPrefab, spawnPosition, Quaternion.identity) as GameObject;

bot.GetComponent<SteeringForArrive>().target = target;

}

}

// Update is called once per frame

void Update () {

}

}

创建AI角色的Prefab。首先将带动画的AI角色模型拖入场景中,为它添加Character Controller组件,然后添加AILocomotion.cs,SteeringForAvoidanceQueue.cs(这个脚本是用于碰撞避免的,与SteeringForCollisionAvoidance.cs脚本略有不同),SteeringForArrive.cs,Randar.cs,SteeringForSeparation.cs,最后添加SteeringForQueue.cs脚本,这个脚本用于实现排队行为。添加好这些脚本并设置好参数后,拖入一个空难干的Prefab。

  using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForQueue : Steering {

public float MAX_QUEUE_AHEAD;

public float MAX_QUEUE_RADIUS;

private Collider[] colliders;

public LayerMask layerChecked;

private Vehicle m_vehicle;

private int layerid;

private LayerMask layerMask;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

//设置碰撞检测时的掩码

layerid = LayerMask.NameToLayer("AI");

layerMask = 1 << layerid;

}

public override Vector3 Force()

{

Vector3 velocity = m_vehicle.velocity;

Vector3 normalizedVelocity = velocity.normalized;

//计算出角色前方的一点

Vector3 ahead = transform.position + normalizedVelocity * MAX_QUEUE_AHEAD;

//如果以ahead为中心,MAX_QUEUE_RADIUS的球体内有其他角色

colliders = Physics.OverlapSphere(ahead, MAX_QUEUE_RADIUS, layerMask);

if(colliders.Length>0)

{

//对于所有位于这个球体内的其他角色,如果他们的速度比当前角色的速度更慢

//当前角色放慢速度,避免发生碰撞

foreach(Collider c in colliders)

{

if((c.gameObject!=this.gameObject)&&(c.gameObject.GetComponent<Vehicle>().velocity.magnitude<velocity.magnitude))

{

m_vehicle.velocity *= 0.2f;

break;

}

}

}

return new Vector3(0, 0, 0);

}

}

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class SteeringForCollisionAvoidanceQueue : Steering {

public bool isPlanar;

private Vector3 desiredVelocity;

private Vehicle m_vehicle;

private float maxSpeed;

private float maxForce;

public float avoidanceForce;

public float MAX_SEE_AHEAD;

private GameObject[] allColliders;

private int layerid;

private LayerMask layerMask;

// Use this for initialization

void Start () {

m_vehicle = GetComponent<Vehicle>();

maxSpeed = m_vehicle.maxSpeed;

maxForce = m_vehicle.maxForce;

isPlanar = m_vehicle.isPlanar;

if (avoidanceForce > maxForce)

avoidanceForce = maxForce;

allColliders = GameObject.FindGameObjectsWithTag("obstacle");

layerid = LayerMask.NameToLayer("obstacle");

layerMask = 1 << layerid;

}

//计算碰撞避免所需的操控力,这里利用了掩码,只考虑与场景中其他角色的碰撞

public override Vector3 Force()

{

RaycastHit hit;

Vector3 force = new Vector3(0, 0, 0);

Vector3 velocity = m_vehicle.velocity;

Vector3 normalizedVelocity = velocity.normalized;

if(Physics.Raycast(transform.position,normalizedVelocity,out hit,MAX_SEE_AHEAD,layerMask))

{

Vector3 ahead = transform.position + normalizedVelocity * MAX_SEE_AHEAD;

force = ahead - hit.collider.transform.position;

force *= avoidanceForce;

if (isPlanar)

force.y = 0;

}

return force;

}

}

 

  1.  操控行为编程的其他问题

    1. 操控力的更新频率

  从前面的代码中可以看出,检测附近的邻居,计算操控力等这些步骤并不像游戏画面和动画一样,每一帧都需要进行更新,而是按照实现预定的更新频率进行更新。在设计系统时,对时间的把握时很关键的。

  正反馈带来震荡,而负反馈会维持群体不至于失控。

  如果反馈过快,反应时间过短,将会引起震荡,而不是缓慢的波动。另外,过快的反馈也会导致输入数据变少,因为没有足够的时间收集足够的数据。

  相反,如果反馈过慢,将会使系统显得很单调。以鸟群为例,如果反馈时间过长,那么上一时刻与同伴过近的某个物体,将会在当前时刻飞的很远,以至于下一时刻,它已经飞离这个群体;而上一时刻与同伴分离的较远的某个个体,为了纠正这个行为,将会在下一时刻撞上其他同伴,如果其他同伴也做出了相同的纠正行为的话。因此,鸟群会在保持队形和避免碰撞之间找到平衡。

  1. AI角色的“思考速度”

  一般来说,我们将帧率与AI角色的思考速度分开。许多游戏对于动画,AI和输入信息在相同的循环中进行更新,但也有许多游戏,将它们分开更新。

  首先,很容易看出,让AI角色的思考速度快于帧率是没有任何意义的,因为即使思考的再快,也必须等到动画系统调用的时候,才能做出移动,绝大多数游戏中,动画系统和物理系统的速率是一致的,AI无需高于这个速率。对于手机游戏,由于资源有限,可能会有少量的快速动画,但较慢的整体速率,此时AI只需要小于等于这个较慢的整体速率就可以了。

  让AI角色的思考速度与系统帧率相同并不是一个好的选择,例如,AI控制的汽车不能频繁的变道,必须加上一些抑制措施。而且,思考过快的AI角色会让玩家十分困惑,不知道它在想什么。因此,在设计游戏的时候,需要对AI角色的反应速度施加限制。

  如果我们把AI角色的思考速度降低到每秒一次,那么就有些慢了,这个速率对确保安全也许是够的,但司机会错过很多机会,看起来像是在做白日梦一样。如果再进一步降低到2秒一次,那么将会引起碰撞,或一些车辆可能会在车道上穿过其他车辆。

实现AI角色的自主移动——操控行为相关推荐

  1. Unity人工智能编程精粹学习笔记 实现AI角色的自主移动——操控行为

    目录 操控行为 操控行为编程的主要基类 个体AI角色的操控行为 群体的操控行为 个体与群体的操控行为组合 几种操控行为的编程解析 操控行为的快速实现 操控行为编程的其他问题 总结 源码工程下载链接 操 ...

  2. 实现AI角色的自主移动-避开障碍物Obstacle行为

    避开障碍物(Obstacle)行为 避开障碍物行为是指操控AI角色避开路上的障碍物,例如动物奔跑时,避免与树.墙碰撞.当AI角色的行进路线上发现比较近的障碍时,产生一个排斥力,是AI角色远离这个障碍物 ...

  3. 实现AI角色的自主移动-追逐Pursuit行为

    追逐(Pursuit)行为 追逐行为也叫拦截行为,与靠近算法很像,只不过如果模拟动物追逐猎物的时候,绝不是直接向猎物的当前位置奔跑,而是预测猎物的未来位置,然后向着未来位置的方向追去,这样才能在最短时 ...

  4. 大型AI已有自主意识了?LeCun开喷Open AI首席科学家

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 来源丨新智元 编辑丨极市平台 导读 作为AI界的明星研究所,老是搞事 ...

  5. Unity人工智能编程精粹学习笔记 AI角色的复杂决策——行为树

    要让游戏里的AI角色能执行预设的逻辑,最直接的方法是依照行为逻辑直接编写代码,但是这种方法工作量大,也容易出错.我们也可以用有限状态机来实现行为逻辑,但是有限状态机难以模块化,编写代码麻烦且容易出错. ...

  6. LeCun的62页新作:未来十年的AI研究计划——自主机器智能

    点击下方卡片,关注"CVer"公众号 AI/CV重磅干货,第一时间送达 点击进入-> CV 微信技术交流群 丰色 发自 凹非寺 转载自:量子位(QbitAI) 这段时间,关于 ...

  7. Unity3D AI角色移动和自动跟随

    AI角色按照一定的移动逻辑进行移动时,AI逻辑处理角色需要移动的位置,还需要移动逻辑实时处理角色的当前速度和角度.官方的示例中角色的移动是通过设置速度之后,依靠动画的位移来实现的,所以代码中只需要设置 ...

  8. LeCun指明下一代AI方向:自主机器智能

    在这篇长达 62 页的论文中,LeCun 表示,这篇论文提炼了他对过去 5 年 - 10 年关于 AI 发展大方向的思考,这基本上是他计划在接下来的 10 年中开展的工作,也是他希望激励其他人从事的工 ...

  9. 飞腾 CPU x 百度昆仑 AI 芯片!自主国产算力时代已经到来!

    近日,百度智能芯片总经理欧阳剑在一次线上公开课中首次展示了百度昆仑 AI 芯片与飞腾 CPU 的适配视频片段.视频显示,搭载飞腾 CPU.昆仑加速卡的服务器,运行流畅,性能强劲,图像分割速度显著加速. ...

最新文章

  1. 用户体验分析:以 “师路南通网站” 为例
  2. 【新星计划】Matlab绘制分岔图
  3. 天翼云从业认证课后习题(3.2天翼云存储产品和3.4云数据库)
  4. c# 中的as,is
  5. tableview 分组显示返回footerviewt和headerView的高度不能为0的问题
  6. node --- [跨域] 预检请求
  7. 基于物理渲染的基础理论
  8. 看到好文章了(linux内核方面),大家共分享
  9. Java 案例大全(详细)一
  10. PMP证书考试试题有多少
  11. 系统讲解一下整站SEO优化都涉及到哪些工作?
  12. 用C语言求解一元二次方程的简单方法
  13. 三星刷完机后一直停留在Samsung页面
  14. 【Paper】法律科技方面论文汇总
  15. 解决linux下svn update 产生Node remains in conflict的问题
  16. SSH连接服务器Secure CRT技巧[Secure CRT连接ubuntu显示密钥交换失败][Ubuntu无法使用root用户登陆的解决办法]
  17. 人工智能在医学影像中的研究与应用
  18. 微信支付服务商,可视化进件特约商户
  19. Mac os x 的一些小技巧
  20. 网易2016实习研发笔试

热门文章

  1. Mybatis面试常问
  2. VLC可用的模块列表
  3. 什么是等保?为什么做等保?如何做等保?
  4. 【echarts】echarts绘制关系图
  5. python避坑_使用Python时多少有人走过的坑!避险!
  6. 微信小程序:强大多流量主自带接口短视频去水印工具箱微信小程序
  7. SAP BW QUERY VARIABLES (BW增强变量)
  8. ArcGIS Runtime SDK是什么?
  9. 常用xshell5基本命令
  10. mac虚拟摄像头插件_Mobiola WebCamera(网络虚拟摄像头) V2.4 Mac版