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),來實現雙向和單向阻擋的效果,經驗證這種方案表現效果比較好,只是在場景制作時就必須確定不同層區域的划分。
上述方案再配合一些魔法牆之類的特效,總體來說表現效果還是不錯的,不過代碼邏輯一定要清晰。