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),來實現雙向和單向阻擋的效果,經驗證這種方案表現效果比較好,只是在場景制作時就必須確定不同層區域的划分。

  上述方案再配合一些魔法牆之類的特效,總體來說表現效果還是不錯的,不過代碼邏輯一定要清晰。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM