ゆべねこの足跡

IT系のはなしを残しておく場所です

NavMeshAgentを使って逃げるAIを作った話

どうも、ゆべしネコです。そういえば最近Unityいじれてないなとふと思ったので、リハビリがてら簡単なゲームを作ってみようと思いましてここ最近はUnityをいじって遊んでました。今回の目標はUniRxを使った鬼ごっこゲームを作ることです。...と言っても、ほとんどの時間を逃げるAIの作成に割かれてしまった訳ですが...笑

結果、こんなんできました。


Unity NavMeshAgentで逃げるAI

逃げる仕組み

逃げるAIって言ってもどうやって逃げるようにしたらいいのか悩みますよね。最初は逃げる方向に適当に次の目的地を設定して、そこが移動可能かどうか判定してもし移動不可ならもう一度実行する...みたいなAIがいいかと思ったのですが、なんか良さげな実装が思いつかなかったので、もっと単純化することにしました。

逃げる位置となるポジションを最初から置いておく

シーンが始まると同時にゲームオブジェクトが生成されます。

MovePointGenerete.cs

using UnityEngine;

public class MovePointGenerete : MonoBehaviour {

    [SerializeField]
    private int _genereteNum = 30;

    void Start ()
    {
        var parent = new GameObject ("MovePoints");
        for (int i = 1; i <= _genereteNum; i++)
        {
            var nextMovePoint = new GameObject();
            nextMovePoint.name = ("MovePoint(" + i + ")");
            nextMovePoint.tag = "MovePoint";
            nextMovePoint.layer = 9;
            nextMovePoint.AddComponent<SphereCollider>();
            nextMovePoint.AddComponent<NextMovePosition>();
            var rb = nextMovePoint.AddComponent<Rigidbody>();
            rb.useGravity = false;
            rb.constraints = RigidbodyConstraints.FreezePosition;
            nextMovePoint.transform.parent = parent.transform;
            nextMovePoint.transform.position = new Vector3 (Random.Range(-50, 50), 0, Random.Range(-50, 50));
        }
    }
}

ポイントはレイヤーとコライダー、Rigidbodyコンポーネントの追加ですね。この時にPhysicsウィンドウを開いてレイヤーでの衝突設定を行い、プレイヤー、逃げる敵がこのオブジェクトに触れられないようにします。この流れでこのあとどうするかわかった方もおられるのではないでしょうか。

プレイヤーに近づかれたら周辺の逃げるポジションを探して逃げる

プレイヤーに近づかれたら周辺のゲームオブジェクトを探してきます。

Enemy.cs一部抜粋

