(この記事の使用環境: Unity5.4.2f2 Personal、Windows10


今回はUnityのナビゲーション関連の学習をしたメモです。

この前の記事 では平面上で骸骨さんを動かしましたが、今回は高低差のある地形で動いてもらいました。
全画面キャプチャ 20170104 191523


動画はこんな感じ。


概要はこんな感じ。
  • 骸骨さんがNavMeshAgentで動く
  • 複数設定されたポイントを経由地とする
  • 一定時間ごとに歩く⇔立ち止まるを繰り返す
  • 傾斜のある坂では移動速度を下げる
  • 高所から飛び降りるポイント、ジャンプで飛び越えるポイントを設定した(OffMeshLinkの利用)
  • 飛び降りと飛び越えについては、まだ専用モーションを付けられていません(次回以降で対応予定)

材料はこんな感じ。
全画面キャプチャ 20170104 190400

  • SkeltonAC(Animation Controller): 骸骨さんのAnimation Controller
  • ImportAssets: いつもの Fantasy Monster - Skeleton を使っています
  • Materials: 地面などの色を設定するMaterial
  • Skelton(Prefab): 骸骨さんの設定を記憶しておくためのPrefab
  • SkeltonMoveThrough(Scripts): 骸骨さんの動きを書いたScript
  • (そのほかは最終的には使用していない素材。途中版など)

NavMeshのBake設定はこんな感じ。
「Generated Off Mesh Links」の「Drop Height」と「Jump Distance」に0より大きな値を設定することで、その値に応じてBake時に「Drop Down」および「Jump Across」のOffMeshLinkが生成されます。
全画面キャプチャ 20170104 191153

全画面キャプチャ 20170104 191523

上図シーンビューで「〇→〇」「〇←→〇」のように表示されているのがOffMeshLinkです。
地面に水色で表示されたNavMeshがつながっていない場所も、OffMeshLinkによって移動することができるようになっています。

また、上手で坂道の部分はNavMeshがピンク色になっています。
これはNavMeshのObject設定で「Navigation Area」を個別に設定しているためです。
また、Generate OffMeshLinkのチェックを外していますが、こうすることで、坂道の場所からは飛び降りないようにできます。
全画面キャプチャ 20170104 192148

これがNavMeshのAreas設定です。「Slope」を追加しています。各エリアの「Cost」はNavMeshAgentが経路計算を行う際に、そのエリアを通るためにかかるコストを示しています。直線距離では近くても、コストが高くなる経路は迂回されるってことだと思います。
全画面キャプチャ 20170104 192555

ここまでがNavMeshの設定です。

で、これは骸骨さんオブジェクトのInspector。
前半。Rigidbodyを付ける場合はNavMeshAgentと競合しないようIsKinematicにしておくのが良いようです。
SkeltonMoveThroughスクリプトでは各種設定値をpublicにし、Inspectorから設定できるようにしています。経由地のTransformもInspectorにドラッグアンドドロップで設定できるようにしています。
全画面キャプチャ 20170104 193103

後半。NavMeshAgentと、地面との接触判定用に足元にBoxCollider(IsTrigger)を敷いて、地面の情報を取得できるようにしています。
全画面キャプチャ 20170104 193529

骸骨さんの足元に敷いたBoxCollider(IsTrigger)。
全画面キャプチャ 20170104 193925

これはAnimatorの遷移図。右側に切れているのはジャンプのアニメーションですが、今回はうまく動かせていません。うまく動かせるようになったら次回以降の記事にします。
全画面キャプチャ 20170104 194308

で、最後に骸骨さんを動かすスクリプト。

まずは初期化までのところ。
ポイントは経由地をTransformの配列として、Inspectorから指定できるようにしているところくらいでしょうか。

using UnityEngine;
using System.Collections;

// --- Class SkeltonMoveThrough : 経由地を通り過ぎる行動パターン ---------------------------------------------------
public class SkeltonMoveThrough : MonoBehaviour
{

    public Transform[] destination;

    public float defaultSpeed;
    public float slopeSpeed;

    public float dropDownSpeed;
    public float jumpAcrossSpeed;

    public float idleMaxTime;
    float _idleTime = 0f;

    public float walkMaxTime;
    float _walkTime = 0f;

    Vector3 _linkEndPos;

    enum eState
    {
        Idle,
        Walk,
        DropDown,
        JumpAcross,
    }
    eState state = eState.Idle;

    Animator anim;
    NavMeshAgent nav;
    Rigidbody rb;

    // --- 初期化 ------------------------------------------------------------------------------------
    void Start ()
    {
        anim = GetComponent<Animator> ();
        nav = GetComponent<NavMeshAgent> ();

        nav.speed = defaultSpeed;
        Debug.Log ("Default Speed: " + nav.speed);
    }

で、Update()での更新処理と、立ち止まっているときの処理。
Update()のなかはswitchで各状態の処理に飛ばすだけにしています。このやり方すき。

    // --- 更新処理 ----------------------------------------------------------------------------------
    void Update ()
    {

        switch (state)
        {
            case eState.Idle:
                Idle ();
                break;

            case eState.Walk:
                Walk ();
                break;

            case eState.DropDown:
                DropDown ();
                break;

            case eState.JumpAcross:
                JumpAcross ();
                break;
        }
    }

    // --- 立ち止まっているときの処理 -----------------------------------------------------------------
    void Idle ()
    {
        _idleTime += Time.deltaTime;

        if (_idleTime > idleMaxTime)     // 一定時間立ち止まると、再び歩き出す
        {
            state = eState.Walk;
            anim.SetTrigger ("Walk");
            nav.Resume ();
            _idleTime = 0f;
        }
    }


で、歩いているときの処理。処理は大きく分けで3つ。
  • 経由地に着くと、次の経由地を目指す
  • 踏んだOffMeshLinkのタイプに応じて、「飛び降り」または「飛び越え」に遷移する
  • 一定時間歩いたら立ち止まる
NavMeshAgent.isOnOffMeshLink のときに、NavMeshAgent.currentOffMeshLinkData を取得するのがポイントでしょうか。これを見つけるまで時間がかかりました・・・。

    // --- 歩いているときの処理 -----------------------------------------------------------------
    void Walk ()
    {

        if (( transform.position - nav.destination ).magnitude < 0.5f)    // 経由地に着くと、次の経由地を目指す
        {
            nav.SetDestination (destination[Random.Range (0, destination.Length)].position);
        }

        if (nav.isOnOffMeshLink)                // 「飛び降り」または「ジャンプで飛び越え」へ遷移する
        {
            OffMeshLinkData linkData = nav.currentOffMeshLinkData;
            _linkEndPos = linkData.endPos;

            if (linkData.linkType == OffMeshLinkType.LinkTypeDropDown)       // 「飛び降り」への遷移
            {
                state = eState.DropDown;
                nav.speed = dropDownSpeed;

                Debug.Log ("Drop Down. Speed: " + nav.speed);
                return;
            }

            if (linkData.linkType == OffMeshLinkType.LinkTypeJumpAcross)    // 「ジャンプで飛び越え」への遷移
            {
                state = eState.JumpAcross;
                nav.speed = jumpAcrossSpeed;
                anim.SetTrigger ("Jump");
                
                Debug.Log ("Jump Across. Speed: " + nav.speed);
                return;
            }
        }

        _walkTime += Time.deltaTime;

        if (_walkTime > walkMaxTime)     // 一定時間歩くと、立ち止まる
        {
            state = eState.Idle;
            anim.SetTrigger ("Idle");
            nav.Stop ();
            //nav.speed = 0f;
            _walkTime = 0f;
        }
    }


「飛び降り」および「飛び越え」状態に入った後、OffMeshLinkの終点に近づいたら、再び普通に歩きだします。
今回の時点ではどちらも同じ処理内容です。今後処理内容が変わる可能性もあるので、1つにまとめずこのままにしておきます。

    // --- 飛び降りているときの処理 -----------------------------------------------------------------
    void DropDown ()
    {
        if (( transform.position - _linkEndPos ).magnitude < 0.1f)       // 終わりを検出し、再び歩く
        {
            state = eState.Walk;
            nav.speed = defaultSpeed;

            Debug.Log ("Restart Walk. Speed: " + nav.speed);
        }
    }

    // --- ジャンプで飛び越えているときの処理 -----------------------------------------------------------------
    void JumpAcross ()
    {
        if (( transform.position - _linkEndPos ).magnitude < 0.1f)       // 終わりを検出し、再び歩く
        {
            state = eState.Walk;
            nav.speed = defaultSpeed;

            Debug.Log ("Restart Walk. Speed: " + nav.speed);
        }
    }


で最後に、坂道に入ったら(出たら)移動速度を変える処理。骸骨さんの足元のBoxColliderで地面との接触判定を取得し、接触したオブジェクト(地面)のタグによって移動速度を変えています。
今後、沼地で移動が遅くなる処理なども追加していけるよう、switch文にしています。

    // --- 特殊地面に入ったら移動速度を変える -----------------------------------------------------------------
    private void OnTriggerEnter (Collider other)
    {
        if (state == eState.Walk)
        {
            switch (other.gameObject.tag)
            {
                case "Slope":
                    nav.speed = slopeSpeed;
                    Debug.Log ("On Slope. Speed: " + nav.speed);
                    break;
            }
        }
    }

    // --- 特殊地面から出たら移動速度を元に戻す -----------------------------------------------------------------
    private void OnTriggerExit (Collider other)
    {
        if (state == eState.Walk)
        {
            switch (other.gameObject.tag)
            {
                case "Slope":
                    nav.speed = defaultSpeed;
                    Debug.Log ("Exit from Slope. Speed: " + nav.speed);
                    break;
            }
        }
    }
}

坂道のオブジェクトにあらかじめ tag "Slope" を付けておき、それを検出するようにしています。
BlogPaint


今回は以上です。