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


今回はターゲットを探して彷徨う骸骨さんを作りました。
全画面キャプチャ 20170102 173158


Assetはこれまでに何回か登場している Fantasy Monster - Skeleton です。
骸骨さんの3DモデルとAnimationClipが入っています。

途中けっこう試行錯誤したのですが、この記事では最終形のみを簡潔にまとめたいと思います。
最終形はこんな感じです。


概要はこんな感じです。
  • 骸骨さんがターゲットを探して彷徨います
  • ターゲットが見つからない間は立ち止まって左右を見まわしたり、ランダムに彷徨ったりします
  • ターゲットを見つけるとターゲットを追いかけます
  • 骸骨さんは視野角を持ち、前方の視野角(と可視距離)の範囲内でターゲットを探し、見つけたら追いかけます。ただしターゲットが視野角内に入っていても、間に障害物がある場合はターゲットを発見できません
  • 至近距離にターゲットが近づいた場合もターゲットを発見できます
  • ターゲットを追っている間、一定時間ターゲットが視界から外れ続けると、再び見失います
  • 動作確認のため、視野角内にターゲットがいる場合は骸骨さんとターゲットを結ぶRayを表示しています。ターゲットを視認できている場合は赤色、間に障害物があり視認できていない場合はグレーで表示しています
  • その他、攻撃モーション、被ダメージモーション、死にモーションを付けています。(今回は詳しい説明を省きます。武器に衝突判定を付ける記事はこちら )

Animatorの遷移図の全体像はこんな感じ。
全画面キャプチャ 20170102 173914

各状態はこんな感じ。
  • Idle: 立ち止まって左右を見まわし、ターゲットを探します
  • Walk: ターゲットを探して彷徨います
  • Run: ターゲットを見つけて追いかけます
  • Attack: ターゲットに接したら攻撃します
  • Damage: 攻撃を受けてダメージを負います
  • Death: 死にます
状態遷移のためのパラメータはこんな感じ。
  • Idle(Trigger): 立ち止まり状態への遷移
  • Walk(Trigger): 彷徨い状態への遷移
  • Run(Trigger): 追いかけ状態への遷移
  • Attacking(Bool): 攻撃状態への遷移(ターゲットと一定距離に近づいたらtrue、離れたらfalse)
  • Damaged(Trigger): ダメージモーションへの遷移
  • Died(Trigger): 死に状態への遷移

次に、骸骨さんを動かすスクリプト側の説明。

変数はこんな感じ。今回は各種変数をpublicにして、Inspector側で値を変えながら試行錯誤できるようにしてみました。

using UnityEngine;
using System.Collections;

public class SkeltonControllerWithEyeSight : MonoBehaviour
{

    public Transform target;        // ターゲットの位置情報

    public int lifeMax;             // ライフ
    int _currentlife;

    Rigidbody rb;
    Animator anim;
    NavMeshAgent nav;

    public float visibleDistance;   // 可視距離
    float targetDistance;           // ターゲットとの距離

    public float sightAngle;        // 視野角

    Transform lineOfSight1;         // 目の位置
    Ray gazeRay1;                   // 目とターゲットを結ぶRay

    public LayerMask visibleLayer;  // 見る対象のLayer(ターゲットと障害物が含まれる)

    public float walkSpeed;         // さまよっているときの速度
    public float runSpeed;          // おいかけているときの速度

    public float targetLostLimitTime;   // ターゲットを見失うまでの時間
    public float targetFindDistance;    // ターゲットを見つける距離(至近距離に近寄ると視野に関係なく見つける)
    float _lostTime = 0f;

    public float idleMaxTime;       // たちどまっている時間
    float _idleTime = 0f;

    public float wanderMaxTime;     // さまよっている時間
    float _wanderTime = 0f;

