NavMesh系统动态碰撞的探讨
Unity3D提供的NavMesh系统可以方便的解决游戏的寻路问题,但是该系统有一个比较让人不理解的问题:
NavMesh导航时会忽略Physics系统本身的碰撞,也就是说NavMeshAgent在移动的过程中不会被Collider阻挡,而是会直接走过去(但是OnTriggerEnter等触发功能正常)。
动态碰撞的功能对很多游戏都是一个基本的需求,而根据NavMesh提供的接口,唯一可以实现阻挡功能的只有NavMeshObstacle,而NavMeshObstacle只有一种形状:圆柱体,而且up方向固定,不能调整为侧向。总结起来就是以下几点:
(1)导航网格的行走/碰撞区域只能预烘焙;
(2)动态碰撞体只能通过挂载NavMeshObstacle组件来实现;
(3)碰撞体的形状只有一种——圆柱体,严格来说就是圆形,而且是正圆还不能是椭圆。
所以说到这里,基本上可以放弃使用各种形状的Collider来制作场景阻挡物了。不过,替代方案也还是有的:如果一定要使用Unity3D提供的NavMesh来做导航,那么可以将圆作为基本元素来模拟其它形状。
上图展示了通过NavMeshObjstacle来模拟立方体阻挡物,为了方便的编辑该立方体的大小,可以写一个辅助脚本来实现:
using UnityEngine; using System.Collections; using System.Collections.Generic; [ExecuteInEditMode] public class MultiObstacleHelper : MonoBehaviour { public float Interval = 1f; // Obstacle之间的间隔 public int Num = 1; // Obstacle的个数 private float curInterval = 1f; private int curNum = 1; private Transform template = null; void Awake() { template = gameObject.transform.Find("Obstacle"); } void Start() { Adjust(); } void Update() { if (Num <= 0) Num = curNum; Adjust(); } private void Adjust() { if (template == null) return; AdjustInterval(AdjustNum()); } private bool AdjustNum() { if (curNum == Num) return false; if (Num > curNum) { for (int i = 0; i < Num - curNum; ++i) { GameObject go = GameObject.Instantiate(template.gameObject) as GameObject; go.transform.parent = template.parent; go.transform.localPosition = Vector3.zero; go.transform.localScale = Vector3.one; go.transform.localRotation = Quaternion.identity; } } else if (Num < curNum) { int count = curNum - Num; List<Transform> lst = new List<Transform>();for (int i = 0; i < template.parent.transform.childCount; ++i) { if (count <= 0) break; if (template.parent.GetChild(i) != template) { lst.Add(template.parent.GetChild(i)); count--; } } while(lst.Count > 0) { Transform tran = lst[0]; GameObject.DestroyImmediate(tran.gameObject); lst.RemoveAt(0); } lst.Clear(); } curNum = Num; return true; } private void AdjustInterval(bool numChange) { if (numChange == false && curInterval == Interval) return; int half = Num / 2; int index = 0; foreach (Transform tran in template.parent.gameObject.transform) { // 奇数个 if (Num % 2 == 1) { Vector3 pos = tran.localPosition; pos.x = (index - half) * Interval; tran.localPosition = pos; } else { Vector3 pos = tran.localPosition; pos.x = (index - half + 0.5f) * Interval; tran.localPosition = pos; } index++; } curInterval = Interval; } }
上述代码可以调整Obstacle的个数和间距,然后再配合调整缩放比例基本上可以做出各种尺寸的立方体。
单向阻挡的实现,可以通过组合Trigger和NavMeshObstacle来实现一个单向阻挡的功能:
实现思路是当角色进入红色Trigger区域时,将后面的阻挡物隐掉,过1秒之后再激活,这样就可以实现一个单向阻挡物的功能,实现的代码比较简单,如下面所示:
using UnityEngine; using System.Collections; #if UNITY_EDITOR using UnityEditor; #endif public class SinglePassTrigger : MonoBehaviour { [HideInInspector] public Transform Object = null; public Transform Collider = null; public float PassTime = 1f; void Start() { Object = transform.parent.transform.Find("Object"); Collider = transform.parent.transform.Find("Collider"); } protected virtual void OnTriggerEnter(Collider other) { StopCoroutine("LetPassCoroutine"); StartCoroutine("LetPassCoroutine"); } protected virtual void OnTriggerExit(Collider other) { } IEnumerator LetPassCoroutine() { SetPassState(true); float startTime = Time.time; while(Time.time < startTime + PassTime) { yield return null; } SetPassState(false); } private void SetPassState(bool value) { if (Collider == null) return; Collider.gameObject.SetActive(!value); } #if UNITY_EDITOR void OnDrawGizmos() { // 设置旋转矩阵 Matrix4x4 rotationMatrix = Matrix4x4.TRS(Vector3.zero, transform.rotation, Vector3.one); Gizmos.matrix = transform.localToWorldMatrix; // 在Local坐标原点绘制标准尺寸的对象 Gizmos.color = new Color(1f, 0f, 0f, 0.8f); Gizmos.DrawCube(Vector3.zero, Vector3.one); Gizmos.color = Color.black; Gizmos.DrawWireCube(Vector3.zero, Vector3.one); Gizmos.DrawIcon(transform.position + Vector3.up, "ban.png"); } #endif }
>>>>>>经测试,上述方案并不是很好用,会碰到以下几个问题:
(1)角色在Obstacle周围挤来挤去,行为很诡异;
(2)通过不断地靠近Obstacle,当遇到卡顿的时候,角色会穿透阻挡物;
(3)Obstacle虽然可以设置Cave属性,也就是动态切割导航面,但由于一些原因,动态切割的效果非常差,尤其是在一些不平的地面部分更是如此。
基于这些思考,推荐使用如下新的方法来做阻挡效果:
通过NavMesh的Layer来实现:
通过动态改变NavMeshAgent所能使用的层(NavMeshWalkable),来实现双向和单向阻挡的效果,经验证这种方案表现效果比较好,只是在场景制作时就必须确定不同层区域的划分。
上述方案再配合一些魔法墙之类的特效,总体来说表现效果还是不错的,不过代码逻辑一定要清晰。