做游戏开发的过程中我们需要使角色完成自动寻路,这不是一个简单的过程。所以下面我们就具体看学习一下unity3d自动寻路的过程。

       众所周知,自动寻路是所有游戏的一个难点,属于AI(人工智能)的范畴。一个游戏的AI的设计是否足够完美,可能决定了这个游戏的命运。然而自动寻路就是AI中的一个十分重要的分支,其算法异常复杂。然而unity3d中提供了一套非常成熟的组件来为我们解决这一难题。今天,我们就来一起欣赏一下Unity3d自带的自动寻路系统。

       我们在学习一个陌生的知识时,免不了要记一些令人烦恼的概念,自动寻路也是一样的。但是记东西也是有技巧的,我认为我们应该先看到一点惊喜再学东西,这样兴趣有了,学东西就不那么抵触了。所以我现在先列举举一个简单的例子,记住,先不要思考为什么我要这样做,如果你是初次学习这个自动寻路的话最好这样,不然你会走很多弯路的。我们需要新建一个工程,将其起名为:NavMeshProject。然后我们可以制作场景了。

像往常一样,先做一个地面,这里我用的是Cube。然后我将我们的朋友Robot也请来了,看这:


       是不是很帅啊!我特意加了个灯光,给地面换了个柔和的颜色,看起来就不那么单调了。然后我们保存一下整个场景,该场景取名为:TestNavgation1。

下面我来烘焙场景,但是记住,先不要想为什么。Unity3d编辑器的菜单下:Window->Navigation,这是我们可以发现编辑器的某一部分出现了一个Navigation窗口,如:

                         

       请注意一下此图的右下角的一个Bake按钮。

       在Hierarchy下选中Plane(就是那个地面),然后我们在Navigation面板中的Object选项卡下找到Navigation Static复选框,勾选它,然后点击Navigation面板右下角的Bake按钮:

                    

       此时我们可以发现Project面板下面多出了一个文件夹,且此文件夹出现了一个子文件NavMesh:


       并且Scene窗口中的地面上出现了一下变化:


       然后我们跟robot添加一个组件:NavMeshAgent。具体做法是 :  在Hierarchy面板下选中robot,然后在Unity3d菜单下:Component->Navigation->NavMeshAgent,你会发现robot身上出现了一个类似胶囊体碰撞器的绿色线框的包围体,调节一下盖NavMeshAgent组件中的一些参数:Height,BaseOffset等,如图:



       最后我们该编写脚本了。我还是新建一个文件夹:Scripts。然后编写两个脚本:
脚本一,专门设置导航网格代理的目的地的:
  1. using UnityEngine;
  2. using System.Collections;
  3. public class NavMeshMove : MonoBehaviour {
  4. public Transform[] NavMeshTransforms;//导航网格的目的地组。
  5. private NavMeshAgent nma;//Robot的导航网格代理
  6. void Start () {
  7. if(NavMeshTransforms == null)
  8. {
  9. return;
  10. }
  11. nma = gameObject.GetComponent<NavMeshAgent>();
  12. nma.SetDestination(NavMeshTransforms[0].position);//初始时刻设置的导航网格代理的目的地
  13. }
  14. void Update () {
  15. if(nma.remainingDistance == 0){
  16. //当导航网格代理到达了目的地时,更换目的地,且是随机的更换
  17. nma.SetDestination(NavMeshTransforms[Random.Range(0,NavMeshTransforms.Length)].position);
  18. }
  19. }
  20. }

