寻路就是提供一个目标点,根据障碍物自动计算出一条最优的路径。Unity寻路使用的是AStar算法。寻路可分为动态寻路和静态寻路。动态寻路就是障碍物的位置可以动态修改。静态寻路表示无法移动的障碍物,静态寻路是效率更高的寻路。

导航和寻路 - Unity 手册 (unity3d.com)

设置网格

参与寻路计算的对象需要选中Navigation Static复选框,接着在导航栏中选择Window——AI——Navigation。

在Bake选项卡中,我们可以设置代理角色胶囊体的一些参数。

  • Agent Radius表示半径

  • Agent Height表示高度

  • Max Slope表示爬坡的最高坡度

  • Step Height表示每次爬楼梯的高度

  • Generated Off Mesh Links合并无法使用可行走表面来表示的导航捷径。例如,跳过沟渠或围栏,或在通过门之前打开门,全都可以描述为网格外链接。

最后,单击Bake按钮即可。

寻路烘焙

我们来做一个简单寻路,让图中的方块越过障碍物自动走路

首先要给方块添加Navmesh Agent组件,然后在方块上挂载此脚本

NavMeshAgent组件

新建NavigatorMove脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using UnityEngine.AI;

public class NavigatorMove : MonoBehaviour
{
public NavMeshAgent navMeshAgent;
private 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);
}
}
}
}

连接两点

寻路系统专门提供了Off Mesh Link组件来处理不连续的导航网格之间的连接,用来处理跳过沟渠、翻过栅栏或者打开门等场景

我们在一个父对象上挂载Off Mesh Link组件,并且新建两个空的子对象,这里命名为Start和End,分别放在需要跨越的两侧NevMesh上,并且在Off Mesh Link组件内添加好引用

子对象

Off Mesh Link

创建的Link

网格外链接 (Off-Mesh Link) - Unity 手册 (unity3d.com)

我们可以在烘焙过程中自动创造Off Mesh Links,上一节所提到的Off Mesh Links选项就是用来做这个的

先选中一个Navigation Static对象,然后在Navigation面板的Object选项卡中勾选Generate OffMeshLinks,烘焙时就会产生link了

勾选生成OffMeshLinks

生成结果

获取寻路路径

我们可以使用NavMesh.CalculatePath()方法来计算出到达目标点的路径,使用此方法提前计算出路径然后通过Debug.DrawLine()方法在Scene中查看

新建ShowNavPath脚本

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
using UnityEngine;
using UnityEngine.AI;

public class ShowNavPath : MonoBehaviour
{
public NavMeshPath m_Path = null;
public NavMeshAgent navMeshAgent;
public Transform target;
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);
}
}
}

展示路径

动态阻挡

如果在一个寻路系统中有空气墙的设定,玩家需要一定的条件才能使用寻路系统穿过空气墙,这时候空气墙就需要设置动态阻挡。

给需要动态阻挡的对象添加Nav Mesh Obstacle组件,只要给这个对象SetActive,即可控制是否发生动态阻挡。

Nav Mesh Obstacle

勾选Carve后,当前对象会把已烘焙的Navmesh切割开。

  • Move Threshold :当Obstacle对象移动超过了这个阈值,Unity会认为它已经移动,然后更新它切开的Navmesh。要使用它请取消勾选Carve Only Stationary。Navmesh的计算会有一帧延迟
  • Time to Stationary:Obstacle对象移动完毕后,把Obstacle对象设为Stationary(静止状态)所需的时间。
  • Carve Only Stationary:勾选后,只有当Obstacle对象静止时,才会切割Navmesh。

已切开的Navmesh

导航网格障碍物 (Nav Mesh Obstacle) - Unity 手册 (unity3d.com)

导出寻路网格信息

网络游戏中,游戏对象的逻辑是由逻辑服务器计算的。逻辑服务器只储存一些逻辑信息,所以需要把寻路的网格信息导出给服务器使用。

我们可以利用发射线的方式来检测当前地面是否可以行走,然后导出一个二维数组,其中0表示可走,1表示不可走,也就是图中红色和蓝色射线区域

计算阻挡

导出信息

Export Navmesh脚本

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
77
78
79
80
#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.AI;
using System.IO;
using UnityEditor;
using System.Text;

public class ExportNavmesh : MonoBehaviour
{
//X坐标格子的数量
[Tooltip("X坐标格子的数量")]
public int width;
//Y坐标格子的数量
[Tooltip("Y坐标格子的数量")]
public int height;
//每个格子的大小
[Tooltip("每个格子的大小")]
public int size;

private void OnDrawGizmosSelected()
{
//确保当前场景烘焙过,CalculateTriangulation().indices返回一个三角索引的int数组
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;
//检测当前格子是否可以行走,其中1f表示最大检测距离,如果当前的格子颜色不正确的话尝试修改它
if(NavMesh.SamplePosition(source,out hit, 1f, NavMesh.AllAreas))
{
color = Color.blue;
a = 1;
}
sb.AppendFormat(j > 0 ? ",{0}" : "{0}", a);//保证第一个是0(或1),后面的是,0(或,1)
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());
}
}
}
#endif

这个脚本总是在原点开始记录Navmesh,所以挂载在任意对象上都可以

反射探头

游戏使用一个Cubemap来预先生成反射效果,节省实时反射的性能。要生成这个Cubemap,需要反射探头

首先需要给具有反射效果的对象添加Reflection Probe组件,然后在scene中就能看到一个矩形区域,在这个区域内的物体都会参与反射,然后给这个对象添加材质,这个材质的Shader是Standard(Specular setup),这种Shader支持镜面反射。

反射区域

然后给需要被反射的物体添加Reflection Probe Static静态属性,这里是红蓝物体,然后将Reflection Probe组件type改为Bake,点击Bake就可以生成反射用的cubemap了,这里点击的Bake和在lighting面板点击Generate Lighting一样

Reflection Probe

生成反射效果