2D寻路我们一般以A*寻路为主,那么,3D游戏世界呢,NavMesh(导航网格) 是3D游戏世界中主动寻路的一种技术,如果你想让游戏人物能自动绕开障碍物到达目的地.那你就来学习下。
Navigation system由四部分组成:
导航网格(即 Navigation Mesh,缩写为 NavMesh)
是一种数据结构,用于描述游戏世界的可行走表面,并允许在游戏世界中寻找从一个可行走位置到另一个可行走位置的路径。该数据结构是从关卡几何体自动构建或烘焙的。
导航网格代理 (NavMesh Agent)
组件可帮助您创建在朝目标移动时能够彼此避开的角色。代理使用导航网格来推断游戏世界,并知道如何避开彼此以及移动的障碍物。
网格外链接 (Off-Mesh Link)
组件允许您合并无法使用可行走表面来表示的导航捷径。例如,跳过沟渠或围栏,或在通过门之前打开门,全都可以描述为网格外链接。
导航网格障碍物 (NavMesh Obstacle)
组件可用于描述代理在世界中导航时应避开的移动障碍物。由物理系统控制的木桶或板条箱便是障碍物的典型例子。障碍物正在移动时,代理将尽力避开它,但是障碍物一旦变为静止状态,便会在导航网格中雕刻一个孔,从而使代理能够改变自己的路径来绕过它,或者如果静止的障碍物阻挡了路径,则代理可寻找其他不同的路线。
接下去我们对一张现有的地图进行导航网格的制作:
构建导航网格:
原始图:
shaded:
wireframe:
选中目标地形,将Inspector面板中Static状态设置为Nav Static
2.打开Navigation窗口(Window->AI->Navigation),选择烘焙即可
以上有些参数需要了解:
参数 Agent Radius 定义代理中心与墙壁或窗台的接近程度。(间隔) Agent Height 定义代理可以达到的空间有多低。(进坑) Max Slope 定义代理走上坡道的陡峭程度。(爬坡) Step Height 定义代理可以踏上的障碍物的高度。(楼梯) Drop Height 可跳下的高度。 Jump Distance 可以跃过的距离。 Min Region Area 可剔除未连接的小型导航网格区域。表面积小于指定值的导航网格区域将被移除。
烘焙出来后,图就如下了,覆盖上了一层蓝绿色的膜:
创建导航主体:
角色,皮卡丘,:
添加组件:
创建静态障碍物:
现在给场景中添加一些静态的障碍物,看他能否绕开他们到达目的地。添加几个Cube,然后调节他们的大小,在把他们在Object栏中的Navigation Static勾选上,最后烘培一下,效果如下,凡是没有被蓝色覆盖到的都是障碍点。
加上导航轨迹:
创建shader
//导航箭头 Shader "Custom/NavPathArrow" { Properties { _MainTex ("Texture", 2D) = "white" {} _ScrollYSpeed("Y Scroll Speed", Range(-20, 20)) = 20 } SubShader { Tags { "Queue" = "Transparent" "RenderType"="Transparent" } LOD 100 //双面渲染 Cull Off //Alpha混合 Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; fixed _ScrollYSpeed; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed2 uv = i.uv; uv.y += _ScrollYSpeed * _Time; fixed4 col = tex2D(_MainTex, uv); return col; } ENDCG } } }
创建材质:
创建material,Shader添加以上创建的shader,贴图添加合适的贴图即可。
创建个3D quad,然后把材质添加上去,制作成一个prefab,
创建脚本,
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player: MonoBehaviour { private NavMeshAgent agent; public MeshRenderer meshRenderer;//箭头3D对象Quad private List<Transform> points = new List<Transform>();//路径点 private List<MeshRenderer> lines = new List<MeshRenderer>();//显示的路径 private Vector3[] path; public float xscale = 1f;//缩放比例 public float yscale = 1f; private void Start() { agent = GetComponent<NavMeshAgent>(); //箭头宽度缩放值 xscale = meshRenderer.transform.localScale.x; //箭头长度缩放值 yscale = meshRenderer.transform.localScale.y; meshRenderer.gameObject.SetActive(false); } [System.Obsolete] private void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit,100)) { HidePath(); Vector3 point = hit.point; agent.destination = hit.point; path = agent.path.corners; //线段整体y轴加高,使轨迹悬浮 for (int i = 0; i < path.Length; i++) { path[i] = path[i] + new Vector3(0, 0.01f, 0);//这里的y要根据地形调整,原则是不要埋到地下去为准 } //绘制路径 DrawPath(); } } } //画路径 public void DrawPath() { if (path == null || path.Length <= 1) return; for (int i = 0; i < path.Length - 1; i++) { DrawLine(path[i], path[i + 1], i); } } //隐藏路径 public void HidePath() { for (int i = 0; i < lines.Count; i++) lines[i].gameObject.SetActive(false); } //画路径 private void DrawLine(Vector3 start, Vector3 end, int index) { MeshRenderer mr; if (index >= lines.Count) { mr = Instantiate(meshRenderer); lines.Add(mr); } else { mr = lines[index]; } var tran = mr.transform; var length = Vector3.Distance(start, end); tran.localScale = new Vector3(xscale, length, 1); tran.position = (start + end) / 2; //指向 tran.LookAt(start); //旋转偏移 tran.Rotate(90, 0, 0); mr.material.mainTextureScale = new Vector2(1, length * yscale); mr.gameObject.SetActive(true); } }
然后把脚本挂载到游戏主体皮卡丘上:
最终效果如下:
总结:
虽然这个AI系统省去了我们的很多麻烦,但还是有些不方便的地方,如不能渲染出竖直方向的NavMesh,不能做到动态的Bake,不能去实现一些有趣的玩法和随机地图的生成功能,不过高级NavMesh将会完成Unity自带NavMesh未完成的使命。