思考并回答以下问题:
1.
2.
3.
4.
本章涵盖
- 寻路网格
寻路网格
寻路就是提供一个目标点,根据障碍物自动计算出一条最优的路径,Unity寻路使用的是A*算法。寻路可分为动态寻路以及静态寻路两种。动态寻路就是障碍物的位置可以动态修改,而静态寻路表示障碍物永远都不会发生改变。由此可见,静态寻路的效率会更高。
设置寻路
参与寻路计算的游戏对象需要选中Navigation Static复选框,接着在导航菜单栏中选择Window->Navigation命令,打开寻路烘焙面板,如下图所示。这里还需要设置控制角色寻路的一些基本信息,其中Agent Radius表示角色胶囊体的半径,Agent Height表示胶囊体的高度,Max Slope表示爬坡的最高坡度,Step Height表示每次爬楼的高度。Generated Off Mesh Links用于设置角色落下或者跳起来没有在连接在一起的两个点的高度和距离,例如角色可以跳过一条水沟。最后,单击Bake按钮即可。
如下图所示,我们来做一个简单的寻路。点击地面,让“方块”越过障碍物自动走过去,点击屏幕时,需要使用射线计算出点在地面的位置,接着就可以控制“方块”寻路过去了。
如下代码所示,控制角色寻路时,需要调用navMeshAgent.SetDestination()来设置它的位移。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24using UnityEngine;
using UnityEngine.AI;
public class Script_09_06 :MonoBehaviour
{
public NavMeshAgent navMeshAgent;
public void Update()
{
if(Input.GetMouseButton(0)) {
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);;
// 穿透所有Mesh直到找到地面
RaycastHit[] hits = Physics.RaycastAll (ray);
foreach (var hit in hits) {
string name = hit.collider.gameObject.name;
if (name == "Plane") {
// 移动方块
navMeshAgent.SetDestination (hit.point);
}
}
}
}
}
连接两点
寻路必须保证两点是能走过去的,但有时候设计上不一定是走过去,例如跳过去、掉下去或空中飞过去等。寻路专门提供了一个OffMesh Link组件来处理两点之间的连接。如下图所示。
获取寻路路径
有时候,需要在寻路之前判断一下目标点是否合法,或者寻路的路径是否合法,此时就要提前获取寻路的完整路径了,如下图所示。在代码中,我们可以使用NavMesh.CalculatePath()方法来提前计算出目标点的路径。
如下代码所示,调用NavMesh.CalculatePath()方法提前计算寻路路径,接着通过Debug.DrawLine()方法将路径绘制在Scene中查看。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24using UnityEngine;
using UnityEngine.AI;
public class Script_09_08 :MonoBehaviour
{
public NavMeshAgent navMeshAgent;
public Transform target;
private NavMeshPath m_Path = null;
void Start()
{
m_Path = new NavMeshPath ();
// 计算路径
NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, m_Path);
}
void Update () {
// 绘制路径
for (int i = 0; i < m_Path.corners.Length-1; i++)
Debug.DrawLine(m_Path.corners[i], m_Path.corners[i+1], Color.red);
}
}
动态阻挡
在Unity的寻路中,很多元素是需要支持动态阻拦的,例如一堵空气墙,玩家在经历某种特殊事件之前是不能走过去的。如下图所示,给需要动态阻挡的游戏对象添加Nav Mesh Obstacle组件。设置游戏对象的隐藏或显示,即可控制是否发生动态阻挡。
这里需要介绍一个Carve属性,一旦选中它,表示这个对象支持动态烘焙。其中Move Threshold表示移动多长的距离后启动动态烘焙,Time To Stationary表示元素停止运动后多久标记为静止状态,Carve Only Stationary表示元素是否需要移动,例如空气墙,只有开启或关闭两个状态。
导出寻路网格信息
Unity的寻路是能满足客户端的,但是如果是网络游戏,服务器需要控制怪物寻找主角,此时就需要将寻路的网格信息导出来。如下图所示,可以利用发射线的方式来检测到当前地面是否可以行走,接着导出一个二维数组,其中0表示不可走,1表示可走,也就是图中红色和蓝色的射线区域。
如下图所示,首先需要设置X坐标格子的数量、Y坐标格子的数量以及每个格子的大小,接着利用Gizmos绘制射线来查看效果。
如下代码所示,绑定脚本后,在Scene视图中单击一个该对象,即可渲染射线区域并自动生成网格信息文本。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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using UnityEngine;
using UnityEngine.AI;
using System.IO;
using UnityEditor;
using System.Text;
public class Script_09_09 :MonoBehaviour
{
// X坐标格子的数量
public int width;
// Y坐标格子的数量
public int height;
// 每个格子的大小
public int size;
void OnDrawGizmosSelected()
{
// 确保当前场景烘焙过
if (NavMesh.CalculateTriangulation ().indices.Length > 0) {
// 获取场景名
string scenePath = UnityEditor.SceneManagement.EditorSceneManager.GetSceneAt(0).path;
string sceneName = System.IO.Path.GetFileName(scenePath);
string filePath = Path.ChangeExtension(Path.Combine (Application.dataPath, sceneName),"txt");
if (File.Exists (filePath)) {
File.Delete (filePath);
}
// 准备写入数据
StringBuilder sb = new StringBuilder ();
sb.AppendFormat ("scene={0}", sceneName).AppendLine ();
sb.AppendFormat ("width={0}", width).AppendLine ();
sb.AppendFormat ("height={0}", height).AppendLine ();
sb.AppendFormat ("size={0}", size).AppendLine ();
sb.Append ("data={").AppendLine ();
Gizmos.color = Color.yellow;
Gizmos.DrawSphere (transform.position, 1);
float widthHalf = (float)width / 2f;
float heightHalf = (float)height / 2f;
float sizeHalf = (float)size / 2f;
// 从左到右从下到上一次写入每个格子的数据
for (int i = 0; i < height; i++) {
sb.Append("\t{");
Vector3 startPos = new Vector3 (-widthHalf + sizeHalf, 0, -heightHalf + (i * size) + sizeHalf);
for (int j = 0; j < width; j++) {
Vector3 source = startPos + Vector3.right * size * j;
NavMeshHit hit;
Color color = Color.red;
int a = 0;
// 检测当前格子是否可以行走
if (NavMesh.SamplePosition (source, out hit, 0.2f, NavMesh.AllAreas)) {
color = Color.blue;
a = 1;
}
sb.AppendFormat (j > 0?",{0}":"{0}", a);
Debug.DrawRay (source, Vector3.up, color);
}
sb.Append ("}").AppendLine ();
}
sb.Append ("}").AppendLine ();
// 绘制格子的总区域
Gizmos.DrawLine (new Vector3 (-widthHalf, 0, -heightHalf), new Vector3 (widthHalf, 0, -heightHalf));
Gizmos.DrawLine (new Vector3 (widthHalf, 0, -heightHalf), new Vector3 (widthHalf, 0, heightHalf));
Gizmos.DrawLine (new Vector3 (widthHalf, 0, heightHalf), new Vector3 (-widthHalf, 0, heightHalf));
Gizmos.DrawLine (new Vector3 (-widthHalf, 0, heightHalf), new Vector3 (-widthHalf, 0, -heightHalf));
// 写入文件
File.WriteAllText (filePath, sb.ToString ());
}
}
}
如图所示,行走区域的二维数组已生成完毕,数据的排序是从左到右、从下到上,服务端拿到这个数据后,即可按照此格式来解析了。