脚本二,是专门为导航网格代理编写的动画控制脚本:

  1. using UnityEngine;
  2. using System.Collections;
  3. public class NavNeshAnimation : MonoBehaviour {
  4. public float***nAnimaitonSpeed = 4.0f;//定义最大的跑步速度,一般为导航网格代理的速度
  5. public float speedThreshold = 0.1f;//定义动画从idle***n过渡的临界速度
  6. private string loadAnimation = "load_Idle";//定义需要执行的协同函数名
  7. private NavMeshAgent nma;//定义导航网格代理
  8. IEnumerator Start () {
  9. nma = gameObject.GetComponent<NavMeshAgent>();
  10. AnimationSetup();//简单的设定一下动画。
  11. while(Application.isPlaying) {
  12. yield return StartCoroutine(loadAnimation);//执行协同函数
  13. }
  14. }
  15. IEnumerator load_Idle()
  16. {
  17. do{
  18. UpdateAnimationBlend();
  19. yield return null;
  20. }while(nma.remainingDistance == 0);
  21. loadAnimation = "load_Run";
  22. yield return null;
  23. }
  24. IEnumerator load_Run()
  25. {
  26. do
  27. {
  28. UpdateAnimationBlend();
  29. yield return null;
  30. } while (nma.remainingDistance != 0);
  31. loadAnimation = "load_Idle";
  32. yield return null;
  33. }
  34. void AnimationSetup()
  35. {
  36. animation["idle"].layer = 1 ;
  37. animation["***n"].layer = 1;
  38. animation.SyncLayer(1);
  39. animation.CrossFade("idle", 0.1f, PlayMode.StopAll);
  40. }
  41. void UpdateAnimationBlend()
  42. {
  43. Vector3 velocityXZ = new Vector3(nma.velocity.x,0.0f,nma.velocity.z);
  44. float speed = velocityXZ.magnitude;
  45. animation["***n"].speed = speed /***nAnimaitonSpeed;
  46. if (speed > speedThreshold)
  47. {
  48. animation.CrossFade("***n");
  49. }
  50. else {
  51. animation.CrossFade("idle");
  52. }
  53. }
  54. }

       我们将这两个脚本绑定到robot身上,然后我们在Hierarchy面板中新建一个空的GameObject,重命名为:NavMeshPoint,并且新建5个spere作为它的子对象被其管理,如下:


然后将这5个spere分开到Plane上的不同位置,如下:


       选中robot,然后在Inspector下的NavMeshMove脚本上设置NavMeshTransforms的个数为5,并且将那5个spere分别拖拽到相应的位置,如下:

                    

好了,我们可以运行一下工程,看一下效果,这里只截了部分图片:




       从图中我们可以看出,我们的robot在不断的跑步,每次跑到目标点之后停下来,然后转身朝向下一个目标点,如果工程没有停止,那么它周而复始的循环下去,这对于编写NPC的AI很有帮助。
       是不是很神奇呢?有没有想要将这部分知识融会贯通的冲动?我把工程一起献上吧。这个星期我的连续几篇文章,会深入的介绍这一系列的知识。敬请期待,不过我建议一下读者实现最好了解一些这个组件,看看与我的理解有什么不同,我们一同探讨,共同切磋。
       接着我们就来学习一下与自动寻路有关的组件吧。Unity中与自动寻路相关的组件主要有两个:NavMeshAgent (  又称导航网格代理 ),Off Mesh Link( 分离网格链接 )。这两个组件的作用与使用范围是不同的,我们唯一可以确定的是我们必须烘焙地形,产生NavMesh(导航网格)。因为导航网格决定我们的角色(带有导航网格代理的角色)活动的范围。NavMeshAgent组件需要附着寻路的角色身上,比如怪物,而OffMeshLink这个组件主要是用来构造寻路角色的寻路路径的某个部分,比如我们有时需要怪物在寻路过程中从一个固定的地方移动到另一个固定的地方,这将会在我下面的例子中清楚的看到。好了,甭废话了,让我们开始吧!
       首先,我们先来了解一下NavMeshAgent组件,这个组件是unity3d提供的寻路系统的核心组件。官方是这样解释的:The NavMeshAgent component is connection with pathfinding,and is the place to put information about how this agent navigates the NavMesh 。意思大致是这样的:NavMeshAgent组件是关于寻路的,它是一个用来存放代理周游导航网格的路径信息的平台。那么代理又是什么呢?原来,角色的移动是要依靠代理来做的,每一个附着这个组件在寻路的过程中都是利用代理进行的,这也就是这个组件为什么叫导航网格代理的原因。每一个你需要让它具有自动寻路功能的角色必须要附着这个组件,除非你利用其它的寻路算法,但那样做实在是太复杂了,因为考虑的情况太多了,然而Unity为我们提供了这样一个组件,我们为啥不用呢?我们先来举一个例子吧,这样学起来也好理解一些。

       我新建了一个Scene,给了它一个名字:TestNavgation2。然后这个场景里面也需要一个地面,我还是用Cube来做。这次我先向这个平面上建一个Spere,重命名为Hero,并且给它加上一个NavMeshAgent组件,方法:Unity菜单,Component->Navgation->NavMeshAgent。给Hero上个颜色就像下面截图一样:


选中Hero,在Inspector下,我们可以看到NavMeshAgent组件的各个属性:

                   

这几个属性我简单的解释一下:

Radius:导航代理的半径,我们可以适当的调节一下这个值
Speed :这个属性代表这个导航网格代理寻路时可以达到的最大速率
Acceleration :加速度,表示代理的速度从0加速到Speed时的最大的加速度
Angular Speed :最高的角速度
Stopping distance : 制动距离,当代理据目的地的距离小于这个值时开始减速
Auto Traverse OffMesh Link :自动移动并关闭OffMeshLinks,这个选项对于我们利用程序来操纵后面我要介绍的OffMeshLink很关键,
Auto Repath   自动重新寻路,如果发现现有路径已失效,那么它将获得新的路径,这个选项我们一般将其勾选上
Height  : 导航代理的高度。
Base Offset : 基本偏移,我们可以通过调整这个变量来调整代理自身的包围盒
Obstacle Avoidace Type  : 代理躲避的水平,一般我们选默认的High Quality就行了

NavMesh Walkable :导航网格代理可以通过的网格层类型

       好了这些属性我们基本上知道了一点,但要真正理解它,我们还有很长的路要走。我们到头来是要用脚本来控制寻路的,也就是说我们必须掌握NavMeshAgent这个类,还是老办法,看文档吧,记住,文档我们必须看,这是我们学习新东西必须做的,那些出视频,出书的没一个不是从看文档开始的。

我们看NavMeshAgent这个类,Unity菜单:help->Scripting Reference 。我们找到这个类,发现里面的变量和方法还不少呢。这次我不可能将其全部讲解到 , 因为有些我自己都没用过,所以我只讲解我们常用的:

NavMeshAgent(导航网格代理组件所对应的类)
假使我们的主角身上添加了一个导航网格组件,我们一般在脚本中这样定义NavMeshAgent类型的成员变量:
private NavMeshAgent nma ;
并在Start或Awake函数中实例化它:

nma = gameObject.GetComponent<NavMeshAgent>();

 重要变量:

1.destination

我们可以这样对导航网格代理设置目的地:

nma.destination = Vector3类型的值。相当于nma.SetDestination( Vector3类型的值 )

2.stoppingDistance

这个与Inspector面板中的Stopping Distance对应,一下再不涉及与Inspector面板中的属性对应的变量

3.velocity

导航网格代理周游时的实时速度,非常重要

4.nextPosion

顾名思义,也就是下一个位置,在Update函数中打印这个属性,你会发现打印出的结果与这个导航网格代理周游过的路径一致

5.steeringTarget(只读)

这个属性是相当重要的,它指的是导航网格代理在导航网格中周游时所经过的拐点,这对于制作寻路网游同步角色的Transform是相当重要的,因为导航网格的寻路路线是直线,我们只需将其寻路的拐点与旋转角度告诉给服务器端,有服务器端广播出去,然后再写一个执行Transform同步的脚本(是特制的)绑定在非角色玩家身上就可以了。

6.desiredVelocity(只读)

这个属性说实话我用的不多,指的是导航网格代理的期望速度,与其当前速度不是等价的。

7.remainingDistance( 只读 )

导航网格代理离目的地还剩的距离,如果其值为0,那么代理已经到达了目的地了,所以我们可一个这样判断一个导航网格代理是否到达了目的地:

if( nma.remainingDistance == 0 ){
 //执行行为

}

8.isOnOffMeshLink
导航网格代理当前的位置是否位于OffMeshLink,因为这个牵扯到了另一个组件,我会在后面说的

 重要方法:

1.SetDestination( Vector3 v )

设置目的地,与nma.destination = v一样的,你想怎么用都行,只是这个函数在设置目的地成功后返回***e,否则返回false,就只比调用属性多了一个返回值

2.ActivateCurrentOffMeshLink( bool activated )
返回值为空

与OffMeshLink有关,当activated为***e激活OffMeshLink,后面会讲到的

3.CompleteOffMeshLink ()

让导航网格代理完成在OffMeshLink上的周游,后面会讲的

4.Move( Vector3 v )

让导航网格代理朝向量v的世界坐标系方向平移v的长度

5.Stop()

让导航网格代理停止寻路,但此寻路状态可以靠下面一个函数恢复到寻路状态,并且目的地也与上次一样

6.Resume()
恢复寻路状态,此时角色会在上一次执行了Stop函数停下来后恢复当时的状态,目的地为上一次的目的地
        这8个属性与Inspector面板上的各个属性并且和这6个函数我们一定要好熟练掌握,这关系到我们是否能熟练书写寻路脚本。还有一些函数我这里没有介绍,就留着读者自己研究研究吧。

       此刻我相信读者对这个组件已经有了相当深刻的认识了,但是还没完,我们必须做的一个步骤就是烘焙场景,生成导航网格。为什么要这样做呢?因为Unity3d自带的寻路系统的原理是事先通过烘焙将地形的信息记录起来存储在NavMesh文件上。我们烘焙一次看看,其实做法很简单 ,我们打开Navigation窗口,做法:Window>Navigation。然后选中Plane,出现下面截图:

                      

       我们可以看到Navigation Static复选框,勾选它。那它的作用是什么呢?原来,每一个GameObject都可以标记成静态的或非静态的,就想这样:

                        

我们看到Plane的右侧有一个Static属性,展开他我们可以看到:

                            

       这里面每一种静态选项背后都包含一种技术,比如Lightmap Static,用于生成光照贴图对场景进行优化。还有Occluder Static与Occludee Static,是关于Unity3d中与遮挡剔除技术有关的。好了,言归正传,导航网格代理是在导航网格上周游的。所以我们的地面必须生成导航网格,这里的Navigation Static属性框必须勾上。这时我们看到了第二个属性框:OffMeshLink Generation,勾选上之后我们就可以不借助OffMeshLink组件来生成OffMeshLink。那么什么是OffMeshLink呢?请看下图:


       看到那些个线没有?每一条线就代表一个OffMeshLink。那么此时我可以引入OffMeshLink组件了,这个组件其实就是自定义像上图那样的样条线,但每一个OffMeshLink组件只能形成一个样条线。这个样条线的作用可不一般啊,但是应用其时我们必须格外注意一些问题,不然我们即使用了这个组件也不会产生丝毫的作用的。
那好吧,我们该做点什么了!

       如上图,我建了连个Plane,分别为Plane1,Plane2。我们一次选择这两个平面,在Navigation窗口将Navigation Static 与Off Mesh Link Generation选项给勾上,并选择Navigation Layer为Default。然后单击Bake按钮,如下:

                  

        我们还建立一个围墙,用Cube做的,我们在Navigation面板中除了勾选Navigation Static之外,还必须将其Navigation Layer下拉框中选择Not Walkable,即让我们的Hero绕过此障碍物达到目的地。Not Walkable只是导航网格层中的一个内建层,我们还可以建立我们自己的导航网格层。关于导航网格层,我会在我的下一篇文章中详细为您讲解。

       我们之前说过,导航网格代理的活动空间只能是导航网格,即NavMesh。但现在看来这句话可能需要修改一下了。因为导航网格代理还可能活动在OffMeshLink上面,所以我们可以在两个平面上面建立OffMeshLink来连通两个平面。我们现在没有用Off Mesh Link组件,这样就可以生成很多的OffMeshLink。但是我们发现烘焙后的场景没有出现Off Mesh Link。到底是什么原因呢?原来,我们还得设置一些参数:

                    

       我们看到了一个选项 :Jump Distance。我们将这个值调到4,再次烘焙一次,则出现了以下场面:


       然后我们新建一个Cube,命名为:DS。将其放置在上图中的白色的Cube所在的位置上,然后我写一个脚本:
  1. using UnityEngine;
  2. using System.Collections;
  3. public class SetHeroDes : MonoBehaviour {
  4. public Transform ds;//目的Cube的Transform
  5. private Vector3 origin;//存储导航网格代理的初始位置
  6. private NavMeshAgent nma;//存储导航网格代理组件
  7. void Start () {
  8. nma =  gameObject.GetComponent<NavMeshAgent>();
  9. //取得导航网格代理组件
  10. origin = transform.position;
  11. //实例化origin
  12. }
  13. void OnGUI()
  14. {
  15. if(GUILayout.Button("Start***n"))
  16. {
  17. nma.SetDestination(tf.position);
  18. //设置导航网格代理的目的地
  19. }
  20. if (GUILayout.Button("Resume"))
  21. {
  22. transform.position = origin;
  23. //恢复导航网格代理的位置为初始位置
  24. }
  25. }
  26. }

我们将这个脚本绑定到Hero上,然后将DS拖拽到指定位置:


然后我们运行一下:




       我们可以清楚的发现,我们的Hero越过了重重阻壑,终于到达了目的地。可是问题此时又随之而来了:如果我们将Hero中的导航网格代理组件中的Auto Traverse Off Ms选项给去掉,它还会越过重重沟壑到达我们的目的地吗?实验证明这样做是无法成功的。官方文档对这个勾选的解释为:
       Automate movement onto and off of OffMeshLinks。大致是这样的:自动移动并且将OffMeshLinks关闭。你想,OffMeshLink都关闭了,Hero还怎么过来呢?

接下来我再来介绍OffMeshLink组件的用法了,操作非常简单:

       1. 新建连个空的GameObject或者你用模型也行,分别取名为StartPoint和EndPoint。为了便于识别,我用的是Sphere,给它上个绿色,并调节StartPoint的位置为Plane1上面,EndPoint的位置咋Plane2上面;


       2. 接着我们新建一个空的GameObject,取名为:SingleOffMeshLink,并让StartPoint与EndPoint成为其子物体,为SingleOffMeshLink加入OffMeshLink组件,做法:选中SingleOffMeshLink,然后再Unity菜单栏中:Component->Navigation->Off Mesh Link。如下图:

          

       我们将StartPont拖拽到Start上,EndPoint拖拽到End上。最后烘焙一下场景(此时Plane1与Plane2在Navigation中的OffMeshLink Generation勾选给去掉,目的是自定义我们的OffMeshLink):


       看到没,只生成了一条样条线。这就是我们自己做的OffMeshLink。我们可以编写一些简单的脚本来测试一下Hero此时是否会到达Plane2上的目的地。我想我们还会碰到一些问题的,但我觉得这些问题都不难解决,只需要花些时间尝试。那么最后留给读者一个问题,如果我们打开两个平面的OffMeshLink,那么Hero到底是从我们自定义的OffMeshLink上掠过还是从Plane自身生成的OffMeshLink上掠过?还有,如果我们将Hero身上的Auto Traverse Off Ms勾选给去掉,那么是不是犹如前一个实验一样发生越不过去的问题?这个问题如果弄清楚了,我的这篇帖子也就完成了它的使命了。