    enum eState                     // 状態
    {
        Idle,       // 立ち止まっている
        Wander,     // さまよっている
        Chase,      // 追っている
        Attack,     // 攻撃している
        Dead,       // 死んでいる
    }
    eState _state = eState.Idle;

更新処理はFixedUpdateで、こんな感じ。switchから各状態の処理に飛ばしています。
ダメージを受ける処理もここ。(今回の本題ではないので、マウスクリックで呼び出されるように仮置きしてあります)

    // --- 初期化 ----------------------------------------------------------
    private void Start ()
    {
        rb = GetComponent<Rigidbody> ();
        anim = GetComponent<Animator> ();
        nav = GetComponent<NavMeshAgent> ();
        _currentlife = lifeMax;
        lineOfSight1 = GameObject.Find ("LineOfSight1").transform;
    }

    // --- 更新処理 ----------------------------------------------------------
    private void FixedUpdate ()
    {
        switch (_state)
        {
            case eState.Idle:
                Idle ();
                break;

            case eState.Wander:
                Wander ();
                break;

            case eState.Chase:
                Chase ();
                break;

            case eState.Attack:
                Attack ();
                break;

            case eState.Dead:
                break;
        }

        if (Input.GetMouseButtonDown (0) && _state != eState.Dead)      // 攻撃を受けたときの処理
        {
            _currentlife--;
            Debug.Log ("Zombie Life: " + _currentlife);

            nav.speed = 0f;

            if (_currentlife <= 0)                                      // 死ぬ
            {
                anim.SetTrigger ("Died");
                _state = eState.Dead;
                nav.Stop ();
            }
            else                                                        // ダメージを受ける
            {
                anim.SetTrigger ("Damaged");
            }
        }
    }

立ち止まっているときと彷徨っているときの処理。一定時間で立ち止まり⇔彷徨いの遷移をおこなうためにタイマー処理を入れています。ターゲットを探す処理は後述のSearch()関数のなかで行っています。

    // --- 立ち止まっているときの処理 ----------------------------------------------------------
    void Idle ()
    {
        Search (_state);

        _idleTime += Time.deltaTime;
        if (_idleTime > idleMaxTime)                        // 一定時間立ち止まったら、さまよう
        {
            Debug.Log ("Wandering");
            anim.SetTrigger ("Walk");
            nav.Resume ();
            nav.SetDestination (new Vector3 (Random.Range (-14f, 14f), 0f, Random.Range (-14f, 14f))); // ランダムな場所へ向かう
            _state = eState.Wander;
            nav.speed = walkSpeed;
            _idleTime = 0f;
        }
    }

    // --- さまよっているときの処理 ----------------------------------------------------------
    void Wander ()
    {
        Search (_state);

        _wanderTime += Time.deltaTime;
        if (_wanderTime > wanderMaxTime || nav.remainingDistance < 0.5f) // 一定時間さまようか行先に着いたら、立ち止まる
        {
            Debug.Log ("Idling");
            anim.SetTrigger ("Idle");
            nav.Stop ();
            _state = eState.Idle;
            _wanderTime = 0f;
            return;
        }
    }


で、これがターゲットを探すSearch()関数。状態を引数に取って、状態に応じて少しだけ処理を変えています。
視界でターゲットを見つけたときと、距離でターゲットを見つけたときの処理を書いています。
(もっとシンプルに書けるような書けないような・・・)