public void RunAway ()
    {
        currentState = EnemyState.TENSION;
        coloring.material.color = Color.red;
        escapeTime = 0;
        agent.angularSpeed = 200;

        //プレイヤーと逆方向を向く
        var diff = (transform.position - player.transform.position).normalized;
        diff.y = 0;
        transform.rotation = Quaternion.FromToRotation(diff, Vector3.up);
        agent.SetDestination(GetNextPosition());
        
        //目的地に近づいたら次の目的地を検索
        agent.ObserveEveryValueChanged(d => agent.remainingDistance)
            .Where (d => d < 2.0f)
            .Where(_ => currentState == EnemyState.TENSION)
            .Subscribe (_ =>
            {
                var nextposition = GetNextPosition ();
                agent.SetDestination(nextposition);
            }).AddTo(gameObject);
        
        //一定距離離れて一定時間経ったら状態を戻す
        this.UpdateAsObservable ()
            .TakeWhile(_ => currentState == EnemyState.TENSION)
            .Select(_ => (transform.position - player.transform.position).sqrMagnitude)
            .Where(distance => distance > targetDistance * targetDistance)
            .Subscribe(distance =>
            {
                escapeTime += Time.deltaTime;
                if (escapeTime >= 10) Usual ();
            });
    }

    public Vector3 GetNextPosition ()
    {
        if (m_foundList.Count > 0) m_foundList.Clear();
        m_foundList.AddRange(Physics.OverlapSphere(transform.position, _searchRadius, mask));
        if (m_foundList.Count == 0)
        {
            //近くになかったときは半径40mの中にあるオブジェクトを獲得し、そこからランダムに選ぶ
            m_foundList.AddRange (Physics.OverlapSphere(transform.position, 40.0f, mask));
            foreach (var obj in m_foundList) Debug.Log (obj.gameObject.name);
        }
        else
        {
            for (int i = 0; i < m_foundList.Count; i++)  
            {
                var foundData = m_foundList[i];
                if (!CheckFoundObject(foundData.gameObject))
                    m_foundList.Remove( foundData );
            }
        }

        return m_foundList.Count > 0 ?  m_foundList[Random.Range(0, m_foundList.Count-1)].transform.position : Vector3.zero;
    }

    private bool CheckFoundObject( GameObject i_target )
    {
        var myPositionXZ = Vector3.Scale( transform.position, new Vector3( 1.0f, 0.0f, 1.0f ) );
        var targetPositionXZ = Vector3.Scale( i_target.transform.position, new Vector3( 1.0f, 0.0f, 1.0f ) );
        var toTargetFlatDir = ( targetPositionXZ - myPositionXZ ).normalized;

        //同位置にいるときは範囲内にいるとみなす
        if (toTargetFlatDir.sqrMagnitude <= Mathf.Epsilon) return true;
        return (Vector3.Dot (transform.forward, toTargetFlatDir)) >= m_searchCosTheta;
    }
}

まず最初にプレイヤーと逆の方向を向かせます。その後、次のポジションを見つけるのですが、ここで先ほど作ったゲームオブジェクトが役立ちます。 Physics.OverlapSphere(transform.position, _searchRadius, mask)を使って探索範囲中にある次のポジションのコライダーの配列を取得し、リストに追加します。しかし、球の中全てからランダムに選ぶのではあまりよろしくありません。なぜなら、プレイヤーの方向にある次のポジションも選択可能になってしまうからです。そこで、得られたリストのなかのデータの選別を行います。それにはこちらの記事を参考にしました。

www.urablog.xyz

得られたコライダーのポジション情報とEnemy自身のポジション情報から角度を判定し、一定範囲内にないものをリストから除外するという流れですね。これで自身の前方の一定範囲内にある次の移動地点が入ったリストができるというわけです。最後にこれらのリストからランダムに一点を選ぶようにして次のポジションを決定します。一も見つからなかったときは操作範囲を広げて、Physics.OverlapSphereメソッドを実行し、こちらでは角度判定を行わないで最終移動地点を選択するようにしています。

このように、逃げる位置を最初から作っておき、そこから角度を判定して適切な逃げる位置を選択し続けることでプレイヤーから遠ざかるような動きをするAIを作りました。

欠点

角度を取ってくるときに壁の向こうも取ってきてしまうことですね。そのため、壁の向こう側が次の移動地点に選ばれてしまうと壁でものすごく加速します。まぁ、それはそれで面白いのですが、改善点ではありますね。

あと、次のポジションのY座標が0で固定なので、Terrainや階層構造を持つステージでは使えない点ですね。平面ステージ限定でしか使えません。

その他やったこと

初めてAudioManagerを実装してみました。以前はC#の知識やUnityの知識が足りなかったため敬遠していたのですが、そろそろやってみてもいいだろうという訳でやってみました。実装にはこちらの記事のものを利用させていただきました。

kan-kikuchi.hatenablog.com

やばい...すごく使いやすい!!

感動しました。ただ、立体音響には使えなさそうなのでVRでやるには工夫が必要ですね。

まとめ

鬼ごっこで使うには十分かな? と思えるようなAIができました。リハビリにもなったので自分としては満足です。一応GitHubにもあげておきましたので、中身の詳細が気になる方はぜひダウンロードしてみてください。

GitHub - yubesi/Tag: 鬼ごっことかで使えそうな逃げるAIをUniRxを使って作ってみました。ただし、平面だけでしか使えないです。