Unityのナビメッシュとレイキャストでステルスゲームを作る

投稿者: | 2019-10-30

ナビメッシュでNPCを動かす

普段は同じところを巡回させて、プレイヤーを見つけると追ってくるNPCを作ります。

まずNPC(エージェント)の体をカプセルで作り、簡単な頭をカプセルの子オブジェクトに設定します。

顔のオブジェクトを体のオブジェクトにドラッグアンドドロップするだけです。

前の記事の方法でナビメッシュをベイクします。

4つ角に目的地となる小さなオブジェクトがあり、普段はエージェントがこのオブジェクトを目指して進みます。

目的地が近づくと、次の目的地が設定されるようにスクリプトを書きます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class agentScript : MonoBehaviour
{

    public Transform[] points;

    int n = 0;

    NavMeshAgent agent;

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.destination = points[0].position;
    }

    // Update is called once per frame
    void Update()
    {

        if (!agent.pathPending && agent.remainingDistance < 0.5f)
        {
            n += 1;

            if (n > 3)
            {
                n = 0;
            }

            agent.destination = points[n].position;

        }
            
    }
}

参考:https://docs.unity3d.com/ja/2018.1/Manual/nav-AgentPatrol.html

レイキャストでプレイヤーを見つける


Unityでは、レイというプレイ中は見えない光線を飛ばして、当たったオブジェクトや当たった場所までの距離などの情報を得る事ができます。
これを使って、プレイヤーが視界にはいったときだけ目的地をプレイヤーの現在位置に設定することで、エージェントはプレイヤーを発見次第追ってきます。
参考:https://docs.unity3d.com/jp/current/ScriptReference/Physics.Raycast.html

しかし、プレイヤーにレイが当たったときにだと、視線の先にプレイヤーがいないといけないので不自然ですね。

そこで今回は常にプレイヤーの方へレイを飛ばしておいて、そのレイが障害物に当たらずにプレイヤーに当たっているときで、しかも、エージェントの視線との角度が小さいときにだけ発見されるようにしてみます。

さらに、巡回しているときとプレイヤーを追っているときなど、エージェントの状態に合わせて、エージェントのアニメーションや移動速度を変えてみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

public class agentScript : MonoBehaviour
{

    public Transform[] points;
    public Transform player;
    public GameObject face;

    int n = 0;

    NavMeshAgent agent;

    RaycastHit hit_player;

    Vector3 playerPos;
    float angle;

    Animator faceAnimator;

    

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.destination = points[0].position;

        faceAnimator = face.GetComponent<Animator>();
    }

    int agentState = 0;

    // Update is called once per frame
    void Update()
    {
        // エージェントから見たプレイヤーの方向
        playerPos = player.position - transform.position;

        // y座標だけ1下げる
        Vector3 newPos = new Vector3(playerPos.x, playerPos.y - 1, playerPos.z);
        playerPos = newPos;

        // プレイヤーの方へレイを飛ばす
        if (Physics.Raycast(face.transform.position, playerPos, out hit_player, Mathf.Infinity))
        {
            Debug.DrawRay(face.transform.position, playerPos * hit_player.distance, Color.red);

            // エージェントの視線とレイとの角度(3次元)
            angle = Vector3.Angle(face.transform.forward, playerPos);

            // プレイヤーにあたって、角度が小さいときは、エージェントの状態を 1 にする。
            if ((hit_player.collider.tag == "Player" || hit_player.collider.tag == "MainCamera") && angle * angle < 4900) //
            {

                agentState = 1;

                // エージェントの移動速度を上げる
                agent.speed = 7;
                // エージェントのアニメーションを切り替える。
                faceAnimator.SetBool("player_run", true);

            }
            else
            {
                // プレイヤーを見失うと状態0に戻る
                agentState = 0;

                agent.speed = 3.5f;
                faceAnimator.SetBool("player_run", false);

            }            

        }
        else {
            
        }      

        // エージェントの状態が 1 のとき
        if (agentState == 1)
        {
            // 目的地をプレイヤーの現在位置にする
            agent.destination = player.position;
            // エージェントん顔がプレイヤーの方を向く
            face.transform.LookAt(player);
        }
        else {

            // エージェントの状態が 0 のときは巡回する
            agent.destination = points[n].position;

        }


        // エージェントの目的地に到着する寸前のとき
        if (!agent.pathPending && agent.remainingDistance < 1f)
        {

            if (agentState == 1)
            {
                // 状態が 1 ならシーンをロード(ゲームオーバー等)
                SceneManager.LoadScene("navmeshTest");

            }
            else {

                // 状態が 0 なら、次の目的地を伝える
                n += 1;

                if (n > 3)
                {
                    n = 0;
                }

                agent.destination = points[n].position;

            }

        }
            
    }
}


なぜか、レイがプレイヤーの頭上を通過してしまうので、プレイヤーの座標の高さだけを調節しました。

レイが当たったオブジェクトはタグで判別しています。プレイヤーとカメラ用のタグはデフォルトで用意されています。

シーンをロードするには、Files -> Build Settings… の Scenes In Buildに、ロードしたいシーンをドラッグアンドドロップしておく必要があります。

かなり稚拙なコードなので、このままでは不自然な動きになります。

プレイヤーを見失った瞬間にエージェントがプレイヤーへの興味を無くすのもおかしいですね。

一度プレイヤーを見つけるととりあえずその場所まで行ってみるとか、音で発見されるといったいろいろな機能をつけていくと良いかもしれません。

アニメーションを遷移させる


アニメーションを付けるには、つけたいオブジェクトを選択して、Animation -> Createをクリックします。

角度のプロパティを追加します。

右にあるタイムライン上の好きな場所に白い縦線をおいた状態で、録画マークを押して、角度の値を変えます。

すると、その時点(キーフレーム)までに、その値に向かって、角度がなめらかに動きます。今回は左右に首を振るようなアニメーションにしました。
様々なキーフレームとプロパティ、値を追加することで複雑なアニメーションが簡単に作れます。

再生ボタンで出来上がりを確認できます。

Samplesで再生速度を調節できます。

頭の動きに合わせてレイの方向も変わるのがわかります。

アニメーションを作ると、Animatorに新しいステートが作られます。

アニメーションの遷移をコントロールするためのパラメーターを追加しましょう。今回はBoolを使っています。

何も無いところで空のステートを作ります。プレイヤーを追っているときはこのステートに移ります。

そのステートともともとあったオレンジ色のステートをTransitionで結びます。遷移元のステートで右クリックしてMake Transitionで矢印を出し、遷移先のステートをクリックします。

双方向の遷移を作ったら今作った矢印を1つずつ選択します。

インスペクタで、Conditions(遷移する条件)を追加します。

追加したパラメータを選び、trueを選択します。Has Exit Timeのチェックが入っていると、条件を満たした後、設定した時間がたってから遷移します。今回は外しています。

もう一方の矢印ではfalseを選択します。

そして、スクリプトで、アニメーションがついているオブジェクトにあるAnimatorコンポーネントから、SetBoolメソッドを使うとアニメーションを変えられます。


GetComponent<Animator>().SetBool("パラメータ", 値);

参考:https://docs.unity3d.com/ja/2018.4/Manual/AnimationParameters.html

コメントを残す

メールアドレスが公開されることはありません。