    // --- ターゲットを探す処理 ----------------------------------------------------------
    void Search (eState state)
    {
        float _angle = Vector3.Angle (target.position - transform.position, lineOfSight1.forward);

        if (_angle <= sightAngle)
        {
            Debug.Log ("Target In SightAngle");

            gazeRay1.origin = lineOfSight1.position;
            gazeRay1.direction = target.position - lineOfSight1.position;
            RaycastHit hit;

            if (Physics.Raycast (gazeRay1, out hit, visibleDistance, visibleLayer))
            {
                Debug.DrawRay (gazeRay1.origin, gazeRay1.direction * hit.distance, Color.red);

                if (hit.collider.gameObject.tag != "Obstacle")    // ターゲットとの間に障害物がない
                {
                    if (state == eState.Idle || state == eState.Wander)
                    {
                        TargetFound ();
                    }
                    else if (state == eState.Chase)
                    {
                        TargetInSight ();
                    }
                    return;
                }
            }

            Debug.DrawRay (gazeRay1.origin, gazeRay1.direction * visibleDistance, Color.gray);
        }

        targetDistance = ( transform.position - target.position ).magnitude;

        if (targetDistance < targetFindDistance)            // 距離でターゲット発見
        {
            if (state == eState.Idle || state == eState.Wander)
            {
                TargetFound ();
            }
            else if (state == eState.Chase)
            {
                TargetInSight ();
            }
            return;
        }
    }

立ち止まりorさまよい状態でのターゲット発見を TargetFound() で、ターゲットを追っているときの処理を Chase() で、追っている状態でターゲットを視野に収めたときの処理を TargetInSight() で、それぞれ書いています。

    // --- ターゲットを発見したときの処理 ----------------------------------------------------------
    void TargetFound ()
    {
        Debug.Log ("Target Found");
        anim.SetTrigger ("Run");
        nav.Resume ();
        nav.SetDestination (target.position);
        _state = eState.Chase;
        nav.speed = runSpeed;
        _idleTime = 0f;
        _wanderTime = 0f;
    }

    // --- ターゲットを追っているときの処理 ----------------------------------------------------------
    void Chase ()                                // 
    {
        nav.SetDestination (target.position);
        Search (_state);

        _lostTime += Time.deltaTime;
        // Debug.Log ("LostTime: " + _lostTime);

        if (_lostTime > targetLostLimitTime)                 // 一定時間視界の外なら、見失う
        {
            Debug.Log ("Target Lost");                       // ターゲットロスト
            _state = eState.Idle;
            nav.Stop ();
            anim.SetTrigger ("Idle");
            nav.speed = 0f;
            _lostTime = 0f;
        }
    }

    // --- ターゲットが視野に入っているときの処理 ----------------------------------------------------------
    void TargetInSight ()
    {
        Debug.Log ("Target In Sight");
        _lostTime = 0f;
    }

ここは今回はおまけですが、攻撃状態に入る処理と攻撃状態から外れる処理。
骸骨さんにトリガー用のコライダーを付け、それにターゲットが接したら攻撃開始、外れたら攻撃をやめるようにしています。

    // --- 攻撃しているときの処理 ----------------------------------------------------------
    void Attack ()
    {
    }

    // --- プレイヤーに接したら攻撃を行う ----------------------------------------------------------
    private void OnTriggerEnter (Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            anim.SetBool ("Attacking", true);
            _state = eState.Attack;
            nav.SetDestination (target.position);
            nav.speed = 0f;
        }
    }

    // --- プレイヤーから離れたら攻撃をやめる ----------------------------------------------------------
    private void OnTriggerExit (Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            anim.SetBool ("Attacking", false);
            _state = eState.Chase;
            nav.speed = runSpeed;
        }
    }
}

スクリプトの説明は以上。

参考までに、各種public変数の設定値と、NavMeshAgentの設定値。
全画面キャプチャ 20170102 183727

最後に、視線の位置合わせ。骸骨さんの3Dモデルの頭部にEmptyオブジェクトを付けています。
「LineOfSight1」のTransform.forwardが視線になるよう、「LineOfSightParent1」でRotationの調整を行っています。
これでアニメーションで骸骨さんが首を振るのにあわせて、視線が動くようになります。
全画面キャプチャ 20170102 184015

おまけとして、最終的にこのような視野角でのターゲット探しを行う前に、視線そのものでターゲットを探していた実装のときの動画も載せておきます。Rayが視線になっているので、表示されているRayの線にターゲットが接しないと発見できない仕様でした。視線Rayがターゲットに接したとき赤色に表示されています。これはこれで使う機会があるかもしれません。


今回は以上です。