目录
操控行为
操控行为编程的主要基类
个体AI角色的操控行为
群体的操控行为
个体与群体的操控行为组合
几种操控行为的编程解析
操控行为的快速实现
操控行为编程的其他问题
总结
源码工程下载链接
“操控行为”是指操作控制角色,让它们能以模拟真实的方式在游戏世界中移动。它的工作方式是通过产生一定大小和方向的操控力,使角色以某种方式运动。它属于AI模型中的运动层。
(1)操控行为包括一组基本“行为”。对于单独的AI角色,基本操控行为包括:
基本行为中的每一个行为,都产生相应的操控力,使这些操控力以一定的方式组合起来(实际上就相当于将这些基本“行为”进行了不同的组合),就能够得到更复杂的“行为”,从而实现更为高级的目标。
(2)对于组成小队或群体的多个AI角色,包括基本的组行为如下。
操控行为中的一些中英文术语表如下:
主要涉及到Vehicle类、AILocomotion类和Steering类,它们是实现操控行为的基础。
将AI角色抽象为一个质点——Vehicle类
它包含位置(position)、质量(mass)、速度(velocity)等信息,而速度随着所加力的变化而变化,由于速度是实际物体实体,施加在其上的力和能达到的速度都是有限制的,因此还需要最大力(max_force)和最高速度(max_speed)两个信息,除此之外,还要包含一个朝向(orientation)的信息。
它的位置的计算方法:
(1)确定每一帧的当前操控力(最大不超过max_force);
(2)除以交通工具的质量mass,可以确定一个加速度;
(3)将这个加速度与原来的速度相加,得到新的速度(最大不超过max_speed);
(4)根据速度和这一帧的流逝时间,计算出位置的变化;
(5)与原来的位置相加,得到新位置。
using UnityEngine;
namespace AI
{
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 IsPlannar = true;
///
/// 计算得到的操控力
///
private Vector3 _steeringForce;
///
/// AI角色的加速度
///
protected Vector3 _acceleration;
///
/// 计时器
///
private float _timer;
protected void OnStart()
{
_steeringForce = Vector3.zero;
_sqrMaxSpeed = MaxSpeed * MaxSpeed;
_timer = 0;
_steerings = GetComponents();
}
private void Update()
{
_timer += Time.deltaTime;
_steeringForce = Vector3.zero;
//如果距离上次计算操控力的时间大于了设定的时间间隔
//再次计算操控力
if (_timer > ComputeInterval)
{
foreach (var steering in _steerings)
{
if (steering.enabled)
{
_steeringForce += steering.Force() * steering.Weight;
}
}
//操控力不能大于MaxForce
_steeringForce = Vector3.ClampMagnitude(_steeringForce, MaxForce);
//力除以质量,求出加速度
_acceleration = _steeringForce / Mass;
//计时器归0
_timer = 0;
}
}
}
}
控制AI角色移动AILocomotion类
这个类是Vehicle的派生类,它能真正控制AI角色的移动,包括每次移动的距离,播放动画等
using UnityEngine;
namespace AI
{
public class AILocomotion : Vehicle
{
///
/// AI角色的控制器
///
private CharacterController _controller;
///
/// AI的Rigidbody
///
private Rigidbody _rigidBody;
///
/// AI每次移动的距离
///
private Vector3 _moveDistance;
void Start()
{
_controller = GetComponent();
_rigidBody = GetComponent();
_moveDistance = Vector3.zero;
OnStart();
}
///
/// 物理的操作在此函数中更新
///
private void FixedUpdate()
{
Velocity += _acceleration * Time.fixedDeltaTime;
//限制最大速度
if (Velocity.sqrMagnitude > _sqrMaxSpeed)
{
Velocity = Velocity.normalized * MaxSpeed;
}
_moveDistance = Velocity * Time.fixedDeltaTime;
//在平面上移动
if (IsPlannar)
{
Velocity.y = 0;
_moveDistance.y = 0;
}
//使其移动
if (_controller != null)
{
_controller.SimpleMove(Velocity);
}
else if (_rigidBody == null || _rigidBody.isKinematic)
{
transform.position += _moveDistance;
}
else
{
_rigidBody.MovePosition(_rigidBody.position + _moveDistance);
}
//更新朝向
if (Velocity.sqrMagnitude > 0.00001)
{
//通过当前朝向和速度方向的插值计算新的朝向
Vector3 newForward = Vector3.Slerp(transform.forward,
Velocity, Damping * Time.deltaTime);
if (IsPlannar)
{
newForward.y = 0;
}
transform.forward = newForward;
}
Debug.Log($"播放行走动画!!!!");
}
}
}
各种操控行为的基类——Steering类
Steering类是所有操控行为的基类,包含操控行为共有的变量和方法,操控AI角色的寻找、逃跑、追逐、躲避、徘徊、分离、队列、聚集等都可由此派生。
using UnityEngine;
namespace AI
{
public abstract class Steering : MonoBehaviour
{
public float Weight = 1;
private void Start()
{
}
private void Update()
{
}
public virtual Vector3 Force()
{
return new Vector3();
}
}
}
靠近
操控行为中的靠近是指,指定一个目标位置,根据当前的运动速度向量,返回一个操控AI角色达到该目标位置的操控力,使AI角色自动向该位置移动。
要想让AI角色靠近目标,首先需要计算出AI角色在理想情况下达到目标的预期速度。该速度可以看作是从AI角色的当前位置到目标位置的向量,操控向量是预期速度与AI角色当前速度的差,该向量大小随着当前位置变化而变化,从而形成如下图中该角色的寻找路径
using UnityEngine;
namespace AI
{
public class SteeringForSeek : Steering
{
///
/// 需要寻找的目标
///
public GameObject Target;
///
/// 预期的速度
///
private Vector3 _desiredVelocity;
///
/// 获得被操控的AI角色
///
private Vehicle _vehicle;
///
/// 最大速度
///
private float _maxSpeed;
///
/// 是否在二维平面上运动
///
private bool _isPlanar;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_isPlanar = _vehicle.IsPlannar;
}
public override Vector3 Force()
{
_desiredVelocity = (Target.transform.position
- transform.position).normalized * _maxSpeed;
if (_isPlanar)
{
_desiredVelocity.y = 0;
}
return _desiredVelocity - _vehicle.Velocity;
}
}
}
离开
离开和靠近行为正好相反,它产生一个操控AI角色离开目标的力,而不是靠近目标的力,它们之间的唯一区别是DesiredVelocity具有相反的方向,参见上图中的离开操控力和离开路径。
接着,还可以进一步调整,只有当AI角色进入目标周围一定范围内时,才产生离开的力,这样可以模拟出AI角色的有限感知范围。
这里采用了Vector3.Distance函数来计算当前位置与目标位置之间的距离,事实上,如果采用Vector3.sqrMagnitude函数,将会得到更快的计算速度,因为省去了计算平方根的时间,这时可以预先计算fearDistance的平方并存储到一个变量中。
using UnityEngine;
namespace AI
{
public class SteeringForFlee : Steering
{
public GameObject Target;
///
/// 使AI角色意识到危险并开始逃跑的范围
///
public float FearDistance = 20;
private Vector3 _desiredVelocity;
private Vehicle _vehicle;
private float _maxSpeed;
private float _sqrFearDistance;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_sqrFearDistance = FearDistance * FearDistance;
}
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角色与目标的距离大于逃避距离,那么无作用力
if (Vector3.SqrMagnitude(tmpPos - tmpTargetPos) > _sqrFearDistance)
{
return Vector3.zero;
}
_desiredVelocity = (transform.position - Target.transform.position).normalized * _maxSpeed;
return _desiredVelocity - _vehicle.Velocity;
}
}
}
抵达
有时我们希望AI角色能够减速并停到目标位置,避免冲过目标。
在角色距离目标较远时,抵达与靠近行为的状态是一样的,但是接近目标时,不再是全速向目标移动,而代之以使AI角色减速,直到最终恰好停在目标位置,如下图所示:
何时开始减速是通过参数设置的,这个设置可以看成是停止半径。当角色在停止半径之外时,以最大速度移动;当角色进入停止半径之内时,逐渐减小预期速度,直到减小为0。这个参数的设置很关键,它决定了抵达行为的最终效果。
using UnityEngine;
namespace AI
{
public class SteeringForArrive : Steering
{
public bool IsPlanar = true;
///
/// 当目标小于这个距离时,开始减速
///
public float SlowDownDistance;
public GameObject Target;
private Vehicle _vehicle;
private float _maxSpeed;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
IsPlanar = _vehicle.IsPlannar;
}
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)
{
desiredVelocity = toTarget.normalized * _maxSpeed;
returnForce = desiredVelocity - _vehicle.Velocity;
}
else
{
Debug.Log($"已进入减速区域!!!");
desiredVelocity = toTarget - _vehicle.Velocity;
returnForce = desiredVelocity - _vehicle.Velocity;
}
return returnForce;
}
private void OnDrawGizmos()
{
if (Target == null)
{
return;
}
Gizmos.DrawWireSphere(Target.transform.position, SlowDownDistance);
}
}
}
追逐
追逐行为与靠近行为很相似,只不过目标不再是静止不动,而是另一个可移动的角色。最简单的追逃方式是直接向目标的当前位置靠近,不过这样看上去很不真实。举例来说,当动物追逐猎物的时候,绝不是直接向猎物的当前位置奔跑,而是预测猎物的未来位置,然后向着未来位置的方向追去,这样才能在最短时间内追上猎物。在AI中,把这种操控行为称为“追逐”。
下图画出来被追逐的目标体,图中表明了它的当前位置和一段时间之后的预测位置,还画出了追逐者、它的当前速度和方向以及实际追逐路径。可以看出,追逐是朝向未来位置,而不是当前位置。
怎样实现这种智能的追逐行为呢?我们可以使用一个简单的预测器,在每一帧重新计算它的值。假设采用一个线性预测器,又假设在预测间隔T时间内角色不会转向,角色经过T之后的未来位置可以用当前速度乘以T来确定,然后把得到的值加到角色的当前位置上,就可以得到预测位置了,最后,再以预测位置作为目标,应用靠近行为就可以了。
实现追逐行为的一个关键是如何确定时间间隔T。可以把它设为一个常数,也可以当追逐者距离目标较远时设为较大的值,而接近目标时设为较小的值。
这里,设定预测时间和追逐者与逃避者之间的距离成正比,与二者的速度成反比。
一些情况下,追逐可能会提前结束。例如,如果逃避者在前面,几乎面对追逐者,那么追逐者应该直接向逃避者的当前位置移动。二者之间的关系可以通过计算逃避者朝向向量与AI角色朝向向量的点积得到,当逃避者朝向的方向和AI角色的朝向大约在一个极小的范围(20°)之内,才可以被认为是面对着的。
using UnityEngine;
namespace AI
{
public class SteeringForPursuit : Steering
{
public GameObject Target;
private Vehicle _vehicle;
private float _maxSpeed;
private Vehicle _targetVehicle;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_targetVehicle = Target.GetComponent();
}
public override Vector3 Force()
{
Vector3 toTarget = Target.transform.position - transform.position;
//计算追逐者的前向与逃避者的前向之间的夹角
float relationDirection = Vector3.Dot(transform.forward, Target.transform.forward);
Vector3 desiredVelocity;
//如果夹角大于0,且追逐者基本面对逃避者,那么直接向逃避者当前位置移动
if ((Vector3.Dot(toTarget, transform.forward) > 0) && relationDirection < -0.95f)
{
desiredVelocity = (Target.transform.position - transform.position).normalized * _maxSpeed;
return (desiredVelocity - _vehicle.Velocity);
}
//计算预测时间,正比于追逐者和逃避者之间的距离,反比于追逐者和逃避者的速度和
float lookaheadTime = toTarget.magnitude / (_maxSpeed + _targetVehicle.Velocity.magnitude);
desiredVelocity = (Target.transform.position + _targetVehicle.Velocity * lookaheadTime -
transform.position).normalized * _maxSpeed;
return desiredVelocity - _vehicle.Velocity;
}
}
}
逃避
逃避行为是使猎物躲避捕猎者。
逃避行为与追逐行为不同的是它试图使AI角色逃离预测位置。
using UnityEngine;
namespace AI
{
public class SteeringForEvade : Steering
{
public GameObject Target;
private Vehicle _vehicle;
private Vehicle _targetVehicle;
private float _maxSpeed;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_targetVehicle = Target.GetComponent();
}
public override Vector3 Force()
{
Vector3 toTarget = Target.transform.position - transform.position;
//预测时间
float lookaheadTime = toTarget.magnitude / (_maxSpeed + _targetVehicle.Velocity.magnitude);
//预期速度
Vector3 desiredVelocity =
(transform.position - (Target.transform.position + _targetVehicle.Velocity * lookaheadTime))
.normalized * _maxSpeed;
return desiredVelocity - _vehicle.Velocity;
}
}
}
随机徘徊
许多时候,人们需要让游戏中的角色在游戏环境中随机移动(如巡逻的士兵、惬意吃草的牛羊等),就像这些角色是在等待某些事情发生,或者在寻找什么东西。当角色出现在玩家的视线范围内时,人们通常希望这种随机移动看上去是真实的,如果玩家法线角色实际上是在沿预先定义好的路径移动,就会有不真实的感觉,那么便会影响到他的游戏体验。
随机徘徊操控行为就是让角色产生有真实感的随机移动。这会让玩家感觉到角色是有生命的,而且正在到处移动。
利用操控行为来实现随机徘徊有多种不同的方法,最简单的方式是利用前面提到的靠近行为。在游戏场景中随机地放置目标,让角色靠近目标,这样AI角色就会向目标移动,如果每隔一段时间(如几秒)就改变目标的位置,这样角色就永远靠近目标而又不能到达目标(即使到达了,目标也会再次移动)。这个方法很简单,粗略看上去也很不错,但是最终结果可能不尽如意。角色有时会突然掉头,因为目标移动到了它的后面。CraigReynolds提出的随机徘徊操控行为解决了这个问题。
解决问题的工作原理同内燃机的气缸曲轴传动相似,见下图所示。
在角色(气缸)通过连杆联结到曲轴上,目标被限定曲轴圆周上,移向目标(利用靠近行为)。为了看似随机徘徊,每帧给目标加一个随机位移,这样,目标便会沿着圆周不停地移动。将目标限制在这个圆周上,是为了对角色进行限制,使之不至于突然改变路线。这样,如果角色现在是在向右移动,下一时刻它仍然是在向右移动,只不过与上一时刻相比,有了一个小的角度差,利用不同的连杆长度(Wander距离)、角色到圆心的距离(Wander半径)、每帧随机偏移的大小,就可以产生各种不同的随机运动,像巡逻的士兵、惬意吃草的牛羊等。
using UnityEngine;
namespace AI
{
public class SteeringForWander : Steering
{
///
/// 徘徊半径,Wander圈的半径
///
public float WanderRaius;
///
/// 徘徊距离,Wander圈凸出在AI角色前面的距离
///
public float WanderDistance;
///
/// 每秒加到目标的随机位移的最大值
///
public float WanderJitter;
public bool IsPlannar;
private Vehicle _vehicle;
private float _maxSpeed;
private Vector3 _circleTarget;
private Vector3 _wanderTarget;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
IsPlannar = _vehicle.IsPlannar;
//选取圆圈上的一个点作为初始点
_circleTarget = new Vector3(WanderRaius * 0.707f,0,WanderRaius * 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 (IsPlannar)
{
randomDisplacement.y = 0;
}
//将随机位移加到初始点上,得到新位置
_circleTarget += randomDisplacement;
//由于新位置可能不在圆周上,因此需要投影到圆周上
_circleTarget = WanderRaius * _circleTarget.normalized;
//之前计算出的值是相对于AI角色和AI角色的向前方向的,需要转化为世界坐标
_wanderTarget = _vehicle.Velocity.normalized * WanderDistance
+ _circleTarget + transform.position;
//计算预期速度
Vector3 desiredVelocity = (_wanderTarget - transform.position).normalized * _maxSpeed;
return desiredVelocity - _vehicle.Velocity;
}
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, WanderRaius);
Gizmos.DrawWireSphere(_wanderTarget, 3);
}
}
}
路径跟随
就像赛车在赛道上需要导航一样,路径跟随会产生一个操控力,使AI角色沿着预先设置的轨迹,构成路径的一系列路径移动。
最简单的跟随路径方式是将当前路点设置为路店列表中的第一个路点,用靠近行为产生操控力来靠近这个路点,直到非常接近这个点;然后寻找下一个路点,设置为当前路点,再次接近它。重复这样的过程直到到达路点列表中的最后一个路点,再根据需要决定是回到第一个路点,还是停止到这最后一个路点上,如下图所示:
这里假设路径是开放的,角色需要减速停止到最后一个路点上,因此需要用到抵达行为和路径跟随行为。
有时路径有起点和终点,有时路径是循环的,是一个永不结束的封闭路径。如果路径是封闭的,那么需要回到起点重新开始;如果是开放的,那么AI角色需要减速(利用抵达行为)停到最后一个路点上。
在实现路径跟随行为时,需要设置一个路点半径参数,即当AI角色距离当前路点多远时,可以认为它已经到达当前路点,从而继续下一个路点前进。这个参数的设置会引起路径形状的变化,如下图所示:
using UnityEngine;
namespace AI
{
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 Vehicle _vehicle;
private float _maxSpeed;
private bool _isPlanar;
///
/// 当与目标的距离小于这个距离时开始减速
///
public float SlowDownDistance;
void Start()
{
_numberOfNodes = WayPoints.Length;
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_isPlanar = _vehicle.IsPlannar;
//设置当前路径点为第0个路点
_currentNode = 0;
_target = WayPoints[_currentNode].transform;
_arriveDistance = 1.0f;
_sqrArriveDistance = _arriveDistance * _arriveDistance;
}
public override Vector3 Force()
{
Vector3 force = Vector3.zero;
Vector3 desiredVelocity;
Vector3 dist = _target.position - transform.position;
if (_isPlanar)
{
dist.y = 0;
}
//如果当前路点已经时最后一个
if (_currentNode == _numberOfNodes - 1)
{
//如果与当前路径点的距离大于减速距离
if (dist.magnitude > SlowDownDistance)
{
desiredVelocity = dist.normalized * _maxSpeed;
force = desiredVelocity - _vehicle.Velocity;
}
else
{
//与当前路点的距离小于减速距离,开始减速,计算操控向量
desiredVelocity = dist - _vehicle.Velocity;
force = desiredVelocity - _vehicle.Velocity;
}
}
else
{
//当前路点不是最后一个路点,判断是否在抵达范围内
if (dist.sqrMagnitude < _sqrArriveDistance)
{
//已经在抵达范围内了,将下一个路点设置为目标点
_currentNode++;
_target = WayPoints[_currentNode].transform;
}
else
{
desiredVelocity = dist.normalized * _maxSpeed;
force = desiredVelocity - _vehicle.Velocity;
}
}
return force;
}
private void OnDrawGizmos()
{
if (WayPoints == null)
{
return;
}
foreach (var wayPoint in WayPoints)
{
Gizmos.DrawWireSphere(wayPoint.transform.position, SlowDownDistance);
}
}
}
}
避开障碍
避开障碍行为是指操控AI角色避开路上的障碍物,例如在动物奔跑时避免与树、墙碰撞。当AI角色的行进路线上发现比较近的障碍时,产生一个排斥力,使AI角色远离这个障碍物;当前方有多个障碍物时,只产生躲避最近的障碍物的力,如下图所示,这样,AI角色就会一个接一个地躲避这些障碍物。
在这个算法中,首先需要发现障碍物。每个AI角色唯一需要担心地障碍物就是挡在它行进路线前方地那些物体,其他远离地障碍物可以不必考虑。该算法地分析步骤如下:
(1)速度向量代表了AI角色地行进方向,可以利用这个速度向量生成一个新的向量ahead,它的方向与速度向量一致,但长度有所不同。这个ahead向量代表了AI角色的视线范围,其计算方法为:
ahead = positition + normalize(velocity) * Max_See_Ahead
其中ahead向量的长度(Max_See_Ahead)定义了AI角色能看到的距离。Max_See_Ahead值越大,AI角色看到障碍的时间就越早,因此,它开始躲避障碍的时间也越早。
(2)每个障碍物都要用一个几何形状来表示,这里采用包围球来标识场景中的每个障碍。
一种可能的方法是检测从AI角色向前延申的ahead向量与障碍物的包围球是否相交。但这里采用简化的方法。
使用另外一个向量ahead2,这个向量的长度是ahead的一般
ahead = positition + normalize(velocity) * Max_See_Ahead * 0.5
(3)接下来进行一个碰撞检测,来测试这两个向量是否在障碍物的包围球内。方法很简单,只需要比较向量的终点与球心的距离d就可以了,如果小于等于球的半径,那么就认为向量在包围球内,会发生碰撞。
如果ahead和ahead2中的任一向量在包围球内,那么说明障碍物挡在前方。
如果有多个障碍物挡住了路,那么选择最近的那个障碍物进行计算。
(4)接下来,计算将AI角色推离障碍物的操控力。
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角色全速移动时,它的值就是1。
using UnityEngine;
namespace AI
{
public class SteeringForCollisionAvoidance : Steering
{
public bool IsPlannar;
private Vector3 _desiredVelocity;
private Vehicle _vehicle;
private float _maxSpeed;
private float _maxForce;
///
/// 避免障碍所产生的操控力
///
public float AvoidanceForce;
///
/// 能向前看到的最大距离
///
public float MAX_SEE_AHEAD = 2.0f;
///
/// 场景中所有碰撞体组成的数组
///
private GameObject[] _allColliders;
void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
_maxForce = _vehicle.MaxForce;
IsPlannar = _vehicle.IsPlannar;
//如果避免障碍物产生的操控力大于最大操控力,将它截断到最大操控力
if (AvoidanceForce > _maxForce)
{
AvoidanceForce = _maxForce;
}
//存储场景中的所有碰撞体,即Tag为obstacle的那些游戏体
_allColliders = GameObject.FindGameObjectsWithTag("obstacle");
}
public override Vector3 Force()
{
RaycastHit hit;
Vector3 force = Vector3.zero;
Vector3 velocity = _vehicle.Velocity;
Vector3 normalizedVelocity = velocity.normalized;
float dynamicLength = MAX_SEE_AHEAD * (velocity.magnitude / _maxSpeed);
//画出一条射线,需要考查与这条射线相交的碰撞体
Debug.DrawLine(transform.position,
transform.position + normalizedVelocity *dynamicLength );
if (Physics.Raycast(transform.position, normalizedVelocity, out hit, dynamicLength))
{
//如果射线与某个碰撞体相交,表示可能跟该碰撞体发生碰撞
Vector3 ahead = transform.position + normalizedVelocity * dynamicLength;
force = ahead - hit.collider.transform.position;
force *= AvoidanceForce;
if (IsPlannar)
{
force.y = 0;
}
foreach (var collider in _allColliders)
{
if (hit.collider.gameObject == collider)
{
//有可能相撞
collider.GetComponent().material.color = Color.black;
}
else
{
collider.GetComponent().material.color = Color.white;
}
}
}
else
{
//如果向前看的范围内,没有发生碰撞的可能
foreach (var collider in _allColliders)
{
collider.GetComponent().material.color = Color.white;
}
}
return force;
}
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, MAX_SEE_AHEAD);
}
}
}
组行为
正如大多数人工生命仿真一样,组行为是展示操控行为的一个很好的例子,它的复杂性来源于个体之间的交互,并遵守一些简单的规则。
模仿群体行为需要以下几种操控行为:
检测附近的AI角色
从上面的几种操控行为可以看出,每种操控行为都决定角色对相邻的其他角色做出何种反应。为了实现组行为,首先需要检测位于当前AI角色“邻域”中的其他AI角色,这要用一个雷达脚本来实现。
如下图2.27(a)所示,一个角色的邻域由一个距离和一个角度来定义。例如图中的灰色区域当其他角色位于这个灰色区域的邻域时,便认为是AI角色的邻居,否则将会被忽略,而只用一个圆来定义邻域。
在图2.27(b)中,白色的小三角表示当前操控的AI角色,灰色的圆显示它的邻域,因此,所有黑色的小三角是AI角色的邻居,而灰色的不是。
using System.Collections.Generic;
using AI;
using UnityEngine;
public class Radar : MonoBehaviour
{
///
/// 碰撞体的数组
///
private Collider[] _colliders;
///
/// 计时器
///
private float _timer = 0;
///
/// 邻居列表
///
public List NeighBors;
///
/// 检测时间间隔
///
public float CheckInterval = 0.3f;
///
/// 检测半径
///
public float DetectRadius = 10;
///
/// 设置检测到哪一层的游戏对象
///
public LayerMask LayerChecked;
private void Start()
{
NeighBors = new List();
}
private void Update()
{
_timer += Time.deltaTime;
//如果距离上次检测的时间大于所设置的检测时间间隔,那么重新检测
if (_timer > CheckInterval)
{
//清除邻居列表
NeighBors.Clear();
//查找当前AI角色邻域内的所有碰撞体
_colliders = Physics.OverlapSphere(transform.position, DetectRadius, LayerChecked);
for (int i = 0,count = _colliders.Length; i < count; i++)
{
var colliderItem = _colliders[i];
if (colliderItem.GetComponent())
{
NeighBors.Add(colliderItem.gameObject);
}
}
_timer = 0;
}
}
}
当然,这只是一个简单的实现,使用时还可以进一步增加可视区域的限制(只需测试AI角色的朝向向量与潜在邻居的向量之间的点积,就可以实现这个功能),甚至可以动态调整AI角色的可视域,例如,在战争游戏中,士兵的可视域可能会受到疲劳的影响,降低察觉环境的能力。
与群中邻居保持适当距离——分离
分离行为的作用是使角色与周围的其他角色保持一定的距离,这样可以避免多个角色相互挤到一起。当分离行为应用在许多AI角色(如鸟群中的鸟)上时,它们将会向四周散开,尽可能地拉开距离。
在图2.28中,黑色的小三角表示当前的AI角色,它有三个邻居,用白色的小三角表示,分离行为会试图使AI角色与邻居保持一定的距离。图中的黑色箭头表示在三个邻居的“排斥”作用下,AI角色所受到的操控力。在这个操控力的作用下,AI角色会增大与三个邻居的距离,防止过于拥挤。
实现时,为了计算分离行为所需的操控力,首先搜索指定邻域内的其他邻居,然后对每个邻居,计算AI角色到该邻居的向量r,将向量r归一化( r/|r| ),得到排斥力的方向,由于排斥力的大小与距离成反比,因此还需要除以 |r|,,得到该邻居对它的排斥力,然后把所有来自邻居的排斥力相加,得到分离行为的总操控力。
Force = ∑ r/ (|r| * |r|) ,r取AI角色到邻居的向量
using UnityEngine;
namespace AI
{
public class SteeringForSeparation : Steering
{
///
/// 可接受的距离
///
public float ComfortDistance = 1;
///
/// 当AI角色与邻居之间距离过近时的惩罚因子
///
public float MultiplierInsideComfortDistance = 2;
public override Vector3 Force()
{
Vector3 force = Vector3.zero;
//遍历这个AI角色的邻居列表中的每个邻居
foreach (var s in GetComponent().NeighBors)
{
//如果s不是当前AI角色
if ((s != null) && s != gameObject)
{
//计算AI角色与邻居s之间的距离
Vector3 toNeighBor = transform.position - s.transform.position;
float length = toNeighBor.magnitude;
force += toNeighBor.normalized / length;
//如果二者之间的距离小于了最大可接受距离,额外乘以一个惩罚因子,让它更快的远离
if (length < ComfortDistance)
{
force *= MultiplierInsideComfortDistance;
}
}
}
return force;
}
}
}
与群中邻居朝向一致——队列
队列行为试图保持AI角色的运动朝向与邻居一致,这样就会得到像鸟群朝着一个方向飞行的效果。
通过迭代所有邻居,可以求出AI角色朝向向量的平均值以及速度向量的平均值,得到想要的朝向,然后减去AI角色的当前朝向,就可以得到队列操控力。
如图2.30所示,中间的黑色小三角表示当前的AI角色,它当前的运动方向是尖端的灰色线指示的方向,周围的白色小三角表示它的邻居。通过计算得到这些邻居的平均朝向是中间黑小三角尖端的深色线指示的方向。这条深色线(目标朝向)与灰色线(当前朝向)之间的差值便是操控力的方向,即操控向量。
using UnityEngine;
namespace AI
{
public class SteeringForAlignment : Steering
{
public override Vector3 Force()
{
//当前AI角色的邻居的平均朝向
Vector3 averageDirection = Vector3.zero;
//邻居的数量
int neighBorCount = 0;
foreach (var s in GetComponent().NeighBors)
{
//如果不是当前角色
if ((s != null) && s != gameObject)
{
//将s的朝向向量加到averageDirection上
averageDirection += s.transform.forward;
//邻居数量
neighBorCount++;
}
}
if (neighBorCount > 0)
{
//将累加得到的朝向向量除以邻居的个数,求出平均朝向向量
averageDirection /= (float) (neighBorCount);
//平均朝向向量减去当前朝向向量,得到操控向量
averageDirection -= transform.forward;
}
return averageDirection;
}
}
}
成群聚集在一起——聚集
聚集行为产生一个使AI角色移向邻居的质心的操控力。这个操控力使得多个AI角色聚集到一起。
如图2.32所示,中心的黑色小三角表示这个AI角色的4个邻居,与这4个邻居都有连接线的小点是这4个邻居位置的平均值,从黑色小三角指向这个小点的箭头表示作用于AI角色的操控力。
实现时,迭代所有邻居求出AI角色位置的平均值,然后利用靠近行为,将这个平均值作为目标位置。
using UnityEngine;
namespace AI
{
public class SteeringForCohesion : Steering
{
private Vector3 _desiredVelocity;
private Vehicle _vehicle;
private float _maxSpeed;
private void Start()
{
_vehicle = GetComponent();
_maxSpeed = _vehicle.MaxSpeed;
}
public override Vector3 Force()
{
Vector3 force = Vector3.zero;
//AI角色的所有邻居的质心,即平均位置
Vector3 centerOfMass = Vector3.zero;
//AI角色的邻居的数量
int neighborCount = 0;
foreach (var s in GetComponent().NeighBors)
{
//如果s不是当前AI角色
if (s != null && s != gameObject)
{
//累加s的位置
centerOfMass += s.transform.position;
//邻居数量增加
neighborCount++;
}
}
if (neighborCount > 0)
{
//得到平均值
centerOfMass /= (float) neighborCount;
}
//预期速度为邻居位置平均值与当前速度之差
_desiredVelocity = (centerOfMass - transform.position).normalized * _maxSpeed;
//预期速度减去当前速度求出操控向量
force = _desiredVelocity - _vehicle.Velocity;
return force;
}
}
}
组行为的基础是分离、队列和聚集,根据实际需要,可以与前面的个体操控行为相结合,例如避开障碍、随机徘徊、靠近与抵达,从而产生更复杂的行为。
示例
如果想让角色从道路A上移动到B(路径跟随),在路上要避开障碍,还要避开其他角色(分离),就要将这三种行为组合起来。
如果想要实现一个鹿群的行为,它们是群聚的(包括分离、聚集和队列),同时还在环境中随机徘徊,当然还要避开石头和树木(避开障碍)。另外,当它们遇到狼走近时就四处逃散(逃避),该怎么办?
前面介绍了一些基本的操控行为,可以实现追逐、逃避、路径跟随、聚集等行为。在实际应用中,为了得到理想的行为,常常将操控行为组合起来使用,当然,也可以直接把这些行为都加进去,但问题是,当AI角色徘徊时,就不需要逃散,当逃散时,既不需要徘徊,也不需要聚集和队列等,这时该怎么办呢?
解决方案
在AI角色的Vehicle类中,有一个sterring的实例,可以通过它来动态开启或关闭某种行为,从而激活或注销这个行为,那么如何决定开启或关闭的不同行为的时间呢?这就需要在更高的决策层做出决定,这个决策层可以利用后面的状态机来实现。例如:
如果与玩家的距离小于60m,AI角色的生命值大于80,那么追逐玩家;
如果与玩家的距离小于60m,AI角色的生命值小于80,那么逃避;
否则,在场景中徘徊。
那么,如何组合多个行为呢?最简单的方式时直接把各个行为所产生的操控力加到一起,这样得到的总操控力会反映这些行为。
foreach (var steering in _steerings)
{
if (steering.enabled)
{
_steeringForce += steering.Force() * steering.Weight;
}
}
//操控力不能大于MaxForce
_steeringForce = Vector3.ClampMagnitude(_steeringForce, MaxForce);
这段代码的目的就是计算所有已激活行为的合力,当实现组合行为时,就把所有的行为产生的操控力都考虑在内。但是,需要确保得到的值不能超过最大力的限制。实现截断有不同的操控方式,最简单的称为加权截断总和。即为每个操控力乘上一个权重,将它们加到一起,然后将结果截断到允许的最大操控力。
这种方式很简单,但当操控力相互冲突时,有时会产生问题。
其他截断方式还有带优先级的加权截断累计、带优先级的抖动等方法。
模拟鸟群飞行
模拟鸟群飞行时,我们希望鸟群看上去既不要像行军般单调一致,也不要像分子运动那样完全随机,这里的关键在于只设定了每只鸟的简单移动方式。每只鸟都有这样的行为模式:
由于某些实际物体的可视范围有限,有可能出现某个物体与它的集群“隔绝”的情况,这时,它将停下来什么都不做。为了避免这种情况的发生,可以把随机徘徊行为也包含在内。
多AI角色障碍赛
在原先抵达和躲避障碍的行为上增加了分离行为。
实现动物迁徙中的跟随领队行为
跟随领队属于一种组合行为。实现此效果只要用靠近或追逐行为不就可以了吗?
我们知道在靠近行为中,AI会被推向目标,最终与目标占据相同的位置,而追逐行为把AI角色推向另一个角色,目的在于抓住它而不是跟随它。
在跟随领队行为中,目的是接近领队,但稍微落后,当角色距离领队较远时,可能会较快地移动,但是当距离领队较近时,会减慢速度。这可以通过下列行为地组合来实现。
要实现这种行为,首先需要找到正确的跟随点,如图2.44所示,AI跟随者要与领队保持一定距离,就像士兵跟随在队长后面一样,跟随点可以利用目标(即领队)的速度来确定,因为它也表示了领队的行进方向。
LEADER_BEHIND_DIST的值越大,跟随点距离领队越远,这代表跟随者与领队的距离越大。
接下来,只要以behind点为目标,应用抵达行为就可以了,返回的操控力也就是FollowLeader的返回力。
然后为了避免跟随者之间过于拥挤,需要加上分离行为。
如果领队突然改变方向,那么跟随着可能会挡住领头者,因为我们需要跟随者在领队之后,而不是挡在它前面,因此这种情况下,跟随者必须马上移开,这时就需要加上逃避行为,如图2.45所示:
为了检测AI跟随者角色是否在头领的视线内,我们采用和碰撞行为中类似的检测方法。基于领队的当前速度和方向,找到它前方的一个点(ahead),如果领队的ahead点与AI跟随者的距离小于某个值,那么认为跟随者在领队的视线之内并且需要移开。
ahead点的计算方法与behind点几乎相同,差别在于速度向量不再取负值。
tv = leader.velocity
tv = normalize(tv) * LEADER_BEHIND_DIST
ahead = leader.position + tv
using UnityEngine;
namespace AI
{
[RequireComponent(typeof(SteeringForArrive))]
public class SteeringForLeaderFollowing : Steering
{
private Vector3 _target;
///
/// 领队游戏物体
///
public GameObject Leader;
///
/// 领队的控制脚本
///
private Vehicle _leaderController;
///
/// 跟随着落后领队的距离
///
private float LEADER_BEHIND_DIST = 2.0f;
private SteeringForArrive _steeringForArrive;
private void Start()
{
_leaderController = Leader.GetComponent();
//为抵达行为指定目标点
_steeringForArrive = GetComponent();
_steeringForArrive.Target = new GameObject("ArriveTarget");
_steeringForArrive.Target.transform.position = Leader.transform.position;
}
public override Vector3 Force()
{
Vector3 leaderVelocity = _leaderController.Velocity;
//计算目标点
_target = Leader.transform.position + LEADER_BEHIND_DIST
* (-leaderVelocity).normalized;
_steeringForArrive.Target.transform.position = _target;
return Vector3.zero;
}
}
}
排队通过狭窄通道
这个场景可以模拟在室外的一队AI角色从宽阔的地方来到一个狭窄的通道。例如,如果发现玩家在大厅内,它们就会通过唯一的门进入大厅,捕捉玩家。这时我们希望它们能有序地进入,这就是排队行为所要实现的目标。
实现排队行为,需要用到两种操控行为。
假设用两个长方体作为墙,中间留有狭窄的过道,供AI角色行走,AI角色的目标点位于墙的另一侧。
如果没有排队行为,那么AI角色会试图从彼此的头上走过,而排队行为会让这些AI角色排起队,有序地通过这个通道。图2.53展示了排队行为的操控力计算方法。
首先,角色需要确定前方是否有其他AI角色,然后根据这个信息来确定自己是继续前进还是停止等待。我们利用与碰撞行为中相同的方法来检测前方是否有其他AI角色。首先计算出角色前方的ahead点,如果这个点与其他AI的距离小于某个值MAX_QUEUE_RADIUS,那么表示这个AI角色的前方还有其他AI角色,必须减速或停止等待。
ahead的计算方法是这样的:
ahead = normalize(velocity) * MAX_QUEUE_RADIUS;
那么,如何让AI角色停止等待呢?需要知道的是,操控行为是基于变化的力的,因此系统是动态的。即使某个行为返回的力为0,甚至总的力为0,还是不会让AI角色的速度变为0。
那么如果让AI角色停止等待呢?一种方法是计算其他所有力的合力,然后像抵达行为中一样,消除这些力的影响,让它停下来。另一种方法是直接控制AI角色的速度变量,而忽略其他的力。这种方式便是此处的方式:直接将速度值乘以一个比例因子,例如0.2,在此因子的作用下,AI角色的移动速度会迅速减慢,当前方没有遮挡时,会慢慢恢复到原来的速度。
然后,再加上靠近和避开障碍行为,就大功告成了。当然根据具体情况,还可以加入分离行为等。
排队行为脚本如下:
using UnityEngine;
namespace AI
{
public class SteeringForQueue : Steering
{
public float MAX_QUEUE_AHEAD;
public float MAX_QUEUE_RADIUS;
public float VelocityReduceFactor = 0.3f;
private Collider[] _colliders;
private Vehicle _vehicle;
private LayerMask _layerMask;
void Start()
{
_vehicle = GetComponent();
//设置碰撞检测时的掩码
int layerId = LayerMask.NameToLayer("Vehicles");
_layerMask = 1 << layerId;
}
public override Vector3 Force()
{
Vector3 velocity = _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 (var c in _colliders)
{
float sqrOtherVelocityLength = c.gameObject.GetComponent().Velocity.sqrMagnitude;
if (c.gameObject != gameObject && sqrOtherVelocityLength < velocity.sqrMagnitude)
{
Debug.Log($"------减速!!!");
_vehicle.Velocity *= VelocityReduceFactor;
break;
}
}
}
return Vector3.zero;
}
}
}
避开障碍脚本如下:
using UnityEngine;
namespace AI
{
public class SteeringForCollisionAvoidanceQueue : Steering
{
public bool IsPlanar;
private Vehicle _vehicle;
private float _maxForce;
public float AvoidanceForce;
public float MAX_SEE_HEAD;
private LayerMask _layerMask;
void Start()
{
_vehicle = GetComponent();
_maxForce = _vehicle.MaxForce;
IsPlanar = _vehicle.IsPlannar;
if (AvoidanceForce > _maxForce)
{
AvoidanceForce = _maxForce;
}
int layerId = LayerMask.NameToLayer("Obstacle");
_layerMask = 1 << layerId;
}
public override Vector3 Force()
{
Vector3 force = Vector3.zero;
Vector3 velocity = _vehicle.Velocity;
Vector3 normalizedVelocity = velocity.normalized;
if (Physics.Raycast(transform.position, normalizedVelocity,
out var hit, MAX_SEE_HEAD, _layerMask))
{
Vector3 ahead = transform.position + normalizedVelocity * MAX_SEE_HEAD;
force = ahead - hit.collider.transform.position;
force *= AvoidanceForce;
if (IsPlanar)
{
force.y = 0;
}
}
return force;
}
}
}
使用Unity3D开源库 UnitySteer,下载地址是:
https://github.com/ricardojmendez/UnitySteer
1.操控力的更新频率
正反馈带来振荡,负反馈维持群体不至于失控。
如果反馈过快,反应时间过短,那么将引起振荡,而不是缓慢地波动。另外过快的反馈也会导致输入数据变少,因为没有足够的时间收集足够的数据。
相反,如果反馈过慢,将会使得系统显得单调。
设计系统时需要注意,每个正反馈都会带来潜在的不稳定因素,必须以某种方式被平衡,如果只遵循一些简单的行为,那么可以证明,系统会找到一个平衡点。
2.AI角色的思考速度
一般来说,我们会将帧率和AI角色的思考速度区分开。许多游戏对于动画、AI和输入信息在相同的循环中进行更新,但也有许多游戏,将它们分开更新。
首先,很容易看出,让AI角色的思考速度快于帧率是没有任何意义的,因为即使思考地再快,也必须等到动画系统调用地时候才能做出移动。绝大多数游戏中,动画系统和物理系统的速率是一致的,AI无需高于这个速率。对于手机游戏来说,由于资源有限,可能会有少量的快速动画、但较慢的整体速率,此时AI只需小于等于这个较慢的整体速率就可以了。
让AI角色的思考速度和系统帧率相同并不是一个好的选择,例如AI控制的汽车不能过于频繁的变道,必须加上一些抑制措施。因此,在设计游戏的时候,需要对AI角色的反应速度施加限制。
3.“操控行为”与“A*寻路”的对比
操控行为最大的有趣之处在于:
假定某AI的当前速度为velocity,最大速度大小为maxSpeed,当前位置为nowPosition,目标位置为targetPosition。
靠近行为
离开行为
抵达行为
追逐行为
逃避行为
随机徘徊
路径跟随
避开障碍
组行为分离
组行为队列
组行为聚集
跟随领队行为
排队通过狭窄通道行为
https://download.csdn.net/download/dmk17771552304/15511493