这个系列是我阅读 UNITY5.X AI PROGRAMMING COOKBOOK 的笔记。有好又坏 请酌情阅读,有不当之处还请指出。

本章主要概述让 AI 更智能的移动。对应到原书也就是第一章节。

使AI移动

这三个类的工作如下。

  1. Agent负责 计算当前帧的移动与下一帧的移动。
  2. AgentBehaviour负责抽象Steering的计算过程。
  3. Steering包含了Agent需要计算的数据。

每当需要扩展一个新的移动行为,则只需要增加一个 AgentBehaviour 类型的派生即可。

脚本时序

脚本调用频率
所有的脚本按照优先级调用。例如 检测与执行AI的移动的Agent脚本调用最频繁。其他由 AgentBehaviour 派生的脚本次之。

基础结构

这里详细介绍了利用 Agent/AgentBehaviour/Steering 三个类进行基础的 AI 结构搭建。当然代码里有一些 Bad Smell ,比如 Steering 类的 new ,每个行为返回一个Steering毫无例外的都会new一次。当然这完全是可以规避的,这里并不作代码优化的讨论 所以只是提到。

Agent 代理

Agent 通过 Update 与 LateUpdate 来计算当前帧与下一帧的移动速度角度。
其中实际给 GameObject 做位移的代码就只有两句 Translate/Rotate 。

  • Update :执行当前帧的物体的移动与角度
  • LateUpdate :计算下一帧的物体的移动与角度数值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using UnityEngine;
using System.Collections;
public class Agent : MonoBehaviour
{
public float maxSpeed;
public float maxAccel;
public float orientation;
public float rotation;
public Vector3 velocity;
protected Steering steering;
void Start ()
{
velocity = Vector3.zero;
steering = new Steering();
}
public void SetSteering (Steering steering)
{
this.steering = steering;
}

public virtual void Update ()
{
Vector3 displacement = velocity * Time.deltaTime;
orientation += rotation * Time.deltaTime;
// we need to limit the orientation values
// to be in the range (0 – 360)
if (orientation < 0.0f)
orientation += 360.0f;
else if (orientation > 360.0f)
orientation -= 360.0f;
transform.Translate(displacement, Space.World);
transform.rotation = new Quaternion();
transform.Rotate(Vector3.up, orientation);
}

public virtual void LateUpdate ()
{
velocity += steering.linear * Time.deltaTime;
rotation += steering.angular * Time.deltaTime;
if (velocity.magnitude > maxSpeed)
{
velocity.Normalize();
velocity = velocity * maxSpeed;
}
if (steering.angular == 0.0f)
{
rotation = 0.0f;
}
if (steering.linear.sqrMagnitude == 0.0f)
{
velocity = Vector3.zero;
}
steering = new Steering();
}
}

Steering 数据载体

Steering 承载了代理(Agent)需要计算的原始数据。数据的表现直接会影响到计算的AgentBehaviour / Agent 的计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;
public class Steering
{
public float angular;
public Vector3 linear;
public Steering ()
{
angular = 0.0f;
linear = new Vector3();
}
}

AgentBehaviour 代理行为

由于 gentBehaviour 是Agent的数据提供方。所以他主要的职责就是派生与扩展。
新的类型将会继承自 AgentBehaviour 并且会重写 GetSteering() 函数来丰富表现。
接下来都是介绍 AgentBehaviour 脚本派生的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;
public class AgentBehaviour : MonoBehaviour
{
public GameObject target;
protected Agent agent;
public virtual void Awake ()
{
agent = gameObject.GetComponent<Agent>();
}
public virtual void Update ()
{
agent.SetSteering(GetSteering());
}
public virtual Steering GetSteering ()
{
return new Steering();
}
}

更加智能的移动

Seek 搜索

上面说到 AgentBehaviour 代理行为,是可以派生的。我们可以直接重写其 GetSteering 函数,来增加不同的表现。
这里干了两件事

  1. 获取朝向目标的向量
  2. 目标朝向目标的向量归一化,乘以当前速度。也就是获得了 朝向目标的速度的向量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using System.Collections;
public class Seek : AgentBehaviour
{
public override Steering GetSteering()
{
Steering steering = new Steering();
steering.linear = target.transform.position - transform.position;

steering.linear.Normalize();
steering.linear = steering.linear * agent.maxAccel;
return steering;
}
}

Purse 追逐

在 GetSteering() 函数中有这样的逻辑存在。
追逐的目标 targetAux , 所以需要预先计算(预判单位时间内) targetAux 可能到达的位置(这里用target表示)。maxPrediction变量在这里用来控制预判的时间。
我们知道 任何数值都需要规范化的 例如常用的 Math.Clamp 函数。我们应该尽量将 maxPrediction 值控制在正常范围之内。

通过 speed <= distance / maxPrediction

  • speed :为当前追踪的物体真实速度。
  • distance :为当前追踪的物体一共走过的距离。
  • maxPrediction :预设的预测时间。

这样可以大胆猜测 为当前追踪的物体 正处于匀速 或者 加速中

distance / maxPrediction 我们最终得到的 速度 就是物体的 预测速度。我们拿着 预测速度 与 speed 做一个比较。如果预测的速度小于真实速度,那么这个预测值出乎意料的错误。我们需要重新计算一个更真实合理的预测值。
prediction = distance / speed; 这里计算了一个中规中矩的预测值。
最终通过 targetAgent.velocity * prediction 获取 预测时间 内运动的距离。

对于刚才的猜测结果,有一种条件下是有问题的,那就是当 当前追踪的物体 突然静止/或者本身就是静止的。由于脚本执行的时序问题,追逐到物体的时候会直接穿插过去(追过头了),然后接着反复运动。这里就要用到 Arraying 脚本了,后文就会介绍到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Pursue : Seek 
{
public float maxPrediction;
private GameObject targetAux;
private Agent targetAgent;

public override void Awake()
{
base.Awake();
targetAgent = target.GetComponent<Agent>();
targetAux = target;
target = new GameObject();
}

void OnDestroy ()
{
Destroy(targetAux);
}

public override Steering GetSteering()
{
Vector3 direction = targetAux.transform.position - transform.position;
float distance = direction.magnitude;
float speed = agent.velocity.magnitude;
float prediction;

if (speed <= distance / maxPrediction)
prediction = maxPrediction;
else
prediction = distance / speed;
target.transform.position = targetAux.transform.position;
target.transform.position += targetAgent.velocity * prediction;
return base.GetSteering();
}
}

Flee 逃离

顾名思义 Flee 肯定和 Seek 相反。事实也正是如此,获取了与目标相反方向的速度的向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System.Collections;
public class Flee : AgentBehaviour
{
public override Steering GetSteering()
{
Steering steering = new Steering();
steering.linear = transform.position - target.transform.position;
steering.linear.Normalize();
steering.linear = steering.linear * agent.maxAccel;
return steering;
}
}

Evade 规避

规避(Evade) 代码与 Pursue 代码一致。在 Flee/Seek 层,二者做了区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using UnityEngine;
using System.Collections;

public class Evade : Flee
{
public float maxPrediction;
private GameObject targetAux;
private Agent targetAgent;

public override void Awake()
{
base.Awake();
targetAgent = target.GetComponent<Agent>();
targetAux = target;
target = new GameObject();
}

public override Steering GetSteering()
{
Vector3 direction = targetAux.transform.position - transform.position;
float distance = direction.magnitude;
float speed = agent.velocity.magnitude;
float prediction;

if (speed <= distance / maxPrediction)
prediction = maxPrediction;
else
prediction = distance / speed;
target.transform.position = targetAux.transform.position;
target.transform.position += targetAgent.velocity * prediction;
return base.GetSteering();
}



void OnDestroy ()
{
Destroy(targetAux);
}
}

本篇到此结束,本文主要介绍了 AI 行为的基础结构 与 其何如派生出来行为。下一篇会着重介绍派生出的其他行为与详细算法(实现)。